Skip to content

Upgrade: the post-commit child phase (CP-106 MR A) ​

A companion tool call now runs only after the commit that holds the call. Before, a step's companion__spawnAgent started its child inside the step, so a step a stop refused could leave a child running with a sub-session ref and no committed call (CP-106). Now the call is announced with the step, the step commits, and only then does the call write its ref and start its child.

This is a major release of @helix-agents/core, @helix-agents/runtime-js and @helix-agents/runtime-cloudflare (all at 1.0 or later). It is a breaking change for them:

  • a custom SessionStateStore must implement the fenced ref write below;
  • a step's companion calls now run after its commit, so the order of its stream chunks changes.

Every store package (@helix-agents/store-memory, @helix-agents/store-redis, @helix-agents/store-postgres, @helix-agents/store-cloudflare) takes a minor: they are 0.x, where a breaking change is a minor (release process).

Design: 2026-10-14-cp106-post-commit-child-phase-design.md (read its "As built (MR A)" section first).

Who is affected ​

  • Apps on runtime-js or the CF Durable Object that use persistentAgents (the companion__* tools).
  • Custom SessionStateStore implementations.
  • Clients or tests that read the order of a step's stream chunks.

CF Workflows, Temporal and DBOS are unchanged in this release. Remote and ephemeral sub-agent calls are unchanged too (they move into the phase in MR B).

Deploying ​

  • Run the store migrations (below) before the new runtimes: the fenced ref write stores the parent run's id.
  • Deploy with or after the D-2 request-key release. A child start in the phase sends a request key (X-Helix-Request-Key on the CF DO's child /start and /resume).

1. What changed for companion calls ​

  • Order. A step's calls are still announced in call order (tool_start). A companion call's body runs after the step commit (or after the suspension commit, when the step also suspends), one call at a time in call order. A blocking spawn or waitForResult lets the step's next companion call start as soon as its own child started. Its result is appended in its own append commit after the step's.
  • Stream order. A companion call's tool_end now follows the step's step_committed.
  • A stop. A stop that refuses the step commit leaves no ref and no child. A stop recorded before a call writes its ref ends the phase: the call's result row is the stop pairing (helixSynthetic.reason: 'run_stopped', as for any call a stop leaves unfinished). A stop recorded while a child was starting stops that child, by its consult, and ends the phase the same way. On the CF DO, a child stop that does not land is owed to the run's stop cascade and retried.
  • finishWith in the same step. It runs after the step's companion results, in the same iteration.
  • Recovery. A CF DO run evicted inside the phase re-enters it on its wake, under the same run. A resume() of a run that suspended re-enters the previous run's unfinished companion calls. Either way the replay check (a committed result row) and the keyed child start make each call run once: one child run, one result row.
  • A call's start is keyed on the run that committed it and its call id (core childStartRequestKey / childResumeRequestKey). On the CF DO, a key whose run already finished counts as a start of that run.

2. Custom stores ​

addSubSessionRefs(sessionId, refs, opts?) takes AddSubSessionRefsOptions:

ts
interface AddSubSessionRefsOptions {
  /** Refuse with `RunSupersededError` unless this run is the session's current live run. */
  fenceRunId?: string;
  /** With `fenceRunId`: refuse with `CommitStoppedError` when that run has a stop record. */
  refuseIfStopped?: boolean;
}
  • Check the fence and the stop in the same atomic step as the insert. A refusal writes nothing.
  • refuseIfStopped without fenceRunId is a validation_error (core assertValidAddSubSessionRefsOptions).
  • Stamp each inserted ref's SubSessionRef.parentRunId with fenceRunId (else the session's current run).
  • A ref reopen (a respawn reset, a continue, a resumed companion in the run-start commit) re-stamps parentRunId with the reopening run (core reopenParentRunStamp).
  • A re-add of an existing subSessionId stays the insert-if-absent no-op it was.

Run the store contract suites from @helix-agents/core/testing: subSessionRefOperationTests ("the fenced add and the reopen stamp, CP-106") and runStartOperationTests ("commit.start-companion-ref-stamps-parent-run").

3. Migrations ​

StoreMigrationChange
PostgresV24__agents_sub_session_refs.parent_run_id TEXT DEFAULT NULL
D1V28sub_session_refs.parent_run_id TEXT
DO-SQLitenonethe column exists already
Redisnonea parentRunId hash field
Memorynone

No backfill: a ref written before the upgrade has no parentRunId.

Released under the MIT License.