Skip to content

Remote child chunks are written once; a write reports its result (RM-86, RM-89) ​

A parent agent proxies a remote child's stream chunks into its own stream. Before this release, two faults could change what the parent's stream held:

  • Duplicates (RM-86). A forwarder that re-attaches (a crash, a Durable Object eviction, a Temporal activity or CF Workflows / DBOS step retry) re-reads the child's stream from the progress it last persisted, which can lag what it already wrote, and a zombie forwarder can keep running beside its replacement. The same child chunk could land in the parent stream twice. Only the ai-sdk RemotePatchFilter dropped repeats, and only for data-state-patch, so re-proxied text and tool chunks rendered twice.
  • Loss (RM-89). Every forwarder recorded a chunk as forwarded BEFORE its write. A write that threw (a store outage) left that record ahead of the stream: runtime-js and the CF Durable Object retried from it in-process, core's dispatch (and Temporal through it) persisted it on the drop path, and DBOS recorded it on the failed ref, so the chunk was never forwarded again.

Now every forwarder writes first and then records the chunk, and every stream manager writes a proxied chunk at most once per parent run. It is a minor release of @helix-agents/core, @helix-agents/store-memory, @helix-agents/store-redis, @helix-agents/store-cloudflare, @helix-agents/runtime-js, @helix-agents/runtime-cloudflare, @helix-agents/runtime-temporal and @helix-agents/runtime-dbos.

StreamWriter.write resolves with what it did ​

typescript
// Before
write(chunk: StreamChunk, options?: StreamWriteOptions): Promise<void>;

// After
write(chunk: StreamChunk, options?: StreamWriteOptions): Promise<StreamWriteResult | void>;

type StreamWriteResult =
  | { written: true }
  | { written: false; reason: 'fenced' | 'guest_inactive' | 'duplicate_remote_chunk' };

Every built-in stream manager (memory, Redis, the Cloudflare DO's DOStreamManager, store-cloudflare's stream Durable Object and its binding) now resolves every write with its result. A deliberate drop is still never a throw: fenced (CP-85, a superseded run's chunk), guest_inactive (CP-129, a child's chunk to an ended / failed / paused parent stream) and duplicate_remote_chunk (RM-86). Code that ignores the result is unaffected.

  • If you wrap a StreamWriter, return the inner write's result (return inner.write(chunk, options)). A wrapper that resolves with nothing reports nothing.
  • If you typed a promise of a write as Promise<void>, widen it to Promise<StreamWriteResult | void> (or map it with .then(() => undefined)).

remoteSource.childRunId ​

RemoteSource gains an optional childRunId: the runId the child stamped on the chunk, which every built-in forwarder records before the parent's own writer can fill a missing runId with the parent's run. It is the parent stream's dedup key. If you forward a remote child's chunks yourself (a bring-your-own loop), record it: remoteSource: { childSessionId, childSequence, streamId, ...(chunk.runId ? { childRunId: chunk.runId } : {}) }, and advance your own cursor only after the write resolved. executeRemoteSubAgentDispatch does both.

Custom StreamManager implementations ​

The store contract (@helix-agents/core/testing, streamManagerContractTests) adds the RD.* cases. To pass them:

  • Keep a high-water per remote child run. Key it with core remoteChunkDedup(chunk) (kind: 'keyed' gives key, from (remoteSource.childSessionId, remoteSource.childRunId), and childSequence).
  • Inside the same atomic read that decides the write, pass the stored high-water for that key to decideStreamWrite as remoteHighWater. Map the decision to the result with streamWriteResultOf.
  • Raise the high-water to childSequence only AFTER the chunk is stored, in the same atomic step.
  • Clear it in resetStream, and in claimStream when the owner run CHANGES. A re-claim by the current owner keeps it.
  • A proxied chunk with no remoteSource.childRunId (kind: 'unstamped') is appended without the dedup. Report it with createUnstampedRemoteChunkWarn(logger): one warn per stream, core.stream.remote_chunk_unstamped.

Storage ​

ManagerNew state
store-memoryan in-memory map per stream
store-redisa hash stream:{id}:remote-hw (same TTL as the stream's keys)
runtime-cloudflare (DO)a stream_remote_high_water table in the DO's SQLite, created on demand
store-cloudflare (stream DO)a remote_high_water table in the stream DO's SQLite, created on demand

No migration is needed. Deploy store-cloudflare's binding and its stream Durable Object together: the binding now parses the /write answer, and an answer of an unknown shape is a typed transport_error.

Behaviour changes ​

WhereBeforeAfter
Any parent stream, a remote child re-forwarded in one runevery re-forwarded chunk written againwritten once; a repeat resolves duplicate_remote_chunk
A parent's next run re-attaching an owed childre-forwarded chunks writtenunchanged: a new run clears the high-water
A parent-stream write that throws mid-forward (every runtime)the chunk could be skipped for good (RM-89)the chunk is forwarded again by the retry
A guest chunk dropped from an inactive streamthe write resolved undefined{ written: false, reason: 'guest_inactive' }

Released under the MIT License.