#2370 · Auto input can fail during a slow turn start
Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high
1. TL;DR
PR #2379 fixed the reported active-turn injection before the tested base commit. Two focused daemon tests confirm that result. A third test confirms a separate slow-start case. After five seconds, the daemon returns a generic error for input sent before the first turn ID exists. The server then drops that input, adds an error event, and changes a healthy thread to error. The host does not retry one command in a loop.
2. Claims vs findings
| Claim from the issue | Status | Evidence |
|---|---|---|
--mode auto injects input into an existing busy turn. |
Fixed at base | PR #2379 is in the base. The active-turn and stale-target tests both pass and select steer. |
| A slow start can reject the second input after five seconds. | Verified | The focused daemon test passes because it expects the exact refusal after the fixed wait. |
The refusal adds an error and changes a healthy thread to error. |
Verified | The server test finds system/error. It also receives error instead of active. |
| The refused input is lost. | Verified for the slow-start case | The server records client/turn/rejected. The failure path does not put the input in a queue. |
| The host retries one logical command in a loop. | Refuted | runLiveHostCommand makes one host call. Separate sends can create separate failures. |
| The issue appeared after stable 0.39.0. | Partly verified | PR #2242 added the five-second refusal. The archived production transcripts were not available. |
| The two production starts took 11.3 and 28.4 seconds. | Unverified | The production data was not available. The shared timeout path exists at the base commit. |
3. Environment
- bb base commit:
ad79bbb5ec909524f8f281e62d860c588a86f332 - Repository package version:
0.40.0 - OS: Linux 7.0.0-29-generic, x86_64
- Node:
v24.18.0; pnpm:9.15.0; Vitest:4.1.1 - The tests did not use a provider, a dev server, a port, or a bb data directory.
- Current
origin/main:1d97c63ed13b5231c1051b7af7ac36661d1f65ab - No affected source file changed between the base and current
origin/main.
4. Minimal reproduction
-
Prepare the base checkout.
git checkout ad79bbb5ec909524f8f281e62d860c588a86f332 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build
-
Run the two fixed-path tests and the residual test.
pnpm exec turbo run test --filter=@bb/host-daemon --force -- \ src/command-dispatch.test.ts \ -t "steers an auto submit when the active turn appears after the server snapshot|rebases auto input onto the daemon's newer active turn|rejects auto input when a pending turn still has no id after the wait"
Actual test result:
Test Files 1 passed (1) Tests 3 passed | 37 skipped (40)
The first two tests prove that active-turn auto input uses
steer. The third test expects this residual refusal:Refusing to start a competing turn while thread-1 is still starting
-
Download the server test patch to an exact local path.
curl --fail --location \ https://get-bb.github.io/reports/issues/2370/repro/server-failure-repro.patch \ --output /tmp/bb-2370-server-failure-repro.patch git apply --check /tmp/bb-2370-server-failure-repro.patch git apply /tmp/bb-2370-server-failure-repro.patch
-
Run the server test.
cd apps/server pnpm exec vitest run test/threads/thread-send-dispatch.test.ts \ -t "issue 2370 keeps a live thread active after a pending-start refusal"
Expected:
No system/error event Thread status: active
Actual:
AssertionError: expected [ 'thread/identity', …(5) ] to not include 'system/error' Expected: "active" Received: "error" Test Files 1 failed (1) Tests 1 failed | 10 skipped (11)
Reproduction test
The complete patch is server-failure-repro.patch.
The daemon test creates the exact refusal. This server test isolates the server response to that refusal.
it("issue 2370 keeps a live thread active after a pending-start refusal", async () => {
await withTestHarness(async (harness) => {
const { environment, thread } = seedProviderThreadFixture({
harness,
status: "active",
value: 2370,
});
seedTurnStarted(harness.deps, {
environmentId: environment.id,
providerThreadId: "provider-send-dispatch-2370",
sequence: 3,
threadId: thread.id,
turnId: "turn-still-healthy",
});
const activeThread = getThread(harness.db, thread.id);
if (!activeThread) throw new Error("Expected an active thread");
await sendThreadMessage(harness.deps, {
environment,
payload: {
input: textInput("send while the first turn is still starting"),
mode: "auto",
model: "gpt-5",
permissionMode: "full",
reasoningLevel: "medium",
serviceTier: "default",
},
thread: activeThread,
trigger: "user",
});
const queued = await waitForQueuedCommand(
harness,
(candidate) =>
candidate.command.type === "turn.submit" &&
candidate.command.threadId === thread.id,
);
await reportQueuedCommandError(harness, queued, {
errorCode: "command_failed",
errorMessage:
`Refusing to start a competing turn while ${thread.id} is still starting`,
});
const eventTypes = listEvents(harness.db, {
threadId: thread.id,
}).map((event) => event.type);
expect.soft(eventTypes).not.toContain("system/error");
expect.soft(getThread(harness.db, thread.id)?.status).toBe("active");
});
});
See the commands, daemon log, and server log.
5. Root cause
PR #2379 fixed the original active-turn path
The daemon now reads the live turn target. It also rebases a stale target once.
const liveTurnId = await resolveLiveSubmittedTurnTarget(command, entry); ... targetTurnId = liveTurnId; ... return ( (await resolveLiveSubmittedTurnTarget(command, entry)) ?? command.target.expectedTurnId );
Source: stale-target rebase and live target selection.
The merge commit 2ddd1b8e is an ancestor of the tested base.
The pending-start path still has a fixed wait
A provider can take more than five seconds to publish its first turn ID. The daemon then throws a generic error.
const TURN_SUBMIT_ACTIVE_TURN_WAIT_MS = 5_000;
const awaitedTurnId = await entry.runtime.waitForActiveTurn(
command.threadId,
{ timeoutMs: TURN_SUBMIT_ACTIVE_TURN_WAIT_MS },
);
...
if (entry.runtime.getLiveThreadIds().includes(command.threadId)) {
throw new Error(
`Refusing to start a competing turn while ${command.threadId} is still starting`,
);
}
Source: wait constant and refusal path.
The server changes the refusal into a run failure
The live command code makes one host call. It does not retry that call.
Source: single host call.
The settlement code treats each failed turn.submit as terminal. It rejects the request, adds system/error, and applies run.failed.
type: "client/turn/rejected",
...
appendSystemErrorEventInTransaction(...);
...
event: { type: "run.failed" },
Source: failure settlement.
This path has no queue operation. Therefore, the refused input has no later delivery path.
The original pi-provider refusal was not reproduced at this base. The prior report joined that claim to the separate slow-start defect.
6. Proposed fix (first principles)
- Add a typed pending-start busy error to the host daemon contract.
- Increase
HOST_DAEMON_PROTOCOL_VERSIONbecause the error meaning crosses the daemon wire. - Keep the live thread status when the server receives this error.
- Do not add
system/errorfor this temporary refusal. - Put refused direct input into the thread queue.
- Restore a consumed queued group at the queue head.
- Test late turn IDs, thread completion, duplicate delivery, and restart recovery.
Closed PR #2417 used this design. It did not merge.
7. Related issues
- Issue #2370 is the investigated report.
- PR #2242 added the five-second pending-start refusal.
- PR #2379 fixed active-turn auto sends before this base.
- Closed PR #2417 contained a residual fix.
8. Appendix
Commands
gh issue view 2370 --comments -R get-bb/bb git fetch origin main git merge-base --is-ancestor 2ddd1b8e75ce4e01a9b75ba94d827ac1c74fb202 ad79bbb5ec909524f8f281e62d860c588a86f332 git diff --name-only ad79bbb5ec90..origin/main -- <affected paths> pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --filter=@bb/host-daemon --force -- src/command-dispatch.test.ts -t "steers an auto submit when the active turn appears after the server snapshot|rebases auto input onto the daemon's newer active turn|rejects auto input when a pending turn still has no id after the wait" curl --fail --location https://get-bb.github.io/reports/issues/2370/repro/server-failure-repro.patch --output /tmp/bb-2370-server-failure-repro.patch git apply --check /tmp/bb-2370-server-failure-repro.patch git apply /tmp/bb-2370-server-failure-repro.patch cd apps/server pnpm exec vitest run test/threads/thread-send-dispatch.test.ts -t "issue 2370 keeps a live thread active after a pending-start refusal"
Raw evidence
No open pull request links to this issue. Therefore, this report has no PR review section.
Verification
The verifier ran the daemon test and the linked server patch. The verifier confirmed both residual assertions.
This revision adds an exact patch path and changes the verdict. It separates the fixed active-turn path from the residual slow-start path.
The revision also records current origin/main. No affected source file changed after the base.