A companion paused on client calls (CP-40)
This guide covers the release in which a persistent companion that pauses on a client-executed or approval-gated call is answered suspended instead of hanging its parent, read as completed (runtime-js) or recorded failed (CF Workflows), and in which the CF Durable Object refuses such a companion at spawn time. It is aimed at people who read companion tool results, switch over core's companion unions, call resolveChildName, or implement their own companion dispatch deps.
Versions:
@helix-agents/coretakes a major (it is at 1.x: a breaking interface change; read this guide).@helix-agents/runtime-cloudflaretakes a major (an observable behaviour change: a companion with client / approval tools is refused on the DO).@helix-agents/runtime-jstakes a minor (observable: CP-127, a paused companion is no longercompleted).@helix-agents/runtime-dbostakes a patch (it adapts toresolveChildName's new shape; its behaviour is unchanged).@helix-agents/ai-sdktakes a major through the peer cascade (it peer-depends onruntime-cloudflare); no change of its own.
No migration. Deploy client and server together; a CF Durable Object parent and its companion children must run the same version.
What changed
New union members
Any exhaustive switch over these core types needs the new members:
ChildTerminalResult,CompanionSpawnResultandCompanionWaitResultgain thesuspendedarm:{ name, status: 'suspended', pendingCalls, message }(ChildTerminalResult:{ status: 'suspended', pendingCalls }).pendingCallsisPendingCallSummary[]:{ toolCallId, toolName, kind: 'client' | 'approval' }, never the call's input. A paused companion is NOT terminal: its ref readspaused_awaiting_client(nocompletedAt, never marked delivered), so its later outcome still reaches the parent.CompanionNotSpawnedReason(COMPANION_NOT_SPAWNED_REASONS) gainschild_not_supported,child_interruptedandchild_awaiting_client(seven reasons).CompanionEntryOutcome.reasonstays the four entry reasons (COMPANION_ENTRY_FAILURE_REASONS).CompanionNotDeliveredReason(COMPANION_NOT_DELIVERED_REASONS) gainschild_awaiting_client(eight reasons).ErrorCodegainsframework_companion_not_supported(categoryframework, non-retryable, HTTP 500): a companion refused because its tree can call a client / approval tool nothing on the runtime can answer.
resolveChildName returns a union
// Before
const name: string = resolveChildName(providedName, agentType, refs); // threw for a live name
// After
const resolved = resolveChildName(providedName, agentType, refs, { pendingCalls });
if (resolved.kind === 'refused') {
// resolved.outcome: { started: false, reason: 'child_busy' | 'child_interrupted' | 'child_awaiting_client',
// errorDetail }; answer it, e.g. companionSpawnResultFromEntry(resolved.name, resolved.outcome)
} else {
use(resolved.name);
}Only a name over 128 characters still throws.
Answers that used to be errors
- A same-name
companion__spawnAgentof a live companion answersnot_spawned(child_busy,child_interrupted,child_awaiting_client), a successful tool result, instead of thealready running/is interruptederror (DBOS still answers the error). companion__sendMessageto a child paused on calls answers{ delivered: false, reason: 'child_awaiting_client', errorDetail }instead of theunresolved client-tool callserror (DBOS still answers the error).
The CF Durable Object refuses a companion with client / approval tools
CompanionToolDispatchDeps.refuseChild? (core) lets a runtime refuse a companion before anything is written; only the DO supplies it, with core companionNotSupportedDetail(agent) (a walk of the agent's tools, its sub-agent tools' agents and its persistentAgents, at any depth; remote tools are opaque). A refused spawn answers { status: 'not_spawned', reason: 'child_not_supported', errorDetail } with the code framework_companion_not_supported. The child DO refuses the same agent at its own consult entry. Move a client / approval tool out of a DO companion (the parent can call it), or run the companion on runtime-js or CF Workflows, where a client answers its calls through the root.
pendingCalls order and a stale pause
On runtime-js and CF Workflows pendingCalls follows the model's call order (the child's log); on the CF DO it follows the child's /status listing (CP-157 RemoteStatusResponse.pendingCalls: oldest first, then by call id; core pendingCallSummariesFromListing). A pause whose calls were all answered meanwhile is not reported as a suspension: the wait goes on and sendMessage / a same-name spawn answer child_busy.
RemoteStatusResponse.stopping
The sub-agent /status carries stopping?: boolean (a stop is recorded for the current run and has not ended it yet), and committedOutcome: 'suspension' for a committed suspension.
Reference
- A companion paused on client or approval calls
- Core reference: the
suspendedarm - Design:
docs/superpowers/specs/2026-10-17-do-companion-mr-c-design.md§2 (h).