馃毃 SLOP COP 馃毃 路 new-issue-autopilot
#3624 路 Dynamic tool retention at the Claude compaction boundary
2026-09-14 路 trusted base d89160eb8c69c1e3ebc2ba2514f1711af8d7c506
Verdict: NOT REPRODUCED 路 Root-cause confidence: low 路 reproduction label: no-repro
1. TL;DR
The reported symptom is loss of injected tools from a long-running Claude session after reconnect and compaction. Two clean-checkout probes found that BB retained its attached MCP server and that the server still listed its injected tool after a simulated compact event. This checks BB鈥檚 compaction handler with a mocked Claude query and a real in-memory MCP client/server; it does not exercise Claude鈥檚 deferred catalog or a daemon reconnect. The production symptom and its initiating cause therefore remain unverified, and no production fix is justified by this result.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Tools disappear from a live Claude catalog after reconnect and compaction. | Unverified | No live Claude process, deferred ToolSearch, or daemon reconnect was exercised. |
| Tool configuration is attached at session construction. | Verified in source | Nonempty dynamicTools creates the bb-bridge MCP server; the SDK query receives that configuration. |
| Ordinary turns do not refresh dynamicTools for an existing runtime. | Verified in source | The host returns early for an existing thread and runTurn receives instructions and environment, without dynamicTools. |
| BB鈥檚 compact handler removes the attached MCP tools. | Not observed in the tested boundary | Same server identity and tools/list entry before and after compact_boundary in both runs. This narrower hypothesis does not encompass Claude鈥檚 internal index. |
| A process rebuild restores the affected production session. | Unverified | Reporter鈥檚 runtime, transcripts, and service logs were not accessed. |
3. Environment
Darwin arm64; Node 22.22.3; repository-pinned pnpm 9.15.0 via Corepack; installed Claude Agent SDK 0.3.245. Two separate temporary clones at the full commit above. No service ports or development data directories were used: the probe uses in-memory MCP transport and the repository鈥檚 controlled SDK query fixture. No authenticated Claude process was started; this investigation did not establish an isolated authenticated provider setup, and did not use personal credentials or the production runtime.
The default pnpm launcher was broken locally. A temporary PATH shim invoking corepack pnpm restored the normal frozen install and Turbo commands; no dependency was added. The full first build succeeded (56 tasks). The second full build also succeeded (56 tasks).
4. Minimal reproduction attempt
This is a repeatable boundary probe, not a reproduction of deferred-catalog loss. Download compact-probe.patch, then run:
git clone https://github.com/get-bb/bb.git bb-3624-check cd bb-3624-check git checkout --detach d89160eb8c69c1e3ebc2ba2514f1711af8d7c506 corepack pnpm install --frozen-lockfile --prefer-offline corepack pnpm exec turbo run build git apply /absolute/path/to/compact-probe.patch corepack pnpm exec turbo run test --filter=bb-plugin-provider-claude-code --force -- --testNamePattern='retains the attached MCP server'
- The patch extends the existing controlled-query compaction fixture with one synthetic tool.
- It connects a real MCP client to the server passed to the mocked Claude SDK query and checks tools/list and allowedTools before compaction.
- It emits a compact_boundary event and waits for the existing context invalidation assertions, proving the handler consumed the event.
- It checks that the query was constructed only once, the same MCP server remains attached, and tools/list still returns catalog_probe.
Expected at this boundary: the tool remains listed. Actual in both runs:
Test Files 1 passed | 25 skipped (26) Tests 1 passed | 353 skipped (354)
The passing probe does not demonstrate that Claude ToolSearch sees the tool. It also does not replay the long-lived busy session, provider-worker replacement, or process rebuild.
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..90f530b61 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,
@@ -106,6 +108,8 @@ interface ControlledClaudeQuery {
}
interface ClaudeQueryCallOptions {
+ mcpServers?: BridgeSessionOptions["mcpServers"];
+ allowedTools?: string[];
canUseTool?: CanUseTool;
env?: Record<string, string | undefined>;
extraArgs?: Record<string, string | null>;
@@ -706,7 +710,7 @@ describe("bridge", () => {
}
});
- it("publishes a context snapshot after a turn and invalidates it at compaction", async () => {
+ it("retains the attached MCP server and tool listing across a compact boundary", async () => {
const bridge = createBridgeJsonRpcTestHarness(handleLine);
const queries: ControlledClaudeQuery[] = [];
queryMock.mockImplementation(() => {
@@ -727,6 +731,7 @@ describe("bridge", () => {
threadId,
cwd: "/tmp/worktree",
instructionMode: "append",
+ dynamicTools: [{ name: "catalog_probe", description: "Check tool visibility", inputSchema: { type: "object" } }],
options: {
permissionMode: "accept-edits",
permissionScope: "workspace",
@@ -737,6 +742,15 @@ describe("bridge", () => {
},
});
await bridge.waitForResponse(1);
+ const attachedOptions = getLatestQueryOptions();
+ const mcpServer = attachedOptions.mcpServers?.["bb-bridge"];
+ if (!mcpServer || mcpServer.type !== "sdk") throw new Error("Expected attached SDK MCP server");
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+ await mcpServer.instance.connect(serverTransport);
+ const client = new Client({ name: "catalog-probe", version: "1.0.0" });
+ await client.connect(clientTransport);
+ expect((await client.listTools()).tools.map((tool) => tool.name)).toEqual(["catalog_probe"]);
+ expect(attachedOptions.allowedTools).toContain("mcp__bb-bridge__catalog_probe");
bridge.sendRequest(
2,
"turn/start",
@@ -807,6 +821,10 @@ describe("bridge", () => {
});
expect(queries[0].getContextUsage).toHaveBeenCalledTimes(2);
});
+ expect(queryMock).toHaveBeenCalledTimes(1);
+ expect(getLatestQueryOptions().mcpServers?.["bb-bridge"]).toBe(mcpServer);
+ expect((await client.listTools()).tools.map((tool) => tool.name)).toEqual(["catalog_probe"]);
+ await client.close();
} finally {
await stopBridgeThread({ bridge, queries, threadId });
bridge.restore();
5. Root cause
Unresolved. A construction-time tool snapshot can explain why later turns would not repair an initially empty attachment, but it does not prove that the reported reconnect created such an attachment.
- resumeThreadRuntimeIfMissing: returns for an existing runtime; otherwise forwards resumeContext.dynamicTools.
- runSubmittedTurn: forwards input, options, environment, and instructions, without a fresh tool list.
- attachThreadSession: constructs bb-bridge only for a nonempty dynamicTools array.
- SDK query configuration: passes the attached MCP server into Claude鈥檚 query options.
- Compaction event handling: clears context usage and captures a fresh usage snapshot. The probe shows no deletion of the attached tool at this boundary.
if (entry.runtime.hasThread(command.threadId)) {
return;
}
...
dynamicTools: resumeContext.dynamicTools,
Tools/list availability and Claude鈥檚 deferred catalog are different observations. A successful tools/list response cannot distinguish a Claude indexing failure from a previously incorrect BB attachment in the reporter鈥檚 session.
6. Proposed next experiment
Use a separately authenticated, isolated Claude process. Record only synthetic dynamic-tool names at each BB attachment, MCP connection state, and actual deferred ToolSearch results. First establish a successful tool lookup; then independently exercise compaction and provider-worker reconnect, followed by their combination, while recording whether a new attachment was constructed. A nonempty attachment plus a failed ToolSearch would focus the investigation on Claude鈥檚 connection/index behavior; an empty attachment would focus it on resume-context assembly. Avoid logging credentials, user prompts, or full environment objects.
No pull request: the reported bug was not reproduced on trusted main and no failing root-cause regression exists. Open-PR search and issue cross-reference metadata returned no linked open PR at investigation time.
7. Related issues
Repository search found #2384, concerning conditional tools on release/resume. It remains open. Shared construction-time configuration does not establish a duplicate.
8. Verification
The same agent repeated the probe in a second clean clone, /tmp/slopcop-3624.1NY1xt/verify, at the recorded base commit. Only the published test patch was applied. Frozen installation succeeded; the Turbo probe used --force and reported zero cached tasks, with one test passing. The first clone was /tmp/slopcop-3624.1NY1xt/base. No ports were allocated; each run used fresh in-memory transports and fixture temporary directories.
Both runs support only the narrow result that the attached server and tools/list survive a simulated compact event. The report makes no claim that the real Claude catalog or reconnect sequence passed. No causal conclusion was upgraded after the second run.
9. Appendix
First run log 路 Second run log 路 Probe patch. Logs retain test output with local tool paths removed. Source inspection used git fetch, git rev-parse, git diff, rg, and sed; GitHub reads covered issue properties, comments, visibility, related issues, and PR cross-references. No production code was changed. No dev server was started; test fixtures clean their own temporary resources.
Untrusted-data handling: issue prose contained action directives. Those directives were ignored; only the trusted automation rules and repository evidence guided this investigation. Reporter-supplied scripts, links, branches, logs, and local paths were not executed or fetched.
> AGENT GENERATED