← reports

#2460 · thread.start fails with “Internal error” when omp uses Ox Alpha

BugPriority: HighEffort: Unset ompprovidersprovider-acp open on GitHub 2026-08-27 · base ad79bbb5ec90

Verdict: ALREADY FIXED · Root-cause confidence: high

1. TL;DR

bb 0.39.0 gave omp the provider-bridge bootstrap as a dynamic-tool MCP server.

The bootstrap did not accept the MCP flag, so it stopped before the ACP session opened.

OMP returned bb-bridge: Transport closed, but bb 0.39.0 removed that useful text.

The selected Ox Alpha model did not cause this failure.

Commit aab0cd710d fixed both faults before this report base.

2. Claims vs findings

Claim from the issueStatusEvidence
bb 0.39.0 returns HTTP 502 and -32000 Internal error at thread.start.VerifiedThe exact 0.39.0 bridge and OMP 18.0.5 returned the same JSON-RPC error. See the live output.
The failure occurs before a model response.VerifiedThe raw ACP response contains error.data.details: "bb-bridge: Transport closed" during session/new. See the raw ACP and bootstrap output.
The failure is not transient.VerifiedThe faulty MCP command always names the wrong program in the 0.39.0 process layout.
Ox Alpha or OpenRouter causes the failure.RefutedThe minimal run used no OpenRouter account and no selected model. It failed before model work.
The provider request never starts.VerifiedThe bootstrap exited with status 1 and printed its usage error. OMP then rejected the ACP session.

3. Environment

4. Minimal reproduction

The script starts the 0.39.0 bridge through its real worker entry. It starts OMP 18.0.5 with one dynamic tool.

  1. Prepare the old source and exact OMP release.
    git clone https://github.com/get-bb/bb.git bb-2460
    cd bb-2460
    git checkout desktop-v0.39.0
    pnpm install --frozen-lockfile --prefer-offline
    gh release download v18.0.5 -R can1357/oh-my-pi -p omp-linux-x64 -D .repro-bin
    chmod u+x .repro-bin/omp-linux-x64
  2. Download the repro script into the checkout.
    curl --fail --location \
      https://get-bb.github.io/reports/issues/2460/repro/repro-v039-omp.mjs \
      --output ./repro-v039-omp.mjs
  3. Run the script with separate temporary directories.
    mkdir -p .repro-profile .repro-workspace .repro-data
    node repro-v039-omp.mjs "$PWD" \
      "$PWD/.repro-bin/omp-linux-x64" \
      "$PWD/.repro-profile" \
      "$PWD/.repro-workspace" \
      "$PWD/.repro-data"

Expected: thread.start returns a provider thread ID.

Actual:

