You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Think keeps an agent-tool child's starting messages (its pre-turn baseline) in memory only #2608
That map is in memory only. After an eviction or deploy it is empty, and two things read it:
_getAgentToolFinalText(runId) (the default getAgentToolSummary) returns the text of the first assistant
message not in the baseline. With an empty baseline that is the oldest assistant message in the facet.
_hasRecoveredAgentToolAssistantTurn(runId) (used by _reconcileStaleAgentToolChildRun) treats "any assistant
message not in the baseline" as proof the run produced a turn. Its comment says "A dedicated child facet
starts with no assistant messages, so a missing in-memory pre-turn snapshot is treated as empty."
That assumption is false for a child seeded with context before its run starts (a forked sub-agent: the parent
calls child.addMessages(parentContext) and then runAgentTool). After an eviction:
the summary becomes the forked context's first assistant reply (an old answer of the parent), and
a child whose turn was lost before it wrote anything is sealed completed (with that wrong summary) instead of error/interrupted, so the parent reports a result the child never produced.
A child without seeded context hits the other branch: the run is sealed error with
"Agent tool run was interrupted before the child could finish.", even when its own chat recovery would have
resumed it moments later (the reconcile runs on the parent's inspect, which can come before the child's recovery
has recorded an incident).
Evidence
Production, 2026-10-15 16:28 UTC: a detached worker (w-toolu_015JsF4y216TTBR4XpgyQbmK, "Restart of a
stopped worker …") ended error: Agent tool run was interrupted before the child could finish.; our agent then
had to pick the work up in a separate recovery turn. Nothing of the worker's own turn was resumed.
We override getAgentToolSummary because the default returned the parent's earlier reply for forked workers
after an eviction (our worker.ts: "Think's own summary is never used: it joins every line of the first
assistant message it finds, which after an eviction is the forked context's").
Repro notes
Parent Think runs runAgentTool(Child, { runId, input, detached: { onFinish } }); before that it seeds the
child with await (await this.subAgent(Child, runId)).addMessages([userMsg, assistantMsg("old answer")]).
While the child is inside its first model step (a slow scripted model), abort the child facet
(this.abortSubAgent(Child, runId) from the parent, or ctx.abort() in the child) and make the child's chat
recovery unable to resume (e.g. recovery disabled, or the model call failing).
Let the parent's backbone inspect the child (_cfDetachedReconcileTick).
Seeded child: the run is completed with summary "old answer". Unseeded child: error with
"Agent tool run was interrupted before the child could finish.".
What would help
Persist the baseline with the run (e.g. a pre_turn_message_ids_json column or the id of the run's input
message on cf_agent_tool_child_runs) and use it after restarts.
Or anchor on the run's own input message: everything after the message formatAgentToolInput() produced is
the run's turn.
Before sealing a stale run, give the child's own chat recovery a chance to classify the interrupted turn
(or report interrupted with a reason instead of error, so callers can tell "lost" from "failed").
Our workaround
We anchor the answer on the run's own task message id (task:<runId>), and treat Think's
"interrupted before the child could finish" error (and a no-progress give-up) as a run to carry on once from what
it did.
Activity