Chat: a running Claude Code workflow (background subagents) reads as needs-input / idle, not working #39

Open
opened 2026-07-08 01:25:17 +02:00 by dominik.polakovics · 2 comments

This was generated by AI during triage.

Summary

When a Claude Code workflow (multi-agent orchestration) is running, it
dispatches subagents that keep working in the background. lab does not recognize
this as "working": the run shows as needs-input (or idle) and the composer
unlocks as if the agent were waiting on the operator, even though background
agents are still running.

Reproduction

  1. Start a run and have the agent launch a workflow / background subagents (the
    main turn ends with an ordinary assistant message while the subagents keep
    going).
  2. Observed: the run's badge shows needs-input (or no badge / idle) and the
    chat composer presents an unlocked Send box.
    Expected: the run reads as "working" while the workflow's subagents run.

Notes for triage

Root cause is that lab derives activity state from only the type of the last
content block in the main transcript .jsonl
:

  • deriveStateinternal/provider/claudecode/chat.go:229-250: a trailing
    assistant:text block → StateNeedsInput; only user:text | tool_use | tool_result | assistant:thinking as the last block → StateWorking.
  • A workflow dispatch ends its visible turn with an end_turn assistant text
    message, so lastKey == "assistant:text"needs-input (or idle if the
    located transcript is empty / subagent-only).
  • lab parses no stop_reason, no isSidechain / parentUuid, and has no
    notion of an in-flight Task / subagent (tItem in
    internal/provider/claudecode/chat_types.go:18-26 omits them; compat.md §5
    says every other transcript key is ignored). The Task tool is recognized
    cosmetically only (chat_types.go:130-133) and counts as "working" only while
    it is the literal last block.
  • The only live overlays are attention signals (pending-dialog → question,
    blocked marker → needs-input, via dialogspool.go); there is no "background
    working" signal.
  • If subagents write to their own session .jsonl, the main transcript is frozen,
    so the tailer's single-file mtime/size watch (internal/chat/tailer.go:150)
    never fires and the UI stops refetching.
  • FE effect: the badge map (web/src/lib/conversation.ts:14-25) gives idle/ended
    no badge, and the working/interrupt affordance is gated on state === 'working'
    (web/src/routes/RunChat.tsx:793).

Likely fix surface (for the maintainer to weigh): teach deriveState / tItem
about stop_reason and/or an unresolved-Task / isSidechain "sub-task in
flight → working" rule, plus widen the tailer's single-file watch to observe
sidecar session writes. Shares the deriveState coupling with #38 ("Interrupt
button does nothing").

> *This was generated by AI during triage.* ## Summary When a Claude Code **workflow** (multi-agent orchestration) is running, it dispatches subagents that keep working in the background. lab does not recognize this as "working": the run shows as **needs-input** (or idle) and the composer unlocks as if the agent were waiting on the operator, even though background agents are still running. ## Reproduction 1. Start a run and have the agent launch a workflow / background subagents (the main turn ends with an ordinary assistant message while the subagents keep going). 2. **Observed:** the run's badge shows needs-input (or no badge / idle) and the chat composer presents an unlocked Send box. **Expected:** the run reads as "working" while the workflow's subagents run. ## Notes for triage Root cause is that lab derives activity state from **only the type of the last content block in the main transcript `.jsonl`**: - `deriveState` — `internal/provider/claudecode/chat.go:229-250`: a trailing `assistant:text` block → `StateNeedsInput`; only `user:text | tool_use | tool_result | assistant:thinking` as the *last* block → `StateWorking`. - A workflow dispatch ends its visible turn with an `end_turn` assistant text message, so `lastKey == "assistant:text"` → **needs-input** (or **idle** if the located transcript is empty / subagent-only). - lab parses **no** `stop_reason`, **no** `isSidechain` / `parentUuid`, and has no notion of an in-flight `Task` / subagent (`tItem` in `internal/provider/claudecode/chat_types.go:18-26` omits them; `compat.md` §5 says every other transcript key is ignored). The `Task` tool is recognized cosmetically only (`chat_types.go:130-133`) and counts as "working" only while it is the literal last block. - The only live overlays are *attention* signals (pending-dialog → question, blocked marker → needs-input, via `dialogspool.go`); there is no "background working" signal. - If subagents write to their own session `.jsonl`, the main transcript is frozen, so the tailer's single-file mtime/size watch (`internal/chat/tailer.go:150`) never fires and the UI stops refetching. - FE effect: the badge map (`web/src/lib/conversation.ts:14-25`) gives idle/ended no badge, and the working/interrupt affordance is gated on `state === 'working'` (`web/src/routes/RunChat.tsx:793`). Likely fix surface (for the maintainer to weigh): teach `deriveState` / `tItem` about `stop_reason` and/or an unresolved-`Task` / `isSidechain` "sub-task in flight → working" rule, plus widen the tailer's single-file watch to observe sidecar session writes. Shares the `deriveState` coupling with #38 ("Interrupt button does nothing").
Author
Owner