bridge stdout: {"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"Internal error"}}
thread.start result: {"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"Internal error"}}
raw ACP stdout:
{"jsonrpc":"2.0","id":2,"error":{"code":-32603,"message":"Internal error","data":{"details":"bb-bridge: Transport closed"}}}
bootstrap command exit: 1
bootstrap stderr: provider bridge bootstrap usage: <bridgeModulePath> <pluginId> <pluginDataDir> (absolute paths)

See the complete raw output.

Base fix check

git checkout ad79bbb5ec909524f8f281e62d860c588a86f332
pnpm install --frozen-lockfile --prefer-offline
cd packages/provider-bridge-acp
pnpm exec vitest run --config vitest.config.ts \
  src/bridge/mcp-server-entry.test.ts \
  src/bridge/agent-connection.test.ts
Test Files  2 passed (2)
Tests  7 passed (7)

See the base test output.

Repro script

The saved artifact contains the full script. The script traces OMP output and runs the faulty MCP child command.

import { spawn, spawnSync } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { createInterface } from "node:readline";
import { pathToFileURL } from "node:url";

const [repoRoot, ompBinary, profileDir, workspaceDir, dataDir] =
  process.argv.slice(2);

if (!repoRoot || !ompBinary || !profileDir || !workspaceDir || !dataDir) {
  process.stderr.write(
    "usage: node repro-v039-omp.mjs <repo> <omp> <profile> <workspace> <data-dir>\n",
  );
  process.exit(2);
}

const requireFromRepo = createRequire(
  pathToFileURL(resolve(repoRoot, "package.json")),
);
const tsxLoader = requireFromRepo.resolve("tsx");
const workerEntry = resolve(
  repoRoot,
  "packages/provider-bridge-protocol/src/bridge-worker-entry.ts",
);
const bridgeModule = resolve(
  repoRoot,
  "plugins/provider-acp/src/bridge/bridge.ts",
);
const proxyPath = resolve(dataDir, "omp-stdio-proxy.mjs");
const tracePath = resolve(dataDir, "omp-stdout.jsonl");

writeFileSync(
  proxyPath,
  `import { spawn } from "node:child_process";
import { createWriteStream } from "node:fs";

const [command, ...args] = process.argv.slice(2);
const trace = createWriteStream(process.env.BB_REPRO_OMP_STDOUT_TRACE);
const child = spawn(command, args, {
  env: process.env,
  stdio: ["pipe", "pipe", "pipe"],
});
process.stdin.pipe(child.stdin);
child.stdout.on("data", (chunk) => {
  trace.write(chunk);
  process.stdout.write(chunk);
});
child.stderr.pipe(process.stderr);
child.on("exit", (code) => {
  trace.end(() => process.exit(code ?? 1));
});
`,
);

const bridge = spawn(
  process.execPath,
  [
    "--conditions=source",
    "--import",
    tsxLoader,
    workerEntry,
    bridgeModule,
    "provider-acp",
    dataDir,
  ],
  { stdio: ["pipe", "pipe", "pipe"] },
);

let bridgeStderr = "";
bridge.stderr.on("data", (chunk) => {
  bridgeStderr += chunk.toString();
});

const responses = new Map();
createInterface({ input: bridge.stdout }).on("line", (line) => {
  process.stdout.write(`bridge stdout: ${line}\n`);
  try {
    const message = JSON.parse(line);
    if (message.id !== undefined && message.method === undefined) {
      responses.set(message.id, message);
    }
  } catch {
    // Keep non-JSON output in the transcript.
  }
});

function send(id, method, params) {
  bridge.stdin.write(
    `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`,
  );
}

async function waitForResponse(id, timeoutMs = 30000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const response = responses.get(id);
    if (response) return response;
    await new Promise((resolveDelay) => setTimeout(resolveDelay, 20));
  }
  throw new Error(
    `timed out waiting for response ${id}; bridge stderr: ${bridgeStderr}`,
  );
}

try {
  send(1, "initialize", {
    protocolVersion: 1,
    client: { name: "bb-repro", version: "0" },
  });
  await waitForResponse(1);

  send(2, "thread/start", {
    threadId: "thr_repro_2460",
    cwd: workspaceDir,
    instructionMode: "append",
    options: {
      permissionMode: "full",
      permissionScope: "full",
      approvalReviewer: null,
      permissionEscalation: null,
      providerOptions: {
        acpLaunchSpec: {
          displayName: "omp",
          command: process.execPath,
          args: [proxyPath, ompBinary, "acp"],
          env: {
            BB_REPRO_OMP_STDOUT_TRACE: tracePath,
            PI_CODING_AGENT_DIR: profileDir,
          },
        },
      },
    },
    dynamicTools: [
      {
        name: "repro_tool",
        description: "A minimal dynamic tool for the MCP bridge.",
        inputSchema: { type: "object", properties: {} },
      },
    ],
  });
  const start = await waitForResponse(2);
  process.stdout.write(`thread.start result: ${JSON.stringify(start)}\n`);
  await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
  const rawAcpOutput = existsSync(tracePath)
    ? readFileSync(tracePath, "utf8").trim()
    : "trace file was not created";
  process.stdout.write(`raw ACP stdout:\n${rawAcpOutput}\n`);

  const bootstrap = spawnSync(
    process.execPath,
    [
      "--conditions=source",
      "--import",
      tsxLoader,
      workerEntry,
      "--mcp-stdio",
    ],
    { cwd: repoRoot, encoding: "utf8" },
  );
  process.stdout.write(`bootstrap command exit: ${bootstrap.status}\n`);
  process.stdout.write(`bootstrap stdout: ${bootstrap.stdout.trim()}\n`);
  process.stdout.write(`bootstrap stderr: ${bootstrap.stderr.trim()}\n`);
  process.stdout.write(`bridge stderr: ${bridgeStderr.trim()}\n`);
} finally {
  bridge.kill("SIGKILL");
}

5. Root cause

bb 0.39.0 ran each provider bridge through a general worker program.

The ACP bridge used process.argv[1] as the dynamic-tool MCP program.

That value named the worker, not the ACP bridge module.

The worker received --mcp-stdio, printed its usage text, and stopped.

OMP then rejected ACP session/new because its required bb-bridge transport closed.

The direct run confirms both events. The raw ACP response contains bb-bridge: Transport closed.

The same MCP child command exits with status 1 and prints the bootstrap usage error.

See the raw ACP and bootstrap output.

The faulty code appears in the 0.39.0 bridge.

const entryPoint = process.argv[1]
  ? resolve(process.argv[1])
  : fileURLToPath(import.meta.url);
return [...process.execArgv, entryPoint, "--mcp-stdio"];

The base code uses its module URL. Its comment records the old failure.

See bridge.ts lines 367–373.

// process.argv[1] is the provider-bridge bootstrap, not this module.
return [...process.execArgv, fileURLToPath(import.meta.url), "--mcp-stdio"];

A second fault hid the cause.

The 0.39.0 parser kept only error.message and removed error.data.details.

See the old parser.

The base parser now adds the details. See agent-connection.ts lines 109–120.

const details = formatAgentErrorData(error.data);
return details === undefined ? message : `${message}: ${details}`;

This shared path affected all ACP agents that required the session MCP server.

Ox Alpha only identified the model that the reporter selected.

6. Proposed fix (first principles)

No new code change is necessary.

Commit aab0cd710d implemented the correct fix.

It uses import.meta.url for the MCP child and keeps ACP error details.

Users must update from bb 0.39.0 to bb 0.40.0 or a later release.

Close #2460 as already fixed and as a duplicate of #1918.

7. Related issues

8. Appendix

Artifacts

Important commands

gh issue view 2460 --comments
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
git checkout desktop-v0.39.0
node repro-v039-omp.mjs <repo> <omp> <profile> <workspace> <data-dir>
git checkout ad79bbb5ec909524f8f281e62d860c588a86f332
git fetch origin main
pnpm exec vitest run --config vitest.config.ts src/bridge/mcp-server-entry.test.ts src/bridge/agent-connection.test.ts

Caveat: The run did not use the reporter’s OpenRouter account.

This limit does not affect the conclusion because the fault occurs before model work.

Verification

The independent verifier used an external package store because the shared /tmp file system had no free file records.

The verifier ran the script and received -32000 Internal error. The verifier also ran all seven base tests.

This revision added a copy command. It also added direct raw ACP and bootstrap evidence from a new clean run.