SendMessage to a completed agent resumes it and re-executes side-effectful plans without fresh approval

Status Open
Maintainer reply None cached
Activity 5 comments · opened Jul 15, 2026

Summary

When SendMessage targets an agent whose task has completed, the platform responds "Agent had no active task; resumed from transcript in the background with your message" and spawns a background copy resumed from the agent's transcript. In practice the resumed copy re-enters its prior plan and acts on it, treating the resume itself as a go-ahead. When that plan includes side effects, the results range from duplicated work to actions the user never approved.

Observed incidents (one orchestration session, multiple subagents coordinating test recordings)

  1. An informational reply ("you are clear to proceed") sent to a recorder agent between its tool rounds forked a copy that re-ran the same seeded test recording concurrently with the original instance, writing to the same output paths.
  2. A second informational message to a different recorder forked a copy that was stopped (TaskStop) just before it duplicated the recording again.
  3. The sharp case: an agent's final message before completing was a proposal ("want me to also do X and Y?") where X and Y were changes the user had explicitly deferred. A later resume of that agent executed both, including local git commits, with no fresh instruction. Silence was effectively treated as approval.

Also observed: agents spawned by the Workflow tool cannot be reached by SendMessage at all while running. A send always creates a resumed background copy rather than delivering to the live instance, so any answer to a workflow agent's question produces a duplicate actor.

Expected

Any of:

  • (a) mailbox-only delivery that does not resume a completed agent,
  • (b) a resume mode where a resumed agent must receive an explicit new instruction before taking non-read-only actions,
  • (c) at minimum, documented resume-on-send semantics so orchestrators know that replies to completed agents fork executing copies.

Repro sketch

  1. Spawn a background agent whose task ends with a proposal for further action ("shall I also do X?").
  2. Let it complete.
  3. SendMessage its agent id with any informational text.
  4. Observe the resumed copy execute the proposal without explicit approval.

Related (distinct) issues

#76602, #76386, #67345, #73095, #76681, #77146 cover mechanical breakage around SendMessage resumes (rendering, model overrides, task UI, notification delivery, tool rehydration). None cover the behavioral hazard above: resumed agents re-executing side-effectful plans without fresh approval, or Workflow-agent unaddressability producing duplicate actors.

Claude Code CLI, agent-teams/subagent messaging, observed 2026-07-14. Fuller transcripts available on request.

View original on GitHub ↗

5 Comments

0xbrainkid · 3 months ago

This is a strong case for treating a resumed agent as a new execution principal unless the resume carries a fresh, explicit intent record. The dangerous part is not only the duplicate actor; it is that the resumed copy inherits stale plan intent and current tool authority at the same time.

A practical guardrail would be to split the transcript state into at least three fields before any non-read-only action after resume:

  • prior plan: what the completed agent had proposed or queued
  • resume message: what the user actually said after completion
  • executable intent: the newly authorized action set derived from the resume message

If executable intent is empty or ambiguous, writes should fail closed and ask for confirmation. In particular, "you are clear to proceed" should only authorize the live task it was addressed to; it should not rehydrate a completed agent and convert a prior proposal into approval.

The audit trail should also name the actor lineage: original agent id, resumed-copy id, resume trigger message id, and whether a fresh write authorization was collected. That gives orchestrators a way to detect duplicate actors and distinguish "continued the live job" from "forked a completed transcript."

This maps to the AgentFolio/SATP pattern we use for agent work: stable identity is useful, but every side-effectful step still needs a current intent receipt. Identity continuity without fresh intent is exactly how stale authority leaks across sessions.

discorev · 2 months ago

This issue comes up a lot for me now Agent has an enum of allowed models meaning whenever I want Claude to use a GPT model via my gateway it has to use a workflow. The issue occurs in two directions: the main actor sends a message to an agent within the workflow, creating a fork or a workflow agent spawns a sub-agent that then uses SendMessage to send a response back. The result is the same in both cases - a fork of whichever agent called SendMessage with the prompt of the target agent. This then starts to cause more messages being sent as work starts to get duplicated and the agent detects another making changes and sends a message back to the main orchestration agent, who then uses SendMessage to attempt to set boundaries for the agents, spawning even more!

There are two ways this could be fixed: (1) in SendMessage check if the the agentId is a workflow agent currently running that does not have a pendingMessages queue and reject the send or (2) add pendingMessages queue for workflow agents so they can receive the messages allowing the communication to work correctly.

It would be really nice to see a fix for this, as it causes a lot of token burn, particularly if you are using fable as your main conversation agent and then it gets forked trying to message a cheaper agent like Sonnet, Opus or gpt.

saif-raja · 1 month ago

Independent verification at v2.1.252 (2026-09-01): we can confirm the "Also observed" workflow-agent behavior in the issue body, with one clarification that we believe reframes the whole class.

