#2814 · Startup reconciliation omits a marker for idle agent runs
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
| Claim | Status | Evidence |
|---|---|---|
| The idle-thread path writes a successful status without a restart marker. | Verified | The regression test received status: "succeeded" and skipReason: null twice. |
| Other terminal reconciliation paths store either a reason or an error. | Verified | Trusted 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. | Unverified | The test did not use private runtime data. This claim is not necessary for the verdict. |
3. Environment
- Trusted bb commit:
06277fa1c6e680792e344ff50cd5933df07c0fe7. - OS: Darwin 25.6.0, arm64.
- Node: v22.22.3. pnpm: 9.15.0.
- No server, provider, port, or data directory was used.
- Each checkout passed
pnpm install --frozen-lockfile --prefer-offlineandpnpm exec turbo run build.
4. Minimal reproduction
- Check out the trusted base commit.
- Add the test below as
plugins/automations/src/startup-reconciliation-marker.test.ts. - 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.