← reports

#2635 · Temporary thread lookup errors disable target automations

Bug High Effort: Low threads automations open on GitHub 2026-08-28 · base ec8f4ef04105

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

ClaimStatusEvidence
A temporary lookup error disables the automation that started the run.VerifiedBoth clean test runs changed enabled from the expected true to false.
The same failure disables another automation for the same target thread.VerifiedThe 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.VerifiedThe 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 sourceThe status check calls the same terminal function for every state except idle and active.

3. Environment

4. Minimal reproduction

  1. Check out the trusted base commit.
  2. Install and build the repository.
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  3. Place the saved test at plugins/automations/src/target-thread-read-failure.test.ts.
  4. 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.