Skip to content

runtime: sandbox run-loop and providerAsSandboxClient.prompt retain every cumulative part frame #1212

Description

@drewstone

What happens

Two Runtime paths still keep every streamed frame of a turn in memory.
A harness streams text and reasoning parts cumulatively: each message.part.updated frame restates the part's whole text so far.
Retained memory then grows with frames times text length, and settlement hashes and stores the whole array.
#1211 measured this on the provider executor: 27,144 frames of one pi reasoning part exhausted a 4 GB supervisor heap.
The fix for #1211 changes the provider executor archive only.

  • src/runtime/run-loop.ts pushes every SandboxEvent into slot.events, and output.parse(events) receives the whole array.
    The supervise Sandbox executor parses it with sandboxLeafOutputFromEvents, which stores the array as SandboxLeafOut.events.
    A long pi turn through a Sandbox SDK client therefore grows the same way.
  • providerAsSandboxClient(...).prompt in src/runtime/environment-provider.ts pushes every event, but it reads only the last result text.

Why the provider fix does not transfer directly

OutputAdapter.parse(events) is public, and at least one parser in this repository concatenates each text frame's delta: coderOutputAdapter calls collectAssistantText in src/mcp/detached-coder.ts.
If slot.events left superseded frames out, that parser would return only the last delta of each part, with no error.
The run-loop path needs a decision first: fold the deltas into the kept frame, move delta readers to part.text, or bound the archive another way.

prompt() needs no archive; it can track the last result text while it streams.

Related finding on the provider path

textFromEnvironmentEvent returns normalized.delta for every message.part.updated frame, including reasoning parts.
When no event carries result text, ProviderLeafOut.content falls back to that concatenation, so hidden reasoning becomes the settled answer.

How to reproduce

Drive the path with a fake Sandbox client whose stream yields N message.part.updated frames for one reasoning part, each frame produced by JSON.parse of its wire text so every frame owns a flat copy of the text.
On the provider executor before the #1211 fix, 27,144 frames growing to 64 KB raised the post-GC heap from 75 MB to 904 MB during the stream, and settlement aborted at a 1,536 MB heap cap inside contentAddress (Array.map then Array.join).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions