← reports

#2790 · Claude resume transcript integrity

Bug High Effort: Medium providers threads provider-claude-code open on GitHub 2026-09-01 · base 688eb4251c5b5c53c5a56f85baae6d47966355ec

Verdict: NOT REPRODUCED · Root-cause confidence: low

1. TL;DR

The report says that a resumed Claude turn can omit an earlier tool result. I tested the closest complete case from repository evidence. The test ended a persisted transcript on a tool result. It then resumed the session through the bundled Claude binary. The next model request contained the exact tool output in two clean runs. The issue does not include the exact transcript state, so this result does not refute every possible resume case.

2. Claims vs findings

ClaimStatusEvidence
A resumed provider session can omit a prior tool result.Not reproducedThe actual Claude Agent SDK resume request retained the seeded trailing tool result in two clean runs.
The behavior affects BB 0.40.0.Version verifiedThe trusted base builds package bb-app as version 0.40.0.
A specific durable transcript demonstrates the omission.UnverifiedThe issue supplies no transcript, parent chain, compact entry, or request diff.

3. Environment

4. Minimal reproduction

  1. Install the trusted workspace.
    pnpm install --frozen-lockfile --prefer-offline
  2. Save the test below as plugins/provider-claude-code/src/bridge/__tests__/resume-transcript-repro.test.ts.
  3. Run the focused test.
    cd plugins/provider-claude-code
    pnpm exec vitest run src/bridge/__tests__/resume-transcript-repro.test.ts

Defect expectation: The captured model request omits tool output that must survive. The assertion fails.

Actual result: The captured model request contains that marker. The test passes.

Test Files  1 passed (1)
Tests       1 passed (1)
Executed regression test source
import { randomUUID } from "node:crypto";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import { afterEach, expect, it } from "vitest";

const temporaryPaths: string[] = [];

afterEach(async () => {
  await Promise.all(
    temporaryPaths.splice(0).map((path) => rm(path, { force: true, recursive: true })),
  );
});

function projectDirectoryName(path: string): string {
  return path.replace(/[^a-zA-Z0-9]/g, "-");
}

