Skip to content

Fix: keep stdin open through a result that lands right after a task settles (#1190) - #1212

Open
sergiobuilds wants to merge 1 commit into
anthropics:mainfrom
sergiobuilds:fix-late-continuation-stdin-close-1190
Open

Fix: keep stdin open through a result that lands right after a task settles (#1190)#1212
sergiobuilds wants to merge 1 commit into
anthropics:mainfrom
sergiobuilds:fix-late-continuation-stdin-close-1190

Conversation

@sergiobuilds

Copy link
Copy Markdown

Summary

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. 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-Transport repro from the issue (no API/model call) against a clean install of main:

stdin_closed_before_late_control=True
late_control_response_delivered=False
late_response_write_after_close=True
end_input_calls=1

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:

  • If a task was already seen inflight during a previously-processed result, that result already covered "turn ended, task still running" — the result following its later settlement is the ordinary next-turn result and should close stdin immediately. This is the case test_result_with_inflight_task_keeps_stdin_open (existing test) encodes, and it still passes unmodified.
  • If a task settles without ever having been seen inflight during a result, its exit isn't covered by anything the caller has observed yet — the very next result might be the pre-continuation marker the settled task is still owed a round trip for.

_tasks_pending_first_result tracks 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_messages finally block'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_startedtask_notification → result → (late can_use_tool control 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.
  • All three fail on unpatched main and pass with the fix (verified via git stash).
  • python -m pytest tests/ — 1369 passed, 5 skipped (no regressions, including the pre-existing TestStdinStaysOpenWithInflightTasks suite 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.

…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
sergiobuilds force-pushed the fix-late-continuation-stdin-close-1190 branch from 49142cb to 40bb6c1 Compare August 15, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stdin closes after a delegated task settles before Result, dropping a later continuation control response

1 participant