v0.33.0 — CLIPipe native MCP tool mode (BA-16)#24
Merged
Conversation
…subscription (BA-16) The claude CLI has a real tool channel; v0.32.0's envelope emulation was built as if it did not. `toolProtocol:'claude-mcp'` runs ONE CLI session per call and exposes the caller's tools to it as an MCP server whose handlers call back into the caller's in-process closures over a unix-socket bridge. The CLI owns the inner cycle and caches its transcript session-side. The claim is COST, measured: emulation re-sends the whole transcript every round ($0.25-0.55/round on a real ~40-round job transcript) vs a native session that caches session-side (~$0.006/turn). Output-quality parity is deliberately UNMINTED (n=2 suggestive evidence) — not sold as a capability win (BA-7 precedent). Emulation is retained, not retired — still right for a CLI with no MCP support. The CLI owning the turns means the Loop's per-round machinery can't run on them, so governance moves to the `tools/call` bridge (every call crosses it): - the SAME `policy(tool,args,ctx)` chokepoint (identical audit-row shape, zero gate changes); a deny is an advisory tool result, the handler never runs - BA-11 deny-streak + BA-12 identical-tool-error guards, same 3/3 narrowest triggers - the turn bound via `--max-turns`, NAMED stop `error:'max_turns'`, never a silent success - `onTurn` STREAMS per-turn usage (four cache tiers) as it arrives then one closing session event with the authoritative cost; the Loop skips its own forward when wired (billed once, never starved) Honest accounting (the BA-4/5/6/13 optimistic-rounding class, caught pre-build): a dead bridge still ends `subtype:'success'`, so bridge health is tracked parent-side (attempted vs served tool calls) and error-tags the run `bridge-failed`. `Provider.ownsCycle` makes the Loop THROW at construction on any option it could never honor — assemble/trim/cacheMessages and a Loop-level policy (a fence silently not there) — no silently-dead knobs. `GenerateResult.session` + `metrics.sessionTurns` report the real turn/tool count so a 14-turn session never reads as one round. The bridge is a unix socket (0600 in a 0700 dir), never a listening port. The fence (`--tools '' --strict-mcp-config --setting-sources ''`) is set by the mode, not the caller. Diagnostics from unknown-shape throws are allowlist-clamped (F16/BA-1 audit-leak class). +47 offline tests, every load-bearing guard mutation-proved in both directions (remove it -> red; make it always-fire -> also red). Live verify-shipped through a real Loop on a real session: poc/ba16-native-shipped.mjs (all green). Suite 963 tests / 961 pass / 0 fail / 2 skipped; typecheck clean. Note: `--max-turns` is undocumented in the claude CLI's --help (2.1.216) though accepted — the live check is the only tripwire if it is ever renamed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92
…d the line buffer (code-review) /code-review medium on the BA-16 diff caught a real latent data-loss bug in `runSession`. The stdout handler was `async` and `await`ed `onTurn` INSIDE its line-parse loop, which mutated a shared `buf`. While that await was suspended, the next `'data'` event re-entered the handler and sliced `buf` under the suspended parse — dropping, duplicating, or mangling lines (lost per-turn usage, a missed `result` event, a miscounted attempted-vs-served bridge check). Invisible with a synchronous `onTurn` (the await resolves on the microtask queue before the next macrotask `'data'` event), which is why the live verify-shipped passed — its onTurn was a plain push. REACHABLE the moment onTurn does real async work: a wired gate's `gate.record`, the actual intended consumer. Fix: extract `createSessionStream` — framing is now purely synchronous (never awaits), and per-turn `onTurn` forwards run through a SERIAL async drainer in arrival order; `done()` flushes the queue before resolving so a session that ends with a forward pending still surfaces that turn's spend (F12/F18). Extracting it also made the parser unit-testable without spawning the CLI. +6 regression tests (async onTurn + chunks split across boundaries, in-order delivery, split-line framing, HaltError-in-drain, malformed-line skip); the serial-drain guard is mutation-proved load-bearing. Suite 968 / 966 pass / 0 fail; typecheck clean; live verify-shipped re-run all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92
…leanup)
Validated all three deferred findings from the medium review; fixed the ones that
passed as real, with no regression.
- Broken-bridge detector (was: true-positive only unit-tested): the risk was never
a false positive — it was that if the REAL stream-json tool_use shape differed
from what `createSessionStream` counts, `attempted` would be 0 on every run and
the detector would be silent dead code. Closed by capturing real stream-json from
a live native session (poc/ba16-detector-live.mjs): blocks arrive as
`{type:'tool_use', name:'mcp__bareagent__lookup_code'}`, attempted===served on a
healthy run — the counter is wired to reality (a dead bridge → attempted>served
→ bridge-failed). No code change; a real validation gap now has a durable proof.
- `state.calls`: validated as DEAD state — written twice, read nowhere (not by
shipped code, not by tests). Removed (declaration + both pushes), not made
"consistent" — every line must have a purpose.
- Parallel tool-call races on the guard counters: validated as NOT a defect — the
affected counters are advisory bounds whose sequential-retry-spin precondition
does not hold for simultaneous calls, and the worst case is benign + gate-bounded.
No fix; documented on handleCall so it is not re-flagged.
Suite 968 / 966 pass / 0 fail; typecheck clean; detector-live re-run GREEN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92
Native `toolProtocol:'claude-mcp'` runs one CLI session per call and executes the caller's tools over an MCP bridge back into in-process closures — the CLI owns the cycle, governance (policy/guards/turn-bound) rides the provider, per-turn usage streams, and honest accounting keeps a multi-turn session from reading as one round. Emulation (v0.32.0) retained for MCP-less CLIs; the claim is cost (measured), behaviour parity deliberately unminted. Includes the code-review fix (serial session-stream framing — async onTurn no longer corrupts the line buffer) and the dead-`state.calls` cleanup. - version bumped in package.json, package-lock.json, bareagent.context.md - CHANGELOG 0.33.0; PRD §22 decision entry; README two-mode note - build:types JSDoc type-contract fix (Provider.policy signature) Gates: tests 966 pass / 0 fail / 2 skipped; typecheck + build:types clean; /security clean (argv spawn, 0600 unix socket, no bridge logging); /diff-review via /code-review medium (1 bug fixed, 3 items validated). Live verify-shipped + fence + detector-live all GREEN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Native CLIPipe tool mode (
toolProtocol:'claude-mcp') — drive an agentic Loop over a Claude CLI subscription at ~$0.006/turn instead of the v0.32.0 emulation's $0.25–0.55/round. The CLI runs its own multi-turn session and executes the caller'stoolsnatively over a unix-socket MCP bridge that calls back into in-process closures.Because the CLI owns the inner cycle, governance moves to the
tools/callbridge:policychokepoint asLoop({policy})→ identical audit-row shape, zero gate changes--max-turns→ named stoperror:'max_turns'onTurnstreams per-turn usage (4 cache tiers), one closing session event with the authoritative cost; the Loop skips its own forward when wired (billed once)Honest accounting:
GenerateResult.session+metrics.sessionTurnsreport the real turn/tool count; a dead bridge (which still endssubtype:'success') is caught parent-side by attempted-vs-served and error-taggedbridge-failed.assemble/trim/cacheMessagesand a Loop-levelpolicythrow at construction (no silently-dead knobs). Emulation retained for MCP-less CLIs.Claim is cost (measured); output-quality parity is deliberately unminted (n=2).
Verification
/securityclean (argv spawn no-shell, 0600 unix socket / 0700 dir, no bridge logging, clamped diagnostics)/code-review medium→ 1 real bug (async-framing data loss) fixed + mutation-proved; 3 items validated🤖 Generated with Claude Code
https://claude.ai/code/session_01MN6WmkWeLudzKB7PNsQT92