[stacked on #2164] WS1b-claude: Claude Code bridge to grammar v3 with presentation - #2178
Conversation
8766c32 to
2aa65d1
Compare
2aa65d1 to
9d05317
Compare
…ridge lanes The claude-code recordings' bridge→runtime.current.ndjson lanes are what this checkout's bridge emits (pnpm rerecord --plan-with <main checkout>); the recordings themselves are untouched and the self-suite's row-count pins are unchanged. recordings/parity-allowlist.json names every intended byte-inequivalence against main with #2178 and a reason: presentation on items (9 cells), Read → fileRead (plan-mode: 4 events, the two Read rows' tool name, arguments, output and intent rendering), Agent → delegation (subagent: 2 events, the row's tool name and sub-agent type). Event and row counts are equal in every cell; provider/unhandled is flat. Co-Authored-By: Claude <noreply@anthropic.com>
e235d67 to
a3f8d7d
Compare
|
Coordinator review — APPROVE (SlopCop disabled; this review is the gate). Oracle — as clean as codex's: claude parity 13/13 with 27 allowlist entries in three named classes, zero unlisted, zero stale; codex 16/16 and acp 10/10 with zero new diffs (you did not touch them); A4 zero row diffs across all 307 production threads; G11 flat on every cell. Recorded conformance green for all 14 claude cells; self-suite 43/43 with row-count pins unchanged. On the zero corpus diffs — you did this better than I specified. I told you to expect claude row diffs and allowlist them. You got zero, and I verified why rather than accept it: your thread-view changes are additive — suppression is Fixtures: 12 redacted transcripts (1,076 messages, 1.85 MB) converted from Sawyer's own corpus sessions, with a manifest naming every source and a pinned projection per fixture. I swept the 12 new files independently: zero survivors — my one scare was my own unanchored Wire: HOST_DAEMON stays 150 — correct, translation-only, no daemon change. Honest gaps noted (not this PR's debt): live QA cells; Lands after #2164 codex (rebase onto its vendor-dropped head first). Sawyer merges; I do not.
|
…ridge lanes The claude-code recordings' bridge→runtime.current.ndjson lanes are what this checkout's bridge emits (pnpm rerecord --plan-with <main checkout>); the recordings themselves are untouched and the self-suite's row-count pins are unchanged. recordings/parity-allowlist.json names every intended byte-inequivalence against main with #2178 and a reason: presentation on items (9 cells), Read → fileRead (plan-mode: 4 events, the two Read rows' tool name, arguments, output and intent rendering), Agent → delegation (subagent: 2 events, the row's tool name and sub-agent type). Event and row counts are equal in every cell; provider/unhandled is flat. Co-Authored-By: Claude <noreply@anthropic.com>
a3f8d7d to
e6387bd
Compare
…ator fixtures A Claude Code transcript is a persisted conversation (user/assistant records with tool_use and tool_result blocks, a few system notices, bookkeeping rows), not the streaming SDK message shapes the claude-code bridge consumes: it has no `result`, no `system/init`, no `system/task_*` family, and its subagents live in `<session>/subagents/agent-*.jsonl` sidechains. `scripts/provider-recordings/convert-claude-transcript.mjs` turns one session (plus its sidechains, interleaved by timestamp with `parent_tool_use_id` from the subagent's `toolUseId`) into the SDK stream the bridge would have seen: a `result` per segment (a root message that stops with a non-tool-use reason, the next prompt, EOF), `task_started` / `task_updated` / `task_notification` for Agent calls from the call, its result (`async_launched` ⇒ backgrounded) and the `<task-notification>` resume, `api_retry` / model-fallback / `compact_boundary` from the system records, and a `system/init` from the first record. Human prompts only delimit turns — a live stream never echoes them; CLI-injected user messages (isMeta context, task notifications) do stream and are kept. Every synthesized message is deterministic. `--turns A-B` slices a session by human-prompt turn. `convert-claude-transcripts-sample.sh` rebuilds the committed sample: 12 sessions / windows from the owner's own corpus threads (plan mode + AskUserQuestion, ExitPlanMode, WebSearch, WebFetch + Read, Edit/Write, foreground and backgrounded Agents with their sidechains, TaskCreate/ TaskUpdate + model_refusal_fallback, Workflow + Monitor + TaskStop, TaskOutput, ScheduleWakeup, SendMessage, api_retry, compact_boundary, mcp__bb-bridge__ tools), redacted with `redact.mjs` — which now also handles bare-message ndjson and the dash-encoded home directory Claude uses for its project paths — 1.85 MB, 1,081 messages. Grep, Glob, TodoWrite, MultiEdit and NotebookEdit appear in none of the 2,559 local transcripts; those paths stay covered by the scripted unit tests. `transcript-fixtures.test.ts` drives each fixture through the sdk/message envelope into a real assembler and checks the structural invariants (each tool_use opens an item its tool_result settles, sidechain items nest under the spawning call, every turn settles, nothing is left open) plus a pinned projection per fixture (item kinds, tool names, presentation coverage, provider/unhandled — which may only go down) in `expected.json`. The pins are the regression oracle the translation layers update deliberately. Co-Authored-By: Claude <noreply@anthropic.com>
…o map to the v3 kinds
Grammar v3 for the Claude Code bridge. `presentation.ts` is the one place
Claude's tool-name knowledge lives (which built-in is a command, a read, a
search, an edit, a sub-agent, a plan update, a low-value housekeeping call,
and how each reads as a row); `tool-classification.ts` maps every tool_use
to its item shape with that presentation, and `plan-fold.ts` folds the
task-list tools into plan snapshots. Core keeps no new table.
- Read → `fileRead` (the top generic tool in the corpus, 7,568 calls);
Grep → `search{content}`, Glob → `search{path}`; Edit/Write/MultiEdit/
NotebookEdit → `fileChange` with per-verb labels; Bash → `command`
(a backgrounded call is labelled as a launch); WebSearch/WebFetch as
before, now with presentation. A file read, a search and a delegation
carry no output on the canonical item.
- Agent/Task → foreground `delegation` (`childRef` = the call id, which is
how the SDK streams the sub-agent's messages: `parent_tool_use_id`);
`background: true` for `run_in_background`, settling at the launch ack on
the thread-scoped family; the child's summary is the result text without
Claude's `agentId:`/`<usage>` metadata lines; the sub-agent type and
requested model ride the presentation detail.
- TodoWrite → a collapsed call row plus a settled `planSteps` snapshot from
its arguments; TaskCreate/TaskUpdate/TaskList/TaskGet → a collapsed call
row plus a `planSteps` snapshot of the thread's folded task list after
each successful call, read from the SDK's envelope-level
`tool_use_result` (the persisted string result never parsed, so the task
banner had not worked for these in production). Channel-keyed close
deltas, the latest snapshot superseding — the same shape as codex
update_plan.
- ToolSearch, TaskOutput, Monitor, ScheduleWakeup, SendMessage,
AskUserQuestion, TodoRead, BashOutput → `tool` with
`presentation.suppress`; plan mode, Workflow, TaskStop, Skill,
StructuredOutput, worktrees, ListAgents and the rest → `tool` with their
own labels, glyphs and argument-derived titles; an unknown tool reads
`Running <tool>` / `Ran <tool>`.
- `mcp__<server>__<tool>` splits into `{ server, tool }` (bb's own server
gets its definition-driven presentation in the next layer).
- The compaction item and every close-without-open fallback carry a
presentation too; the close re-states the open's.
Thread-view keeps the new kinds rendering until the presentation-driven
projection lands (the same minimal bridge codex added for `delegation`):
`fileRead` and `search` items project to the tool row with the intents the
legacy Read/Grep/Glob calls produced; a tool call whose presentation says
`suppress` is hidden like the legacy name list (failures still render);
the todo banner reads a `planSteps` snapshot as-is. No persisted-event
projection changes: the legacy tables stay for old rows.
Pins: the scripted suites move to the v3 shapes; the transcript fixtures
pin fileRead/delegation/plan-snapshot counts and an every-started-item-
is-presented invariant per session.
Co-Authored-By: Claude <noreply@anthropic.com>
The SDK task family — dynamic workflows (the Workflow tool), backgrounded shell commands and backgrounded sub-agents — stays the core `backgroundTask` kind (the genericity rule: background work is core), and its item.open and item.close now say how the row reads: "Running workflow" / "Workflow finished" titled by the workflow name, "Running background command" / "Background command finished", "Running background subagent" / "Background subagent finished", each titled by the task description. Progress deltas carry no presentation by schema; the close re-states the open's. Model fallback and `/clear` stay the core events they are. With this every item the Claude bridge opens carries a presentation; the transcript fixtures now pin that invariant for background tasks too. Co-Authored-By: Claude <noreply@anthropic.com>
…'s presentation
Q31 for the Claude bridge. Claude names the tools bb injects through the
bridge's MCP server `mcp__bb-bridge__<name>`; the item now reads
`{ server: "bb", tool: <bare name> }`, and its presentation is the one the
server resolved onto the `DynamicTool` definition at session construction
(`experimental_presentation` → status labels → generic), which the bridge
learns through `configureInjectedTools`. A definition without one (a server
from before the field existed) presents generically under bb's glyph, and a
`bb-bridge` call the session was not constructed with still names bb as its
origin. The server's `statusLabels` enrichment skips items that carry a
server, so the row no longer relies on it.
The `bb-bridge` server name moves to the classification module; the MCP
proxy reads it from there.
Co-Authored-By: Claude <noreply@anthropic.com>
…ults The last Claude-specific result structure core had to interpret is gone: TaskCreate/TaskUpdate/TaskList/TaskGet results no longer parse into a structured `result` on the tool item (the shape thread-view's legacy todo reducer read, and which never matched in production because the persisted result was the SDK's friendly string). The tool row now carries the text result like every other tool; the plan fold reads the envelope-level `tool_use_result` (or JSON text when there is none) and the `planSteps` snapshot is the one structured form core sees. The claude task-tool schemas the bridge imported from the SDK for it are no longer referenced there; the legacy reducer in thread-view keeps reading persisted rows until the projection workstream removes it. What stays is the point, not a remnant: the bridge's knowledge of its own tool names lives in `presentation.ts`/`tool-classification.ts`, and the close-without-open fallback still needs a terminal shape for a result whose call it never saw. Co-Authored-By: Claude <noreply@anthropic.com>
…ridge lanes The claude-code recordings' bridge→runtime.current.ndjson lanes are what this checkout's bridge emits (pnpm rerecord --plan-with <main checkout>); the recordings themselves are untouched and the self-suite's row-count pins are unchanged. recordings/parity-allowlist.json names every intended byte-inequivalence against main with #2178 and a reason: presentation on items (9 cells), Read → fileRead (plan-mode: 4 events, the two Read rows' tool name, arguments, output and intent rendering), Agent → delegation (subagent: 2 events, the row's tool name and sub-agent type). Event and row counts are equal in every cell; provider/unhandled is flat. Co-Authored-By: Claude <noreply@anthropic.com>
e6387bd to
61a0a96
Compare
…rpus row snapshots, timeline perf, permission matrix (#2121) > **Stacked in the chain**: contract #2124 → WS1a #2136 → recordings #2153 → WS2a #2148 → **this PR** → codex #2164 → claude #2178 → acp. Base `bb/ws2a-registry-providerinfo-stack-on-2124-thr_hbi4kggyzb`. WS1a deleted the `ProviderAdapter` interface and the fake adapter, so the runtime permission matrix drives `handleRuntimeProviderRequest` through the real bridge-protocol adapter built for the scripted echo launch (`createProviderForId` + `createScriptedEchoLaunch`), with the initialize handshake setting `approvalEnforcedBy` and a canonical `interaction/request` on the wire. The 80 outcomes pinned on `main` are unchanged; the `tool_use` subject the v3 contract added gets 20 measured cells of its own (the type-level union guard demanded them). `resolvePermissionEscalation` now takes only the initiator, so the server table is 3 initiator cells + the 54 policy-shape cells. The row snapshots minted on `main` compare byte-identical (307/307, 0 diffs) under WS1a, #2153, and WS2a. Pre-stack history: `4af0367`. ## What was wrong The provider-plugin migration abandons byte-equivalence with the old goldens on purpose, which removes the regression oracle. Before any provider code moves, `main` needs machine checks on real data that every later layer can run: projected rows for the 307 production threads in the private corpus (A4), a timeline-build and event-size baseline, and the permission-decision matrix (A5/G12) pinned as a literal table before WS5 changes the unions. None of these existed. ## What changed No production code changes. Tests, test helpers, one script, one turbo task, docs. - **Corpus reader** — `packages/test-helpers/src/provider-corpus.ts`: `corpusAvailable()`, `listCorpusThreads({ provider?, reasons? })`, `loadCorpusThread(id)`. Reads `BB_PROVIDER_CORPUS_DIR` (`manifest.json`, `threads/<provider>/<id>/{meta.json,events.ndjson}`), validates rows at the boundary with zod (ids must be one safe path segment), resolves each thread through its manifest entry and fails unless `meta.json` and every event row agree on id, provider, reasons, and row count, and decodes event payloads with `@bb/domain` `parseStoredThreadEvent` + `buildThreadEventRow` — the same path as the server's `parseStoredEventRow`. `.gitignore` gets `**/provider-corpus/**` with re-includes for the two in-repo directories of that name. - **Row snapshots** — `apps/server/test/provider-corpus/row-snapshots.test.ts` + `corpus-harness.ts`. Each thread is inserted into in-memory SQLite with its original ids, sequences, and timestamps (raw `INSERT` so nothing is minted), then every timeline page is built via `buildThreadTimelineWithProfile` with the options the route uses — event budget from `defaultFeatureFlags`, inline-output limit from `DEFAULT_MAX_INLINE_OUTPUT_CHARS`, display name and plan command from the real provider registry (`createTestProviderRegistry()` loads the first-party plugin declarations) through `resolveProviderPlanCommand`, unhandled ops included, output truncation + preview — following the route's own `olderCursor`. Two variants per thread: `default` (turn rows summarized) and `nested` (`includeNestedRows`, children materialized). Snapshots go to `$BB_PROVIDER_CORPUS_DIR/snapshots/rows/<provider>/<threadId>.json`, keys sorted. Nothing is blanked: no wall-clock value reaches the rows, and write mode proves it by projecting every thread twice and requiring byte equality. Compare mode fails on any diff not covered by `snapshots/allowlist.json` (`threadId` | `provider` | `"*"` scope, JSON-pointer or `*`/`**` glob path, `pr`, `reason`), prints a unified diff for the first 3 differing threads plus a count, lists the entries it used, and fails on entries that cover nothing. - **Perf baselines** — `timeline-perf.test.ts`: 10 largest threads per provider, latest page and full page walk, 5 profiled builds each after a warm-up, stage p50s from `ThreadTimelineBuildProfile`, persisted `data` bytes median/p95/total, rows produced. Written to `snapshots/perf-baseline.json`; compare fails at baseline × 1.10 for build cost and × 1.15 for median event size. **Deviation from the brief, with data:** the gate uses a normalized cost — min build time ÷ min time of a fixed CPU workload that shares no code with the timeline (JSON codec + sort over a deterministic document, run once per sample right before the builds) — not raw p50/p95. Raw p50 of the same commit swung up to 30% between two back-to-back runs on this 16-core box at load ~6 (14 of 20 threads tripped a literal 1.10 gate on the very next run); each side's minimum discards its own contended samples, interleaving keeps both minima in one short window, and a workload outside the timeline path means a uniform regression still moves the ratio. Raw p50/p95 are still recorded and printed. Up to 3 attempts per thread (write mode keeps the median attempt; compare stops at the first pass), a 5 ms floor for tiny latest-page builds, and compare mode refuses a baseline written with different gate settings. The table header reports the load average and flags an oversubscribed machine. - **CI micro-benchmark** — same file, no corpus: `synthetic-thread.ts` builds a 10,019-event thread (every item kind, deltas, background tasks, usage events) and walks all 12 pages. Gate: minimum of 5 walks under 1,500 ms (local minimum 150–170 ms; the ceiling is ~10× so a slow runner passes while a quadratic regression still fails). - **Permission matrix** — `packages/agent-runtime/src/permission-matrix.test.ts`: the runtime chokepoint `handleRuntimeProviderRequest` over permission policy (5 members of the discriminated union) × approval subject (5: command, file_change, permission_grant, plan, tool_use) × `approvalEnforcedBy` (2) × deny availability (2) = 100 cells, each a literal; `satisfies Record<CellKey, Outcome>` plus `SameUnion` type guards against the domain unions (dropping a row or a union member fails `tsc`, verified on the stack), plus runtime assertions that every local subject kind parses as a payload. The request goes through WS1a's real bridge-protocol adapter. `apps/server/test/permissions/permission-matrix.test.ts`: `resolvePermissionEscalation` over the 3 initiators and the 54-cell runtime-permission-policy shape cross product (5 accepted), with the reviewer vocabulary pinned to the policy union at the type level. - **Seeds** — `scripts/provider-corpus/snapshot-rows.sh [write|compare]`, `@bb/server#test:provider-corpus` (uncacheable, `passThroughEnv` for the two variables — strict turbo env mode strips them from the plain `test` task), and a "Provider corpus" section in `docs/debugging-and-qa.md`. ### Permission matrix (runtime) Outcome: `forward` = reaches `onInteractiveRequest` (user decides); `auto-deny` = runtime answers deny; `encode-error` = runtime wants to auto-deny but `deny` is not in `availableDecisions`, so the provider gets a JSON-RPC error. Subject kind (command, file_change, permission_grant, plan, tool_use) never changes the outcome, so the table is collapsed over it (each row below is 5 cells). | policy (mode/escalation) | approvalEnforcedBy | deny available | outcome | | --- | --- | --- | --- | | accept-edits/ask | runtime | yes / no | forward | | accept-edits/ask | provider | yes / no | forward | | accept-edits/deny | runtime | yes | **auto-deny** | | accept-edits/deny | runtime | no | **encode-error** | | accept-edits/deny | provider | yes / no | forward | | auto/ask | runtime | yes / no | forward | | auto/ask | provider | yes / no | forward | | auto/deny | runtime | yes | **auto-deny** | | auto/deny | runtime | no | **encode-error** | | auto/deny | provider | yes / no | forward | | full/– | runtime | yes / no | forward | | full/– | provider | yes / no | forward | Bridge-kit predicate: `shouldAutoDenyInteractiveRequest` → ask: false, deny: true, null: false. Server escalation by initiator: user → ask; agent → deny; system → deny. (On `main` the function also took the thread and was measured identically for root, delegated-child, and fork threads; WS1a removed the unused argument.) Accepted runtime policy shapes (5 of 54): accept-edits/workspace/user/{ask,deny}, auto/workspace/automatic/{ask,deny}, full/full/–/–. Observations (pinned, not fixed): 1. Permission **mode** never auto-decides anything in the runtime or the server. Only escalation does, and escalation is purely a function of the turn initiator. Mode is translated into provider-native settings (codex `approvalPolicy`/sandbox, Claude SDK `permissionMode`) and enforced by the provider. So a runtime-enforced provider in `full` mode that did send an approval would prompt the user. 2. The `encode-error` cells: on an agent- or system-initiated turn, a runtime-enforced provider whose approval omits `deny` receives a JSON-RPC error instead of a decision. Codex forwards the provider's own decision list, so this is reachable in principle; no first-party bridge omits deny today. 3. `plan` approvals are treated like any other subject by the runtime: on a system-initiated turn with a runtime-enforced provider they are auto-denied. Claude is provider-enforced and always forwards `ExitPlanMode`, so only codex plan approvals can hit this. ## How you verified - `pnpm exec turbo run typecheck --filter=@bb/server --filter=@bb/test-helpers --filter=@bb/agent-runtime` — 6 tasks successful. - On the WS1a stack (`--concurrency 4`): `pnpm exec turbo run typecheck --filter=@bb/agent-runtime --filter=@bb/server --filter=@bb/test-helpers` green; `pnpm exec turbo run test --filter=@bb/agent-runtime --filter=@bb/test-helpers` — 31 files, 434 tests passed (102 of them the matrix); `pnpm exec turbo run test --filter=@bb/server` — 197 files, 1,886 tests passed, the one failure again the local umask assertion. With the corpus set, the row snapshots minted on `main` compare byte-identical under WS1a's v3 assembler: 307/307, 0 diffs. - On `main` before the stack, corpus absent: `pnpm exec turbo run test --filter=@bb/agent-runtime --filter=@bb/test-helpers` — 32 files, 504 tests passed (82 of them the matrix). `pnpm exec turbo run test --filter=@bb/server` — 196 files passed, 1 skipped (the row-snapshot suite), 1,884 tests passed; the single failure is the pre-existing `internal-skill-trees` file-mode assertion (this checkout's umask 0002 yields 0664 where the test expects 0644; it fails on clean `main` here and passes in CI). The two corpus suites report as skipped; the synthetic benchmark runs (`Synthetic 10019-event thread: 12 pages, 1165 rows projected, full walk p50 174 ms`). - Corpus present: `scripts/provider-corpus/snapshot-rows.sh compare` → 2 files, 328 tests passed (307 row snapshots + 20 perf threads + 1 synthetic). - Row snapshot write mode: **307 threads, 93,262 rows (top-level + nested children), 270.5 MB (283,656,293 bytes), 96.9 s wall**; compare mode on the same commit: 73.1 s, 0 diffs. After switching the harness from copied provider literals to the real registry, compare was still 307/307 with 0 diffs — the snapshots are byte-identical. - Perf gate on the final estimator: baseline written at load 9.2, compare on the same commit at load 5.7 passed 20/20 threads on the first attempt, ratios 0.80–1.09× of baseline. Every one of the 330,626 corpus events decodes with today's `parseStoredThreadEvent` (426 MB of `data`). - Allowlist machinery exercised by mutating one snapshot: compare fails with the unified diff; an entry covering `/variants/*/pages/*/rows/*/text` for that thread passes; a stale entry fails with "every snapshots/allowlist.json entry must cover at least one diff". - Exhaustiveness guards exercised: deleting one matrix row → `TS2741 Property '"full/-|plan|provider|deny-unavailable"' is missing`; dropping `"provider"` from the enforcer list → `SameUnion` becomes `false` and the `satisfies` rejects the extra keys. ### Perf baseline (write mode, this commit, load 9.2/16 cores; ms; norm = min build ÷ min `json-sort-v1` calibration) | thread | provider | events | data bytes p50/p95 | latest rows | latest p50/p95 ms | latest norm | pages | walk rows | walk p50/p95 ms | walk norm | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | thr_62bp724ett | claude-code | 10207 | 570/6375 | 18 | 23.3/27 | 0.103 | 8 | 241 | 922.3/1228 | 4.855 | | thr_zqfgksiw39 | claude-code | 9012 | 365/3587 | 50 | 90.5/119 | 0.388 | 7 | 279 | 532/564.4 | 2.466 | | thr_zc9yxt8mwk | codex | 8795 | 316/3025 | 10 | 24.9/58.3 | 0.102 | 5 | 45 | 323.1/395.5 | 1.510 | | thr_j4hfucc5bb | claude-code | 7679 | 331/1285 | 52 | 29.9/35.5 | 0.132 | 8 | 335 | 404.6/472.4 | 1.591 | | thr_f9jfidwbx8 | claude-code | 6163 | 454/4912 | 26 | 19.5/23.7 | 0.156 | 6 | 132 | 198.7/225.5 | 1.720 | | thr_w6i678ha85 | claude-code | 5826 | 371/2820 | 19 | 25.7/40 | 0.204 | 3 | 37 | 149.6/172.4 | 1.163 | | thr_qvau3b2d5b | claude-code | 5685 | 355/3613 | 111 | 36.9/50.4 | 0.298 | 5 | 168 | 154.3/173.4 | 1.311 | | thr_2pkc3q5imw | codex | 5487 | 316/3642 | 3 | 32.9/36.6 | 0.307 | 4 | 15 | 87/89.5 | 0.808 | | thr_7wvd28jzpj | claude-code | 5015 | 345/3497 | 36 | 25.8/31.6 | 0.219 | 3 | 94 | 130.7/137.4 | 1.102 | | thr_nbghhtnurj | codex | 4341 | 316/8824 | 11 | 20.8/27.8 | 0.180 | 4 | 35 | 90.4/94.6 | 0.849 | | thr_wb9c2q8gm4 | claude-code | 4228 | 378/2469 | 65 | 31.3/36.4 | 0.268 | 3 | 150 | 86.3/89 | 0.801 | | thr_ksr44swb3q | codex | 4063 | 316/4513 | 10 | 18.7/20.2 | 0.178 | 3 | 23 | 64/67.4 | 0.589 | | thr_tsfz8rtt72 | codex | 4055 | 316/4571 | 18 | 20.6/22 | 0.185 | 2 | 29 | 71/74.3 | 0.633 | | thr_qe8y73b26t | claude-code | 3880 | 245/991 | 2 | 2.8/2.9 | 0.026 | 2 | 9 | 54.3/55.5 | 0.520 | | thr_n3xqyz69nk | claude-code | 3454 | 341/4341 | 42 | 31.6/35.4 | 0.307 | 4 | 59 | 84.8/87.8 | 0.824 | | thr_nm27yahx34 | codex | 3314 | 174/5030 | 17 | 14.3/17.5 | 0.135 | 3 | 61 | 54.6/56.3 | 0.519 | | thr_ssix3mhxk5 | codex | 3088 | 316/3703 | 44 | 12.8/15 | 0.122 | 3 | 73 | 50/52.5 | 0.478 | | thr_72dhrw3s8a | codex | 3067 | 174/1659 | 5 | 35/37.3 | 0.305 | 2 | 14 | 45.6/63.9 | 0.430 | | thr_u8agadfekn | codex | 2892 | 334/2361 | 5 | 51.2/52.5 | 0.453 | 1 | 5 | 49/49.5 | 0.433 | | thr_me47xr5c3y | codex | 2466 | 316/4318 | 55 | 21.2/22.1 | 0.202 | 2 | 99 | 35.1/36.5 | 0.347 | Part of the provider-plugin migration: the Step 0 baselines PR from the design spec's "Regression confidence" section. No tracking issue. > AGENT GENERATED: by Claude Code (claude-marshmallow-ht-eap) --------- Co-authored-by: Claude <noreply@anthropic.com>
…bridge to grammar v3 with presentation (#2164) Stacked on #2121 (corpus harness + permission matrix, `bb/provider-baselines`) ← #2148 (WS2a) ← #2153 (recordings) ← #2136 (WS1a) ← #2124 (contract). WS1b-codex of the provider-plugin migration: the codex bridge speaks grammar v3 with a presentation on every item, its natives map to the core kinds, bb-injected tools carry their presentation, goals and the macOS permission profile are codex extension kinds, and the codex v2 path is deleted. **Do not merge.** Coordinator reviews, Sawyer merges the stack. ## Stack contract #2124 → WS1a #2136 → recordings #2153 (incl. #2177 and the `PARITY_INITIALIZE_ID` follow-up) → WS2a #2148 → corpus harness #2121 → **this PR** → WS1b-claude #2178. Six commits: the five codex layers below plus one `parity:` commit carrying this PR's allowlist entries and the codex `bridge→runtime.current.ndjson` lanes re-recorded against this bridge with #2153's `pnpm rerecord --provider codex --plan-with <main checkout>`. No vendored harness copies remain (they were needed only while #2153 was outside the stack). The A4 corpus check was run from a throwaway worktree with #2121's harness cherry-picked on top, since #2121 is not in the stack. ## What was wrong The codex bridge emitted v2-shaped items: no presentation (core thread-view kept codex's tool-name tables), native sub-agents as `tool` items named `spawnAgent`, `update_plan` as a turn-level event the UI discards, goals as core `thread/goal/*` events, bb-injected tools without `server`, and a macOS permission profile on a command approval failed the whole approval. Open work rode an out-of-band `thread/openWork` notification. ## What changed (one commit per layer; the last deletes) 1. **Presentation on every item.open/close** — `plugins/provider-codex/src/presentation.ts` is the one place codex tool-name knowledge lives: shell commands (wrapper stripped from the headline), file edits, the bundled `node_repl` server ("Ran JavaScript" with the call's title), other MCP servers by tool name, dynamic tools, collab verbs, web search/fetch, image views, reasoning, messages, plans, compactions, the synthesized sub-agent spawn. An invariant suite drives one item per codex native through the real translator and asserts every lifecycle delta carries one. 2. **Delegation + planSteps** — the synthesized native sub-agent is a foreground `delegation` (`childRef` = agent thread id, `label` = agentPath); a follow-up to a settled agent re-opens the same item and the row closes only when the agent owes nothing more (the exact open-work predicate); a dead app-server child settles its open delegations as failed on the wire. A collab call that names its receiver is a delegation to it; a bare `wait` stays a tool item with its collab presentation. `turn/plan/updated` becomes a settled `planSteps` snapshot per update. `RuntimeBackgroundWorkState` counts a pending delegation as open work. **Thread-view projects a `delegation` item to the existing delegation row with the child content nested** — without this the row and every child message under it vanish before the projection workstream lands (the projection suppresses orphans with a `parentToolCallId`). `planSteps` items are not projected yet (status quo: codex plans were discarded). 3. **bb-injected tools carry their presentation** (Q31) — `bb.agents.registerTool({ experimental_presentation })` (+ `docs/api_to_audit.md`); the server resolves one presentation per tool at its boundary (declaration → status labels → generic label + the plugin's branding glyph / `Toolbox`) onto `DynamicTool.presentation`; the codex bridge emits calls to injected tools as `{ server: "bb", tool }` with that presentation. ask-user-question and workflows declare theirs (AskUserQuestion and `bb_workflow_result` collapse by default). `HOST_DAEMON_PROTOCOL_VERSION` 149 → 150 (149 is WS2a's #2148, this PR's base; the history reads 147 → 148 → 149 → 150). The field is optional and `dynamicToolSchema` is not strict, so an older daemon strips it and keeps working; the bump follows the repository rule for a widened server↔daemon wire. Optional per A1: every committed recording's runtime lane predates the field and must keep replaying; the stabilization pass makes it required. 4. **Goals + macOS profile as codex extension kinds** — `provider-codex/goal` (state; `null` once cleared) and `provider-codex/macos-permission` (item) declared on the registration with plugin-owned zod schemas (`extension-kinds.ts`). The bridge emits goals as extension state. A command approval asking for macOS capabilities now reaches the user for the command; the profile rides the timeline as its own row saying bb cannot grant it (the plugin-rendered approval is WS5's). **Read-time conversion in core:** `parseStoredThreadEvent` decodes persisted `thread/goal/updated|cleared` rows into the extension state (the one path every stored-event read takes); thread-view goal extraction and the runtime's goal-clear wait read that state. The sidebar's latest-goal query becomes a latest-thread-state-by-kind query (partial index widened to `thread/extensionState/updated`, migration **0106** `thread_state_index` — regenerated with Drizzle so its snapshot chains from WS2a's 0105 `provider_settings_to_plugins`; `json_extract` kind filter). Verified against the corpus: the 721-row goal thread renders its goal unchanged. 5. **Delete the codex v2 path** — `thread.goal`/`thread.goalCleared` leave the grammar and assembler (G3 snapshot updated; `PROVIDER_BRIDGE_PROTOCOL_VERSION` stays 2 under the grammar range, as WS1a's v2 deletion did); the `thread/openWork` notification leaves the protocol, adapter and reaper (an unknown notification is ignored); the bridge drops its open-work reporting and the hard-coded AskUserQuestion presentation. Kept: the bridge's knowledge of its spawn/resume collab verbs for a receiver-less call — tool-name knowledge in the bridge is the point, not a remnant. The `thread/goal/*` domain event types remain as read-only legacy vocabulary. Naming note: the spec says `codex/goal`; the extension-kind namespace is the **plugin id**, which is `provider-codex`, and that is how the registry resolves the schema. ## Regression oracle All turbo invocations with `--concurrency 4`; perf suite ignored (known-noisy, flagged on #2121). - **Conformance**: codex scripted suite + recorded conformance over all 17 recorded cells (incl. archived-resume, auth-failure, empty-rollout, missing-rollout) green. - **Parity (A2)**, `pnpm parity --old <origin/main worktree at f6fb434> --new . --provider codex`: **16 passed, 0 failed, 1 skipped** (process-scoped `model-list`). Event and row counts equal in every cell. `recordings/parity-allowlist.json` names every intended byte-inequivalence with `#2164` and a reason — 26 entries, four classes: `presentation` on items (15 cells), `server: "bb"` on the AskUserQuestion call (user-question), the sub-agent spawn as a `delegation` item + its row (`subagent`, events `/6` `/28`, rows `/0/children/0/{toolName,output}`), and `thread/goal/cleared` → `provider-codex/goal` extension state (one event index in each of 6 goal-bearing cells). Zero unlisted diffs, zero stale entries. claude-code **13/13** and acp-cursor **10/10** replay against main with **zero diffs** (no entries). - **Corpus (A4)**, 307 threads / 93,262 rows: **zero diffs**, claude-code and codex alike; no corpus allowlist entry was needed (the only read-time change, goals, projects identically by design). - **G11**: `provider/unhandled` flat on every codex cell (0→0; auth-failure 1→1) and on the corpus. - **G1**: 209 → 209 (no provider-id literal added or removed in core; `"provider-codex/goal"` is not a provider-id literal by the ratchet's regex). - **Parity self-suite** (#2153's, in the stack): 43/43 with the codex current lanes re-recorded against this bridge; row-count pins **unchanged** (every current lane assembles to exactly the pinned counts); the claude/acp current lanes #2153 produced on WS1a's bridges hold as they are. Tests on the final stack (all forced, `--concurrency 4`, one turbo at a time): db 406 (migration chain 0104 → 0105 → 0106 incl. replay-on-existing-DB cases), agent-runtime 337, codex 193 (scripted + recorded conformance), host-daemon-contract 52, provider-parity 43; typecheck green for codex, agent-runtime, provider-parity, server, db. Earlier full sweep before the re-stacks: domain 150, thread-view 382, provider-bridge-protocol 218, plugin-sdk 127, host-daemon 552, integration 55, claude-code 262, acp 182, workflows 223, ask-user-question 36, scripted echo 1; server 1828/1829 (the one failure is the known local umask case in `internal-skill-trees`, passes in CI); typecheck green across 25 packages incl. app, mobile, cli. ## Not done - **Live QA cells via bb-dev-app** (turn/steer/stop/approve/deny/question/subagent/resume/fork/plan with screenshots) — not run; the coordinator schedules them separately. - `planSteps` rows are not projected by thread-view (no regression: codex plans were discarded before); the projection workstream owns it. - The plugin-rendered macOS approval (the profile round-trips only as a visible row today) — WS5. > AGENT GENERATED: by Claude Opus 5 --------- Co-authored-by: Claude <noreply@anthropic.com>
…on (#2179) > **Stacked on #2178 (WS1b-claude).** Chain: contract → WS1a → WS2a → codex #2164 → claude #2178 → **this PR** (6 commits above claude head `61a0a9656`). Do not merge before the chain beneath it lands. ## What was wrong The ACP bridge threw away the agent's native tool-call kind (`read`, `edit`, `execute`, `search`, `fetch`, `think`, `other`) and wrote the human `title` into the `tool` slot of a generic `tool` item (`provider-acp/src/delta-translation.ts`, the old `classifyAcpToolCall`). In the production database that is ~1,100 ACP rows whose "tool name" is ``Read `/path/to/file` ``, plus `Web search:`, `Updating plan`, `MCP: tool`, and `tool`. Generic tool permissions were faked as `command` approvals (`interactions.ts`) because that was the one subject with free text. No ACP item carried a presentation. ## What changed One commit per layer; the last deletes. 1. **Presentation on every item** — `plugins/provider-acp/src/presentation.ts`: a label pair and host glyph per native kind, commands with the agent's Markdown code ticks stripped from the headline, file changes by verb (write/edit/delete) with their file names, plan snapshots, compactions. Every `item.open`/`item.close` carries it. 2. **Native kind → core kinds; title → `presentation.title`** — `plugins/provider-acp/src/tool-classification.ts`: `execute` → `command`, diff / `edit` / `delete` with a path → `fileChange` (the shared operation classifier the permission mapping uses is untouched, #1803), `read` → `fileRead` when the path is known (`locations`, a `rawInput` path field, or a single code-ticked token in the title — how grok titles its reads), `search` → `search` when `rawInput` has a pattern/query/regex (content) or glob (path), `fetch` → `webFetch` when `rawInput` or the title carries the URL, `think` → `reasoning`. Everything else (`other`, `move`, no kind, or a kind whose shape the agent left unfilled — Cursor's `read`/`fetch` arrive with an empty `rawInput` and no `locations`) is a generic `tool` whose `tool` slot is the kind and whose presentation is the kind's label with the agent's title as headline. A row is never a `fileRead` without a path. ACP `plan` updates become settled `planSteps` snapshots (channel-keyed, collapsed by default, read by the todo banner) instead of `turn.plan`. 3. **bb-injected tools (Q31)** — the translator learns the session's `dynamicTools` at construction and binds each call the MCP proxy forwards (`forwardDynamicToolCall` → `noteInjectedToolCall`) to the agent's announcement. ACP gives no id linking the two (Cursor announces every MCP call as `"MCP: tool"`, kind `other`), so the binding is positional: the unbound `other` candidate whose title names the tool, else one that mentions MCP, else the oldest; a proxied call with no candidate open waits for the next announcement. A bound call's open or close reads `{ server: "bb", tool, presentation }` from the `DynamicTool` definition (generic under bb's glyph when the definition predates the field); the close re-states it, so late binding still settles as the bb tool. 4. **`tool_use` approval subject** — `interactions.ts` raises `{ kind: "tool_use", itemId, tool, presentation }` for every permission that is neither a command nor a file change, with the same classification the timeline row gets. `apps/server/.../pending-interaction-timeline.ts` stops throwing on the subject: a tool-use approval has no timeline item of its own (the agent's own `tool_call` row is the record; the banner renders the presentation) until WS5's interaction-lifecycle event. The runtime conformance harness validates the subject instead of throwing; the "no producer yet" comments on the client renderers are updated. 5. **Delete the ACP v2 translation path** — ACP was the last `turn.plan` speaker, so the grammar member leaves `thread-delta.ts` and the assembler under the grammar range (the way #2164 dropped `thread.goal`/`thread/openWork`); `PROVIDER_BRIDGE_PROTOCOL_VERSION` stays 2, the G3 snapshot loses the entry. The persisted `turn/plan/updated` type stays as read-only history the view already ignores. **Wire:** `HOST_DAEMON_PROTOCOL_VERSION` stays at 150. Nothing this PR sends between server and daemon changed shape: `tool_use` has been in the pending-interaction schema since the contract PR, and `DynamicTool.presentation` rode the wire since #2164. **Interim UX to know about:** until the presentation-driven projection lands, the legacy tool row shows the `tool` slot as its name, so a generic ACP row reads `other`/`read`/`fetch` where it used to read the agent's title (the title is in the persisted `presentation.title`, which that projection will read). A denied `tool_use` approval leaves no denied row in the timeline (the old fake command did); the agent's own `tool_call_update` (usually `failed`) is the record. ## How you verified Typecheck (turbo, `--concurrency 4`): `bb-plugin-provider-acp`, `bb-plugin-provider-codex`, `bb-plugin-provider-claude-code`, `@bb/agent-runtime`, `@bb/server`, `@bb/provider-bridge-protocol`, `@get-bb/plugin-sdk`. npm version guard: PASS (0.4.11). eslint + prettier on every changed file. Tests: `bb-plugin-provider-acp` 196/196 (new: presentation coverage, native-kind mapping incl. title-path/URL recovery and the unfilled-shape fallbacks, bb-injected binding in Cursor order / proxy-first order / by-name / never-a-command, `tool_use` subjects incl. bound bb tool and in-flight-call headline); `@bb/provider-bridge-protocol` 217/217; `@bb/agent-runtime` 439/439; `@bb/thread-view` 385/385; `@bb/domain`, `@bb/core-ui`, `@get-bb/plugin-sdk`; `@bb/server` pending-interactions / internal-interactive-requests / public-thread-interactions (65/65, new: tool_use approval registers, appends no item event, resolves with deny). **Conformance (acp-cursor):** scripted protocol suite against the fake agent: pass. Recorded conformance: all 11 committed cells replayed through this bridge (10 replayable + process-scoped model-list), every rule passes. **Parity A2** (`pnpm parity --old ~/.bb/parity-old-main --new . --provider <id>`; old = `origin/main` f6fb434): - acp-cursor **10 passed, 0 failed, 1 skipped**, zero unlisted, zero stale. Allowlist entries (17, all `#2179`): - `events /*/item/presentation` × approval-allow, approval-deny, steer, stop-interrupt, subagent, turn-tools, user-question, web-search — presentation is new; items otherwise byte-identical. - `events /*/item/tool` × subagent, turn-tools, user-question, web-search — `"Task: Subagent task"`→`other`, `"Read File"`→`read`, `"Edit File"`→`edit`, `"MCP: tool"`→`other`, `"Web Fetch"`→`fetch`; the title now rides `presentation.title`. - `rows /0/children/1/toolName` × subagent, turn-tools, user-question, web-search and `rows /0/children/3/toolName` × turn-tools — the legacy row projects the tool slot. - codex **16 passed, 0 failed, 1 skipped** — zero new diffs, zero stale. - claude-code **13 passed, 0 failed, 1 skipped** — zero new diffs, zero stale. - acp-cursor `bridge→runtime.current` lanes re-recorded (`pnpm rerecord --provider acp-cursor --plan-with ~/.bb/parity-old-main`); parity self-suite 43/43; `row-counts.json` pins unchanged. **Corpus A4:** `BB_PROVIDER_CORPUS_SNAPSHOT=compare` → 307/307 threads, 93,262 rows, `diffThreads: []`, `allowedDiffs: 0`; timeline-perf gate 21/21 (load 4.4 on 16 cores). **A4 cannot prove ACP:** the extracted corpus is codex (166) + claude-code (141) only; the 29 ACP production threads (acp-grok 25, acp-cursor 2, acp-pi-acp 2) were not extracted. What A4 proves here is that this PR does not touch codex or claude rows. **G11:** acp-cursor `provider/unhandled` 0 → 0 in every cell (pins unchanged); the one pre-existing grammar drop in turn-tools is 1 → 1. The production ACP unhandled rows (155, all grok: `tool_call_update` 83, `agent_message_chunk` 37, `agent_thought_chunk` 35) are outside-turn/visibility residue the kind mapping does not touch; no ACP corpus exists to re-measure them. ## What the recordings cannot prove (needs live QA) - **The native-kind → core-kind class is not in any recording.** Cursor's `read`/`fetch` calls carry no path/URL, so in acp-cursor every such row stays a generic `tool` (the `/*/item/tool` class above). `fileRead`, `webFetch`, `search`, `reasoning` from ACP are covered by unit tests only. The "every read/edit/execute row changes kind" class the spec predicted did not materialize on this agent; grok (which titles reads with the path) is the agent that will exercise it. - **Q31 binding is not replayable.** The recorded user-question cell had 1 `item/tool/call`; a replay has 0 because the MCP proxy socket is not part of a recording, so the replayed row is `other`, not `ask_user_question`. The positional binding is unit-tested in both orders; live QA with Cursor + ask-user-question is needed. - **`tool_use` is not in any recording:** both approval cells are `execute` (command subject). Unit tests + the server lifecycle test cover it; no generic-permission cell exists for acp. - **No plan-mode, compaction, or auth-failure cell for acp** (not capturable at recording time); **fork is recorded as the agent's refusal** (`fork: "tip"`, 0 events) and stays that way. - The positional Q31 binding is a heuristic: two parallel MCP calls announced in a different order than they execute would swap labels (never the flow). > AGENT GENERATED: by Claude Opus 5 --------- Co-authored-by: Claude <noreply@anthropic.com>
Stacked on #2164 (WS1b-codex,
bb/ws1b-codex-codex-bridge-to-v3-stack-on-2136-thr_xnid5ftd87), which sits on WS2a #2148 → the corpus harness #2121 → the recordings harness #2153 → WS1a #2136 → the contract #2124 (SDK 0.4.11 at the bottom). WS1b-claude of the provider-plugin migration: the Claude Code bridge speaks grammar v3 with a presentation on every item, Claude's tool-name knowledge moves out of core thread-view into the bridge, and the bridge's last Claude-specific result structure is deleted.Do not merge. Coordinator reviews, Sawyer merges the stack.
Stacked on codex rather than WS1a on purpose: codex added the minimal thread-view projection of
delegationitems (row + nested child content) that Claude'sAgent/Tasksub-agents need. The recordings + parity harness beneath this PR is the real #2153 (in the chain, withpnpm rerecordand thePARITY_INITIALIZE_IDfix); the one harness file this PR touches isredact.mjs(bare-message ndjson, dash-encoded home paths). The corpus harness #2121 is in the chain too; the A4 run below is native to this tree.What was wrong
The Claude bridge emitted v2-shaped items: no presentation (core thread-view kept Claude's tool-name tables —
Read/Grep/Globintents, theAgent/Taskdelegation row, the TodoWrite/Task* suppression list and todo reducer),Readas an opaquetoolitem (the top generic tool in the production corpus: 7,568 calls),Agentas atoolitem named Agent, TodoWrite/TaskCreate/TaskUpdate as tool rows a core reducer had to understand, bb-injected tools asmcp__bb-bridge__<name>withoutserver, and a structured task-toolresultthat core parsed — and which never matched in production, because the persisted result was the SDK's friendly string, so the task banner did not work for the Task tools.What changed (one commit per layer; the last deletes)
scripts/provider-recordings/convert-claude-transcript.mjsturns a~/.claude/projectssession (plus its<session>/subagents/agent-*.jsonlsidechains, interleaved by timestamp withparent_tool_use_idfrom the subagent'stoolUseId) into the SDK stream the bridge would have seen. A transcript has noresult, nosystem/initand notask_*family, so the converter synthesizes them deterministically: aresultper segment (a root message that stops with a non-tool-use reason, the next prompt, EOF),task_started/task_updated/task_notificationfor Agent calls from the call, its result (async_launched⇒ backgrounded) and the<task-notification>resume,api_retry/model-fallback/compact_boundaryfrom the system records. Human prompts only delimit turns — a live stream never echoes them (verified against every committed recording); CLI-injected user messages (isMeta context, task notifications) do stream and are kept.convert-claude-transcripts-sample.shrebuilds the committed sample: 12 sessions/windows, 1,076 messages (331 sidechain records), 1.85 MB after redaction, all from the owner's own corpus threads: plan mode + AskUserQuestion + ExitPlanMode, WebSearch, WebFetch + Read, Edit/Write, foreground and backgrounded Agents with their sidechains, TaskCreate/TaskUpdate +model_refusal_fallback, Workflow + Monitor + TaskStop, TaskOutput, ScheduleWakeup, SendMessage,api_retry,compact_boundary,mcp__bb-bridge__tools. Grep, Glob, TodoWrite, MultiEdit and NotebookEdit appear in none of the 2,559 local transcripts; those paths are covered by scripted unit tests.transcript-fixtures.test.tsdrives each fixture through thesdk/messageenvelope into a real assembler and checks structural invariants (every tool_use opens an item its tool_result settles, sidechain items nest under the spawning call, every turn settles, nothing left open, every started item presented) plus a pinned projection per fixture inexpected.json(item kinds, tool names, plan snapshots,provider/unhandled, which may only go down).plugins/provider-claude-code/src/presentation.tsis the one place Claude's tool-name knowledge lives;tool-classification.tsmaps every tool_use to its shape with that presentation.Read→fileRead;Grep→search{content},Glob→search{path};Edit/Write/MultiEdit/NotebookEdit→fileChangewith per-verb labels;Bash→command(a backgrounded call is labelled as a launch);WebSearch/WebFetchas before with presentation.Agent/Task→delegation(childRef= the call id, which is how the SDK identifies the sub-agent's stream:parent_tool_use_id;background: trueforrun_in_background, settling at the launch ack on the thread-scoped family; summary = the result text without Claude'sagentId:/<usage>lines; sub-agent type and model in the presentation detail).TodoWrite→ a collapsed call row plus a settledplanStepssnapshot from its arguments;TaskCreate/TaskUpdate/TaskList/TaskGet→ a collapsed call row plus aplanStepssnapshot of the thread's folded task list (plan-fold.ts, reading the SDK's envelope-leveltool_use_result) after each successful call — channel-keyed close deltas, the latest superseding, the same shape as codexupdate_plan.ToolSearch,TaskOutput,Monitor,ScheduleWakeup,SendMessage,AskUserQuestion,TodoRead,BashOutput→toolwithpresentation.suppress; plan mode, Workflow, TaskStop, Skill, StructuredOutput, worktrees, ListAgents →toolwith their own labels/glyphs/titles; an unknown tool readsRunning <tool>/Ran <tool>.mcp__<server>__<tool>splits into{ server, tool }. The compaction item and every close-without-open fallback carry one too; the close re-states the open's. Thread-view keeps the new kinds rendering until the presentation-driven projection lands (the same minimal bridge codex added fordelegation):fileRead/searchitems project to the tool row with the intents the legacy Read/Grep/Glob calls produced (tested equal to the legacy rows); a tool call whose presentation sayssuppressis hidden like the legacy name list (failures still render); the todo banner reads aplanStepssnapshot as-is. No persisted-event projection changed: the legacy tables stay for old rows (G1 unchanged).backgroundTaskkind (genericity rule) and say how they read on open/close. Model fallback and/clearstay the core events they are.mcp__bb-bridge__<name>call is{ server: "bb", tool: <bare name> }with the presentation the server resolved onto theDynamicTooldefinition, learned throughconfigureInjectedToolsat session construction; a definition without one presents generically under bb's glyph. The server'sstatusLabelsenrichment skips items with a server, so nothing relies on it.result(the shape core's legacy todo reducer read) is gone; the tool row carries the text result like every tool andplanStepsis the one structured form core sees. The bridge no longer imports the SDK's claude task-tool schemas. Kept on purpose: the bridge's knowledge of its own tool names, and the close-without-open fallback.HOST_DAEMON_PROTOCOL_VERSIONstays 150: nothing on the server↔daemon wire changed (translation and an item-shape change inside thethread/deltalane only).Regression oracle
All turbo invocations with
--concurrency 4; perf suite ignored (known-noisy, flagged on #2121). Old leg: anorigin/mainworktree atf6fb434ab.bridge→runtime.current.ndjsonlanes are re-recorded with this bridge (pnpm rerecord --plan-with <main checkout> --provider claude-code); the recordings themselves are untouched and the self-suite's row-count pins are unchanged (43/43).pnpm parity --old <origin/main worktree> --new . --provider claude-code: 13 passed, 0 failed, 1 skipped (process-scopedmodel-list). Event and row counts equal in every cell.recordings/parity-allowlist.jsongains 27 entries naming#2178, three classes:presentationon items —/*/item/presentation, events, 9 cells (approval-allow, approval-deny, compaction, plan-mode, steer, subagent, turn-tools, user-question, web-search; the other 4 cells have no items).fileRead— plan-mode events/2,/4,/24,/25; rows/0/children/{0,5}/{toolName, toolArgs, output, activityIntents/0/command, activityIntents/0/name}(the row projects from a fileRead item: no tool name or arguments, the file contents are not row data, the intent command isRead <path>).delegation— subagent events/6,/13; rows/0/children/0/toolName("delegation") and/0/children/0/subagentType(the delegation item has no such field; the type rides the presentation detail).Zero unlisted diffs, zero stale entries. codex 16/16 and acp-cursor 10/10 replay against main with zero new diffs (no entries added; nothing of theirs touched). The expected "suppressed low-value rows" and "planSteps" classes produce no parity diff: no recorded cell calls Monitor/TaskOutput/ScheduleWakeup/SendMessage or TodoWrite/Task*, and ToolSearch/AskUserQuestion were already hidden by name.
toolCallitems without presentation, and every thread-view change here applies only to the v3 kinds and topresentation.suppress— the legacy tables and reducer are untouched — so old rows project identically by construction.provider/unhandledflat on every claude cell (0→0; compaction and steer 2→2) and on the corpus; pinned per transcript fixture (23 across 12 fixtures, every one a CLI-injected string-contentusermessage — compaction summaries,<task-notification>resumes — which visibility classifiesunknowntoday; lowering that is a separate change).Tests (forced): thread-view 385, claude-code 322, codex 193, provider-bridge-protocol 218, provider-parity 43; server 1832/1833 (the one failure is the known local umask case in
internal-skill-trees, which passes in CI). Typecheck green for the claude plugin, thread-view, server and agent-runtime on the re-stacked base.Not done
planStepsrows are not projected as timeline rows (status quo: TodoWrite/Task* rows were hidden); the banner reads them. The presentation-driven projection workstream owns the rows.Agentcalls keep today's two-row structure (the delegation settles at the launch ack; thelocal_agentbackground task tracks the work). Folding the task into a single background delegation is a larger change to the runtime's open-work tracking and the background-commands card.sdk/usermessages (task-notification resumes, compaction summaries) still surface asprovider/unhandled; the fixtures show they are the whole G11 residue for Claude. A one-line visibility change would lower it but changes parity for the steer/compaction cells, so it is left for a deliberate follow-up.