Skip to content

fix(dev): bound route-invocation render history by count and bytes end to end - #699

Closed
ScriptedAlchemy wants to merge 6 commits into
mainfrom
i681-bounded-envelopes
Closed

fix(dev): bound route-invocation render history by count and bytes end to end#699
ScriptedAlchemy wants to merge 6 commits into
mainfrom
i681-bounded-envelopes

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes #681

What

One retention policy for an invocation's render history, applied everywhere it is kept:

  • RENDER_EVENT_RETENTION (maxEvents: 256, maxBytes: 1 MiB of JSON) and retainRenderEvent / retainRenderEvents live in route-invocation-result.ts, re-exported from contracts/invocations.ts for the Workbench. Oldest-first eviction; the newest event is never evicted, so a completed stream always keeps its complete event whatever its size. document, result, outcome, and correlation identifiers sit outside the window and are never truncated (no plugin-result limit is introduced).
  • Producer (renderCompiled) retains in place while streaming and reports evictedEvents.
  • IPC: the child reply carries only the retained window for both production and unit-render surfaces.
  • Service: the live-replay record holds a RetainedRenderEvents window plus latestDocument tracked outside it; the completed envelope re-applies the bound and carries evictedEvents (absent when nothing was evicted). Cancelled and failed runs keep the retained window and the latest document, even after the event that carried it was evicted. invocationSummary omits evictedEvents.
  • History / reads / final stream message all serve that bounded envelope, so nothing rehydrates the discarded history.
  • SSE replay (#stream) is paced by socket drain instead of being burst into the live-consumer queue — a reconnect whose retained window exceeded the 256 KiB / 128-record queue limits previously destroyed the response (TypeError: terminated), which made the reconnect-after-eviction acceptance impossible. Live frames arriving behind a draining replay are held to the same 256 KiB / 128-record bound on the current backlog.
  • Workbench: the live buffer uses the same primitive (state.retained), the client schema accepts evictedEvents, and the raw view says how many events were evicted.

Replay order is now: truncated marker (if any), the retained render window, kernel trace messages, final. No consumer depended on trace/render interleaving.

Tests

  • tests/route-invocation-retention.test.ts (unit, no sleeps): count-bound with outcome/correlation kept and history ring eviction; byte-bound with 128 KiB snapshots; the newest event survives at any size; failed run keeps retained history and summaries omit evictedEvents; a full 256-render replay served through RouteInvocationRoutes over HTTP; reconnect after eviction replays truncated + bounded window with no final, then cancellation keeps evictedEvents, correlation, and the latest document after the shell was evicted.
  • tests/route-invocation-dev-server.test.ts: new case through a real compiled CLI child (cli:flood, 300 Suspense boundaries settling one per macrotask). Completed envelope, GET read, and post-completion stream replay are bounded by bytes (well under 256 events) and agree on evictedEvents; the final document and result are intact. A gated run: live stream past eviction, reconnect replays truncated + window only, cancel behind the gate yields cancelled with no outcome, bounded events with the shell evicted, evictedEvents, and the latest document.
  • packages/workbench/tests/invocation-model.test.ts: count eviction reports retained.evicted; byte eviction with 128 KiB snapshots.

Local gate (branch contains origin/main @ 5fc8de3; final tree 3bb5240)

pnpm build           → 0
pnpm typecheck       → 0
pnpm lint            → 0
pnpm test:unit       → 0  (4400 passed, 6 skipped)
pnpm docs:site:build → 0  (0 broken links / 28305 anchors)
integration pool (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, audiobook-curator.acceptance.e2e, examples-real.e2e, overview.e2e)
                     → 0  (131 passed, 0 skipped)

One earlier full-suite run hit a load-induced 30 s timeout in an unrelated child-spawning test (install.test.ts, then route-invocation-service.test.ts "resolves a .js import"); both pass standalone and the final full run above is clean.

Deslop: Claude, 3 edits (dropped the redundant events alias from the Workbench running state; early return in the child render; replaced findLast with the retained tail).

Coordination

#686 owns streaming; retention is settled here — see the comments on #681 and #686. Any chunked Flight delivery should feed publishRender one event at a time and reuse this primitive; the SSE route keeps its live-consumer limits.

Self-review

Reviewer: gpt-5.6-sol-max (change-risk-reviewer), three passes.

  1. Count-bounded SSE replays destroyed by the live counter — fixed (only live arrivals count; HTTP replay test added).
  2. invocationSummary leaked evictedEvents into strict Workbench list decoding — fixed + test.
  3. JSON.stringify(...).length is UTF-16 code units, not UTF-8 bytes — dismissed: the bound protects the dev server's heap, and V8 stores non-Latin1 strings two-byte, so code units track resident size more closely than UTF-8; the unit is stated on the constant.
  4. Failed envelopes discarded retained evidence while the stream replayed it — fixed (retainedHistory(record) shared by cancelled and failed paths) + test, docs, changeset.
  5. unit-render still collects the full stream in the harness before the IPC bound — dismissed: renderRouteEvents is the public test-harness contract and returns every event by design; IPC is bounded here and the runtime caps a stream at 10 000 events.
  6. (pass 2) Live backlog behind a replay used a cumulative counter with no byte bound — fixed (pre-encoded frames; current-backlog records and bytes, decremented as pumped).

Pass 3: no remaining merge risks.

@changeset-bot

changeset-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7f3b53

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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Producer, IPC reply, live replay, completed envelope, history, and the
Workbench buffer share RENDER_EVENT_RETENTION (256 events, 1 MiB JSON).
Envelopes report evictedEvents; cancellation keeps the latest document
after eviction; SSE replay is paced by drain instead of the live queue.
…om summaries, count only live arrivals during replay
ScriptedAlchemy added a commit that referenced this pull request Sep 6, 2026
…ion, pinned-newest module test, history ring, HTTP full-window replay
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Closing as a duplicate of #715, which also fixes #681.

Both PRs bound producer, IPC, replay, completed/cancelled/failed envelopes and history by event count and bytes and pin the final outcome/correlation. #715 survives because it goes further on the producer side: the child no longer carries any events array over IPC (each event is published as it happens and the send is awaited, so the channel paces the producer), the newest document-bearing event is pinned so a replay after eviction still folds to a coherent document, the truncation account (retention) is the one truthful record, and it forwards compiled routes' progress reports as render events, which is what lets a compiled MCP tool exceed the window at all.

Ported from this PR to #715 (a643d08): the real-compiled-child cli:flood dev-server test (byte-bound snapshots, GET/replay parity, mid-run reconnect after the shell was evicted, cancel after eviction), the pinned-newest module test, the history-ring eviction test, and the full-window HTTP replay through RouteInvocationRoutes. Retention module: packages/agent-bundle/src/dev/routes/route-invocation-render-history.ts (256 events / 2 MiB).

@ScriptedAlchemy
ScriptedAlchemy deleted the i681-bounded-envelopes branch September 6, 2026 22:04
ScriptedAlchemy added a commit that referenced this pull request Sep 6, 2026
…ion, pinned-newest module test, history ring, HTTP full-window replay
ScriptedAlchemy added a commit that referenced this pull request Sep 6, 2026
…ion, pinned-newest module test, history ring, HTTP full-window replay
ScriptedAlchemy added a commit that referenced this pull request Sep 6, 2026
…d to end (#715)

One render-history window (`route-invocation-render-history.ts`: at most 256 events and 2 MiB serialized, always keeping the newest event and the newest document-bearing event) bounds the child/IPC producer, the service replay buffer, completed/cancelled/failed envelopes, GET reads, history, the `final` message, the Workbench live window, and runtime-run envelopes. Envelopes whose events were evicted carry `retention`; the stream sends `truncated` once; `document`, `result`, `outcome`, and `correlationId` are never truncated. Stream replay is paced by socket drain. Compiled routes' progress reports reach the Workbench as `progress` render events.

Survivor of the #699 / #715 duplicate; #699's unique tests (compiled-child reconnect and cancel after eviction, pinned-newest, history ring, HTTP full-window replay) are ported here.

Closes #681
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

1 participant