Skip to content

A lost remote sub-agent settles typed (S-C) ​

A parent that awaits a remote sub-agent (a createRemoteSubAgentTool call, or on the Cloudflare Durable Object any sub-agent tool, which the DO runs in a sibling DO) used to wait forever when it could not read the child's status on a resume or a recovery. That was the 2026-10-17 writer-chat incident's third gap: the child's state read as missing, the parent kept awaiting it, and a new message sent meanwhile could be dropped.

Now the parent settles a child it cannot find with a typed failure, and an entry that carries a new message never puts that message between a call and its result. This is a minor release of @helix-agents/core, @helix-agents/runtime-js and @helix-agents/runtime-cloudflare, with two behaviour changes (below). CF Workflows applies the same core rule (RM-80).

A child the parent cannot find settles framework_subagent_lost ​

On a resume or a recovery, reconcile reads each running remote child's status (transport.getStatus). When that read fails, the child is settled failed with a typed tool result (metadata.helixErrorDetail, new core error code framework_subagent_lost, category framework, non-retryable) when:

causeWhen
not_foundthe child's producer does not know the session (RemoteAgentNotFoundError, a 404)
refused:<code>the producer refused the status read typed and non-retryable (<code> is its error code)
status_unreachablethe status stayed unreadable past the child's own timeoutMs plus one minute of grace

Any other failure is transient: it is logged at warn, and the next reconcile retries it. The model sees a failed tool result for the call and the run goes on. If you branch on tool-result error codes, add framework_subagent_lost.

Reconcile now reads the session's live sub-session refs. It used the resumed state's refs, which a resume from a checkpoint rebuilds from that checkpoint, so a child dispatched after the checkpoint was never reconciled. A remote ref is identified by ref.location === 'remote' (new core export isRemoteSubSessionRef(ref)), not by ref.remote (the child's stream identity, recorded only once the child answers its /start), so a child whose /start never answered is reconciled too.

An entry with a new message settles its remote children first ​

resume({ mode: 'with_message' }), a retry() with a message and a new turn (execute(), a chat send, the CF DO's /start) first settle each remote child whose call is still unpaired at the end of the transcript:

  • a child that finished (completed / failed) has its real result committed before the message;
  • a child that is lost (above) has its typed framework_subagent_lost result committed before the message;
  • a child that is still running:
    • on a resume / retry with a message: behaviour change, the entry is refused busy (AgentAlreadyRunningError, state_already_running) and writes nothing. Hold the message and send it again once the child has finished. A chat client handles it like any refused send (409 state_already_running).
    • on a new turn over a parent whose run ended: behaviour change, the child is stopped (a best-effort transport.interrupt(), its ref set failed) and its call keeps the continuation heal's not-executed (unpaired_on_continuation) result. Finish the child, or resume the parent, before a new turn if you need its output.
    • on an entry with no new message (a continue resume, a retry that re-appends nothing): nothing changes, and the run re-attaches to the child.

Cloudflare Durable Object ​

  • Typed 404 from the status route. A DO with no session answers /subagent/:agentType/status with the typed 404 NOT_FOUND protocol envelope (it was plain text), so DOStubTransport.getStatus throws RemoteAgentNotFoundError.
  • DO-SQLite migration 19 adds the nullable sub_session_refs.remote_json column (SubSessionRef.remote). It is read through a schema: a malformed row is a typed InvalidStoredStateError. The DO used to drop this field, so a recovered parent re-sent its child's /start and re-read the child's stream from 0; it now re-attaches from the stored lastSequence. The migration applies on the DO's first access; nothing to run.
  • A DOStateStore.loadState of a session id that is not the DO's own logs "a foreign session id was read from this DO's store" with both ids.

See Cloudflare Durable Objects › A lost remote sub-agent settles typed and Remote agents.

Released under the MIT License.