Summary
On 0.40, when I start several ACP sessions at once (a workflow with parallel Claude workers), some of the provider processes just die. The workers never get to talk.
What I expected: the session starts, the helper says it is up, and the worker keeps running. What happens: the helper says it is up, then Node treats the hangup as a crash and the whole provider process exits.
Versions and environment
- bb 0.40.0 (desktop app), macOS 26.6.2
- ACP providers
acp-sub-claude-icloud and acp-sub-claude-woktowalk (Claude Code over ACP, not the native claude-code provider)
- Claude Code CLI 2.1.246
- Local machine, no Connect, default data dir
- Reproduced on
main at e4c873521abbcab1ea3e1aa127b73bb28e7905bb
Steps to reproduce
Live path (needs an ACP Claude provider and a workflow that starts several child sessions at once):
- Use bb 0.40.0 desktop with an ACP Claude provider (not native
claude-code).
- Run a workflow that starts several ACP child threads in parallel. Each session gets bb's
bb-bridge helper.
- Watch
~/.bb/logs/host-daemon*.log.
Did not reproduce with: the native claude-code provider, which does not use this TCP helper.
Closest faithful repro on main (no live ACP account needed): the new test keeps the dynamic-tool TCP server alive after a client reset on initialize in packages/provider-bridge-acp/src/bridge/bridge.test.ts. On current main it fails with an uncaught socket error (write EPIPE in the test; live logs are read ECONNRESET). Same hole: the TCP server has no error listener, so Node kills the process.
pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- --run -t "keeps the dynamic-tool TCP server alive"
Expected vs actual
Expected: after bb-bridge answers initialize, the provider process stays up and later tool calls still work.
Actual, from ~/.bb/logs/host-daemon.14.log (provider acp-sub-claude-icloud, thread thr_xvvsythpr4):
acp bridge: built "bb-bridge" session MCP config for thread "thr_xvvsythpr4" (3 tools)
acp bridge: "bb-bridge" answered initialize for thread "thr_xvvsythpr4" (3 tools)
node:events:487
throw er; // Unhandled 'error' event
^
Error: read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:216:20)
Emitted 'error' event on Socket instance at:
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
errno: -54,
code: 'ECONNRESET',
syscall: 'read'
}
Node.js v24.15.0
Host daemon message: Unexpected provider process exited with stderr (exit code 1). Same pattern on acp-sub-claude-woktowalk for threads including thr_drf49rrpsm and thr_pumvkn2wbi.
Evidence
Observed: the helper answers initialize, then the process dies on an unhandled socket error. Thought cause, labeled as such: 0.40 made bb-bridge open a short local TCP connection at initialize to tell the host it started. Both sides hang up right after that. The client already listens for errors. The server does not, so Node kills the process.
- Server handler, no error listener:
|
function handleDynamicToolBridgeSocket( |
|
bridge: AcpDynamicToolBridge, |
|
socket: Socket, |
|
): void { |
|
let buffer = ""; |
|
socket.setEncoding("utf8"); |
|
socket.on("data", (chunk) => { |
|
buffer += chunk; |
|
const newlineIndex = buffer.indexOf("\n"); |
|
if (newlineIndex === -1) { |
|
return; |
|
} |
|
const line = buffer.slice(0, newlineIndex); |
|
let parsed: unknown; |
|
try { |
|
parsed = JSON.parse(line); |
|
} catch { |
|
socket.end(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}\n`); |
|
return; |
|
} |
|
const request = dynamicToolBridgeRequestSchema.safeParse(parsed); |
|
if (!request.success || request.data.token !== bridge.token) { |
|
socket.end( |
|
`${JSON.stringify({ ok: false, error: "Invalid dynamic tool request" })}\n`, |
|
); |
|
return; |
|
} |
|
if (request.data.kind === "initialized") { |
|
process.stderr.write( |
|
`acp bridge: "${ACP_BRIDGE_MCP_SERVER_NAME}" answered initialize for thread "${request.data.threadId}" (${request.data.toolCount} tools)\n`, |
|
); |
|
socket.end(`${JSON.stringify({ ok: true, content: "" })}\n`); |
|
return; |
|
} |
|
void forwardDynamicToolCall(request.data).then((response) => { |
|
socket.end(`${JSON.stringify(response)}\n`); |
|
}); |
|
}); |
|
} |
- Accept path, same hole:
|
const server = createServer((socket) => { |
|
void dynamicToolBridgePromise?.then((bridge) => { |
|
handleDynamicToolBridgeSocket(bridge, socket); |
|
}); |
|
}); |
- 0.40 initialize ping, then client
socket.end():
|
switch (message.method) { |
|
case "initialize": |
|
writeResult(message.id, { |
|
protocolVersion: |
|
typeof objectParams(message.params).protocolVersion === "string" |
|
? objectParams(message.params).protocolVersion |
|
: "2024-11-05", |
|
capabilities: { tools: {} }, |
|
serverInfo: { name: ACP_BRIDGE_MCP_SERVER_NAME, version: "1.0.0" }, |
|
}); |
|
void callBridge(env, { |
|
kind: "initialized", |
|
toolCount: env.tools.length, |
|
}).catch((error) => { |
|
process.stderr.write( |
|
`bb-bridge MCP: failed to report initialize: ${ |
|
error instanceof Error ? error.message : String(error) |
|
}\n`, |
|
); |
|
}); |
- Client already has
socket.on("error", reject):
|
socket.on("data", (chunk) => { |
|
buffer += chunk; |
|
const newlineIndex = buffer.indexOf("\n"); |
|
if (newlineIndex === -1) { |
|
return; |
|
} |
|
const line = buffer.slice(0, newlineIndex); |
|
socket.end(); |
|
try { |
|
resolve(bridgeToolCallResponseSchema.parse(JSON.parse(line))); |
|
} catch (error) { |
|
reject(error); |
|
} |
|
}); |
|
socket.on("error", reject); |
0.39 already had bb-bridge for tool calls. It did not send this initialize ping, so session start did not hit the hangup. That matches "giant ACP fleets worked on 0.39."
Suggested fix: listen for error on the accepted socket and ignore it. These connections are one-shot. A hangup after the reply is normal. Do not bump HOST_DAEMON_PROTOCOL_VERSION; this is inside the ACP provider process.
What you ruled out
Suggested priority and effort
High for anyone running several ACP sessions at once (workflows). Workaround is native claude-code, which does not keep per-lane named Max accounts. In-flight workers die; no board/data loss. Effort is low: one error listener plus a regression test.
AGENT GENERATED: by Grok 4.6
Summary
On 0.40, when I start several ACP sessions at once (a workflow with parallel Claude workers), some of the provider processes just die. The workers never get to talk.
What I expected: the session starts, the helper says it is up, and the worker keeps running. What happens: the helper says it is up, then Node treats the hangup as a crash and the whole provider process exits.
Versions and environment
acp-sub-claude-icloudandacp-sub-claude-woktowalk(Claude Code over ACP, not the nativeclaude-codeprovider)mainate4c873521abbcab1ea3e1aa127b73bb28e7905bbSteps to reproduce
Live path (needs an ACP Claude provider and a workflow that starts several child sessions at once):
claude-code).bb-bridgehelper.~/.bb/logs/host-daemon*.log.Did not reproduce with: the native
claude-codeprovider, which does not use this TCP helper.Closest faithful repro on
main(no live ACP account needed): the new testkeeps the dynamic-tool TCP server alive after a client reset on initializeinpackages/provider-bridge-acp/src/bridge/bridge.test.ts. On currentmainit fails with an uncaught socket error (write EPIPEin the test; live logs areread ECONNRESET). Same hole: the TCP server has no error listener, so Node kills the process.Expected vs actual
Expected: after
bb-bridgeanswers initialize, the provider process stays up and later tool calls still work.Actual, from
~/.bb/logs/host-daemon.14.log(provideracp-sub-claude-icloud, threadthr_xvvsythpr4):Host daemon message:
Unexpected provider process exited with stderr(exit code 1). Same pattern onacp-sub-claude-woktowalkfor threads includingthr_drf49rrpsmandthr_pumvkn2wbi.Evidence
Observed: the helper answers initialize, then the process dies on an unhandled socket error. Thought cause, labeled as such: 0.40 made
bb-bridgeopen a short local TCP connection at initialize to tell the host it started. Both sides hang up right after that. The client already listens for errors. The server does not, so Node kills the process.bb/packages/provider-bridge-acp/src/bridge/bridge.ts
Lines 427 to 465 in e4c8735
bb/packages/provider-bridge-acp/src/bridge/bridge.ts
Lines 474 to 478 in e4c8735
socket.end():bb/packages/provider-bridge-acp/src/bridge/tool-proxy-mcp.ts
Lines 250 to 269 in e4c8735
socket.on("error", reject):bb/packages/provider-bridge-acp/src/bridge/tool-proxy-mcp.ts
Lines 186 to 200 in e4c8735
0.39 already had
bb-bridgefor tool calls. It did not send this initialize ping, so session start did not hit the hangup. That matches "giant ACP fleets worked on 0.39."Suggested fix: listen for
erroron the accepted socket and ignore it. These connections are one-shot. A hangup after the reply is normal. Do not bumpHOST_DAEMON_PROTOCOL_VERSION; this is inside the ACP provider process.What you ruled out
claude-codeon the same machine stays up.bb-bridge. Herebb-bridgedid start (answered initialize).session/new. Herebb-bridgeis present.bb-bridge,ECONNRESET, and "provider process exited".mainate4c873521abbcab1ea3e1aa127b73bb28e7905bb.Suggested priority and effort
High for anyone running several ACP sessions at once (workflows). Workaround is native
claude-code, which does not keep per-lane named Max accounts. In-flight workers die; no board/data loss. Effort is low: one error listener plus a regression test.