Fix: keep stdin open through a result that lands right after a task settles (#1190) - #1212
Open
sergiobuilds wants to merge 1 commit into
Open
Conversation
…hropics#1212) DEFERRING_TASK_TYPES agent work (local_agent/local_workflow) that settles (task_notification, or a task_updated patch with a terminal status) before its own turn's result frame has ever been observed while it was inflight leaves _inflight_tasks empty at that result. The stdin-closing waiter (wait_for_result_and_end_input) then closes stdin immediately, even though the CLI may still owe a continuation control round trip (a permission check, hook callback, or SDK-MCP call) for the work that task's completion triggers. The later control request fails with 'stdin already closed' / BrokenPipeError, silently dropping hooks/SDK-MCP for that turn. Track, per task, whether a result was ever processed while it was inflight (_tasks_pending_first_result). A task that settles without one sets _task_settled_since_last_result, which makes the very next result frame - empty inflight set or not - keep stdin open once more before closing, covering the case where that result is the pre-continuation marker the settled task is still owed a round trip for. A task that was already seen inflight during an earlier result does not set the flag: that earlier result already covered 'turn ended, task still running', so the result that follows its settlement is the ordinary next-turn result and closes stdin immediately, unchanged from before this fix (verified against the existing test_result_with_inflight_task_keeps_stdin_open, which encodes exactly that case and still passes unmodified). The _read_messages finally block's unconditional _first_result_event.set() remains the backstop if no further result frame ever arrives, so deferring here cannot hang the waiter beyond process exit. Adds a synthetic-Transport regression test (adapted from the issue's own repro) verifying a late control response is delivered rather than dropped, plus two unit tests on the new tracking state covering both the deferred and non-deferred branches. Fixes anthropics#1190
sergiobuilds
force-pushed
the
fix-late-continuation-stdin-close-1190
branch
from
August 15, 2026 18:41
49142cb to
40bb6c1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DEFERRING_TASK_TYPESagent work (local_agent/local_workflow) that settles (task_notification, or atask_updatedpatch with a terminal status) before its own turn's result frame has ever been observed while it was inflight leaves_inflight_tasksempty at that result.wait_for_result_and_end_input()then closes stdin immediately, even though the CLI may still owe a continuation control round trip (a permission check, hook callback, or SDK-MCP call) for the work that task's completion triggers. The later control request fails with "stdin already closed" /BrokenPipeError, silently dropping hooks/SDK-MCP for that turn.Fixes #1190.
Reproduction
Ran the synthetic-
Transportrepro from the issue (no API/model call) against a clean install ofmain:Exit code 1 — the late control response can't cross the already-closed channel.
Fix
_track_task_lifecycle()'s existing docstring already explains why a blanket "defer the next result" mitigation can't work: no ledger can tell a settled task with a pending continuation apart from no work at all, in general (that needs a run-boundary signal from the CLI). But the two cases are locally distinguishable in the one specific way #1190 reports:test_result_with_inflight_task_keeps_stdin_open(existing test) encodes, and it still passes unmodified._tasks_pending_first_resulttracks the first case per task; a settlement out of that set (i.e., the second case) sets_task_settled_since_last_result, which makes the next result frame — regardless of_inflight_tasks— keep stdin open once more before closing.The
_read_messagesfinallyblock's unconditional_first_result_event.set()remains the backstop if no further result frame ever arrives, so this cannot hang the waiter beyond process exit — worst case, the close is delayed until the CLI process exits, same safety net the existing inflight-count mitigation already relies on.Testing
test_task_settled_before_any_result_defers_close_for_late_control— synthetic-transport integration test adapted from the issue's repro:task_started→task_notification→ result → (latecan_use_toolcontrol request, delivered successfully) → result → stdin closes exactly once, after the second result.test_settle_before_any_result_defers_the_next_result/test_settle_after_a_seen_result_does_not_defer— unit tests on the new tracking state, covering both branches.mainand pass with the fix (verified viagit stash).python -m pytest tests/— 1369 passed, 5 skipped (no regressions, including the pre-existingTestStdinStaysOpenWithInflightTaskssuite this change sits next to).python -m ruff check/ruff format --check— clean.python -m mypy src/— no issues.What I did not change
Left
_track_task_lifecycle's framing of the general problem (#1088) as unsolved without a CLI-side run-boundary signal — this PR narrows one specific, locally-distinguishable instance of it rather than claiming the whole class is fixed.