This was generated by AI during triage.

Triage: ready-for-human

Category: bug
Summary: shares its root cause with #62 (filed as the umbrella issue for
this symptom plus #38) — deriveState derives state from only the last
transcript content-block type, with no notion of an in-flight Task /
isSidechain subagent. A workflow's background subagents keep running after
the main transcript's visible turn ends in an ordinary assistant:text, so the
run reads needs-input/idle instead of working.

Why this is ready-for-human, not ready-for-agent: fixing this properly
means picking the same liveness mechanism #62 is blocked on (parsing
stop_reason/isSidechain/in-flight Task, a pane/CPU probe, and/or widening
the tailer past the single main-transcript file) — that choice is a design
call for the maintainer, not something to decide independently per-issue.
Landing a narrow fix here without that decision would risk a second
transcript-state heuristic alongside #62's, which is exactly the kind of
heuristic-on-a-heuristic #62's notes already flag as rejected.

Acceptance criteria (once a direction is picked, shared with #62):

  • A run with live background subagents reads as working, not
    idle/needs-input, for as long as those subagents are running.
  • The composer's Send/Interrupt affordance (gated on state === 'working'
    in RunChat.tsx) reflects that background-working state correctly.

Out of scope:

  • Re-deciding the liveness mechanism independently of #62 — see that issue for
    the open design question this is blocked on.

Maintainer note: #62 itself suggests "consider folding #39 in, or closing
both against this once designed" — leaving both open and cross-linked for now
rather than merging them, since that consolidation call is yours to make.

Relabeling needs-triageready-for-human, cross-linked to #62.

> *This was generated by AI during triage.* ## Triage: ready-for-human **Category:** bug **Summary:** shares its root cause with #62 (filed as the umbrella issue for this symptom plus #38) — `deriveState` derives state from only the last transcript content-block type, with no notion of an in-flight `Task` / `isSidechain` subagent. A workflow's background subagents keep running after the main transcript's visible turn ends in an ordinary `assistant:text`, so the run reads `needs-input`/idle instead of `working`. **Why this is `ready-for-human`, not `ready-for-agent`:** fixing this properly means picking the same liveness mechanism #62 is blocked on (parsing `stop_reason`/`isSidechain`/in-flight `Task`, a pane/CPU probe, and/or widening the tailer past the single main-transcript file) — that choice is a design call for the maintainer, not something to decide independently per-issue. Landing a narrow fix here without that decision would risk a second transcript-state heuristic alongside #62's, which is exactly the kind of heuristic-on-a-heuristic #62's notes already flag as rejected. **Acceptance criteria (once a direction is picked, shared with #62):** - [ ] A run with live background subagents reads as `working`, not idle/needs-input, for as long as those subagents are running. - [ ] The composer's Send/Interrupt affordance (gated on `state === 'working'` in `RunChat.tsx`) reflects that background-working state correctly. **Out of scope:** - Re-deciding the liveness mechanism independently of #62 — see that issue for the open design question this is blocked on. **Maintainer note:** #62 itself suggests "consider folding #39 in, or closing both against this once designed" — leaving both open and cross-linked for now rather than merging them, since that consolidation call is yours to make. Relabeling `needs-triage` → `ready-for-human`, cross-linked to #62.
Author
Owner

Root fix designed: #62 now carries the full decision record (2026-07-10 grill). A working-edge from the liveness markers overrides the assistant:text tail, so a run with live background work reads as working — with one honest caveat: whether background subagents are reliably hook-observable is a pinned spike question (does Stop fire while they run; does subagent tool-use refresh the parent's edges; SubagentStop pairing). Per decision 5 on #62, this issue's acceptance criterion is scoped to what the spike proves — if hooks can't see background work, the criterion is renegotiated on evidence rather than met with inference. Stays open as the symptom tracker; #62's implementation closes it (prerequisites: #92, #93).

Root fix designed: #62 now carries the full decision record (2026-07-10 grill). A working-edge from the liveness markers overrides the `assistant:text` tail, so a run with live background work reads as working — with one honest caveat: whether background subagents are reliably hook-observable is a pinned spike question (does Stop fire while they run; does subagent tool-use refresh the parent's edges; SubagentStop pairing). Per decision 5 on #62, this issue's acceptance criterion is scoped to what the spike proves — if hooks can't see background work, the criterion is renegotiated on evidence rather than met with inference. Stays open as the symptom tracker; #62's implementation closes it (prerequisites: #92, #93).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#39
No description provided.