Skip to content

fix(workbench): bound producer / completed-envelope render history end to end - #715

Merged
ScriptedAlchemy merged 9 commits into
mainfrom
cursor/bound-invocation-render-history-02ec
Sep 6, 2026
Merged

fix(workbench): bound producer / completed-envelope render history end to end#715
ScriptedAlchemy merged 9 commits into
mainfrom
cursor/bound-invocation-render-history-02ec

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes #681. Survivor of the duplicate pair #699 / #715 (#699 closed; its unique tests are ported here).

What changed

One retention policy now governs every retained copy of a route invocation's render stream (packages/agent-bundle/src/dev/routes/route-invocation-render-history.ts):

  • Bounds: newest 256 events and at most 2 MiB serialized (UTF-8 of the JSON event). The newest event and the newest document-bearing event (shell / replace / complete) are pinned, so a reader always folds to a coherent latest document after eviction; those two alone may carry a window past 2 MiB (≤ 2 MiB + 2 KiB for runtime-produced streams, whose events are capped at 1 MiB + 1 KiB).
  • Applied identically to: the service's live replay buffer, the completed / cancelled / failed envelopes (events, document), GET /api/routes/invocations/<id>, the terminal final stream message, and the Workbench live window (reduceInvocationState), which imports the same pure retainRenderEvent through contracts/invocations.ts. The reducer stays pure: the window is an immutable value and each push returns a new one.
  • Producer / IPC: RouteInvocationChildResult no longer carries events; the child publishes each event as a render IPC message and awaits the send, so the channel paces a fast producer. renderCompiled keeps only the complete document. Nothing can rehydrate what the live buffer evicted.
  • Truncation indication: envelopes whose events were evicted carry retention: { producedEvents, evictedEvents, evictedBytes, retainedBytes }; the stream still sends truncated once. document, result, outcome, and correlationId are never truncated (the runtime's maxDocumentBytes bounds the document before it reaches the service). Cancelled and failed runs keep the retained window and the latest document it reached. The Raw tab shows the account; the browser schema rejects an account that does not match the events beside it.
  • Compiled-route progress: streamFromWorker dropped the worker's progress messages although the dispatcher was ready to turn them into progress render events; they are now forwarded the way the generated CLI session does. Without this no compiled route could produce more than a handful of render events, so the acceptance criterion could not be met.
  • Stream queue: the invocation SSE queue derives its limits from the window (8 × 2 MiB, 2 × 256 records) so a backpressured socket can replay a whole retained record; the previous 256 KiB / 128-record queue destroyed the connection on replay of a heavy window.

Tests

  • route-invocation-service.test.ts: byte-bound eviction with 300 KiB replace snapshots + pinned latest document; final envelope / read() / stream replay parity over 1,001 events; cancel after the shell was evicted (601 events); child failure after eviction; a real forked child (unit-render) rendering 400 progress reports plus five 150 KiB Suspense chunks, asserting bounded events, retainedBytes ≤ 2 MiB, and identical replay.
  • route-invocation-dev-server.test.ts: a compiled tool reporting 400 × 16 KiB progress ticks (6.4 MiB) through the foreground server; POST envelope, GET read, stream replay (truncated first, same events, same final), and list summaries all agree.
  • invocation-model.test.ts, invocation-client.test.ts, route-workspace.test.ts: pinned document across 400 progress events, byte-bound live window, strict decoding of retention.
  • Ported from fix(dev): bound route-invocation render history by count and bytes end to end #699: route-invocation-dev-server.test.ts — a compiled CLI route (cli:flood, 300 Suspense boundaries settling one per macrotask) through the foreground server: byte-bound replace snapshots, GET/replay parity, mid-run reconnect after the shell was evicted, and cancel after eviction with the pinned latest document; new route-invocation-retention.test.ts — pinned newest/document entries survive oversized events, history-ring eviction drops the replay with the record, and a full count-bounded replay over RouteInvocationRoutes through a real HTTP socket.
  • runtime-backend.test.ts: a 301-event runtime run document folds to the same window with retention.

Not covered here

  • Cancel-after-eviction through a compiled MCP child: the generated Flight worker buffers the whole response until completion (Stream generated MCP/event Flight bytes before render completion #686 / fix: stream generated MCP/event Flight bytes before render completion #718). The ported cli:flood test covers it through the compiled CLI surface, which streams.
  • unit-render still collects the full stream inside the disposable child (renderRouteEvents in the public test harness) before forwarding; it is bounded by the runtime's own maxEvents / maxEventBytes and never reaches the service or IPC as a whole. A streaming harness primitive would be a public agent-bundle/test API change.
  • audiobook-curator.acceptance.e2e.test.ts was not extended: the public example has no long-stream route and examples/* are user-facing products.

Coordination with #684 and #718

Gate

Run on Node 22.23.2 in /fast/projects/agent-bundle-wt/i715 at the final head, rebased onto origin/main @ a057fdf (#718), with the two predicted #718 conflicts resolved as posted on #718 (kept #718's cancel hook and message set plus this PR's AgentRenderDispatch signature and progress entry; kept #718's invocationMessages stream reader and rewrote the ported cli:flood test on it):

pnpm build && pnpm typecheck && pnpm lint      → build=0 typecheck=0 lint=0 (1479 files, 88 rules)
pnpm test:unit                                 → 4416 passed, 1 failed*, 6 skipped
rstest --config rstest.integration.config.ts   → 151 passed, 0 failed
  route-invocation-dev-server, trace-dev-server, dev-workbench, workbench-surface-dev-server,
  lifecycle-replay-dev-server, dev-web-host-routes, hook-receipt-pipe, mcp-session-service,
  host-mcp-proxy, mcp-probe-dev-server, host-discovery-dev-server, dev-contract-adoption,
  target-hook-contract, cli, eval-workbench, native-host-sessions, generated-route-server,
  entry-shell, workbench: streaming-render.e2e, audiobook-curator.acceptance.e2e,
  examples-real.e2e, overview.e2e
pnpm docs:site:build                           → language parity ok, 0 broken links / 28317 anchors

* route-invocation-service.test.ts :: resolves a .jsimport of a.tsx sibling… — a pre-existing real-compile test, untouched here — hit its 30 s timeout with "no expect assertions completed" while the machine's load average was 60–95 (concurrent gates in other worktrees); the file passes 32/32 on every standalone rerun (three times this session). #718's gate log records the same load timeout on main.

Previous full runs of the same gate before the #718 rebase (at fa2a871 and 678f00e on top of #720): unit 4417/0, integration 148/0 and 149/0, docs ok.

Deslop: Claude Fable 5.1, 4 edits (flattened cancelledInvocation to an expression body, removed a redundant latestDocument helper in favour of the shared retainedHistory, dropped a stale "Type-only" header comment, replaced the Workbench's duplicate documentFor with the shared retainedLatestDocument and fixed the Raw-tab note that called a cancelled run's document "final").

Self-review

Reviewer: gpt-5.6-sol-high (generalPurpose subagent), two passes against the diff vs origin/main.

# Finding Disposition
1 Child process.send for render events was fire-and-forget, so a fast producer could pile up in the child's IPC queue Fixed — forwardRenderEvent returns the send promise; renderCompiled and renderUnitRoute await each event
2 unit-render still collects the whole stream in the child via renderRouteEvents Dismissed — disposable child, bounded by runtime limits, public harness API out of scope; recorded above
3 "Strict 2 MiB" claim was false by the two pinned events Fixed — module comment, en/zh docs, changeset state the allowance
4 Failed envelopes discarded retained history while replay kept it Fixed — failedInvocation uses the record's window; new test
5 Browser schema accepted a retention account inconsistent with events; fixture was inconsistent Fixed — retentionMatchesEvents refine, fixture corrected, mismatch case added

Second pass: no findings.

Reviewer (post-dedupe): gpt-5.6-sol-max (change-risk-reviewer), two passes against the diff vs origin/main after porting #699's tests.

# Finding Disposition
1 unit-render still collects the whole stream in the child (renderRouteEvents) Dismissed — disposable child, bounded by the runtime's render limits; a streaming harness is a public agent-bundle/test API change (recorded above)
2 Worker→child progress forwarding is unpaced; a worker error can drop a report still awaiting the dispatcher's queue Dismissed — the in-child queue is transient and drains through the awaited IPC send; a progress event lost immediately before the render fails is not part of the retained contract; transport pacing is #686/#718's
3 The Workbench runtime backend (invocationForRun) built RouteInvocations from whole run documents without the window Fixed — folds through retainRenderEvent, uses retainedLatestDocument (replacing the duplicate documentFor), attaches retention; test added
4 SSE replay was bulk-queued, relying on the raised queue limits Fixed — #stream paces replay one frame per socket drain; live frames arriving during replay wait behind it under their own byte/record accounting (ported from #699)
5 Browser refine checks counts, not retainedBytes Dismissed — same-process trust domain; recomputing over up to 2 MiB per decode buys nothing
6 Docs said "newest 256 events plus the pinned events" Fixed — changeset, en/zh reference and guide pages say "at most 256 events and 2 MiB, always keeping the newest event and the newest document-bearing event, which alone may exceed 2 MiB"

Second pass: no defects in the pump or the runtime-backend fold; #2 re-raised and dismissed as above, #6 wording completed.

Open in Web Open in Cursor 

@changeset-bot

changeset-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 83e90af

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

cursor Bot pushed a commit that referenced this pull request Sep 6, 2026
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review September 6, 2026 21:33
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T21:38:26.833222Z e83bc18 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

cursoragent and others added 9 commits September 6, 2026 23:04
One retention policy (route-invocation-render-history.ts: 256 events,
2 MiB, newest and newest document-bearing event pinned) now governs the
live replay buffer, the completed and cancelled envelopes, history reads,
the final stream message, and the Workbench live window. The child no
longer returns the event stream over IPC and the production producer keeps
only the complete document. Envelopes carry a retention account when
events were evicted.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
…replay

The production worker host dropped the route's progress messages the
dispatcher was ready to turn into progress render events; forward them as
the generated CLI session does. The invocation SSE queue now derives its
limits from the render-history window so a backpressured socket can still
replay a whole retained record. The dev-server test drives a compiled tool
through 400 progress ticks of 16 KiB and checks every reader sees the same
bounded window.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
…, honest bounds

Failed envelopes now carry the same retained window and latest document
as cancelled ones, so replay and envelope agree after eviction. The child
awaits each render IPC send so the channel paces a fast producer. The
browser schema rejects a retention account that does not match the events
beside it. Bound prose states the pinned-event allowance.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
…ion, pinned-newest module test, history ring, HTTP full-window replay
@ScriptedAlchemy
ScriptedAlchemy force-pushed the cursor/bound-invocation-render-history-02ec branch from 678f00e to 83e90af Compare September 6, 2026 23:09
@ScriptedAlchemy
ScriptedAlchemy merged commit 3ddbead into main Sep 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bound Workbench producer / completed-envelope render history end to end

2 participants