Skip to content

claude harness: end the turn on the SDK's command lifecycle, not a result-per-prompt count - #945

Open
ReganBell wants to merge 1 commit into
mainfrom
claude-harness-steer-lifecycle
Open

claude harness: end the turn on the SDK's command lifecycle, not a result-per-prompt count#945
ReganBell wants to merge 1 commit into
mainfrom
claude-harness-steer-lifecycle

Conversation

@ReganBell

@ReganBell ReganBell commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

Users on the Claude harness were seeing I hit an error and couldn't finish — Claude turn exceeded 2000s wall clock (or whatever TURN_WALL_CLOCK_SEC is set to) on turns that had in fact already produced their reply. With no wall clock configured the same turns hung until the run reaper.

The trigger is a mid-turn steer. The Claude Agent SDK runs Claude Code as a child process with a streaming input queue. A user message pushed while a turn is in flight is folded into that running turn at the next tool boundary and shares its single result message. The harness assumed one result per prompt it pushed (pendingPrompts), so after a folded steer it waited for a second result that never came, idled until the wall clock fired, and threw the wall-clock error in place of the reply it had already streamed.

Verified against the real SDK (0.3.211 / CLI 2.1.259):

  • steer pushed during a tool call → folded into the turn, one result, harness hung
  • steer pushed after the model's last message → its own turn, two results, harness fine

Fix

The SDK emits command_lifecycle events (queuedstartedcompleted/cancelled) for every uuid-stamped inbound user message and advertises this as the msg_lifecycle_v1 capability on system/init. The harness now:

  • stamps every pushed user message with a uuid
  • tracks open command uuids in a set and settles one on a terminal lifecycle state
  • closes its prompt queue (after stopping the signal poll) when the set empties, whether a command ran as its own turn or was folded into an earlier one
  • falls back to the old one-result-per-prompt accounting, through the same settle path, when init does not advertise the capability (older CLIs)

Tests

  • New: a steer folded into the running turn ends the turn on the lifecycle signal. Times out at 5s on the previous code, passes now.
  • New: a steer queued behind the model's last reply still gets its own turn and the harness waits for it.
  • Existing steer tests exercise the legacy (no-capability) path unchanged.
  • Live: ran the real harness against the real SDK with a steer sent during an Agent subagent call; the steer was absorbed into the reply and the turn returned in ~15s.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…sult-per-prompt count

A steer pushed while the Claude Agent SDK is mid-turn is folded into the running
turn at the next tool boundary and shares that turn's single `result` message. The
harness counted one `result` per prompt it pushed, so after a folded steer it waited
for a second result that never came, sat idle until the turn wall clock fired, and
surfaced "Claude turn exceeded Ns wall clock" to the user in place of the reply it had
already streamed. With no wall clock configured the turn hung until the run reaper.

Stamp every pushed user message with a uuid and track completion through the SDK's
`command_lifecycle` events (advertised as the `msg_lifecycle_v1` capability on init):
the turn ends when every command the harness pushed has reached a terminal state,
whether it ran as its own turn or was folded into an earlier one. CLIs that do not
advertise the capability keep the old one-result-per-prompt accounting through the
same settle path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant