Repository navigation
fix(agent-status): a Claude background workflow counts as live work from its launch - #24214
brennanb2025 wants to merge 6 commits into
Conversation
…rom its launch The launch reader records any main-agent async launch naming a taskId, typed through the shared kind table (agents stay with the rosters, unknown kinds fail active), beside a shell's backgroundTaskId. The kind table learns the inventory's 'workflow' name. Stories from the r6/r7 workflow captures, the replay helpers moved into a shared test fixture, and the shell-only comments and doc lines rewritten.
…in its launch turn Built from r8-w8-run2: in run 1 the first agent hook landed inside the renderer's 500 ms settle window, where the inferred Ctrl+C is dropped on every build.
…ows, not on the record's contents
…eflow the end-row comment
|
| const kind = classifyClaudeBackgroundTaskKind(response.taskType) | ||
| return isAgentChildWorkKind(kind) ? undefined : { taskId: response.taskId, kind } |
There was a problem hiding this comment.
Stopped workflows remain live. If a workflow is stopped or paused from
/tasks while it waits between agents after Ctrl+C in its launch turn, this new record keeps the pane Monitoring even though the workflow has ended. Captured stops and pauses produce neither a hook nor a transcript end line, so the record remains until a later turn provides a task inventory. If no later turn arrives, the false working status persists and can hold Auto keep-awake. The end state needs a way to be reconciled without relying solely on the next inventory.
Knowledge Base Used: Agent integrations and hooks
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the hook-lane record now admits a non-shell background launch so a Claude Workflow counts as live work from its launching tool call, closing the between-agents window after a Ctrl+C in the launch turn.
- Launch reader:
readClaudeBackgroundTaskLaunchhandles a shell'sbackgroundTaskIdas before, and otherwise Claude'sstatus: "async_launched"+taskIdresult, typing it fromtaskTypeand skipping agent kinds so the rosters keep owning agents. - One admission rule: the launch now admits the same set the
Stopinventory already admits (every non-agent kind, unknown included), so a kind can no longer read live after a normal turn end but done after a Ctrl+C. - Kind table:
workflowjoinslocal_workflowasworkflow, covering the inventory's spelling; the launch's kind is kept only when an inventory has not already typed the task. - Tests/fixtures/docs: eight new captured workflow stories (W1–W8) replayed through the server's HTTP ingress, launch-reader units, a relayed-pane story, plus a shared capture-replay fixture and the
agent-status-store.mdrewrite.
Verified via a focused sub-review that the shared-table 'workflow' alias does not change the native-chat / SDK-stream lane (claude-background-task-tracker.ts backgrounded, MATERIALIZED_TASK_KINDS): every in-repo stream frame names local_workflow (already mapped), and bare workflow appears only in the hook inventory this change targets. No anchored or non-anchored actionable findings; the documented /tasks-stop residual and the missing live QA are correctly scoped as future work by the author.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…e the shared capture helpers
Review status: ready for your review (not merged; auto-merge not enabled)The problem. If you press Ctrl+C in the Claude turn that launched a background workflow, the workflow keeps running. But whenever its script waits between two agents, Orca shows the pane as done/Interrupted: the sidebar, What you see after this PR. In that stretch the pane reads Monitoring, then Working when the next agent starts, then Done when Claude's report turn finishes. The mid-workflow "Claude stopped" notification is gone. A workflow with no Ctrl+C in its launch turn looks the same as before. Live QA (M4Air, real Claude 2.1.285 in an isolated home, base = #24116 vs this PR): Ctrl+C in the launch turn followed by a 60 s script wait reads Interrupted through the wait on the base and Monitoring on this PR, with notifications 2 vs 1. A single-agent workflow whose launch turn ends normally shows no difference. The body now says "Fixes STA-3344 (#12382)", and still notes that #12382's measured case (an interrupted
Decision for you: conflict with #22322. #22322 makes a running workflow read Working and stops Monitoring from holding Auto keep-awake. This PR relies on Monitoring holding keep-awake, so the laptop doesn't sleep mid-workflow. Whichever PR lands second must settle how a running workflow is shown. Recommendation: #22322's rule, where a running workflow reads Working and holds keep-awake. This PR changes no code for it. Fixed in review (tests, comments and docs only; production behaviour unchanged):
Deferred, labelled temporary in the body:
Found in review, not caused by this PR:
Verified:
Not verified: Linux, Windows, WSL and SSH runs (SSH never infers a Ctrl+C, so nothing visible changes there); the 30 min and 2 h limits (derived from code, not waited out live); teammate and Monitor-tool launches. The QA seat was Claude, because Grok's usage balance was exhausted. Merge order: after #24116, before F1 and the Deny/Esc PR. Retarget to |
|
Closing this older workflow patch. Rebuild workflow lifetime together with the sleep rule so a cancelled foreground reply cannot hide a workflow that is still running; that work remains tracked. |



ELI5
The bug. Claude's
Workflowtool runs a script of agents, and it always runs in the background. In a Claude terminal pane, if you press Ctrl+C in the turn that launched a workflow, the workflow keeps running, but whenever its script is waiting between two agents, Orca shows the pane done, interrupted.orca worktree pssays done, and Auto keep-awake lets the laptop sleep in the middle of the workflow.How long. The wait is whatever the script waits. Captured: 20 s and 60 s with no hook and no transcript line at all; there is no limit.
Why it happens. After a Ctrl+C, Claude sends no hook and no end-of-turn
Stop(so no task list either). #24116 (this PR's base) records a background task from the tool call that launches it, but it reads only a shell's field,tool_response.backgroundTaskId. A workflow's launch names its tasktool_response.taskId, withstatus: "async_launched"andtaskType: "local_workflow", so #24116 never records it. The only thing that kept the pane live was whichever of the workflow's agents happened to be running.What you see after this PR. After a Ctrl+C in the launch turn, the pane reads Working while one of the workflow's agents runs and Monitoring while none does, then done within about a second of Claude writing the workflow's end line to its transcript.
Where you will notice it. Only after a Ctrl+C in the launch turn, while none of the workflow's agents runs. A workflow that runs to completion, a one-agent workflow Ctrl+C'd in its launch turn, and a workflow across
/clearshow no visible change: the only difference there is a sub-second blip at the very end.One fewer notification. Today, when the workflow's agent stops after that Ctrl+C, the pane flips to Interrupted and Orca sends a "Claude stopped" notification in the middle of the still-running workflow. After this PR it doesn't; the only notification is the one when Claude's report turn finishes (live QA: 2 on the base, 1 here).
Keep-awake (Auto mode). Monitoring publishes as
state: workingwithworkingMode: monitoring, so a pane in the wait now holds Auto keep-awake, as a Monitoring pane with a running shell already does. Nothing renews that hold on a quiet pane: only an accepted hook renews it (Claude never sends the terminal status sequence that is the other path, and the transcript watch publishes nothing while nothing changes), so it lasts at most 2 h after the last hook.Open decision: conflict with #22322. #22322 (open, keep-awake per kind) makes a running workflow read Working and stops Monitoring from holding Auto keep-awake. This PR shows a workflow whose launch turn was Ctrl+C'd as Monitoring and relies on Monitoring holding keep-awake, so the laptop does not sleep mid-workflow. If #22322 lands as written after this PR, that workflow would stop holding keep-awake again. Whichever lands second must settle how a running workflow is shown; the recommendation is #22322's rule (a running workflow reads Working and holds keep-awake). This PR changes no code for it.
Scope.
worktree ps, mobile, dashboard).The plan. This PR is step 4 of 6, and they merge in this order:
F1 must not merge before this PR (or must be based on it): once Esc and SSH stops count as cancels, a workflow would read done during every no-agent stretch after them.
What Changed
The launch reader (
readClaudeBackgroundTaskLaunch,claude-non-agent-work.ts), for the main agent's ownPostToolUseonly (a subagent's launch is its own work, as for shells):tool_response.backgroundTaskId, recorded with kind unknown. Unchanged from fix(agent-status): a Claude background shell counts as live work from the tool call that starts it #24116.status === "async_launched"with ataskId. Its kind comes fromtaskTypethrough the shared kind table (classifyClaudeBackgroundTaskKind).Agentlaunches carryagentIdand notaskId, so they never reach this branch; a future one that namestaskType: local_agentwould be skipped by the table.One admission rule for one fact. Claude's end-of-turn task list (
claude-background-task-inventory.ts) already records every non-agent kind, unknown included. The launch reader now admits the same set, so no kind can read live after a normal turn end but done after a Ctrl+C in its launch turn.The kind table learns the task list's name for a workflow.
workflowjoinslocal_workflowasworkflow. Without it, a recorded workflow's kind flipped to unknown at the nextStop. No liveness reader reads the kind (the fold reads only whether the record is non-empty), and the chat view's stream names workflowslocal_workflow, so nothing it shows changes. This is the table's only change; teammates are left alone (see Review).The record keeps the launch's kind.
recordClaudeNonAgentTaskLaunchtakes the launch's kind; a kind a task list already gave the task wins.Nothing else changes. Every way #24116's record ends a task applies to workflows unchanged: Claude's
queue-operationend line matched by both the task id and the launching tool call's id (the captured workflow end lines carry both, equal to the launch's), aTaskStopresult, the next main-agent task list, a new Claude process, and the pane's process, terminal or relay ending.Comments and doc. #24116's shell-only wording that this makes false is rewritten, notably that a
/tasksstop always writes an end line: it does for a shell, not for a workflow stopped or paused from/tasks, which leaves on the next task list.docs/reference/agent-status-store.md(tracked) states the launch shapes, the shared admission rule and that exception.Tests. The capture replay helpers move from #24116's shell test into
claude-background-task-capture.test-fixture.ts, which both the shell and the new workflow stories import.Why
Root cause over patch. The workflow is the live thing; its agents come and go inside it. Recording the workflow from its launch, through the record and end edges #24116 already built, fixes the model instead of patching the between-agents window. It adds no new state, no new end reader and no wire change.
Alternatives considered:
taskType === "local_workflow": a string special case, and it would disagree with the task list, which already admits every non-agent kind. That disagreement is this bug again for other kinds.<session>/workflows/<runId>.json) to learn a stopped or paused workflow: a second end reader for one kind; no payload names the file; it moves to the new session on/clearwhile the launch still names the old one; every captured copy is written only at the end.The stack is not a partial design. #24116 alone is the right design for shells; this PR widens what it admits and undoes nothing. What lands after both is what one would write from scratch.
Wire compatibility. No wire change. The reader is in shared code that the SSH and WSL relays run, and the relay publishes the Monitoring row shape it already publishes for shells, which older desktops render.
Differences from the reference implementations, labelled:
taskId+taskType), not a task-start event from a streaming session. Intended: the hook path has no start event, and the launch ids reappear in Claude's end line and task list.taskIdwould be admitted as unknown. Temporary: the shared table mapsin_process_teammateto unknown; the follow-up maps it to an agent with a test for the chat view, which shares the table./tasksleaves only on the next task list (the references retire on a terminal status update). Temporary: Claude writes nothing Orca can read for it; see Review for the follow-up and its exit.Linked Issue
Fixes STA-3344 (#12382).
Live QA (below) shows the base (#24116) reading done/Interrupted and this PR reading Monitoring through a 60 s script wait between two agents, after a Ctrl+C in the launch turn.
#12382's measured case, an interrupted
Stopwhose task list names a running workflow, already reads live on main: the task list sets the running-task fact with no interrupt argument, and the status fold does not consult how the turn ended. Current Claude sends noStopat all on Ctrl+C, so the reporter's steps now reach the launch-only path, which is what this PR covers.Does not fix: #10997 (approvals and plans), STA-2413 / #10114 (F1, the interrupt notice), #21327 (a subagent killed without
SubagentStoppinning Working, the class the paused-workflow case below belongs to).Related: #24116, #24075, #22722, #22322 (keep-awake per kind).
Visual Proof
Live QA on a remote Mac (M4Air) with real Claude Code 2.1.285 in an isolated home, a hidden dev Orca per build, driven over the DevTools protocol, status sampled every 100 ms. Base = #24116 (production-identical
a3d97b99ddf); head = this PR at70cc59395f0(later commits change only tests and comments). Each Ctrl+C was pressed while agent 1'ssleepran, more than 1 s from any hook, so the renderer's 500 ms settle window could not drop it./tasksduring the wait (known temporary gap)/tasksstop/tasksstop: still MonitoringSeen on both builds, not caused by this PR: a working row's age label counts from the previous Done (it read "2m" 20 s into Q3's Monitoring), and about 1.3 s of Monitoring between a normal launch turn's
Stopand the workflow agent's first hook.Testing
New capture fixtures (
src/shared/__fixtures__/claude-background-workflow-*-hooks.jsonl, sidecars beside them), recorded from Claude Code 2.1.285 over a real terminal, in #24116's format:…-script-wait-ctrl-c(W8, fromr8-w8-run2): two agents with a 60 s script wait, Ctrl+C in the launch turn. The discriminating fixture.…-ctrl-c(W2): one agent, Ctrl+C in the launch turn; keeps Claude's interrupt notice line as a line that is not an end line.…-completed(W1),…-clear(W4),…-script-wait(W6a, 20 s wait, normal launch turn),…-idle-ctrl-c(W5a),…-tasks-kill(W3),…-pause(W6b).transcript_path,agent_transcript_path,transcriptDir,scriptPath,<output-file>and<diagnostics>) maps to a placeholder distinct per fixture; each workflow agent id to a same-shape placeholder;msg_ids to a same-length placeholder; task ids, tool call ids andwf_run ids stay verbatim. The capture scanner reports only the placeholder UUIDs; a grep for the machine's user, host and account names finds none.tfor a transcript line (the rig's later poll time): in W1, W4, W6a and W8 the end line precedes the report turn's prompt hook by 40–43 ms.New tests, and what each reads on #24116's head (production files at
18047aa61a3, tests kept: 13 of the new tests fail there, each for the reason below):Stop./clearand settles on the end line in the new transcript with the pre-/clearcompletion time. Base: Monitoring after the end line.Stop)./tasksstop, no hook and no line for 135 s; still live on a watch tick 1 s before the next prompt; that prompt's empty task list clears it. Passes on both builds.SubagentStop; Working until the next prompt's empty task list. Passes on both builds.workflow; a shell still records frombackgroundTaskId;remote_agent,monitor_wsand a missingtaskTyperecord as unknown;in_process_teammaterecords as unknown (labelled temporary); the captured backgroundAgentlaunch and agent-kindtaskTypes record nothing; ataskIdwithoutasync_launchedrecords nothing; a subagent's workflow launch records nothing; an earlier task list's kind wins. Hand-built rows are labelled as such.<result>that contains a<task-notification>literal is refused (fails toward live); a<result>naming another recorded task's ids retires only the task the line names.Stopentry types the workflow asworkflow. Base: unknown.Ablations (one production change reverted at a time, the five named test files, restored from HEAD after each):
async_launchedcheck: 1 fails ("ignores a taskId that is not an async launch").workflowtable entry: 1 fails (the task list unit test).Runs:
tc-gate pnpm tc:nodeandtc-gate pnpm tc:clipass (no renderer file changed;tc:nodere-run after the review's test changes).pnpm run check:code-quality:changed: 0 new findings.oxlinton every touched file: clean. Vitest, 104 named files (#24116's list plus the new workflow stories, includingorca-runtime.test.ts): 2538 passed, 1 skipped; the 8 files this PR changes pass 97/97 at the head. CI green.Platforms: macOS vitest, plus the live macOS run above. No Linux, Windows, WSL or SSH run.
Review
Still not fixed, labelled:
/taskswhile none of its agents runs, after a Ctrl+C in its launch turn, reads Monitoring until your next prompt (then Working, then done at that turn'sStop, whose task list clears it). The sidebar,orca worktree psand mobile stop showing Monitoring after 30 min without a hook, and Auto keep-awake releases at most 2 h after the last hook; nothing renews it. Confirmed live (Q3 above). fix(agent-status): a Claude background shell counts as live work from the tool call that starts it #24116 reads done there, which happens to be right for a stopped workflow, but it reads done in the same spot for a workflow still running. Claude writes no end line and fires no hook for these stops. This extends the existing residual for a workflow stopped or paused after a normal launch turn. Temporary. Follow-up (one ticket for the stop and the pause, not yet filed): it closes when a capture shows Claude writing akilledend line for a workflow stopped or paused from/tasks(fix(agent-status): a Claude background shell counts as live work from the tool call that starts it #24116's matcher then retires it with no code change), or a paused status in the task list, decided from that capture./taskswhile its agent runs (captured) reads Working until your next prompt on every build, because the pause kills the agent with noSubagentStopand the subagent roster keeps it ([Bug]: A lost SubagentStop pins the pane 'working' for the rest of the session — the subagent roster has no expiry #21327's class). Unchanged by this PR.taskIdandtaskType: in_process_teammate(uncaptured) would hold Monitoring until the next task list. Temporary: the shared table maps that type to unknown; the follow-up maps it to an agent in its own PR with a chat-view test./clear; Deny-with-feedback then a launch.TaskStopon a workflow; resumed and predefined workflows; a workflow launched by a subagent (not recorded, as for shells); a background agent plus a running workflow followed by an idle Ctrl+C that writesagents_killed.Not verified: the 30 min and 2 h limits of the
/tasksgap were derived from code, not waited out live; Linux, Windows, WSL and SSH; teammate and Monitor-tool launches (a Monitor launch is not recorded by this PR, the same as its base).Merge: after #24116, and before F1 and the Deny/Esc PR. Retarget to
mainonce #24116 lands.Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.