End subagents on the CLI's own task lifecycle, and detect a limit two ways
Subagents were showing "running" long after they had finished. Measured against 2.1.237 by running a session that launched one Task agent and reading its stdout: a subagent's lines carry no `stream_event` at all -- they are whole `user`/`assistant` lines with a null `stop_reason` -- and no `result` line is sent for one. So `ends_a_turn`, which watches for a raw `message_delta` saying `end_turn`, could never fire for a subagent, and nothing finished one until its session's process exited. What the CLI does send is a task lifecycle, as top-level `system` lines: `task_started` (with the tool_use id), `task_progress`, `task_updated` (status, naming the task only) and `task_notification` (tool id, status, and the agent's own summary). `translate_task` keeps the task -> tool mapping, records the summary as the subagent's closing text -- the run showed its child lines stop at its last tool_result, so without this a finished subagent reads as stopping mid-tool -- and ends it. A `completed` update is deliberately not the end, since its notification carries the summary; any other terminal status is, because the failure to avoid is a subagent nothing ever finishes. `ends_a_turn` stays as a second detector and must never be the only one again. Verified by replaying the captured stream through the server as a fake CLI: running, prompt, Bash call, output, report, exited. `finish_all` now reads the directory rather than the live map, which is what clears the ones already stuck: a subagent left running by an earlier run of the server is exactly the one this process never touched, so it read "running" again every time its session was started. Auto-resume gets the same treatment on its own single point of failure. The only thing that scheduled a resume was the CLI's error sentence at the end of a failed turn; the CLI also sends `rate_limit_event` lines saying where the account stands, and this server ignored them entirely. Both are read now. Anything that is not an `allowed...` status counts as refused and is logged if unfamiliar -- being wrong that way costs one question to the usage meter, which is still what decides whether anything is sent, and being wrong the other way is the feature silently not existing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
13d2d11c2d
commit
74c07d687a
5 files changed
+371
-22
No files matched your search
@@ -609,7 +609,17 @@ itself.
|
||||
carries the dialect's reset time where it gave one, and recognising it
|
||||
belongs to the driver — the Claude CLI ends the turn with `is_error` and
|
||||
`Claude AI usage limit reached|1788546972`, and nothing above the driver
|
||||
matches on a string. The transcript draws it as a divider, like a clear or a
|
||||
matches on a string. **Two detectors, since 2026-09-06**: that sentence, and
|
||||
the CLI's own `rate_limit_event` lines, whose `rate_limit_info.status` says
|
||||
where the account stands and whose `resetsAt` is the same hint. One detector
|
||||
was a single point of failure for a feature whose whole job runs unattended
|
||||
— if the wording or the shape of a failed turn ever changes, nothing is
|
||||
scheduled and the session simply never comes back, with nothing on screen
|
||||
saying why. Only the change *into* being refused is reported, and only a
|
||||
status that is not an `allowed…` word counts as refused: an unfamiliar word
|
||||
is read as out of quota and logged, because the cost of being wrong that way
|
||||
is one extra question to the meter, and the cost the other way is the
|
||||
feature silently not existing. The transcript draws it as a divider, like a clear or a
|
||||
compaction: what a reader scrolling back wants from it is why the
|
||||
conversation stops at that line.
|
||||
|
||||
|
||||
Reference in new issue
Block a user