Skip to content

De-overfit the provider bridge kit: unwind frames, split the session registry, repatriate single-consumer helpers - #1742

Merged
ymichael merged 6 commits into
mainfrom
bridge-kit-deoverfit
Aug 18, 2026
Merged

De-overfit the provider bridge kit: unwind frames, split the session registry, repatriate single-consumer helpers#1742
ymichael merged 6 commits into
mainfrom
bridge-kit-deoverfit

Conversation

@ymichael

Copy link
Copy Markdown
Collaborator

De-overfits bridge-kit following a two-reviewer adversarial audit of every module and export, calibrated on one ruling: GOOD = code encoding bb-side invariants (canonical event/item semantics, wire contracts both sides validate); BAD = a frame hosting per-provider knowledge injected through generics, callbacks, or config objects. ~80% of the kit survived the audit as genuine invariants (notably turn-state, whose suspect generic-plus-hooks shape is driven identically by all three consumers); this PR removes the part that didn't. One commit per finding group:

  1. Unwind buildToolUseItem — the router was an if-chain each bridge could write itself, inverted into the kit via name-sets + parser callbacks + an escape hatch supplied by exactly one provider. The invariant constructors stay (buildFileChangeItem, new buildGenericToolCallItem fallback); claude-code and pi own explicit switches, preserving null-parse→generic fallback and parent-id threading.
  2. Split bridge-session-registry — only 2 of 4 bridges used it, and over half its option surface existed for one consumer each (nextToolCallRequestId, resolveAdditionalPendingWork, stopSession claude-code-only; TCloseResult pi-only). The genuinely shared kernel survives as createPendingToolCallTracker (flat requestId→resolver map — also removes the per-response all-sessions scan); session orchestration inlined into the two bridges. Reviewer note: claude-code's interactive requests previously shared the tool-call id counter; they now use string-prefixed ids (interaction-N), keeping the id spaces collision-free. Wire-compatible: JSON-RPC ids are string | number and both ends treat them opaquely.
  3. One decoder for canonical tool-call requestsdecodeNativeProviderToolCallRequest had zero consumers anywhere (deleted); bridge-protocol-adapter reimplemented decodeNormalizedProviderToolCallRequest inline (now delegates); providerToolCallResponseSchema privatized.
  4. RepatriationsbuildShellEnvironmentPolicyConfig → provider-codex (its shell_environment_policy.set.* namespace is codex-native config); claude-code stops round-tripping env vars through codex-style keys and carries a plain map under its own bag key (both ends verified inside the plugin — nothing crosses the daemon wire schema). diffCumulativeText → pi, its only consumer.
  5. Dead surface + trimsfinishOpenProviderTurn (only caller was a test) deleted; two per-provider policy booleans removed from the good constructors (completeWebItems, preserveUndefinedToolCallFields); the universally-identical drainAcceptedUserMessages call folded into the turn-state core; CounterScopedItemIdState's lying reasoning* field names renamed (compaction ids flow through them too); contentWrapperSchema and dead arg interfaces un-exported. Includes the docs/api_to_audit.md record and regenerated bundled types.

Numbers

  • Published surface: 196 → 192 names (−7 removed, +3 added: the two kept constructors and the tracker).
  • 35 files, +972/−908 (+825/−738 excluding regenerated bundled types). Insertions exceed deletions by design: the audit trades kit generics and single-consumer options for deliberate, explicit per-consumer code, plus ~200 lines of new tests (tracker + moved diff tests).

Verification

  • Typecheck green via turbo: provider-bridge-protocol, plugin-sdk, agent-runtime, provider-claude-code, provider-codex, provider-acp, @bb/server.
  • Tests green via turbo (post-rebase onto the merged Repatriate single-consumer provider-bridge SDK exports to their owning plugins #1729): 74 + 98 + 316 + 260 + 164 + 144 + 1722 across those packages.
  • No wire-shape changes, no protocol or SDK version bumps, pi's daemon-bundled path untouched.

🤖 Generated with Claude Code

AGENT GENERATED: by Claude Fable 5

@bb-slop-cop

bb-slop-cop Bot commented Aug 17, 2026

Copy link
Copy Markdown

🚨 SLOP COP 🚨 · review

I am the SlopCop. I started the security, quality, performance, architecture, and end-to-end review.

I will post one final review after I combine the results.

}
return true;
},
resolvePendingToolCalls: (scope, message) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — Low: Scope cleanup can become quadratic.

Each scope cleanup scans the full map. Closing many sessions can cost O(S × P) instead of work local to each session.

A benchmark took 264 ms for 10,000 scopes and 1,041 ms for 20,000 scopes. Please add a per-scope request index.

