Skip to content

Orchestrator amnesia on claude-cli: subagent-completion announce restarts the live process every turn (extraSystemPromptHash false-invalidation class survives 2026.6 rewrite, two-layer root cause + concrete fix) #92438

Description

@oiGaDio

TL;DR — what's broken for real users

Multi-agent orchestrator pipelines on claude-cli lose parent context after every subagent completion. The orchestrator forgets it spawned the child mid-pipeline, mis-tracks pipeline_status, and asks the user "wasn't that done already?" — i.e. the parent agent becomes operationally amnesic.

This is not theoretical. It is the dominant failure mode when running a planner/coder/checker pipeline over more than 2-3 turns. The class has been open since at least 2026-04 (#69118) with 5+ distinct triggers reported by separate users in that thread. Two upgrades (2026.6 live-session rewrite, 2026.6.5) changed the surface but not the class. I'm filing this as a fresh issue because the canonical thread has lost signal — automation stripped P1, the last 5 maintainer-visible signals are "still reproducing" with no acknowledgment, and the new live-session path warrants a clean root-cause writeup.

Environment

Concrete repro (deterministic, full pipeline)

  1. Orchestrator main session, holding a multi-turn conversation establishing a project, plan, and pipeline state.
  2. Spawn a planner subagent with sessions_spawn({ agentId: "planner", mode: "run", thinking: "high" }). Planner runs ~5-15 min, writes to project DB, returns a completion message.
  3. Orchestrator receives the completion message and continues the conversation.

Expected: orchestrator references the planner's output, knows it spawned the planner, advances pipeline_status accordingly.

Actual: orchestrator behaves as if the conversation just started. It re-asks the user what step we're on, re-checks DB state it should already remember, and frequently fails to "see" the completion event at all — the user has to say "isn't task X done?" before the orchestrator queries the DB and discovers the work was completed minutes ago. This repeats every spawn → completion cycle.

In a single 6-hour pipeline session I just ran, this happened at every single child completion (12+ cycles), and several times the orchestrator missed the completion event entirely on its own; the user explicitly had to ask "is wave X done?" before the parent re-discovered state from the DB.

The on-disk evidence: my orchestrator session jsonl is currently 100 KB / 81 lines for a 6-hour, ~30-tool-call conversation. The line count is suspicious for the wall-time and operation density — consistent with aggressive prefix discarding at every subagent completion event.

Root cause on 2026.6.5 (verified at dist source)

Two layers, both still unaddressed in the released dist:

Layer 1 — announce caller drops extraSystemPromptStatic

subagent-announce-delivery-D8I4cA_R.js (line lookups via the released dist) constructs directAgentParams for the in-process dispatchGatewayMethodInProcess("agent", …) call without extraSystemPromptStatic or extraSystemPrompt. Grep on the released dist:

$ grep -c "extraSystemPromptStatic" dist/subagent-announce-delivery-*.js
0

When the gateway agent handler forwards this, prepare sees extraSystemPromptStatic === undefined && extraSystemPrompt === "" and computes extraSystemPromptHash over empty content. Same orchestrator session, but the static prompt the binding was originally cached against (e.g., "You are in a WebChat direct conversation. ... NO_REPLY ...", ~219 chars) is absent on the announce-driven turn.

Layer 2 — comparator treats "absent" identically to "now empty"

2026.6 moved the per-turn comparator into the live-session fingerprint: claude-live-session-CSrEm4Vm.jsbuildClaudeLiveFingerprint(…) hashes systemPromptHash, extraSystemPromptHash, promptToolNamesHash, mcpConfigHash, skillsFingerprint, normalized env values, and argv. On mismatch, runClaudeLiveSessionTurn calls closeLiveSession(session, "restart") and spawns a fresh claude --resume <id>.

The released dist hashes 4 prompt-derived inputs (verified by grep: extraSystemPromptHash, promptToolNamesHash, skillsFingerprint, systemPromptHash all referenced inside buildClaudeLiveFingerprint).

The comparator does not distinguish "the caller did not assert a static prompt this turn" from "the static prompt is now legitimately empty." Layer 1 produces the first case; the comparator interprets it as the second and forks/restarts.

Net effect on 2026.6.5

  • Session binding survives (resume id stays stable, so claude --resume <id> does retrieve prior turns from disk).
  • The live process is recycled on every turn that comes through the announce path. Persistent-process / stream-input optimization never engages for orchestrator runs that include any subagent.
  • Conversation effective recency is lost — the resume mechanism delivers history via --resume <id> but the running orchestrator's working memory of the current turn (the in-flight tool call sequence, the freshly spawned children, the recent completion events) is interrupted by the process restart. The orchestrator behaves as if just resumed cold — repeatedly.

z1pp090's 2026-06-11 comment in #69118 documents the close/start pair firing back-to-back on turns ~2 s apart, ruling out idle-driven restart. My session shows the same signature.

Why the canonical thread isn't moving

Thread #69118 has at least 5 distinct triggers reported (groupIntro drift, subagent-announce delivery, dmScope:"main" channel switching, single-channel Telegram per-channel-peer, 2026.6 live-session fingerprint mismatch). ClawSweeper auto-stripped the original P1 label and added clawsweeper:needs-info based on a failed Codex review run, without engagement with the actual content. The last 3 substantive comments are users confirming "still reproducing on the next version" with no maintainer reply. The original comparator-tolerance proposal was never implemented; reports have been moved into this issue from at least 3 other issues (#81460, #83250, others), so the thread has become a duplicate sink with no progress.

This is not a complaint about the bot — it's a request for the issue to be re-scoped at the class level so a fix can land.

Proposed fix (two-part, minimal surgery)

Fix A — comparator symmetry (Layer 2, was Patch E in our local repro)

Make buildClaudeLiveFingerprint's prompt-derived hash comparison treat "absent / empty" as "no assertion, do not invalidate." One-line semantic change:

// pseudocode against the equivalent comparator point inside buildClaudeLiveFingerprint
- if (binding.extraSystemPromptHash !== currentExtraSystemPromptHash)
-   return { invalidatedReason: "system-prompt" };
+ const storedHash = normalizeOptionalString(binding?.extraSystemPromptHash);
+ const currentHash = normalizeOptionalString(currentExtraSystemPromptHash);
+ if (storedHash && currentHash && storedHash !== currentHash)
+   return { invalidatedReason: "system-prompt" };

Same pattern applies to promptToolNamesHash if it can legitimately be unset on a turn.

Rationale (also from the canonical thread): the hash field's semantic intent is "did the static system context change?" undefined/empty means "caller has nothing to assert this turn," not "the static context is now empty." The current code conflates these and falsely invalidates.

We applied Fix A locally on 2026.5.3 in May (single-line dist edit), Boss-verified the orchestrator retained context across announce within 30 minutes of patching. The patch was wiped by the 2026.6 upgrade. We re-verify it works each upgrade and re-apply.

Fix B — caller-side, thread static through announce (Layer 1)

subagent-announce-delivery-*.ts should populate extraSystemPromptStatic from session-bound directChatContext (or pass a sentinel that tells the gateway agent handler to compute the same static parts it would for a normal inbound turn).

This is the architecturally correct fix. Without it, Fix A papers over the symptom; with it, the comparator never sees the mismatch in the first place because the announce turn carries the same static prompt content as a user-typed turn.

Both fixes are non-breaking. The comparator change is the safer immediate landing; Fix B is the durable solution.

Acceptance criteria

  1. Orchestrator on claude-cli running a multi-agent pipeline retains turn-local working memory across subagent completion events (no process restart, no rollback to resume-cold).
  2. cli exec log shows reuse=reusable on announce-driven turns when binding is otherwise valid; no cli session reset: provider=claude-cli reason=system-prompt on every spawn cycle.
  3. Live process count stays stable across N consecutive turns with subagent spawns (currently one close … reason=restart per turn).
  4. No regression on the legitimate-invalidation case: changing the bound channel's static directChatContext (e.g. moving to a different provider's DM) does invalidate.

Relationship to existing reports

I'm happy to attach instrumented [cli-prompt-trace] output and the dist file/line numbers for the relevant function bodies if maintainers want them — say the word.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions