Skip to content

feat(dev): PR 2b — unified live trace: streamed route invocations, cancel, correlated Trace page (#600) - #666

Merged
ScriptedAlchemy merged 110 commits into
mainfrom
wb600-pr2-trace
Sep 6, 2026
Merged

feat(dev): PR 2b — unified live trace: streamed route invocations, cancel, correlated Trace page (#600)#666
ScriptedAlchemy merged 110 commits into
mainfrom
wb600-pr2-trace

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Part of #600PR 2b: unified live trace. Follows PR 2a (#643, 3afcdc9d87). Host sessions (PR 3) are gated on the owner's answer on #600.

What changes

One timeline for a development plugin, fed by what already runs — no second trace framework.

  • Server trace hub (packages/agent-bundle/src/dev/trace/**): TraceHub is the bounded in-memory log the dev server publishes TraceEntry records into; GET /api/trace (replay, after cursor) and GET /api/trace/stream (NDJSON live) read it. Publishers: route invocations (invocation.started at admission, completed/failed/cancelled at the end, the kernel EventTraceEvents forwarded live from the child over the existing IPC), MCP session frames (mcp.request/response/notification, from McpSessionTraceLog), hook receipts from generated hook wrappers running under agent-bundle dev (POST /api/trace/receipts: loopback-only, bearer token, no Origin, 16 KiB cap — AB8247AB8249), dev-log warnings/errors with request-scoped correlation, project diagnostics. The round-1 reviewer's runtime source (MCP App runtime controller) was deleted again — nothing in the spec needed it. Absolute paths are redacted before an entry leaves the server. Diagnostics AB8240AB8243.
  • Live route invocations (the owner's core of PR 2): the production child forwards each AgentRenderEvent (shell/progress/replace/error/complete) over IPC as it happens; RouteInvocationService keeps the newest 256 render events per running invocation (one { type: 'truncated' } marker; the Workbench reducer keeps the same bound) and subscribe(id) replays then streams render/trace/final; cancel(id) aborts through the same AbortSignal PR 2a's queue uses and ends the record status: 'cancelled' with no outcome (AB8256 if the run completed first). A request the service rejects before admission (AB8232/AB8237AB8239, disconnected client) leaves no stream record behind. HTTP: POST /api/routes/invocations with stream: true202 + running record, GET /api/routes/invocations/<id>/stream (SSE, ends after final), POST /api/routes/invocations/<id>/cancel (202; 409 AB8256 once final). Non-stream POST (with or without stream: false) is unchanged; every PR 2a fixture still passes. route.invocation project events now also fire at start with a running record, so the payload widens to RouteInvocationSummary | RunningRouteInvocation — the changeset is minor and tells consumers to narrow on status.
  • Workbench: the executable and event route workspaces run with stream: true, render the Suspense shell → replacements → complete as they arrive, show a Cancel button while running, and settle on the unchanged final envelope; the Trace page (packages/workbench/src/trace/**) groups entries by correlation, deep-links each entry (href) to the route workspace (?invocation=<id>) or MCP session, and survives reload. B2 deleted the Trace filter bar, facets, "N new" pill and scroll bookkeeping — nothing in the spec or an existing test used them.
  • Generated hook wrappers (adapters/hook-contract.ts): the standalone/shared/preflight wrappers open an EventTracer and, only when the dev-server receipt env vars are present — or a dev-installed bundle's .agent-bundle/hook-receipts.json record names a dev server whose pid is still alive — post one receipt at exit (750 ms bound on hook exit latency, documented); merged with feat(events): pass preflight data to routes #664's preflight data threading (gate.data in executionInput, preflight in the rendered payload). Outside agent-bundle dev the receipt is a no-op with no network.
  • Docs: website/docs/{en,zh}/guide/development/workbench.mdx, reference/dev-server-http.mdx, reference/runtime-environment.mdx, guide/authoring/hooks.mdx, docs/diagnostics.md. One changeset (.changeset/wb600-live-trace.md).

Lanes: B0 rebase onto PR 2a, B1 live transport, B2 acceptance + deletions, B3 origin/main merge (#664 semantic merge in hook-contract.ts) + deslop, B4 round-1 review fixes. Lane notes are summarised here; the branch carries none.

Acceptance (the spec)

# Fixture Test
1 Live progress: the rendered document fills in while the handler runs (Suspense shell → replace → complete visible before the final outcome); Trace shows the invocation running with entries before completion audiobook-curator.acceptance.e2e.test.ts (live progress block: running badge, Cancel visible, busy non-empty shell document, invocation.started row before succeeded)
2 Cancellation: a running invocation cancelled from the workspace ends cancelled, the child is gone, Trace shows it, no succeeded/outcome same file (cancel block: cancelled badge, no outcome, invocation.cancelled row, GET /api/routes/invocations/<id>status: 'cancelled', no outcome); route-invocation-service.test.ts cancel cases; route-invocation-dev-server.test.ts /cancel → 202 then final cancelled
3 Bounded retained output: more render events than the bound keeps the final document and one truncation marker; the buffer size is asserted route-invocation-service.test.ts "retains only the newest 256 render events and one truncation marker" (300 events → newest 256, first retained sequence 44, exactly one { type: 'truncated' }, final document intact); packages/workbench/tests/invocation-model.test.ts asserts the browser reducer's 256 bound
4 Final outcome: the streamed record ends with the same RouteInvocation envelope PR 2a produces audiobook-curator.acceptance.e2e.test.ts (final-envelope block: status, outcome, first provider name/status, first timing phase/duration compared between the API envelope and the workspace tabs)
5 Deep links: every Trace entry of an invocation links to the route workspace and Trace snapshot; opening either after a reload shows that snapshot; expectHeading (#632) throughout same file (deep-link block: ?invocation=<id> survives reload with status/outcome/correlation; Trace detail survives reload; "Open route" returns to the same snapshot); every heading through expectHeading
6 Correlation: a tools/call from the MCP session workspace and a hook receipt for the same session on one timeline under one correlation id same file (correlation block: real tools/call search_audible from the MCP session workspace + authenticated hook receipt carrying that session id in one Trace group); trace-model.test.ts cross-source grouping; trace-dev-server.test.ts, hook-receipt-pipe.test.ts

Gates (local merge gate, AGENTS.md #662/#671)

Branch contains origin/main at 26293c2354 (#672). Run on 686fa7a612 (the whole pool), then re-run on head ba296091df for the two files the last commit touched:

pnpm build && pnpm typecheck && pnpm lint    # ok (lint: 1456 files, 88 rules)
pnpm test:unit                               # 292 files: 4354 passed, 6 skipped, 0 failed
pnpm test:route-unit                         # 10 files: 89 passed
pnpm test:integration:run                    # WHOLE integration pool (incl. every Workbench browser test and
                                             # audiobook-curator.acceptance.e2e): 98 files, 1158 passed, 4 skipped, 0 failed
pnpm test:packed packages/workbench/tests/packed-release.e2e.test.ts   # 1 passed
pnpm docs:site:build                         # ok (locale parity, diagnostics coverage, 0 broken links / 28,291 anchors)

# head ba296091df (dev-log mirror set + dev-server-http.mdx en/zh):
pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit            # ok; 4354 passed, 6 skipped
AGENT_BUNDLE_WORKBENCH_PREBUILT=1 AGENT_BUNDLE_PACKAGE_PREBUILT=1 pnpm exec rstest --config rstest.integration.config.ts \
  packages/agent-bundle/tests/trace-dev-server.test.ts packages/agent-bundle/tests/dev-workbench.test.ts \
  packages/workbench/tests/logs-real.e2e.test.ts packages/workbench/tests/audiobook-curator.acceptance.e2e.test.ts   # 36 passed
pnpm docs:site:build                                                   # ok

The PR's own CI run on the old head aebe0429d0 finished red on Verify (integration-2, Node 24): route-register-typegen.test.ts "Type 'false' does not satisfy the constraint 'true'" — the #664 preflight widening of AgentEventRouteProps, fixed on main by #670 (72a035a3fe), which that head predated. The branch contains #670 and the test passes in the whole-pool run above.

Deslop

Deslop: GPT-5.6 Sol (lane B3), 13 edits, plus Claude Fable 5.1 (integrator, round 2–3), 5 edits: RouteInvocationService.has() and traceJoinKeys un-exported/deleted as test-only, dev-lock.ts rewired to core/loopback-origin.ts instead of its byte-identical copy, two dead runtime.event mirror entries and the stale runtime correlationId prose removed. B3 — one changeset instead of two; route-invocation-routes.ts reuses dev/http.ts noQuery/badRequest instead of local copies; lane/PR-history and restating comments removed from hook-receipt-endpoint.ts, hook-receipts.ts, events/trace-receipt.ts, trace-entry.ts, trace-client.ts, trace-model.ts, trace-page.tsx; dev-server-http.mdx en+zh gained the stream/cancel endpoints; speculative host-session prose removed from workbench.mdx en+zh. Lane B2 deleted the Trace filter bar, facets, "N new" pill and scroll bookkeeping plus their CSS/tests and an unused test id.

Self-review

Three rounds, Claude Fable 5.1 Thinking High each, read-only against the branch diff vs origin/main.

Round 1 (aebe0429d0) — 11 findings, fixed by lane B4 (fe06da0891) unless noted:

  1. Stream record leaked on non-streaming invoke() errors → record deleted before rethrow (route-invocation-service.ts); test in route-invocation-service.test.ts.
  2. stream: false rejected as malformed → plain POST parses requestBody (route-invocation-routes.ts); test in route-invocation-dev-server.test.ts.
  3. cancel() racing completion returned succeeded with 202 → AB8256 unless the awaited record is cancelled.
  4. Hook receipt not a network no-op for dev-installed bundles → endpoint record carries the server pid; wrapper ignores a record whose pid is dead; 750 ms bound documented en/zh.
  5. truncated.dropped stale for live subscribers → wire marker is { type: 'truncated' } only.
  6. Browser render buffer unbounded → reducer keeps newest 256 (invocation-model.ts); test.
  7. Criterion 6 joins by mcpSessionId value and the fixture supplies the id — dismissed: the join key is what the MCP session publisher and the hook receipt both carry; a host-launched Claude session id will need its own key in PR 3, which is exactly the "PTYs reuse this correlation" clause of the spec.
  8. RouteInvocationEventPayload.invocation widened → changeset minor, docs tell consumers to narrow on status.
  9. Dead/test-only exports and test seams → removed (hook-receipts.ts, events/project.ts, trace-model.ts, RouteInvocationRouteService.has, now/timeoutMs/token options).
  10. runtime trace source not needed by the spec → deleted with its controller/protocol/MCP App changes; log and diagnostic kept.
  11. requestId listed for invocation correlation → removed from the docs row.

Round 2 (632d18020d) — 7 findings, fixed by the integrator (686fa7a612):

  1. runId copied into TraceCorrelation by the dev-log lowering while the type/decoder had none → Trace page would fail AB8243 once an eval run logged. Fixed by deleting runId from the lowering (not by widening the type): traceRequestContextKeys, traceCorrelationFor, traceHrefFor, docs en/zh.
  2. Docs still listed the deleted runtime source → removed en/zh.
  3. core/loopback-origin.ts was a copy of dev-lock.ts's helper, never rewired → dev-lock.ts imports it, copy deleted.
  4. createEventTracer without observer now reads the process-local observer live (was permanently disabled) — public behavior not in the changeset → sentence added; guide/authoring/hooks.mdx en/zh updated.
  5. Orphan running trace entry when a plain-POST client disconnected during #prepared()admissionSignal.throwIfAborted() moved above the invocation.started publish.
  6. RouteInvocationService.has() test-only → deleted.
  7. "Cancellation reaps the child" untested → reaps the render child and its descendants when the invocation is cancelled added beside the success/timeout/close reap tests.

Round 3 (686fa7a612) — verified all seven; 2 residual findings, fixed (ba296091df): the new dev-server-http.mdx page still claimed POST /api/runtime/runs accepts a correlationId copied onto trace entries (deleted with the runtime source) → sentences removed en/zh; project:runtime.event / diagnostic:runtime.event.diagnostic sat in the "already lowered by a dedicated producer" mirror set although nothing lowers them → entries removed so runtime failures fall back to the log source like any other record. Round 3 nit not taken: the Buffer.prototype.toString spy predicting an invocation id in one service test (brittle, not a merge risk).

Trace correlation sources / security posture

Correlation sources on the timeline: route invocations (RouteInvocationServiceTraceHub, kernel EventTraceEvents from the child), MCP session frames (McpSessionTraceLog publisher, mcpSessionId as a join key), hook receipts from generated wrappers (POST /api/trace/receipts), dev-log warnings/errors with request-scoped correlation, project diagnostics. Not on it: host PTY sessions (PR 3, pending the owner's answer on #600).

Security posture of the receipt endpoint: loopback peer only, bearer token minted per dev-server run and passed to the wrapper through the environment, requests with an Origin header refused (AB8247), strict body shape and 16 KiB cap (AB8248/AB8249), absolute paths redacted before an entry is stored. The only file the endpoint writes is <projectRoot>/.agent-bundle/hook-receipts.json (mode 0600: pid, token, loopback URL) so hooks a host launches from a dev-installed bundle can find the running dev server; it is removed on shutdown and a wrapper ignores it when the recorded pid is dead, so a crashed server's token is never posted to whatever later binds the port. Outside agent-bundle dev the wrapper's receipt is a no-op with no network.

…600 PR 2, lane T3)

- McpSessionServiceOptions.trace?: TracePublisher; one createMcpSessionTraceSink per
  session, composed with the dev-log sink (composeMcpSessionTraceSinks, per-sink isolation)
- mcp.request / mcp.response paired by JSON-RPC id with durationMs, mcp.notification,
  mcp.progress, mcp.logging, mcp.stderr (summary through safeDevWireText),
  mcp.session.started / mcp.session.closed; correlation mcpSessionId, mcpRequestId,
  routeId (tool:/prompt:), epochId, host, and _meta-lifted requestId / conversationId /
  sessionId / correlationId; href via applicationNodePath + ?session=
- McpSessionFrameTraceEntry lifts id, method, meta (McpSessionTraceMeta) as optional fields
- tools/call session route accepts correlationId and stamps
  _meta['agent-bundle/correlationId']
- tests: mcp-session-trace-publisher (unit), mcp-session-routes, mcp-session-service (fixture)
…race (#600 PR 2, lane T5)

- trace-client.ts: TraceClient (replay/stream), ForegroundTraceClient, strict
  decoders for TraceEntry / TraceReplayGap / replay body (AB8249), and
  openTraceFeed with replay -> NDJSON stream -> back-off reconnect and a
  restart-from-zero path for a refused non-zero cursor.
- trace-model.ts: merge by sequence (bounded 4096), union-find grouping over
  conversationId > sessionId > invocationId | executionId | runId |
  mcpSessionId:mcpRequestId > correlationId, nesting, span/status/headline,
  filters, facets, ?correlation= and /trace/<id> selection, formatting.
- trace-page.tsx + trace-page.css: timeline with grouped rows, sticky filter
  bar, "N new" pill with scroll anchoring, empty state, detail drawer with
  correlation links and Open route. PR 1 stopgap over /api/routes/invocations
  removed.
- workbench-location.ts: /trace/<entryId> and ?correlation=<id>.
- project-client.ts: 'route.invocation' joins projectEventTypes; activity
  events no longer trigger a status refetch.
- main.tsx: construct ForegroundTraceClient and pass it to TracePage.
- docs/diagnostics.md: register AB8249.
- Tests: trace-model, trace-client (decoders, stream, feed reconnect),
  trace-page (static render), workbench-location, project-client.
Generated hook wrappers of the dev plugin post a slim receipt — the kernel's
EventTraceEvents, EventTraceExecution identity, resolved lineage keys, and the
host/session/request ids the native payload names, never the payload — to the
authenticated dev server's POST /api/trace/receipts, discovered through the
dev install marker (.agent-bundle-dev.json -> <project>/.agent-bundle/hook-receipts.json)
or AGENT_BUNDLE_DEV_TRACE_URL/AGENT_BUNDLE_DEV_TRACE_TOKEN for dev-server-spawned
simulations. HookReceiptRoutes lowers each receipt to hook.received /
hook.completed / hook.failed (+ session.started / session.ended) on the TraceHub.
Diagnostics AB8247-AB8249. Wiring for T1 in LANE-NOTES.md.
# Conflicts:
#	packages/agent-bundle/src/dev/foreground-server.ts
#	packages/agent-bundle/src/dev/workbench-server.ts
…ches.

The PR 1 .trace-table step is gone; the audiobook-curator e2e now covers
populated groups, ?correlation= scoping, /trace/trc_n deep links, Open route,
and a live POST without reload. Leaving Trace is a designed stream abort.
@changeset-bot

changeset-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ba29609

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 Minor

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

@pkg-pr-new

pkg-pr-new Bot commented Sep 6, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@666
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@666
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@666
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@666

commit: 686fa7a

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Round 1 self-review (Claude Fable 5.1 Thinking High) posted 11 findings against aebe0429d0; lane B4 is fixing #1#6, #8#11 (stream-record leak on rejected non-stream invokes, stream: false parsed from the wrong body, cancel racing completion, hook-receipt record pid liveness, truncated wire shape, browser render buffer bound, changeset minor for the widened route.invocation payload, dead/test-only exports, deletion of the runtime trace source, requestId doc nit). #7 (criterion 6 joins by mcpSessionId value; the acceptance fixture supplies the id) is dismissed with reasoning in the PR body. Round 2 review follows the fixes; merge on the local gate after that.

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review September 6, 2026 05:17
@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.

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.

1 participant