requestIdCounter += 1;
const requestId = requestIdCounter;
pendingToolCalls.set(requestId, { resolve, scope: args.scope });
options.sendToolCall({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — Medium: A send error breaks the stated promise contract.

forwardToolCall states that it never rejects. A synchronous sendToolCall error rejects the promise and retains the pending entry.

Please catch the error, delete the entry, and resolve an error result. Add a test with a sender that throws.

buildEditDiff,
buildShellEnvOverrides,
buildShellEnvironmentPolicyConfig,
buildFileChangeItem,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — Medium: The new public helper returns an empty item ID.

The exported helper returns id: "". Its result cannot pass the file-change parser without a caller replacement.

Please add callId to the helper arguments and assign it to id. Add a direct helper test for this contract.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 SLOP COP 🚨 · review

Plain-English summary: This pull request removes a large shared helper that managed provider sessions and tool calls.

It gives each provider control of its session lifecycle. It also moves single-provider helpers back to their providers and reduces the public kit.

Findings

  1. Medium — The new public buildFileChangeItem export returns an empty ID.

    First-party callers replace the ID. External plugin authors can emit an item that the thread view rejects.

    Require callId, assign it to id, and add a direct contract test.

  2. Medium — forwardToolCall can reject after a synchronous sendToolCall error.

    This result conflicts with its no-rejection contract and leaves stale pending state.

    Catch the error, delete the entry, and resolve an isError: true result.

  3. Low — Scope cleanup scans all pending calls for each closing session.

    The cost can grow with the square of the session count.

    A benchmark took 264 ms for 10,000 scopes and 1,041 ms for 20,000 scopes. Add a per-scope request index.

Architecture and security

Pi and Claude repeat some session-close code. Their stop behavior differs, so I do not recommend a shared helper yet.

I found no security issue.

Validation

  • All 1,056 affected-package tests passed.
  • All 13 Turbo test and typecheck tasks passed.
  • git diff --check passed.
  • A local Claude bridge request returned bridge-ok.
  • The development browser showed the prompt and response.
  • The GPT-5.6 final check confirmed all three findings.

This review uses a comment only. I did not approve the pull request or request changes.

@ymichael
ymichael force-pushed the bridge-kit-deoverfit branch from 06a32ed to 65b325a Compare August 18, 2026 00:44
ymichael added a commit that referenced this pull request Aug 18, 2026
…ver rejects

Two findings from the #1742 review:

- buildFileChangeItem returned id: "" and relied on every caller
  spreading its own id over the result — a trap now that the helper is
  published. callId is a required argument and becomes the item id; both
  first-party switches pass it directly.
- forwardToolCall's contract says it never rejects, but a synchronous
  sendToolCall throw rejected the promise and stranded the pending map
  entry. The sender call is now guarded: on throw the entry is removed
  and the call resolves as an isError result. Covered by a throwing-
  sender test that also proves the entry does not linger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ymichael and others added 6 commits August 17, 2026 23:29
The kit's buildToolUseItem router (command/fileChange/special parser
options) existed only so two consumers could feed it callbacks. Each
consumer now writes its own explicit switch over its tool names, calling
the plain constructors (buildFileChangeItem, new buildGenericToolCallItem)
directly, preserving exact behavior: parser-returns-null falls back to the
generic toolCall with raw args, parentToolCallId threads on every branch,
and claude's web-tool special case stays last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The kit keeps only what both bridges genuinely share: a pending-tool-call
tracker that mints item/tool/call requests, matches responses by request id
directly (no per-response all-sessions scan), and error-resolves a
session's calls on close. The sessions map, close dedup (closingSessions +
finally cleanup), and close-all move into the two consumers, which also
absorb the single-consumer options (nextToolCallRequestId,
resolveAdditionalPendingWork, stopSession) and the TSession/TCloseResult
generics. Claude-code's interactive requests now mint their own
string-prefixed ids so they cannot collide with the tracker's numeric ids
on the shared channel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
decodeNativeProviderToolCallRequest and its schema had zero consumers —
deleted. The runtime adapter's inline duplicate of the normalized decode
now delegates to decodeNormalizedProviderToolCallRequest (the test fake
already used it), and providerToolCallResponseSchema moves into
bridge-tool-calls.ts as a kit-private const, its only consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildShellEnvironmentPolicyConfig moves into provider-codex: its
shell_environment_policy.set.* namespace is codex-native and codex is its
only real consumer. Claude-code stops borrowing that namespace: its
plugin-internal session config bag now carries a plain env map under its
own envVars key (still filtered through buildShellEnvOverrides), and the
kit's extractEnvOverrides decode is deleted with it. diffCumulativeText
and its two interfaces move into agent-runtime's pi provider, their only
consumer, tests along with them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the core

- Delete finishOpenProviderTurn + FinishOpenProviderTurnArgs (only caller
  was a claude-code test, which now inlines the three lines).
- Make contentWrapperSchema kit-private (adapter-utils is its only user)
  and inline the NormalizeProviderCommandOutputArgs one-off type.
- Drop preserveUndefinedToolCallFields from completeStartedToolItem's
  public args; buildToolResultItem keeps the historical output shape via a
  private variant.
- Drop completeWebItems from buildToolResultItem; claude-code (the only
  provider that passed true) completes started web items at its own call
  site with completeStartedToolItem.
- Fold the identical drainAcceptedUserMessages call out of all three
  providers' onTurnStart hooks into the turn-state registry core
  (ProviderTurnState now carries pendingAcceptedUserMessages), shrinking
  the hooks to provider-only resets and un-publishing the drain helper.
- Rename CounterScopedItemIdState's fields to openScopedItemIdsByScope /
  scopedItemCounter project-wide: compaction ids flow through them too, so
  the reasoning-only names lied.
- Regenerate the SDK's bundled provider-bridge types and record this
  de-overfitting pass in docs/api_to_audit.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ver rejects

Two findings from the #1742 review:

- buildFileChangeItem returned id: "" and relied on every caller
  spreading its own id over the result — a trap now that the helper is
  published. callId is a required argument and becomes the item id; both
  first-party switches pass it directly.
- forwardToolCall's contract says it never rejects, but a synchronous
  sendToolCall throw rejected the promise and stranded the pending map
  entry. The sender call is now guarded: on throw the entry is removed
  and the call resolves as an isError result. Covered by a throwing-
  sender test that also proves the entry does not linger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ymichael
ymichael force-pushed the bridge-kit-deoverfit branch from 71a6d11 to efc4304 Compare August 18, 2026 06:31
@ymichael
ymichael merged commit 6cb2c6b into main Aug 18, 2026
10 checks passed
@ymichael
ymichael deleted the bridge-kit-deoverfit branch August 18, 2026 06:46
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