it("keeps a trailing tool result when a Claude session resumes", async () => {
  const root = await mkdtemp(join(tmpdir(), "bb-claude-resume-repro-"));
  temporaryPaths.push(root);
  const workspace = join(root, "workspace");
  const config = join(root, "config");
  const sessionId = randomUUID();
  const promptId = randomUUID();
  const toolUseMessageId = randomUUID();
  const toolResultMessageId = randomUUID();
  const toolUseId = "toolu_repro";
  const timestamp = "2026-09-01T00:00:00.000Z";
  const projectDirectory = join(
    config,
    "projects",
    projectDirectoryName(workspace),
  );
  await mkdir(projectDirectory, { recursive: true });
  await mkdir(workspace, { recursive: true });
  const transcript = [
    {
      type: "user",
      uuid: promptId,
      parentUuid: null,
      sessionId,
      timestamp,
      cwd: workspace,
      message: { role: "user", content: "run the tool" },
    },
    {
      type: "assistant",
      uuid: toolUseMessageId,
      parentUuid: promptId,
      sessionId,
      timestamp,
      cwd: workspace,
      message: {
        role: "assistant",
        content: [
          {
            type: "tool_use",
            id: toolUseId,
            name: "Bash",
            input: { command: "true" },
          },
        ],
      },
    },
    {
      type: "user",
      uuid: toolResultMessageId,
      parentUuid: toolUseMessageId,
      sessionId,
      timestamp,
      cwd: workspace,
      message: {
        role: "user",
        content: [
          {
            type: "tool_result",
            tool_use_id: toolUseId,
            content: "tool output that must survive",
            is_error: false,
          },
        ],
      },
      toolUseResult: {
        stdout: "tool output that must survive",
        stderr: "",
        interrupted: false,
      },
    },
  ];
  await writeFile(
    join(projectDirectory, `${sessionId}.jsonl`),
    `${transcript.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
  );

  let requestBody: string | undefined;
  const requestUrls: string[] = [];
  const server = createServer((request, response) => {
    requestUrls.push(request.url ?? "");
    const chunks: Buffer[] = [];
    request.on("data", (chunk: Buffer) => chunks.push(chunk));
    request.on("end", () => {
      const body = Buffer.concat(chunks).toString("utf8");
      if (request.url?.includes("count_tokens")) {
        response.writeHead(200, { "content-type": "application/json" });
        response.end(JSON.stringify({ input_tokens: 1 }));
        return;
      }
      if (request.url?.includes("/v1/messages")) {
        requestBody = body;
        response.writeHead(200, { "content-type": "application/json" });
        response.end(
          JSON.stringify({
            id: "msg_repro",
            type: "message",
            role: "assistant",
            model: "claude-sonnet-4-20250514",
            content: [{ type: "text", text: "ok" }],
            stop_reason: "end_turn",
            stop_sequence: null,
            usage: {
              input_tokens: 1,
              output_tokens: 1,
              cache_creation_input_tokens: 0,
              cache_read_input_tokens: 0,
            },
          }),
        );
        return;
      }
      response.writeHead(404).end();
    });
  });
  await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
  const address = server.address();
  if (address === null || typeof address === "string") {
    throw new Error("Expected a local API port");
  }

  const abortController = new AbortController();
  let stderr = "";
  const prompt = {
    async *[Symbol.asyncIterator]() {
      yield {
        type: "user" as const,
        message: { role: "user" as const, content: "continue" },
        parent_tool_use_id: null,
        session_id: sessionId,
        uuid: randomUUID(),
      };
    },
  };
  const sdkQuery = query({
    prompt,
    options: {
      abortController,
      cwd: workspace,
      systemPrompt: "",
      env: {
        ...process.env,
        ANTHROPIC_API_KEY: "test-key",
        ANTHROPIC_BASE_URL: `http://127.0.0.1:${address.port}`,
        CLAUDE_CONFIG_DIR: config,
        CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
      },
      includePartialMessages: true,
      model: "claude-sonnet-4-20250514",
      persistSession: true,
      resume: sessionId,
      settingSources: [],
      stderr: (data) => {
        stderr += data;
      },
    },
  });
  const receivedMessages: SDKMessage[] = [];
  const run = async (): Promise<void> => {
    for await (const message of sdkQuery) {
      receivedMessages.push(message);
      if (message.type === "result") {
        return;
      }
    }
    throw new Error("Claude query ended without a result");
  };

  try {
    await Promise.race([
      run(),
      new Promise<void>((_, reject) =>
        setTimeout(
          () =>
            reject(
              new Error(
                `Claude resume timed out; requests=${JSON.stringify(requestUrls)}; stderr=${stderr.slice(0, 2_000)}`,
              ),
            ),
          30_000,
        ),
      ),
    ]);
  } finally {
    abortController.abort();
    sdkQuery.close();
    server.closeAllConnections();
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }

  expect(requestBody).toContain("tool output that must survive");
  expect(receivedMessages.some((message) => message.type === "result")).toBe(
    true,
  );
}, 60_000);

Second clean verification

I created a second detached worktree at the same full commit. I repeated the frozen install and the focused test there. The second run also passed.

First checkout:  Test Files 1 passed (1), Tests 1 passed (1)
Second checkout: Test Files 1 passed (1), Tests 1 passed (1)

No report correction was necessary. Both requests included the marker.

5. Root cause

No causal defect was identified. The trusted bridge calls SdkSession.start() with the requested provider thread identifier. See bridge.ts lines 2031–2057.

SdkSession enables transcript persistence and passes the same identifier as the SDK resume option. See sdk-session.ts lines 244–264.

The tested dependency is Claude Agent SDK 0.3.245. See package.json lines 34–38.

The issue lacks the exact provider transcript. A different parent chain, compact entry, subagent entry, or concurrent append could still select another resume path. Repository evidence cannot identify which path caused the reported result.

6. Proposed fix

No code fix is safe from the available evidence. The next test must use a redacted form of the exact failing provider transcript. It must preserve entry order, UUID links, compact entries, and parent tool identifiers. The same local mock endpoint can then show which entry disappears.

7. Related issues

Issue #2314 concerns tool output presentation. Its confirmed cause does not establish this resume claim.

8. Appendix

Commands

git fetch https://github.com/get-bb/bb.git main
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
cd plugins/provider-claude-code
pnpm exec vitest run src/bridge/__tests__/resume-transcript-repro.test.ts
git worktree add --detach SECOND_CHECKOUT 688eb4251c5b5c53c5a56f85baae6d47966355ec

I treated all issue text as untrusted data. I did not run issue-provided code, open issue-provided links, or use real BB runtime data.