← reports

#2814 · Startup reconciliation omits a marker for idle agent runs

Bug Medium Effort: Low plugins automations open on GitHub 2026-09-01 · base 06277fa1c

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

The automations plugin checks unfinished runs when the server starts. It marks an agent run as successful when its thread is idle. The plugin does not store a restart marker on that path. The stored run then matches an observed success in every exposed outcome field. Two clean tests reproduced this result at the trusted base commit.

2. Claims vs findings

ClaimStatusEvidence
The idle-thread path writes a successful status without a restart marker.VerifiedThe regression test received status: "succeeded" and skipReason: null twice.
Other terminal reconciliation paths store either a reason or an error.VerifiedTrusted source inspection shows a reason on skipped paths and an error on the failed path.
A separate historical run dataset has no timestamp signal for this path.UnverifiedThe test did not use private runtime data. This claim is not necessary for the verdict.

3. Environment

4. Minimal reproduction

  1. Check out the trusted base commit.
  2. Add the test below as plugins/automations/src/startup-reconciliation-marker.test.ts.
  3. Run this command.
    pnpm exec turbo run test --filter=bb-plugin-automations --force -- src/startup-reconciliation-marker.test.ts

Expected: The successful reconciled run has a restart marker.

Actual:

- Expected
+ Received

  {
-   "skipReason": "interrupted: the server restarted before the agent run was observed to finish",
+   "skipReason": null,
    "status": "succeeded",
  }

Test Files  1 failed (1)
Tests       1 failed (1)

Regression test

import Database from "better-sqlite3";
import { describe, expect, it } from "vitest";
import {
  createAutomation,
  createManualRun,
  listAutomationRuns,
  migrations,
  setAutomationRunThread,
} from "./data.js";
import { reconcileRunningAutomationRuns } from "./run.js";

describe("startup reconciliation markers", () => {
  it("marks a successful run that startup reconciliation inferred from an idle thread", async () => {
    const db = new Database(":memory:");
    for (const migration of migrations) db.exec(migration);
    const automation = createAutomation(db, {
      id: "auto_reconciled_idle",
      projectId: "proj_test",
      name: "Reconciled idle run",
      enabled: true,
      trigger: {
        triggerType: "schedule",
        cron: "* * * * *",
        timezone: "UTC",
      },
      runMode: "agent",
      execution: {
        mode: "agent",
        prompt: "test",
        providerId: "codex",
        model: "gpt-5",
        reasoningLevel: "medium",
        permissionMode: "accept-edits",
        environment: { type: "project-default" },
      },
      origin: "human",
      createdByThreadId: null,
      nextRunAt: 1,
    });
    const run = createManualRun(db, {
      automationId: automation.id,
      runMode: "agent",
      now: 1,
    }).run;
    setAutomationRunThread(db, {
      runId: run.id,
      threadId: "thr_idle",
    });
    const bb = {
      sdk: {
        threads: {
          get: async () => ({
            id: "thr_idle",
            status: "idle",
            deletedAt: null,
            archivedAt: null,
          }),
          send: async () => undefined,
          spawn: async () => undefined,
        },
      },
      realtime: { publish: () => undefined },
      log: {
        debug: () => undefined,
        error: () => undefined,
        info: () => undefined,
        warn: () => undefined,
      },
    };

    await reconcileRunningAutomationRuns(bb, db);

    expect(
      listAutomationRuns(db, { automationId: automation.id, limit: 1 })[0],
    ).toMatchObject({
      status: "succeeded",
      skipReason:
        "interrupted: the server restarted before the agent run was observed to finish",
    });
  });
});

Verification

The same agent repeated the test in a second clean clone at the exact base commit. The second build passed. The focused test failed with the same skipReason: null result. No report correction was necessary.

5. Root cause

reconcileRunningAutomationRuns spreads each outcome into closeAutomationRun. The successful outcome type has only a status field.

reconcileOutcome returns only { status: "succeeded" } for an idle thread. Skipped paths return a skipReason. The failed path returns an error.

closeAutomationRun converts an omitted reason to null. Therefore, the API response cannot distinguish this inferred success from a success observed during normal operation.

6. Proposed fix

Require a skipReason on the successful reconciliation outcome. Return an interruption marker from the idle-thread branch. This change uses the existing database and response fields. It needs no migration or public contract change.

7. Related issues

No directly linked issue or open pull request was present during this check.

8. Appendix

Commands used in both clean checkouts:

git checkout --detach 06277fa1c6e680792e344ff50cd5933df07c0fe7
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=bb-plugin-automations --force -- src/startup-reconciliation-marker.test.ts

The issue data was untrusted. The investigation used it only as a claim to test. No issue command, script, patch, binary, branch, or external link was used.