Skip to content

A DBOS persistent turn is a turn (CP-69, CP-124, CP-149) ​

This guide covers the release in which a DBOS persistent session reads each turn's outcome, instead of reading active until its receive loop idles out. It is for people who run persistent agents or persistent companions on DBOS.

Versions: @helix-agents/runtime-dbos takes a major. Every change below is a behaviour you can observe. Each one brings DBOS into line with the other runtimes and the documented companion contract. If your code relied on the old DBOS answers, it needs the changes below.

What changed ​

Each persistent turn now writes its terminal outcome to the session: completed, failed, or interrupted after a stop. The receive loop itself stays alive until its persistentIdleTimeoutMs, exactly as before. Previously the session stayed active until the loop idled out, which is 24 hours by default.

BeforeNow
A persistent session reads active until its loop exitsIt reads its last turn's outcome; the loop's liveness is recorded separately
A persistent root's handle and execute() result wait for the loop to exitThey read the last turn's outcome. A follow-up execute() still goes to the live loop
A blocking companion__spawnAgent returns after the child's idle timeoutIt returns when the child's turn ends, with that turn's outcome
A stopped companion turn is answered terminatedIt is answered interrupted, which is resumable
companion__waitForResult on a stopped turn times outIt answers { status: 'interrupted', message } at once
companion__sendMessage to a companion between turns may answer child_exitedIt runs as a new turn on the live loop. A companion whose loop has exited is continued on a restart workflow, as before
A blocking turn leaves the parent's ref runningThe ref records the turn's outcome, so getChildStatus and a re-consult by name read it
A re-spawn of a failed / terminated companion reuses its session; no turn runsIt runs fresh, on a new session (no history, no state) and a workflow of its own, after the previous loop has exited
abort() / interrupt() of a loop idling between turns leaves the session as isThe loop is stopped, its exit is recorded, and the session reads failed / interrupted, replacing its last turn's outcome

What to change ​

  1. Don't read a persistent session's status as "the loop is alive". A session that reads completed may still be served by a live loop: a follow-up execute() goes to that loop. If you used active to mean "still serving", stop. The runtime records the loop's exit itself (in the session's dbosLoopFinalized metadata) and routes on it; treat that field as internal.
  2. Expect a persistent root's handle to settle per turn. handle.result() and an execute() result now report the turn that just ended. Code that awaited a persistent root's result as "the conversation is over" must use its own end condition, or call abort() / interrupt().
  3. Read companion tool results per turn. A blocking spawn's result arrives at the turn's end. A stopped turn is interrupted, never terminated. Prompts, parsers or tests that expected terminated for a stop, or that relied on the long wait, need updating.
  4. terminateChild after a blocking turn completed is a no-op. The companion has already completed, so the answer is core's { terminated: false } and the ref keeps completed. Terminating a companion whose turn is still running is unchanged.
  5. A re-spawn of a failed or terminated companion starts from scratch. It sees only its new message and none of the old session's state. If its previous loop does not exit in time, the spawn answers a retryable not_spawned (child_busy); retry it.
  6. An idle-loop stop can now fail retryably. If neither the shutdown signal nor the cancel reaches the loop, abort() / interrupt() reject with a retryable transport_error and change nothing; retry the call. A turn that starts during the stop is stopped as a live run. In the rare case a second turn starts while that turn is being settled, the call rejects with a retryable state_concurrency_conflict.
  7. Custom forks only: the internal skipSessionStatusUpdate / keepSessionStatus options are removed.

Still open ​

A parent's stop does not yet reach a blocking companion's live turn on DBOS (CP-23's DBOS half). The companion's ref and session keep their previous outcome after the parent's interrupt().

Released under the MIT License.