The root cause is ID-invocation, not messaging per se. There are two addressing modes with very different behavior:

  • By name: workflow-spawned agents never appear in the ListAgents roster (before, during, or after their workflow), and a name-addressed SendMessage is refused outright with No agent named 'X' is reachable. This path is guarded and cannot fork anything.
  • By raw agentId: a send to the agent's internal id (the 17-hex a... string) while its workflow is running returns Resuming agent ... and spawns a second execution of the same transcript outside the workflow. In our heartbeat test the log shows beats 7 through 14 each written twice by two interleaved streams, two MISSION-COMPLETE markers, and the agent's meta.json rewritten from "agentType":"workflow-subagent" to "agentType":"general-purpose" at the moment of the send. Critically, the message reached only the clone: the workflow-owned instance completed normally and reported that no message ever arrived. So messaging a live workflow agent is not merely risky, it is impossible; a send can only manufacture a duplicate actor.

Why this bites in practice: workflow agents can message the coordinator mid-run, and that inbound arrives from the bare agentId. Replying to it (the most natural act there is) is an ID-invocation, and it forks. That is exactly how we first hit this in July. The operator-side fix is a single rule: an id exposed by an inner return or an inbound reply is never used for invocation; agents are addressed only by names on the roster. Since adopting that rule (plus "coordinate workflow agents through their original brief and the journal only"), the failure mode has not recurred for us.

Post-completion the seal is total: name sends stay refused, and even ID resumes are refused with No transcript found for agent ID (an error whose own text already advises replying through the session rather than the raw id). Standalone named teammates are unaffected throughout: mid-run sends queue and deliver at the turn boundary, and post-completion sends resume per the documented contract.

Proposal: extend the post-completion refusal (or the standalone queue-to-turn-boundary behavior) to ID-addressed sends that resolve to a live workflow-owned agent, and consider giving a workflow agent's outbound messages a non-invokable reply address. Either closes the last route into the fork.

Attached is a verification dossier with step-by-step recipes for every claim above (about 10 minutes on throwaway substrate), the full lifecycle addressability matrix, and an interim operating protocol. It is written to be handed directly to coding agents that have hit this problem, so they can re-verify against their own version and derive local mitigations (CLAUDE.md rules and similar). Human readers welcome too.

sendmessage-workflow-fork-verification-dossier.md

discorev · 1 month ago

@saif-raja I'm not sure your claude has dug into the claude code binary, it's reporting based on observation, not code fact.

2.1.257 has not introduced a pendingMessages queue for workflow agents.

  • Normal local agents are registered with pendingMessages: [].
  • Workflow workers are created only as an agent context and passed directly to mT(...).
  • They are not registered in taskRegistry.
  • Their workflow-runner module contains zero pendingMessages, inbox, message-queue, or registry-registration paths.
  • SendMessage still considers an agent live only when its ID resolves to a registered task. Otherwise it takes the evicted/transcript-resume path—the path that can create the competing fork.

This is unchanged since the original report 2.1.257 workflow runner: pendingMessages=0, registerCalls=0

Workflow-based subagents still do not have a live pendingMessages inbox in 2.1.257, and SendMessage remains unsafe for them. The fact that Agent now has a validating enum for claude models means when working with a gateway to combine model providers you are far more likely to run into this issue - which is likely why it's being ignored.

saif-raja · 1 month ago

@discorev You're right, and thanks for the correction.
My Claude went and read the 2.1.258 bundle to check it at the code level rather than by observation.

Confirmed, and it's the type/registration architecture, not a guard: live delivery drains an agent's pendingMessages only when fr(r) holds, and fr is a local_agent-only type guard (e.type==="local_agent"). Workflow workers are type==="local_workflow", so they never take the inbox path - SendMessage falls through to the transcript-resume path, which is the fork. Unchanged in 2.1.258. My earlier "this path is guarded/closed" wording was observation-level and wrong; I withdraw it.

Two small things to add, not corrections to you:

  1. The fork window is bounded by evictAfter. A completed task is stamped evictAfter:Date.now()+WE, and once it passes the eviction reducer deletes both the task and its transcript. So the resume-fork exposure exists only while the transcript is still resumable; after eviction an ID-addressed send fails cleanly with No transcript found for agent ID instead of forking. "Unsafe" holds for the live/resumable window; post-eviction it's inert.
  1. Operator-actionable trigger: a workflow worker's mid-run message arrives from its bare agentId, so replying to it is itself an id-invocation that hits the resume-fork path. The safe rule is to route coordination through the worker's original brief or the run journal, never a reply.

Exact verbatim excerpts (the fr guard, the drain gate, the local_workflow eviction, the No transcript found throw) + our repro are in the attached file, if useful for anyone triaging this via their own agents.

workflow-sendmessage-code-verification.md