#2635 · Temporary thread lookup errors disable target automations
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
A temporary error from the target-thread lookup disables every enabled automation for that thread. The dispatch code sends every lookup error to a terminal cleanup function. That function updates rows by target thread, not by automation. The ordinary failure path already provides retry and pause policy, but this branch does not use it.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| A temporary lookup error disables the automation that started the run. | Verified | Both clean test runs changed enabled from the expected true to false. |
| The same failure disables another automation for the same target thread. | Verified | The focused test creates two target automations. A run for the first also disables the second. |
| The terminal marker does not remain on the automation that started the run. | Verified | The first row ends with the run error. The second row keeps target thread deleted. |
| Non-runnable thread states also use the terminal path. | Verified in source | The status check calls the same terminal function for every state except idle and active. |
3. Environment
- Trusted base:
ec8f4ef04105c2cb5a59f7a9a9328bf2b16b39cefromorigin/main. - Linux 7.0.0-30-generic, x86-64.
- Node v24.18.0 and pnpm 9.15.0.
- No live bb process, provider, port, or data directory was required.
- The test uses the official plugin SDK fake host and the plugin SQLite database.
4. Minimal reproduction
- Check out the trusted base commit.
- Install and build the repository.
pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build
- Place the saved test at
plugins/automations/src/target-thread-read-failure.test.ts. - Run the focused test.
pnpm exec turbo run test --filter=bb-plugin-automations --force -- src/target-thread-read-failure.test.ts
Expected and actual output from both clean runs:
- Expected
+ Received
[
{
- "enabled": true,
- "lastError": "temporary thread read failure",
+ "enabled": false,
+ "lastError": "Target thread thr_target is unavailable: temporary thread read failure",
},
{
- "enabled": true,
- "lastError": null,
+ "enabled": false,
+ "lastError": "target thread deleted",
},
]
Repro files: test, first run, and second run.
Focused test
import { afterEach, describe, expect, it, vi } from "vitest";
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import plugin from "./server.js";
import { createAutomationService } from "./service.js";
const PROJECT_ID = "proj_test";
const THREAD_ID = "thr_target";
async function createHost() {
const host = createFakePluginHost({
pluginId: "automations",
sdk: {
projects: {
async get({ projectId }) {
return { id: projectId, name: "Test Project", deletedAt: null };
},
async list() {
return [{ id: PROJECT_ID, name: "Test Project", deletedAt: null }];
},
},
providers: {
async list() {
return [
{
id: "codex",
capabilities: {
permissionModes: ["accept-edits", "auto", "full"],
},
},
] as never;
},
},
threads: {
async get() {
throw new Error("temporary thread read failure");
},
async send() {
return { ok: true };
},
async spawn() {
throw new Error("unexpected spawn");
},
},
},
});
await plugin(host.bb as unknown as Parameters<typeof plugin>[0]);
return host;
}
describe("target thread read failures", () => {
afterEach(() => {
vi.useRealTimers();
});
it("keeps all target automations enabled after a temporary read failure", async () => {
const host = await createHost();
const service = createAutomationService({
bb: host.bb as never,
db: host.bb.storage.database(),
pluginDataDir: "/tmp/bb-automations-read-failure-test",
serverUrl: "http://127.0.0.1:38886",
});
const execution = {
mode: "agent" as const,
prompt: "run the check",
providerId: "codex",
model: "gpt-5",
permissionMode: "accept-edits" as const,
environment: { type: "project-default" as const },
targetThreadId: THREAD_ID,
};
const trigger = {
triggerType: "once" as const,
runAt: Date.now() + 60_000,
};
const first = await service.create({
projectId: PROJECT_ID,
name: "First target",
enabled: true,
trigger,
execution,
origin: "human",
});
const second = await service.create({
projectId: PROJECT_ID,
name: "Second target",
enabled: true,
trigger,
execution,
origin: "human",
});
await service.run({ projectId: PROJECT_ID, automationId: first.id });
await vi.waitFor(() =>
expect(
service.runs({
projectId: PROJECT_ID,
automationId: first.id,
limit: 50,
}).runs[0]?.status,
).toBe("failed"),
);
const states = await Promise.all([
service.get({ projectId: PROJECT_ID, automationId: first.id }),
service.get({ projectId: PROJECT_ID, automationId: second.id }),
]);
expect(states).toMatchObject([
{ enabled: true, lastError: "temporary thread read failure" },
{ enabled: true, lastError: null },
]);
await host.harness.dispose();
});
});
5. Root cause
The target reuse function catches every exception from the SDK lookup and schema parser. It sends each exception to the terminal cleanup function. See run.ts lines 167–190.
The cleanup function calls a database update before it closes the run. See run.ts lines 220–238. The database update selects every enabled automation with the same target_thread_id. See data.ts lines 911–932.
Run closure then records an ordinary failure for the automation that started the run. This action replaces its prior marker. See data.ts lines 667–724. The file already defines a 404 classifier at run.ts lines 53–60, but this path does not use it.
6. Proposed fix
Use terminal cleanup only when the lookup error matches the existing 404 classifier. Throw other lookup and parse errors so executeAgentRun sends them through the ordinary failure handler. Add a regression test for a temporary error and retain a terminal 404 test. This change does not decide the separate policy for starting, stopping, error, or archived thread states.
7. Related issues
No open pull request links to this issue. The repository search found no open duplicate for this target-thread failure.
8. Appendix
Verification
The same agent repeated the test in a second clean checkout at the recorded commit. Both checkouts completed the frozen install and full Turbo build. Both focused tests failed with the same two-row state difference. No report correction was necessary.
Commands
git fetch origin main git rev-parse origin/main pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --filter=bb-plugin-automations --force -- src/target-thread-read-failure.test.ts git log ec8f4ef04105..origin/main --oneline -- plugins/automations
Untrusted data
The investigation treated the issue title, body, comments, code blocks, and links as untrusted claims. It did not run or follow issue content.