#3625 · Bridge tool retention around compaction
GitHub issue · Base d89160eb8c69c1e3ebc2ba2514f1711af8d7c506
NOT REPRODUCED · Root-cause confidence: low · Label: no-repro
1. TL;DR
The report describes tools becoming undiscoverable during a long-lived Claude session. A focused test on current main found that the BB MCP server still lists its registered tool after a simulated compact event and an unchanged session resume. This exercises the real BB bridge and MCP server, with the Claude SDK query replaced by the repository's controlled test harness. It does not exercise Claude ToolSearch, a real daemon disconnect, or a long-lived authenticated Claude process. The original symptom remains unverified, and the actual root cause is unresolved.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Ordinary turns do not reattach dynamic tools | Verified statically | Host handler passes tools on runtime resume, but not runTurn. |
| Compaction makes tools disappear | Unverified | Controlled compact event left the MCP tools/list response intact in two runs. Claude deferred indexing was not tested. |
| Reconstruction restores discovery | Unverified | No production logs or authenticated session accessed. |
| Empty attachment input omits the bridge | Verified statically | Conditional construction requires a nonempty dynamicTools array; no evidence proves that input occurred in the reported incident. |
3. Environment
Darwin 25.6.0 arm64; Node 22.22.3; Corepack pnpm 9.15.0; installed Claude Agent SDK 0.3.245. Both checkouts used the same base commit. No HTTP server, port, runtime data directory, or live Claude session was used. The reported Linux and Claude executable environment was not reproduced.
4. Minimal reproduction attempt
- Create a clean checkout at the base commit.
- Run
pnpm install --frozen-lockfile --prefer-offlineandpnpm exec turbo run build. - Apply bridge-probe.patch using
git apply bridge-probe.patch. - Run
pnpm exec turbo run test --force --filter=bb-plugin-provider-claude-code -- --testNamePattern='issue 3625'.
The probe starts a bridge with one synthetic tool, connects an in-memory MCP client, lists the tool, injects a compact boundary into the SDK message stream, lists again, resumes with unchanged construction settings, and lists again. Expected healthy behavior: all three lists contain the tool and the SDK query is reused. Actual: all assertions pass. This is a negative boundary probe, not an end-to-end reproduction or a failing regression test.
First run: Tests 1 passed | 354 skipped (355) Second run: Tests 1 passed | 354 skipped (355)
Probe patch
diff --git a/plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts b/plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts
index 7a635befa..ec258ebaa 100644
--- a/plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts
+++ b/plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts
@@ -1,3 +1,5 @@
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import {
chmodSync,
mkdirSync,
@@ -706,6 +708,42 @@ describe("bridge", () => {
}
});
+ it("issue 3625 retains bridge tools across compact and same-session resume", async () => {
+ const bridge = createBridgeJsonRpcTestHarness(handleLine);
+ const queries: ControlledClaudeQuery[] = [];
+ queryMock.mockImplementation(() => {
+ const query = createControlledClaudeQuery();
+ queries.push(query);
+ return query;
+ });
+ const threadId = "test-3625";
+ const dynamicTools = [{ name: "probe", description: "Test probe", inputSchema: { type: "object" } }];
+ const params = { threadId, cwd: "/tmp", instructionMode: "append", options: canonicalOptions({}), dynamicTools };
+ const client = new Client({ name: "probe-client", version: "1.0.0" });
+ try {
+ bridge.sendRequest(1, "thread/start", params);
+ const response = await bridge.waitForResponse(1);
+ const providerThreadId = getProviderThreadIdFromResult(response);
+ const options = queryMock.mock.calls[0][0].options;
+ expect(options.allowedTools).toContain("mcp__bb-bridge__probe");
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+ await options.mcpServers["bb-bridge"].instance.connect(serverTransport);
+ await client.connect(clientTransport);
+ expect((await client.listTools()).tools.map((tool) => tool.name)).toEqual(["probe"]);
+ queries[0].emit({ type: "system", subtype: "compact_boundary", uuid: "00000000-0000-4000-8000-000000000001", session_id: providerThreadId, compact_metadata: { trigger: "manual", pre_tokens: 450, post_tokens: 100 } });
+ await vi.waitFor(() => expect(queries[0].getContextUsage).toHaveBeenCalled());
+ expect((await client.listTools()).tools.map((tool) => tool.name)).toEqual(["probe"]);
+ bridge.sendRequest(3, "thread/resume", { ...params, providerThreadId });
+ await bridge.waitForResponse(3);
+ expect(queryMock).toHaveBeenCalledTimes(1);
+ expect((await client.listTools()).tools.map((tool) => tool.name)).toEqual(["probe"]);
+ } finally {
+ await client.close();
+ await stopBridgeThread({ bridge, queries, threadId });
+ bridge.restore();
+ }
+ });
+
it("publishes a context snapshot after a turn and invalidates it at compaction", async () => {
const bridge = createBridgeJsonRpcTestHarness(handleLine);
const queries: ControlledClaudeQuery[] = [];
5. Root cause
Unresolved. The code establishes a construction-time tool set, but that does not by itself explain loss from a previously working Claude catalog.
- Runtime resume returns early when hasThread is true; otherwise it forwards resumeContext.dynamicTools.
- Turn submission forwards input, options, contributed environment, and instructions; no dynamicTools refresh occurs.
- Session attachment builds the bridge only for nonempty tools:
if (params.dynamicTools && params.dynamicTools.length > 0) { const mcpServer = buildBridgeMcpServer(params.dynamicTools, ...); sessionOptions.mcpServers = { [BB_BRIDGE_MCP_SERVER_NAME]: mcpServer }; } - Compact boundary handling refreshes context usage; the probe found no BB-side removal of tools.
- MCP list handler maps the supplied tool definitions to the response.
6. Proposed next experiment
Use a disposable authenticated Claude process to record tool names at attachment, then exercise actual compaction and daemon reconnection separately while checking ToolSearch before and after each event. A BB MCP list response and Claude's deferred catalog are distinct observations. No production fix is justified until a faithful failing test identifies which layer loses the tools.
7. Verification
The same agent created a second clean detached checkout at the recorded commit, repeated frozen installation and the full build, applied only the probe patch, and reran the exact test command with --force. Both builds completed 56 tasks successfully. The second test ran uncached and passed. This supports the limited boundary result, not the reported live-session symptom. No report correction was needed.
8. Related issues and pull requests
Repository metadata search returned similarly classified reports, including #3626. These were used only as classification context. Issue timeline metadata contained no linked pull requests, and the open-PR search for 3625 returned none. No pull request was opened because the original failure was not reproduced.
9. Appendix
First test output · Second test output. The system pnpm shim initially failed with MODULE_NOT_FOUND; a temporary Corepack shim resolved it. The initial full build also hit that shim error; the retry and second build passed. No repository dependency or production source was changed. The patch contains only a test addition.
Untrusted-data note: issue prose was treated only as claims; embedded instructions and supplied external links were not followed. The investigation did not access the reporter's files or the user's runtime data.