feat(dev): PR 2a — execution parity, provenance, and honest telemetry for Workbench route invocations (#600) - #643
Conversation
Zero was reported as a measurement for providers the child never observed. Record unobserved providers and only measured phases.
A queued Workbench invoke was labelled with the revision seen at enqueue while the child later imported source that may have changed. Acquire the published epoch inside the concurrency slot, release on every exit, and reject waiters whose catalog moved.
#600) Extract the Jiti block from route-invocation-child.ts into dev/routes/route-module-loader.ts (createRouteModuleLoader) and replace the whole-source .js→.tsx string substitution with a TypeScript AST walk over import, export … from, and literal dynamic import() specifiers, so a string such as <Agent.Text>{'./panel.js'}</Agent.Text> renders in the Workbench as the compiled program prints it.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
🦋 Changeset detectedLatest commit: f1eba9c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
commit: |
|
Owner addendum (19:56) folds into this PR before review completes — acceptance list: #600 (comment). Lanes running: A6 request model (canonical operation + selected surface, |
…he queue-aware abort
…00-pr2a-exec
14 edits across 12 files: changeset rewritten as one user-facing paragraph naming the endpoint, env var, surface union, outcome, and AB8239/AB8250-AB8255; dead branch and duplicated throw removed in route-invocation-production and route-invocation-service; stale <epoch>/state comment and fixture paths updated; unused parseCliCommandInput alias deleted; duplicated CLI surface draft hoisted in the Workbench editor; en/zh Workbench docs corrected where timings were said to show 'unobserved'. rewriteTsxSpecifiers now writes .tsx rather than .jsx (trivial fix, behaviour unchanged). LANE-NOTES.md records gate results.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…lector guard, drop dead import.meta.main fallback, exact heading budget
…on instead of overriding a chosen one
…orrelated Trace page (#666) * feat(dev): trace entry contract and bounded TraceHub for the unified Workbench trace (#600 PR 2 foundation) * feat(workbench): correlate route workspace traces * feat(dev): fan runtime and logs into trace * feat: publish route invocation kernel traces * feat(dev): add unified trace hub routes * feat(dev): lower Workbench MCP session traces onto the unified trace (#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) * docs: explain unified Workbench trace * feat(workbench): Trace page as a correlated live timeline over /api/trace (#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. * feat(dev): host-invoked hook receipts on the trace (#600 PR 2, lane T7) 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. * drop LANE-NOTES * drop LANE-NOTES * drop LANE-NOTES * integrate: TraceHub requires projectRoot in lane tests * docs(zh): document unified Workbench trace * feat(dev): wire unified server trace * Rewrite Trace acceptance for the live /trace page and pin heading matches. 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. * drop LANE-NOTES * drop LANE-NOTES * drop LANE-NOTES * W2: browser wiring for unified trace (#600 PR 2) - pass ForegroundTraceClient as trace: TraceClient through ApplicationExplorer to RouteWorkspace - tools/call correlationId: controller stamps params._meta, remote transport lowers it to the top-level route field; appToolCallRequest no longer sends _meta - decode and render lifted frame id/method/meta on the Protocol page; correlationId links to /trace?correlation= - strict requestId on RouteInvocationRequest/Summary; shown on the status line - renumber browser trace decoder code AB8249 -> AB8243 (docs/diagnostics.md row) - drop dead PR 1 trace CSS from shell.css; .trace-link/.trace-status live in trace-page.css - extract ShellLink and route Open in Trace / trace rows through the shell router * fix trace summary signal quality * drop LANE-NOTES * drop LANE-NOTES * fix(dev): stop fabricating route-invocation provider timings Zero was reported as a measurement for providers the child never observed. Record unobserved providers and only measured phases. * fix(dev): pin route invocations to a leased epoch (AB8239) A queued Workbench invoke was labelled with the revision seen at enqueue while the child later imported source that may have changed. Acquire the published epoch inside the concurrency slot, release on every exit, and reject waiters whose catalog moved. * fix(dev): rewrite only module specifiers in the route invocation child (#600) Extract the Jiti block from route-invocation-child.ts into dev/routes/route-module-loader.ts (createRouteModuleLoader) and replace the whole-source .js→.tsx string substitution with a TypeScript AST walk over import, export … from, and literal dynamic import() specifiers, so a string such as <Agent.Text>{'./panel.js'}</Agent.Text> renders in the Workbench as the compiled program prints it. * drop LANE-NOTES * fix merged telemetry/lease test fixtures * fix: carry runtime MCP trace correlation * drop LANE-NOTES * feat: execute routes from compiled artifacts * integrate A2: args check after prepared lease * integrate A2: trace type through the invocations contract; unit-render fixture * integration test: observed provider telemetry from the compiled worker * changeset: PR 2a execution parity + provenance * fix workbench invocation epoch state parity * drop LANE-NOTES * changeset: name #643 * fix(dev): retain state across epoch rebuilds * feat(workbench): select invocation surfaces * drop LANE-NOTES * drop LANE-NOTES * fix stateful test helper call; runtime-environment docs follow the project state root * Report route invocation outcome separately from execution status (#643, lane A7) A completed RouteInvocation gains `outcome`: `success`, `represented-error` (MCP projection `isError: true` or an event route's `deny` decision), or `process-exit` with the exit code the generated CLI bin sets. `status` keeps meaning whether the execution boundary completed. The production path captures the bin's own decision: generated bins export `routeInvocationExitCode`, built on `renderedDocumentExitCode`, the one exit-code rule `runRenderedInvocation` now also uses; plain scripts report their real process status; `unit-render` applies the same rule to the manifest command's policy. The service reads `child.exitCode` and no longer re-derives an exit code for the CLI projection. The Workbench decoder requires `outcome` on every succeeded envelope and summary and forbids it on failed ones. The route workspace, event replay and Trace show an outcome badge beside the execution status (`Completed` / `Failed`); the event canonical tab lists Execution and Outcome rows. Tests: dev-server integration for represented-error, process-exit (production, unit-render, and the generated bin as a real process), deny and success outcomes; workbench decoder, label and rendering tests; entry-shell byte pin moved for the new export. Docs: en+zh workbench.mdx status/outcome table; changeset summary extended. * drop LANE-NOTES * project isolated renders by the route's default surface; port exit fixture to surfaces * test: cover route invocation acceptance fixtures * fix workbench route review findings * drop LANE-NOTES * drop LANE-NOTES * Deslop pass for #643 (lane A11) 14 edits across 12 files: changeset rewritten as one user-facing paragraph naming the endpoint, env var, surface union, outcome, and AB8239/AB8250-AB8255; dead branch and duplicated throw removed in route-invocation-production and route-invocation-service; stale <epoch>/state comment and fixture paths updated; unused parseCliCommandInput alias deleted; duplicated CLI surface draft hoisted in the Workbench editor; en/zh Workbench docs corrected where timings were said to show 'unobserved'. rewriteTsxSpecifiers now writes .tsx rather than .jsx (trivial fix, behaviour unchanged). LANE-NOTES.md records gate results. * drop LANE-NOTES * test: give the compiled-artifact dev-server suites a load-tolerant budget * test: keep the parity fixture's handler marker out of the watched source tree * test(workbench): exact heading locators through expectHeading (#632) * fix: review round 2 — Workbench-only worker input validation, host selector guard, drop dead import.meta.main fallback, exact heading budget * fix(workbench): loaded invocation host wins over the preflight default on mount * drop LANE-NOTES * fix(workbench): the preflight default repairs an invalid host selection instead of overriding a chosen one * feat(workbench): stream live route invocations * drop LANE-NOTES * test(workbench): accept live trace flows * drop LANE-NOTES * chore(workbench): merge and deslop live trace * drop LANE-NOTES * changeset: name PR #666 * fix trace review findings * drop LANE-NOTES * fix(trace): round-2 review — drop runId from trace lowering, stale runtime source in docs, shared loopback helper, abort check before invocation.started, remove has(), cancel reap test, tracer changeset note * docs(trace): drop the stale runtime correlationId prose; runtime.event records fall back to the log source
Part of #600 — PR 2a: execution parity + provenance. Lands the owner audit's P1-A/B/C + P2 items on the PR 1 execution bridge, plus the 19:56 addendum (request model, outcome vs. status, six acceptance fixtures). Acceptance list: #600 (comment). The live trace is PR 2b; host sessions PR 3. No new top-level destination; PR 1's page deletions stand.
What changes
Request model — canonical operation + selected surface.
POST /api/routes/invocationstakes{ routeId, surface?, input? }.routeIdis the canonical operation (tool:<server>/<tool>,event:tool/before,cli:<path>,script:<name>);surfaceselects how it runs:{ kind: "mcp" },{ kind: "cli", command, args },{ kind: "event", host?, fixtureId? },{ kind: "script" }, or{ kind: "unit-render" }. A tool with a #616 CLI projection is invoked through MCP or through that projection under the same operation id — no duplicate CLI operation (AB8253mismatched command,AB8254a projectedcli:id used instead of the canonical id). Defaults follow the route kind;unit-render(live-source component preview via the route-unit harness, disposable state) is never a default and is recorded on the result assurface.Production execution boundary (P1-B). Production surfaces execute the leased published epoch's compiled artifact along the path the generated entries take: the generated hook wrapper's
prepareRouteInvocation(validation → canonical props → #618 preflightcontinue/deny/defer), the generated CLI bin'sprepareRouteInvocation(argv, confirmation,.cli.tsmapInput, canonical validation) and its own exit-code decision, the epoch's Flight worker for tools/resources/prompts/rendered scripts (MCP input validated against the compiledinputSchemaexactly as the generated server does), compiler aliases/defines, the operator.envlayer applied once for every surface,AGENT_BUNDLE_PLUGIN_ROOT=<epoch>andAGENT_BUNDLE_STATE_ROOT=<project>/.agent-bundle/state— the #640 state-root contract, outside the epochs that retirement deletes and shared with the dev MCP session, so session state survives a republish. An event route with a compiled preflight rejects a hostless (canonical) submission withAB8255; the Workbench defaults such routes to the first lifecycle target.Provenance (P1-C).
RouteInvocationService.invoke()acquires theEpochStorelease inside the concurrency slot, revalidates digest/source revision, answers409 AB8239when the published revision moved while the request waited, and releases infinally. A request aborted while queued (browser closed the fetch) is removed from the queue and never leases or spawns.Specifier repair (P1-A).
route-module-loader.tsrewrites only realimport/export … from/import()specifiers through the TypeScript AST when pointing a.jsimport at its.tsxsource; a rendered string such as'./panel.js'is untouched.build/module-imports.ts(#634) is not a dependency.Telemetry (P2 + addendum). Provider and timing rows come only from what the worker measured: unmeasured phases are omitted, unobserved providers are
unobserved, a preflight short-circuit reports no render duration. Executionstatus(succeeded= the boundary completed) is separate from the applicationoutcome:success,represented-error(projectedisError, non-success document, hookdeny), orprocess-exitwith the generated bin's exit code — in the envelope, theroute.invocationproject event, the route workspace, and Trace.Diagnostics.
AB8239,AB8250–AB8255(docs/diagnostics.md). Docs en+zh:guide/development/workbench.mdx,reference/runtime-environment.mdx, MCP session page. Changeset: onepatchforagent-bundle.Acceptance fixtures (all six)
continue/denywith a throwing handler → handler never imported or run (marker files);defer→ gate once, handler once, selected providers respectedtests/route-invocation-dev-server.test.tsmapInput→ same canonical input and confirmation behaviour as the generated bin (unconfirmed run fails the way the bin does;--yesproceeds)define+./panel.jsimport + literal'./panel.js'text → Workbenchmcpprojection equals the epoch's generated MCP server'stools/callresult byte-for-byte (stableJson)tests/route-unit/route-module-loader.test.tsunit-renderresetsAB8239; abort while queued → no lease, no childtests/route-invocation-service.test.tsunobservedprovider visible in the route workspace at 1440×900 (examples/audiobook-curator, real fixture data, never accepted while loading)packages/workbench/tests/audiobook-curator.acceptance.e2e.test.tsGates (local merge gate, AGENTS.md #662)
Branch contains
origin/mainat461a6dc5c9(#659). Commands, run at the head shaf1eba9ce29in this order, results:Earlier heads: a full
pnpm test:integration:runat55be005b0fpassed except one load timeout in the untouchedagent-api.test.ts(30 s budget on a machine at load 60), which passed alone; the Workbench browser filesaudiobook-curator.acceptance,lifecycles,discoveryalso passed at7f2dd2590a.CI failure fixed at the root (
Verify (integration-2)on the draft head): the parity fixture's handler wrote itshandler-ranmarker to the project root; the dev source watcher (which excludes only.agent-bundle,.git,node_modules, output paths) saw it, started a rebuild, and the next request raced the republish and got409"source is newer than the published build". The marker now lives under.agent-bundle/like every other fixture marker, and the test asserts the200before reading the envelope so a recurrence names its cause.#632 folded in:
expectHeading(scope, name, timeout?)inpackages/workbench/tests/support/workbench-acceptance.tsalways passesexact: true; every literal heading locator in the Workbench e2e files (packed-release,overview,discovery,evals-real,mcp-app-real,examples-real,contributor-hmr,mcp-tasks,mcp-session-timeout,audiobook-curator.acceptance) goes through it. Regex heading locators are already unambiguous and stay. Closes #632.Deslop
Claude (Fable 5.1 thinking), 14 edits across 12 files, behaviour unchanged except one trivial loader fix (
rewriteTsxSpecifierswrote./foo.jsxfor a.tsxsibling; now.tsx, covered by the existing loader test): collapsed a duplicatedAB8251throw, removed an unreachableif (aborted)afterthrowIfAborted(), dropped theparseCliCommandInputalias ofmapGeneratedCliInput(both call sites import the callee), deduplicated the CLI-surface draft literal inroute-input-editor.tsx, fixed a stalestateRootcomment and stale<epoch>/statefixture paths, and corrected the en/zh docs sentence that claimed timings showunobserved(they omit unmeasured phases). Theas nevercasts onJsonValueinroute-invocation-production.tsand the leasetry/catch(convertsEPOCH_NOT_FOUNDintoAB8239) stay.Self-review
Reviewer: Claude (Fable 5.1 thinking), read-only, against the diff vs
origin/main.Round 1 (
db99d4a65) — 10 findings, all fixed in lane A10 (/tmpnotes summarised here):AB8255+ Workbench defaults to a host for preflight routes (tests: service, tree, workspace, dev server).AbortSignalthrough the HTTP route andinvoke(), aborted waiters leave the queue, checked again before leasing and spawning (unit tests).render 0 ms+ empty providers on a gate short-circuit → optional render duration,observedabsent →unobserved.inputSchemavalidation → validated in the epoch worker as the server does; failure is an MCP error result (represented-error).import.meta.mainonly → fallback onrealpath(argv[1]) === fileURLToPath(import.meta.url); template hashes regenerated..envlayer applied only for CLI/event → applied once, explicit worker env, for every surface.generatedRouteArtifactEpoch. 8. LOW lease acquired before manifest construction → reordered. 9. LOWunit-renderprovenance mislabelled the route kind → real kind. 10. LOW runtime backend violatedoutcome ⇔ succeeded→ emitssuccess.Docs: providers sentence corrected (no
skipped), runtime-environment state-root sentence made true, PR body refreshed.Round 2 (
55be005b0f) — all 10 round-1 fixes verified in code; 6 new findings:inputSchema.parsealso ran on the generated MCP server path (double parse after the SDK's own) → fixed: the worker validates only when the message carriesvalidateInput: true, which onlyroute-invocation-production.tssends; changeset reworded; template test asserts the gate.invocationOfisundefinedwhile running) → fixed: the loaded invocation's host is applied only when defined; the default is computed from[leaf, lifecycle]and, after round 3's residual, only repairs an invalid selection (canonical on a preflight route) instead of overriding a chosen or loaded host.invocation.tracehas no Workbench consumer → dismissed: it is the kernelEventTraceEventrecord the P1-B audit item names ("→ AgentDocument + real trace"), measured not fabricated; the dev-server parity tests assert its sequence (preflight.start … render.finish) and PR 2b's TraceHub (already written onwb600-pr2-trace) publishes it; the strict Workbench decoder must know the field to accept the envelope. Reviewer accepted the server-side record, noted the envelope field stays consumer-less until 2b.expectHeadingshortened one 30 s budget to 15 s (discovery.e2e) → fixed: passes itsbrowserTimeout.import.meta.main ?? realpathSync(...)fallback is dead onengines >=22.19and throws fornode -e→ fixed: deleted with its imports; hashes re-pinned; changeset sentence removed.eventRequestForthird parameter only used by a test → fixed: removed.Round 3 (
a47f9f86de, then7f2dd2590a) — 1, 2, 4, 5, 6 VERIFIED at file:line; dismissal 3 accepted; the mount-ordering follow-up on 2 (default effect declared first so a loaded host wins the same commit) VERIFIED; the LOW residual (catalogloading → readyre-running the default) fixed by the repair form above.Trace correlation / security posture
Unchanged from PR 1: the dev server stays an authenticated loopback server; browser argv reaches only the generated bin's bounded argv parser; paths compose from the server-owned epoch id, manifest route ids, and an enum host; the state root is fixed per project. PR 2b will stream execution events from this boundary; PR 3 PTYs reuse 2b's correlation.