Repository navigation
fix: zero-injection session identity — outbound tool_use witness routing (#1685) - #1692
Conversation
📦 Built Package ArtifactBranch: Option A — Install this exact build (recommended)npm install -g billion-context@0.0.1715-pr.1692.3e18253This exact version is immutable and reproducible. The rolling Option B — Download artifact
tar xzf billion-context-pr1692.tgz
npm install -g packageInstall instructions are refreshed on each push. |
|
Review follow-up in 5aeec47:
Local: 30/30 green in |
…hrowing (#1692 review) The legacy-conversation_id strip ran '"conversation_id" in obj' on the parsed argument JSON. A degenerate but reachable payload (arguments of null/scalar from a misbehaving upstream, e.g. the google whole-part lane stringifying a null args object) parses to a non-object and made the 'in' operator throw TypeError inside recordToolWitness — on the hot SSE stream path (settleWitnesses runs at stream end AND in the catch), where an observe-only recorder must never perturb the response. Now such payloads return an empty hash = not recorded, matching the existing unparseable-JSON contract. Regression test added.
…ge (#1692 review) The zero-injection commit removed every call site in server.ts but left the token in the session.js import list. Typecheck passes only because noUnusedLocals is off; removing it keeps the import honest.
[bot] 🏷 Independent review complete — mergeable. Two small drive-by fixes added as commits ① Original issues (#1685 / #1611) — direction verified
② Regression audit — existing behaviors touched, verdicts
③ Code review — defects found & fixed directly on the branch
On your ④ Test validityReal and biting — no tautologies found: routing-ladder tests drive actual ⑤ Diff cleanlinessClean — every line maps to the PR's purpose; goldens are legitimate deletions; README en/zh kept in sync; Verification evidence (head
|
|
更新了 master 再看下 |
[bot] 🏷 已按更新后的 master 复查完毕(期间我的会话被平台重启了一次,补报进度)。 你已直接把 master merge 进 PR 分支( 新 master 与本 PR 的交互面检查
验证证据(树 =
|
ca31ad2 to
586671f
Compare
…hrowing (#1692 review) The legacy-conversation_id strip ran '"conversation_id" in obj' on the parsed argument JSON. A degenerate but reachable payload (arguments of null/scalar from a misbehaving upstream, e.g. the google whole-part lane stringifying a null args object) parses to a non-object and made the 'in' operator throw TypeError inside recordToolWitness — on the hot SSE stream path (settleWitnesses runs at stream end AND in the catch), where an observe-only recorder must never perturb the response. Now such payloads return an empty hash = not recorded, matching the existing unparseable-JSON contract. Regression test added.
…ge (#1692 review) The zero-injection commit removed every call site in server.ts but left the token in the session.js import list. Typecheck passes only because noUnusedLocals is off; removing it keeps the import honest.
…y outbound tool_use witness (#1685) - remove every model-visible conversation id: the wire system note (withConversationIdNote), MCP manifest conversation_id params (#760), search_context cross-conversation description (#841) - new src/tool-ring.ts: per-session ring of sha256(name + NUL + stableStringify(args minus conversation_id)) over outbound tool_use; capacity 32, TTL 10min, hash-index evicted in sync - record witnesses on all five plugin response lanes (openai/anthropic/ google chat SSE, responses SSE, non-stream JSON) — observe-only, bytes still forwarded verbatim (#1039) - handlePluginTool routing ladder: unique witness wins (even over a differing body id — claude-code subagent correctness); collision with no id -> loud 400 refuse-to-guess; no witness -> body id path; neither -> single-active arbitration inside the TTL window, else loud 400 - mcp shim no longer hard-fails id-less tool calls: forwards them for proxy-side routing; per-call override + legacy strip kept for compat - wire-contract goldens regenerated (conversation_id param gone) - README zh/en kimi lane notes updated Fixes #1685 Fixes #1611
…s index; pin TTL with a fake clock - tool-ring.ts: dropFromIndexIfAbsent — index membership is only dropped when no surviving ring slot carries the hash. Previously evicting/sweeping the OLDER copy of a duplicated hash (same name+args witnessed twice, e.g. a retried compress) killed the index entry of the surviving newer copy → premature witness miss (safe direction — falls back to body id/arbitration — but wrong). Applies to both the capacity-evict loop and sweep(). - clock seam (_setToolRingClockForTest) + TTL tests: match AT the 10min boundary, miss 1ms past, ring freed on expiry - regression tests for both duplicate-hash paths (capacity + sweep) - negative tests: empty-name witness is a no-op (unit + pipe lane)
…hrowing (#1692 review) The legacy-conversation_id strip ran '"conversation_id" in obj' on the parsed argument JSON. A degenerate but reachable payload (arguments of null/scalar from a misbehaving upstream, e.g. the google whole-part lane stringifying a null args object) parses to a non-object and made the 'in' operator throw TypeError inside recordToolWitness — on the hot SSE stream path (settleWitnesses runs at stream end AND in the catch), where an observe-only recorder must never perturb the response. Now such payloads return an empty hash = not recorded, matching the existing unparseable-JSON contract. Regression test added.
…ge (#1692 review) The zero-injection commit removed every call site in server.ts but left the token in the session.js import list. Typecheck passes only because noUnusedLocals is off; removing it keeps the import honest.
Rebased onto master (post #1713 NO_RANGE manifest locals, post #1745): goldens now reflect zero-injection manifests — every conversation_id param block removed (routing param, search_context cross-session param, one tool left with empty properties). Regenerated via scripts/update-wire-contract-goldens.ts; manifest arrays keep the NO_RANGE-aware locals instead of the pre-rebase plain BILI_ACP_TOOLS_* arrays.
134027d to
86e1811
Compare
…lane Sediment of the 2026-09-30 real-machine verification matrix (issue ranxianglei#1611 / PR ranxianglei#1692 live session): every scenario was first probed against a production instance with curl, then pinned hermetically. - routing ladder: single-active arb → outbound tool_use witness (plain-JSON recording lane, 5th of ranxianglei#1692) beats a two-active refuse-to-guess → loud 404 on an unknown body conversation id (ranxianglei#1158 diagnostic) - MCP shim leg: spawns the real src/mcp.ts over stdio JSON-RPC the way a codex host drives it (BILI_MCP_PROXY + BILI_CONVERSATION_ID env only) and routes a tools/call through the body-id lane - hermetic: XDG_STATE_HOME redirected before server import — boot's loadConversations() would otherwise read the developer's production plugin-conversations.json and count live sessions as fresh conversations, breaking single-active arbitration assertions (auto-restart.test.ts env-first pattern)
Fixes #1685
Fixes #1611
Supersedes #1614 and #1620 (please close those when this lands).
What
The model no longer sees ANY conversation id, in any form. Every model-visible carrier is removed:
[Your bili conversation id: …](withConversationIdNote, all 5 injection sites inserver.ts)conversation_idparams (feat: per-call conversation_id for MCP tools — let the model supply its session id #760withConversationIdParam)search_contextcross-conversation description (feat: 支持search_context显式查询历史 session #841)Session identity moves fully to machine channels; the MCP tool leg routes id-less POSTs by a new outbound tool_use witness ring: the proxy is the only path model output takes, so every complete bili tool call it streams out is recorded as
sha256(normalizeToolName(name) + NUL + stableStringify(args minus conversation_id))per session (ring cap 32, TTL 10min, hash index evicted in sync).Routing ladder (
handlePluginTool)claudeSubagentSplit)conversationId(extensions / legacy — unchanged behavior,resolveConversationincl. canonical-id fallback)Recording points (observe-only, bytes forwarded verbatim per #1039)
All five plugin response lanes: openai chat fragments, anthropic
input_json_delta, google whole-partfunctionCall, responsesoutput_item.added+function_call_arguments.done, non-stream JSON. Witness keys strip a legacyconversation_idarg echo on both record and lookup sides, so old-model echoes never fork the key.Compat
src/mcp.tsshim: no longer hard-fails id-less tool calls — forwards them; per-call override andkeepForSearchpreserveddelete args.conversation_idlegacy strip kept inhandlePluginTool(old models may still echo the param)conversation_idinexecuteSearchContextTargetstill works)Why this shape
#1611root cause: the id line lived in the STATIC system prompt, so fork/--continuechanged system bytes → provider prefix-cache LCP broke inside system →cached=0on a byte-identical history. With zero injection the request is byte-stable across forks.prefix-affinity) already ignores leading system/developer mutations, so Anonymous prefix-affinity bakes OpenAI inline system message into chain identity — dynamic system prompts fork the session every turn (cross-protocol asymmetry) #1148 sessions are unaffected either way; this change removes the model from the identity-trust chain entirely (a hallucinated/echoed id can no longer misroute a tool call — the ring only accepts hashes the proxy itself witnessed, or an explicit machine channel).Evidence
npm run typecheck✅npm run build✅ (9.43MB ESM)tests/tool-ring.test.ts(18 cases: hash normalization/parity, capacity eviction, collision sets, full routing ladder incl. witness>body, arb, loud-400s) andtests/tool-witness-pipes.test.ts(6 cases: all five lanes record; legacy echo parity) — all greentests/issue760-percall-conversation.test.ts8/8,tests/plugin-protocol.test.ts+tests/search-context-cross-session.test.ts24/24,tests/wire-contract.test.ts11/11 (goldens regenerated)git stash;dsh-nativealso hangs on local master (CI green = environmental). Zero regressions from this change.versionuntouched; no wire bytes altered on the request side.