Skip to content

bug(supervisor): pi.sendMessage status injection during in-flight tool call orphans tool_result → Anthropic 400 wedges interactive session #621

Description

@HenryLach

Summary

The supervisor injects custom_message entries via pi.sendMessage(...) on batch
phase transitions without checking whether the interactive agent has a tool call
in flight
. When a batch transition fires between an assistant tool_use being
appended and its tool_result being appended, the injected custom entry is chained
into the session tree between the tool_use and the tool_result. Pi renders
custom_message entries as role:"user" text, so on the next request the tool_result
no longer immediately follows its tool_use, and the Anthropic API hard-rejects the
whole conversation:

Error: 400 invalid_request_error
messages.576.content.3: unexpected `tool_use_id` found in `tool_result` blocks:
toolu_014ge1vbpMPxpoeFCvjmmdq9. Each `tool_result` block must have a corresponding
`tool_use` block in the previous message.

Once this happens the interactive session is wedged: every retry re-sends the
same broken ordering and 400s again. Recovery requires hand-editing the session
.jsonl (re-chaining parentId links) — see "Workaround" below.

  • taskplane: 0.30.4
  • pi (@earendil-works/pi-coding-agent): 0.84.2
  • Platform: Windows (git-bash), Anthropic provider
  • Observed twice in the same long-running session on distinct tool calls.

Root cause

Pi builds the LLM request by walking the parentId chain from the current leaf back
to root (buildSessionPath), applying the latest compaction (buildContextEntries),
then converting each entry to an LLM message. In convertToLlm, a custom message
becomes { role: "user", content: [...] }
(@earendil-works/pi-coding-agent/dist/core/messages.js).

pi.sendMessage() appends the custom entry as a child of the current leaf. If the
interactive agent has just emitted an assistant message containing a toolCall
(tool_use) but the corresponding toolResult has not yet been appended (tool still
executing), the current leaf is that assistant message. The injected custom entry
therefore becomes:

assistant(tool_use X) → custom_message(supervisor-…) → [custom_message …] → toolResult(X)

and after convertToLlm that is:

assistant[…, tool_use X] → user[text] → user[text] → user[tool_result X]

The tool_result block's previous message is a user text block, not the assistant
with tool_use X → Anthropic 400.

There is currently no guard anywhere in the supervisor for "is a tool call in
flight / is the agent mid-turn" before calling pi.sendMessage. (The inFlight*
symbols in resume.ts are about batch segments, unrelated to the interactive turn.)

Injection sites (all fire on batch end / phase transition, triggerTurn: false)

  • extensions/taskplane/extension.ts ~2453 and ~2873 — customType: "supervisor-integration-skipped"
  • extensions/taskplane/supervisor.ts ~2034 (presentBatchSummary via safeSendMessageFromTimer) — customType: "supervisor-batch-summary"
  • customType: "supervisor-routing-transition" / "supervisor-no-tasks" (routing/idle transition banners)

In the captured session, the interstitials that broke the pair were
supervisor-integration-skipped + supervisor-batch-summary (occurrence #1) and
two supervisor banners (occurrence #2) — i.e. this reproduces whenever any of these
fire during an in-flight tool call.

Reproduction

  1. In an interactive Pi session with the taskplane supervisor active, start a batch
    whose phase will end/transition (e.g. a batch that ends in a non-completed phase
    so supervisor-integration-skipped fires).
  2. Have the interactive agent issue a tool call whose execution overlaps the batch's
    phase-transition callback (an orch_status/orch_* tool call is the natural trigger;
    any tool that is still running when the transition fires works).
  3. The supervisor injects its status custom_message between the assistant tool_use
    and the pending toolResult.
  4. The next request 400s with unexpected tool_use_id … in tool_result blocks. All
    retries keep failing.

Evidence (from a real session)

Session 019f89ff-… (penster). Two independent occurrences:

# error index orphaned tool_use_id interstitials chained between tool_use and tool_result
1 messages.262 toolu_01LPpSyy6nJEn1wM7D5Mhvdt supervisor-integration-skipped, supervisor-batch-summary
2 messages.576 toolu_014ge1vbpMPxpoeFCvjmmdq9 2× supervisor banner customs

The injected customs carried timestamps ~1s after the assistant tool_use and before
the toolResult, confirming the in-flight race.

Impact

Proposed fix (supervisor side — preferred)

Do not inject a custom_message while a tool call is in flight. Options, best first:

  1. Defer to a turn boundary. Queue supervisor status messages and flush them only
    when the interactive agent is idle (no assistant tool_use awaiting its
    toolResult). If pi exposes a turn/idle signal or an "after tool results appended"
    hook, gate sendMessage on it; otherwise poll the session tail for an unmatched
    trailing tool_use before sending.
  2. Never append between tool_use and tool_result. Before sendMessage, inspect the
    current leaf/tail: if the leaf is an assistant message with a toolCall that has no
    matching toolResult yet, hold the message until the result lands.

Defense-in-depth (pi side — worth a companion issue upstream)

Pi's request builder (buildContextEntries / provider mapping) could hoist a
toolResult to immediately follow its tool_use
, or relocate/skip non-tool
interstitial messages that fall between a tool_use and its toolResult, so a
mis-timed injection cannot produce an invalid request.

Workaround (for anyone currently wedged)

Re-chain the parentId links so the toolResult directly follows its assistant
tool_use, moving the injected custom_message entries to after the result. A
standalone healer that reproduces Pi's context-building, finds every such violation on
the active path, and repairs it idempotently (with a backup) is available; run with
--check to detect and without to heal. This restored both occurrences above with
zero message loss.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions