Files
PaperClipAI/server/src/__tests__/execution-control-work.test.ts
T
DottaandPaperclip e3fb4bc73a fix(native): recover saved listener startup failures (#19479)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native recovery must preserve saved work and respect current task
authority.
> - A closed local API listener can stop startup before a provider
starts.
> - #19462 gives new failures a bounded bootstrap retry.
> - Older saved failures still have a generic terminal code and a
recovery hold.
> - This pull request verifies the old producer outcome and uses the
existing safe replacement path.
> - Eligible tasks continue once. Unknown provider outcomes and user
holds remain blocked.

## Linked Issues or Issue Description

Refs: #19462

**What happened?**

A restart can leave an old listener startup failure in
`terminal_failure` with `native_session_interrupted`. Startup recovery
does not claim it after #19462. A completed file recovery can also
retain its no-replay hold.

**Expected behavior**

Continue automatically when durable records prove that no provider
started, the source stopped, and the current files were saved. Keep the
existing attempt budget and normal admission checks.

**Steps to reproduce**

1. Close the actual API listener after a fresh native coordinator claim.
2. Persist the old executor's generic failure. Save the workspace and
confirm source termination.
3. Restart recovery. The baseline schedules no successor. The repair
schedules one successor after all receipts pass.

**Paperclip version or commit**

Baseline: `0252421d5c`. Old failure producer: `2f35eb1080`.

**Deployment mode**

Built from source. Local macOS arm64, Node 26.4.0, real embedded
PostgreSQL, independent filesystem execution. No external AI service is
used by the controlled acceptance.

## What Changed

- Recognize the exact saved local-listener guard through one proof
predicate shared by discovery and the locked transaction.
- Require a fresh first attempt, matching execution bindings, no prior
provider lineage, no provider events or tool calls, no checkpoint or
admission receipt, and no cancellation.
- Reuse current snapshot, exact stop, source block, attempt budget, and
successor idempotency checks.
- Wait for completed source file-recovery receipts. Retire only that
same source's verified preservation hold in the successor transaction.
- Bound discovery to 100 candidates per pass, keep completed history
quiet, and wake through existing transactional producer notifications.
- Retain retries after transient database and final ownership-proof
errors; advance the bounded cursor and force a rescan so a failing page
cannot hide later work.
- Let only the replacement duty discharge a proved-empty notification
during drain through its existing read-only readiness check, once per
notification generation. Keep real work and concurrent changes
outstanding without running replacement during drain.
- Wake replacement at canonical local-stop proof writes and the existing
dispatch eligibility boundary; these hints grant no new execution
authority.
- Add database regressions, scheduler quietness checks, real controller
handoff coverage, and an opt-in controlled Runner continuation.

## Verification

Final production head: `0df41b2fef2644195d1224d36984af3827da3e3b`.

Local environment: macOS arm64, Node 26.4.0, TypeScript 7.0.2, Vitest
5.0.3, frozen-lockfile dependencies. Tests execute freshly;
`--fsModuleCache` reuses only content-validated module transforms.

- Native safe replacement: 62/62 PASS on isolated PostgreSQL 17 (63.18
seconds), including clean per-suite database disposal. All 62 assertions
also passed on embedded PostgreSQL 18 beta, but its Darwin cleanup hook
timed out; that invocation is not counted as a pass. Docker's disk was
full, so the disposable PostgreSQL 17 data used a size-limited RAM
mount, with no host mounts. The owned container was removed afterward.
- Actual stopped-process producer suite: 31/31 PASS. A real exited child
produces the stop receipt, and its replacement notification settles only
after the outer transaction commits.
- Workspace and separate-process drain regressions: 33/33 PASS (75.90
seconds).
- Shared coordinator and execution-control lifecycle checks: 39/39 PASS
(16.43 seconds). Pending work, concurrent changes, cold startup,
shutdown, and passive drain boundaries remain covered.
- Actual suppressed Cloud routine/drain workflow: PASS (34.64 seconds),
using the unchanged test and timeouts. Its matching local RED reproduced
CI's 462-query, zero-pending-query, one-idle-hold timeout after an
admitted write committed during drain (69.46 seconds).
- Actual historical controller handoff: PASS (48.40 seconds). A separate
controller loads the original executor from `2f35eb1080`, writes its
generic listener failure, and exits. Recovery at the final head admits
exactly one successor. The real Rust Runner and controlled Codex process
complete governed `paperclip_finish`; attempt two commits and the saved
file remains intact. Only the child loads the old artifact, and both
isolated artifacts are restored afterward. Rust source subtree
`77961b70f177ec3c38d11abcaec92fd3d0e96ec0` matches the built binary. No
external AI service is used.
- Seven corrected existing fixtures pass: transactional stop-proof calls
(5 cases) and notification inventory (2 cases). Their earlier CI
failures exposed fixture calls outside the documented transaction and an
outdated expected queue set.
- Server typecheck: PASS.

Red/green proofs also cover the original saved failure on baseline
`0252421d5c` (zero successors instead of one), held-history quietness,
bounded forward scanning through 100 parked rows and concurrent wakes,
real snapshot/cleanup producer notifications, actual issue-row lock
timeout, and task/history eligibility notifications. An obsolete
prior-run question can be skipped to request another proof check; actual
skip while the task is paused or the source owns provider tool activity
schedules no successor.

The failed-page regression starts with 25 persistent verifier errors and
a healthy candidate 26. Baseline RED repeats the first page. Repair
GREEN advances the healthy candidate, revisits the original failures
after their fault clears, creates all 26 successors exactly once, and
becomes quiet.

Greptile is SUCCESS, 5/5 on the exact final head (2026-11-06 17:09:21
UTC). Final-head CI is GREEN: 56 successful checks, three intentional
skips, one neutral result, and no pending or failing checks. [Attempt
two and
results](https://github.com/paperclipai/paperclip/actions/runs/42258352876/attempts/2)
include passing build, typecheck, Rust, native server, package, browser
E2E, serialized server, general server/chat tests, and final
verification. Five jobs in the first attempt were interrupted together
at 17:13:12 UTC; the native-server log records an explicit runner
shutdown signal. The single infrastructure retry reran only those five
jobs and dependent verification, preserving the 49 successful checks on
the unchanged head. No timeout was weakened, and setup failures or
interrupted invocations are not counted as passes.

Focused commands from the repository root:

```sh
./server/node_modules/.bin/vitest run --project @paperclipai/server server/src/services/remote-workspaces.integration.test.ts server/src/services/native-runtime/native-runner-restart-preparation.integration.test.ts -t 'native safe-replacement decision|reconciles the drained remote backend' --fsModuleCache --reporter=dot
./server/node_modules/.bin/vitest run --project @paperclipai/server server/src/__tests__/execution-control-work.test.ts server/src/__tests__/delivery-work-coordinator.test.ts -t 'execution-control lifecycle|work coordinator' --fsModuleCache --reporter=dot
node node_modules/typescript/bin/tsc -p server/tsconfig.json --noEmit --pretty false
```

The historical acceptance sets `PAPERCLIP_TEST_HISTORICAL_LISTENER=1`
and uses an original-executor child artifact with a `try/finally`
restore. Normal regressions require no Rust binary.

## Risks

- Compatibility is deliberately limited to positively proved fresh
remote startup failures. Other old ambiguous failures remain held.
- Existing provider work, a changed task block, incomplete preservation,
or revoked authority must prevent execution. Normal retry admission
remains in control.
- This PR does not deploy the change or mutate a hosted task. Hosted
recovery still requires the repaired revision and all current proof
gates.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use, and code
execution. The exact backend model identifier and context window are not
exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 11:49:28 -06:00

453 lines
30 KiB
TypeScript

import { writeExecutionControl } from "../services/execution-control-notifications.js";
import { reconcileSafeNativeReplacements } from "../services/native-runtime/native-safe-replacement.js";
import { localAiLoginService } from "../services/local-ai-login.js";
import { randomUUID } from "node:crypto";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { eq, sql } from "drizzle-orm";
import { agents, agentWakeupRequests, companies, createDb, heartbeatRuns, issues, issueRecoveryActions, nativeRunFinalizations, applyPendingMigrations } from "@paperclipai/db";
import { installDatabaseWorkSignals } from "../../../packages/db/src/work-signals.js";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import { resolveExternalTestDatabaseUrl } from "./helpers/external-test-database.js";
import { createDeliveryWorkCoordinator } from "../services/delivery-work-coordinator.js";
import { DELIVERY_QUEUES, notifyDeliveryWork } from "../services/delivery-work-notifications.js";
import { executionControlSchedule, registerExecutionControlWork } from "../services/execution-control-work.js";
import { EXECUTION_RECONCILIATION_INTERVAL_MS } from "../services/execution-control-deadline.js";
import { deliverExecutionStatuses } from "../services/execution-status-delivery.js";
import { terminalizeLegacyExecution } from "../services/legacy-execution-recovery.js";
import { reconcileAbandonedExecutionControl } from "../services/execution-control-reconciliation.js";
import { issueRecoveryActionService } from "../services/issue-recovery-actions.js";
import { markExecutionReconciliation, deliverReconciledExecutions, settleUnrecoverableExecutions } from "../services/execution-recovery-resolution.js";
import { nextWorkspaceRecoveryAt } from "../services/system-recovery.js";
import { createSystemRecovery } from "../services/system-recovery-state.js";
import { retainedWorkspaceCandidates } from "../services/workspace-recovery.js";
import { registerWorkspaceRecoveryWork } from "../services/workspace-recovery-work.js";
import { idleWorkSnapshot } from "../services/task-admission.js";
import { heartbeatService } from "../services/heartbeat.js";
import { requestTaskRetry } from "../services/task-retry.js";
const coordinators: ReturnType<typeof createDeliveryWorkCoordinator>[] = [];
afterEach(async () => { for (const worker of coordinators.splice(0)) await worker.stop(); vi.useRealTimers(); });
function coordinator(owner: object, canRun = () => true, onError: (error: unknown) => void = vi.fn()) {
const c = createDeliveryWorkCoordinator({ owner, canRun, canReconcile: () => true, onError });
coordinators.push(c); return c;
}
function fakeOwner() {
const execute = vi.fn(async () => [{ xid: "42" }]);
const owner = installDatabaseWorkSignals({ execute, async transaction(fn: (tx: any) => Promise<any>): Promise<any> {
return fn({ execute, transaction: owner.transaction });
} });
return owner;
}
describe("execution-control lifecycle", () => {
it.each(["startup", "commit", "failure"])("retains %s work through drain without querying while paused", async kind => {
vi.useFakeTimers();
const owner = fakeOwner(); let draining = kind === "startup";
const errors = vi.fn(); const c = coordinator(owner, () => !draining, errors);
const run = vi.fn(async () => {}), next = vi.fn(async () => null);
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionStatus, run, next }]).ready;
if (kind !== "startup") {
if (kind === "failure") run.mockRejectedValueOnce(new Error("delivery unavailable"));
else draining = true;
await owner.transaction(tx => notifyDeliveryWork(tx, DELIVERY_QUEUES.executionStatus));
await vi.advanceTimersByTimeAsync(15_000);
draining = true;
}
const reads = next.mock.calls.length, runs = run.mock.calls.length;
await vi.advanceTimersByTimeAsync(60_000);
expect(next).toHaveBeenCalledTimes(reads); expect(run).toHaveBeenCalledTimes(runs);
draining = false;
await vi.advanceTimersByTimeAsync(15_000);
expect(run).toHaveBeenCalledTimes(runs + 1);
expect(c.nextWakeAt()).toBeNull();
});
it.each(["empty", "pending", "concurrent"] as const)("settles only proved-empty replacement notifications during drain (%s)", async kind => {
vi.useFakeTimers(); const owner = fakeOwner(); let draining = false;
const c = coordinator(owner, () => !draining);
const run = vi.fn(async () => {}), next = vi.fn(async (): Promise<number | null> => null);
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionReplacement, run, next,
settleEmptyWhilePaused: true }]).ready;
const before = idleWorkSnapshot().active;
draining = true;
let release: (() => void) | undefined;
if (kind === "pending") next.mockResolvedValue(Date.now() + 15_000);
if (kind === "concurrent") next.mockImplementationOnce(() => new Promise(resolve => {
release = () => resolve(null);
}));
await owner.transaction(tx => notifyDeliveryWork(tx, DELIVERY_QUEUES.executionReplacement));
await vi.advanceTimersByTimeAsync(15_000);
if (kind === "concurrent") {
expect(release).toBeTypeOf("function");
next.mockResolvedValue(Date.now() + 15_000);
await owner.transaction(tx => notifyDeliveryWork(tx, DELIVERY_QUEUES.executionReplacement));
release!(); await vi.advanceTimersByTimeAsync(1);
}
expect(run).toHaveBeenCalledTimes(1);
expect(idleWorkSnapshot().active).toBe(before + (kind === "empty" ? 0 : 1));
const reads = next.mock.calls.length;
await vi.advanceTimersByTimeAsync(60_000);
expect(next).toHaveBeenCalledTimes(reads);
expect(run).toHaveBeenCalledTimes(1);
if (kind !== "empty") {
draining = false; next.mockResolvedValue(null);
await vi.advanceTimersByTimeAsync(15_000);
expect(run).toHaveBeenCalledTimes(2);
}
expect(idleWorkSnapshot().active).toBe(before);
expect(c.nextWakeAt()).toBeNull();
});
it("keeps a change arriving during a sweep and does not overlap it", async () => {
vi.useFakeTimers(); const owner = fakeOwner(); const c = coordinator(owner);
let release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
const run = vi.fn(async () => {});
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionStatus, run, next: async () => null }]).ready;
run.mockImplementationOnce(() => gate);
await owner.transaction(tx => notifyDeliveryWork(tx, DELIVERY_QUEUES.executionStatus));
await vi.advanceTimersByTimeAsync(15_000);
await owner.transaction(tx => notifyDeliveryWork(tx, DELIVERY_QUEUES.executionStatus));
await vi.advanceTimersByTimeAsync(1);
expect(run).toHaveBeenCalledTimes(2);
release(); await vi.advanceTimersByTimeAsync(15_000);
expect(run).toHaveBeenCalledTimes(3); expect(c.nextWakeAt()).toBeNull();
});
it("coalesces tenant write bursts without repeatedly scanning global queues", async () => {
vi.useFakeTimers(); const owner = fakeOwner(); const c = coordinator(owner);
const run = vi.fn(async () => {}), next = vi.fn(async () => null);
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionStatus, run, next }]).ready;
for (let i = 0; i < 100; i++) {
await owner.transaction(tx => notifyDeliveryWork(tx, DELIVERY_QUEUES.executionStatus, `company-${i % 2}`));
await vi.advanceTimersByTimeAsync(10);
}
expect(run).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(14_000);
expect(run).toHaveBeenCalledTimes(2); expect(next).toHaveBeenCalledTimes(2);
expect(c.nextWakeAt()).toBeNull();
await vi.advanceTimersByTimeAsync(60_000);
expect(run).toHaveBeenCalledTimes(2);
});
it.each([false, true])("does not start sweeps or inspect deadlines once server shutdown begins (notification=%s)", async notification => {
vi.useFakeTimers(); const owner = fakeOwner(); const c = coordinator(owner); let stopping = false;
const run = vi.fn(async () => {}), next = vi.fn(async () => Date.now() + 1000);
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionStatus, run, next }], () => !stopping).ready;
stopping = true;
if (notification) await owner.transaction(tx => notifyDeliveryWork(tx, DELIVERY_QUEUES.executionStatus));
await vi.advanceTimersByTimeAsync(30_000);
expect(run).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledTimes(1);
await c.stop(); expect(c.nextWakeAt()).toBeNull();
});
it.each([DELIVERY_QUEUES.executionReplacement, DELIVERY_QUEUES.workspaceRecovery])("keeps finalization responsive while %s is waiting, and awaits shutdown", async heldQueue => {
vi.useFakeTimers(); const owner = fakeOwner(); const c = coordinator(owner);
let release!: () => void; const gate = new Promise<void>(resolve => { release = resolve; });
const status = vi.fn(async () => {});
const started = registerExecutionControlWork(c, [
{ queue: heldQueue, run: () => gate, next: async () => null },
{ queue: DELIVERY_QUEUES.executionFinalization, run: status, next: async () => null },
]);
await vi.advanceTimersByTimeAsync(1); expect(status).toHaveBeenCalledTimes(1);
let stopped = false; const stopping = c.stop().then(() => { stopped = true; });
await vi.advanceTimersByTimeAsync(1); expect(stopped).toBe(false);
release(); await started.ready; await stopping; expect(stopped).toBe(true);
});
});
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_EXECUTION_SLEEP_TEST_DATABASE_URL");
const support = external ? { supported: true } : await getEmbeddedPostgresTestSupport();
describe.skipIf(!support.supported)("execution-control durable work", () => {
let db: ReturnType<typeof createDb>, observed: typeof db;
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
let calls: number, companyId: string, agentId: string, issueId: string;
const errors: unknown[] = [];
beforeAll(async () => {
if (external) await applyPendingMigrations(external);
else temporary = await startEmbeddedPostgresTestDatabase("execution-sleep-");
db = createDb(external ?? temporary!.connectionString, { maxConnections: 6 });
await Promise.all(Array.from({ length: 6 }, () => db.execute(sql`select pg_sleep(0.05)`)));
}, 90000);
afterAll(async () => { await db?.$client.end({ timeout: 1 }); await temporary?.cleanup(); });
beforeEach(async () => {
await db.execute(sql`TRUNCATE TABLE companies CASCADE`);
companyId = randomUUID(); agentId = randomUUID(); issueId = randomUUID(); calls = 0; errors.length = 0;
await db.insert(companies).values({ id: companyId, name: "Idle execution", issuePrefix: "EXE" });
await db.insert(agents).values({ id: agentId, companyId, name: "Worker", adapterType: "codex_local" });
await db.insert(issues).values({ id: issueId, companyId, title: "Execution", status: "in_progress", assigneeAgentId: agentId });
observed = new Proxy(db, { get(target, key) {
if (!["select", "update", "insert", "delete", "execute", "transaction"].includes(String(key))) return Reflect.get(target, key);
return (...args: unknown[]) => { calls++; return Reflect.apply(Reflect.get(target, key), target, args); };
} });
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout", "setInterval", "clearInterval"], shouldAdvanceTime: true });
});
async function settle() {
await vi.advanceTimersByTimeAsync(15_000);
await vi.waitFor(() => expect(idleWorkSnapshot().active).toBe(0), { timeout: 5000 });
await vi.advanceTimersByTimeAsync(1);
await vi.waitFor(() => expect(idleWorkSnapshot().active).toBe(0), { timeout: 5000 });
expect(errors).toEqual([]);
}
async function quiet(c: ReturnType<typeof coordinator>) {
await settle(); expect(c.nextWakeAt()).toBeNull(); const before = calls;
await vi.advanceTimersByTimeAsync(90_000); expect(calls).toBe(before);
}
async function runRow(status = "running", sourceIssueId = issueId) {
const [run] = await db.insert(heartbeatRuns).values({ companyId, agentId, status, runtimeMode: "legacy", contextSnapshot: { issueId: sourceIssueId } }).returning();
return run!;
}
async function pausedRecoveredSource(actionId = randomUUID()) {
const sourceIssueId = randomUUID();
await db.insert(issues).values({ id: sourceIssueId, companyId, title: "Recovered execution",
status: "in_progress", assigneeAgentId: agentId });
const run = await runRow("failed", sourceIssueId);
await db.update(heartbeatRuns).set({ resultJson: { startupCancellation: { beforeNativeSelection: true } } })
.where(eq(heartbeatRuns.id, run.id));
await db.update(agents).set({ status: "paused" }).where(eq(agents.id, agentId));
await db.insert(issueRecoveryActions).values({ id: actionId, companyId, sourceIssueId,
kind: "active_run_watchdog", ownerType: "system", cause: "native_workspace_sync_out_retry_exhausted",
fingerprint: run.id, nextAction: "Saved work recovered", evidence: { runId: run.id,
systemRecovery: { ...createSystemRecovery(run.id), state: "complete" },
workspaceRecovery: { schema: "paperclip.workspace-recovery.v1", runId: run.id,
status: "recovered", stage: "saving_receipts", attempt: 1, attemptLimit: 6, requestIds: [] } } });
const retry = await requestTaskRetry({ db, companyId, issueId: sourceIssueId, actorId: "board",
observedRunId: run.id, idempotencyKey: randomUUID() });
expect(retry.accepted).toBe(true);
return { run, retry };
}
it("stops all pending/deadline scans with empty queues and completed history", async () => {
await runRow("succeeded");
const schedule = executionControlSchedule(observed);
const c = coordinator(db, undefined, error => { errors.push(error); });
const topics = [DELIVERY_QUEUES.executionFinalization, DELIVERY_QUEUES.workspaceRecovery, DELIVERY_QUEUES.executionReplacement, DELIVERY_QUEUES.executionContinuation,
DELIVERY_QUEUES.executionStatus, DELIVERY_QUEUES.executionDisposition, DELIVERY_QUEUES.localLoginCleanup];
const runs = [() => reconcileAbandonedExecutionControl(observed), async () => {}, () => reconcileSafeNativeReplacements(observed),
() => deliverReconciledExecutions(observed, vi.fn(async () => null)), () => deliverExecutionStatuses(observed),
() => settleUnrecoverableExecutions(observed), () => localAiLoginService(observed).reapExpired()];
await registerExecutionControlWork(c, Object.values(schedule).map((next, i) => ({ queue: topics[i]!, run: runs[i]!, next }))).ready;
await quiet(c);
});
it("schedules only a current unfinished workspace recovery and becomes quiet with retained history", async () => {
const run = await runRow("failed");
const state = createSystemRecovery(run.id);
const evidence = { runId: run.id, systemRecovery: state,
workspaceRecovery: { schema: "paperclip.workspace-recovery.v1", runId: run.id,
status: "queued", stage: "verifying_stop", attempt: 0, attemptLimit: 6, requestIds: [] } };
const [action] = await db.insert(issueRecoveryActions).values({ companyId, sourceIssueId: issueId,
kind: "active_run_watchdog", ownerType: "system", cause: "native_workspace_sync_out_retry_exhausted",
fingerprint: run.id, nextAction: "Save work", evidence }).returning();
expect(await nextWorkspaceRecoveryAt(db)).toBe(0);
const due = Date.now() + 60_000;
await db.update(issueRecoveryActions).set({ evidence: { ...evidence, systemRecovery: { ...state, nextAttemptAt: due } } })
.where(eq(issueRecoveryActions.id, action!.id));
expect(await executionControlSchedule(db).workspaceRecovery()).toBe(due);
for (const status of ["cancelled", "resolved"] as const) {
await db.update(issueRecoveryActions).set({ status }).where(eq(issueRecoveryActions.id, action!.id));
expect(await nextWorkspaceRecoveryAt(db)).toBeNull();
expect(await retainedWorkspaceCandidates(db)).toEqual([]);
}
for (const system of [{ ...state, sourceRunId: randomUUID() }, { ...state, state: "complete" }, { ...state, state: "exhausted" }]) {
await db.update(issueRecoveryActions).set({ status: "active", evidence: { ...evidence, systemRecovery: system } })
.where(eq(issueRecoveryActions.id, action!.id));
expect(await nextWorkspaceRecoveryAt(db)).toBeNull();
}
await db.update(issueRecoveryActions).set({ evidence, cause: "agent_unreachable" })
.where(eq(issueRecoveryActions.id, action!.id));
expect(await nextWorkspaceRecoveryAt(db)).toBeNull();
await db.update(issueRecoveryActions).set({ cause: "native_workspace_sync_out_retry_exhausted",
evidence: { ...evidence, workspaceRecovery: { ...evidence.workspaceRecovery, runId: randomUUID() } } })
.where(eq(issueRecoveryActions.id, action!.id));
expect(await nextWorkspaceRecoveryAt(db)).toBeNull();
expect(await retainedWorkspaceCandidates(db)).toEqual([]);
await db.update(issueRecoveryActions).set({ status: "cancelled", evidence: { ...evidence,
systemRecovery: { ...state, state: "running" } } }).where(eq(issueRecoveryActions.id, action!.id));
const c = coordinator(db, undefined, error => { errors.push(error); });
const worker = registerWorkspaceRecoveryWork(observed, c,
{} as Parameters<typeof registerWorkspaceRecoveryWork>[2], async () => {});
try { await worker.ready; await quiet(c); } finally { await worker.stop(); }
});
it.each([1, 3, 7])("visits all %i paused Retry sources once and becomes quiet after file recovery", async count => {
const sources = [];
for (let index = 0; index < count; index++) sources.push(await pausedRecoveredSource());
const resume = vi.fn(heartbeatService(observed).resumeRemoteStopComments);
const c = coordinator(db, undefined, error => { errors.push(error); });
const worker = registerWorkspaceRecoveryWork(observed, c,
{} as Parameters<typeof registerWorkspaceRecoveryWork>[2], resume);
try {
await worker.ready;
for (let pass = 0; pass < 10 && resume.mock.calls.length < count; pass++) await settle();
expect(resume).toHaveBeenCalledTimes(count);
await quiet(c);
for (const result of resume.mock.results) {
await expect(result.value).rejects.toThrow("Agent is not invokable in its current state");
}
for (const { retry } of sources) {
const [saved] = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, retry.requestId!));
expect(saved).toMatchObject({ status: "deferred_issue_execution", runId: null });
}
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, companyId))).toHaveLength(count);
expect((await db.select().from(agents).where(eq(agents.id, agentId)))[0]?.status).toBe("paused");
} finally { await c.stop(); await worker.stop(); }
});
it("visits a newly committed earlier Retry during a busy recovered-source pass, then becomes quiet", async () => {
const sources: Awaited<ReturnType<typeof pausedRecoveredSource>>[] = [];
for (let index = 0; index < 3; index++) {
sources.push(await pausedRecoveredSource(`ffffffff-000${index}-4000-8000-${randomUUID().slice(-12)}`));
}
let release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
const heartbeat = heartbeatService(observed);
const resume = vi.fn(async (run: Parameters<typeof heartbeat.resumeRemoteStopComments>[0]) => {
await gate;
return heartbeat.resumeRemoteStopComments(run);
});
const c = coordinator(db, undefined, error => { errors.push(error); });
const worker = registerWorkspaceRecoveryWork(observed, c,
{} as Parameters<typeof registerWorkspaceRecoveryWork>[2], resume);
try {
await worker.ready;
await vi.waitFor(() => expect(resume).toHaveBeenCalledTimes(2));
const earlier = await pausedRecoveredSource(`00000000-0000-4000-8000-${randomUUID().slice(-12)}`);
sources.push(earlier);
release();
await vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(EXECUTION_RECONCILIATION_INTERVAL_MS);
expect(sources.every(({ run }) => resume.mock.calls.some(([visited]) => visited.id === run.id))).toBe(true);
expect(idleWorkSnapshot().active).toBe(0);
}, { timeout: 10_000, interval: 50 });
await quiet(c);
for (const { run, retry } of sources) {
expect(resume.mock.calls.map(([visited]) => visited.id)).toContain(run.id);
const [saved] = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, retry.requestId!));
expect(saved).toMatchObject({ status: "deferred_issue_execution", runId: null });
}
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, companyId))).toHaveLength(4);
expect((await db.select().from(agents).where(eq(agents.id, agentId)))[0]?.status).toBe("paused");
} finally { release(); await c.stop(); await worker.stop(); }
});
it("bounds real producer bursts with another company's completed history present", async () => {
const otherCompany = randomUUID(), otherAgent = randomUUID();
await db.insert(companies).values({ id: otherCompany, name: "Other tenant", issuePrefix: "OTH" });
await db.insert(agents).values({ id: otherAgent, companyId: otherCompany, name: "Other worker", adapterType: "codex_local" });
await db.insert(heartbeatRuns).values({ companyId: otherCompany, agentId: otherAgent, status: "succeeded" });
const run = await runRow(); const publish = vi.fn(); const c = coordinator(db, undefined, error => { errors.push(error); });
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionStatus,
run: () => deliverExecutionStatuses(observed, { publish }), next: executionControlSchedule(observed).status }]).ready;
const before = calls;
for (let i = 0; i < 20; i++) {
await terminalizeLegacyExecution({ db, run, status: "succeeded", fromStatuses: ["running", "succeeded"] });
await vi.advanceTimersByTimeAsync(10);
}
expect(calls).toBe(before); expect(publish).not.toHaveBeenCalled();
await settle(); expect(publish).toHaveBeenCalledTimes(1);
expect(publish.mock.calls[0]?.[0]).toMatchObject({ companyId, payload: { runId: run.id } });
await quiet(c);
});
it("wakes status delivery through the real producer only after outer commit, then becomes quiet", async () => {
const run = await runRow(); const publish = vi.fn(); const c = coordinator(db, undefined, error => { errors.push(error); });
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionStatus,
run: () => deliverExecutionStatuses(observed, { publish }), next: executionControlSchedule(observed).status }]).ready;
await quiet(c);
await db.transaction(async tx => {
await terminalizeLegacyExecution({ db: tx as unknown as typeof db, run, status: "succeeded", patch: { finishedAt: new Date() } });
await vi.advanceTimersByTimeAsync(1); expect(publish).not.toHaveBeenCalled();
});
await settle(); expect(publish).toHaveBeenCalledTimes(1);
await quiet(c);
const [saved] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run.id));
expect(saved?.status).toBe("succeeded"); expect(saved?.executionStatusDeliveryId).toBeNull();
});
it("rolls back a nested producer without delivering or leaving periodic work", async () => {
const run = await runRow(), publish = vi.fn(); const c = coordinator(db, undefined, error => { errors.push(error); });
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionStatus,
run: () => deliverExecutionStatuses(observed, { publish }), next: executionControlSchedule(observed).status }]).ready;
await expect(db.transaction(async tx => {
await terminalizeLegacyExecution({ db: tx as unknown as typeof db, run, status: "succeeded" });
throw new Error("rollback");
})).rejects.toThrow("rollback");
await quiet(c); expect(publish).not.toHaveBeenCalled();
});
it("restores an exact control deadline on restart without querying before it", async () => {
const run = await runRow("succeeded"), due = new Date(Date.now() + 60_000);
await db.update(heartbeatRuns).set({ executionControlDeadlineAt: due }).where(eq(heartbeatRuns.id, run.id));
const c = coordinator(db, undefined, error => { errors.push(error); });
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionFinalization,
run: () => reconcileAbandonedExecutionControl(observed), next: executionControlSchedule(observed).finalization }]).ready;
expect(c.nextWakeAt()).toBe(due.getTime()); const before = calls;
await vi.advanceTimersByTimeAsync(55_000); expect(calls).toBe(before);
await vi.advanceTimersByTimeAsync(5000); await settle(); await quiet(c);
});
it("retains replacement retries for an unresolved failed execution, excludes completed/denied history", async () => {
const run = await runRow("failed");
await db.update(heartbeatRuns).set({ nativeIssueId: issueId }).where(eq(heartbeatRuns.id, run.id));
await db.insert(nativeRunFinalizations).values({ runId: run.id, companyId, issueId, phase: "terminal_failure", failureCode: "provider_transport_failed" });
const schedule = executionControlSchedule(observed);
expect(await schedule.replacement()).toBeGreaterThan(Date.now());
await db.update(nativeRunFinalizations).set({ failureDetail: { replacementDenied: "operator_reconciled" } }).where(eq(nativeRunFinalizations.runId, run.id));
expect(await schedule.replacement()).toBeNull();
});
it("a recovery-action producer wakes automatic disposition and leaves resolved history quiet", async () => {
const run = await runRow("failed"); const c = coordinator(db, undefined, error => { errors.push(error); });
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionDisposition,
run: () => settleUnrecoverableExecutions(observed), next: executionControlSchedule(observed).disposition }]).ready;
await quiet(c);
const action = await issueRecoveryActionService(db).upsertSourceScoped({ companyId, sourceIssueId: issueId,
kind: "active_run_watchdog", cause: "execution_finalization_deadline_exceeded", fingerprint: run.id,
returnOwnerAgentId: agentId, evidence: { runId: run.id }, nextAction: "Recover execution" });
await settle();
expect(await issueRecoveryActionService(db).getActiveForIssue(companyId, issueId)).toBeNull();
expect(action.id).toBeTruthy(); await quiet(c);
});
it("retries a failed broadcast only while its durable marker remains, including after restart", async () => {
const run = await runRow();
const publish = vi.fn(); publish.mockImplementation(() => { throw new Error("receiver unavailable"); });
let c = coordinator(db, undefined, error => { errors.push(error); });
const duty = { queue: DELIVERY_QUEUES.executionStatus,
run: () => deliverExecutionStatuses(observed, { publish }), next: executionControlSchedule(observed).status };
await registerExecutionControlWork(c, [duty]).ready;
await terminalizeLegacyExecution({ db, run, status: "succeeded" });
await settle(); expect(publish).toHaveBeenCalledTimes(1);
const before = calls;
await vi.advanceTimersByTimeAsync(10_000); expect(calls).toBe(before);
await c.stop();
publish.mockImplementation(() => undefined);
c = coordinator(db, undefined, error => { errors.push(error); });
await registerExecutionControlWork(c, [duty]).ready;
expect(publish).toHaveBeenCalledTimes(2); await quiet(c);
});
it("reschedules an earlier deadline and disarms a cancelled deadline", async () => {
const run = await runRow("succeeded"); const c = coordinator(db, undefined, error => { errors.push(error); });
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionFinalization,
run: () => reconcileAbandonedExecutionControl(observed), next: executionControlSchedule(observed).finalization }]).ready;
const due = new Date(Date.now() + 120_000), earlier = new Date(Date.now() + 60_000);
for (const at of [due, earlier]) {
await writeExecutionControl(db, DELIVERY_QUEUES.executionFinalization, tx => tx.update(heartbeatRuns)
.set({ executionControlDeadlineAt: at }).where(eq(heartbeatRuns.id, run.id)), { companyId });
await settle(); expect(c.nextWakeAt()).toBe(at.getTime());
}
await writeExecutionControl(db, DELIVERY_QUEUES.executionFinalization, tx => tx.update(heartbeatRuns)
.set({ executionControlDeadlineAt: null }).where(eq(heartbeatRuns.id, run.id)), { companyId });
await quiet(c);
});
it("delivers a reconciled continuation when marking and resolving commit together", async () => {
const run = await runRow("failed"), successor = await runRow("queued");
const action = await issueRecoveryActionService(db).upsertSourceScoped({ companyId, sourceIssueId: issueId,
kind: "active_run_watchdog", cause: "execution_finalization_deadline_exceeded", fingerprint: run.id,
returnOwnerAgentId: agentId, evidence: { runId: run.id }, nextAction: "Recover execution" });
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, recoveryActionId: action.id, previousRunId: run.id } })
.where(eq(heartbeatRuns.id, successor.id));
const wake = vi.fn(async () => successor), c = coordinator(db, undefined, error => { errors.push(error); });
await registerExecutionControlWork(c, [{ queue: DELIVERY_QUEUES.executionContinuation,
run: () => deliverReconciledExecutions(observed, wake), next: executionControlSchedule(observed).continuation }]).ready;
await db.transaction(async tx => {
await markExecutionReconciliation(tx as unknown as typeof db, action,
{ runId: run.id, providerStopped: true, actionOutcome: "not_performed", outcomeEvidence: "Verified the provider stopped without performing an external action." }, "operator");
await issueRecoveryActionService(tx).resolveActiveForIssue({ companyId, sourceIssueId: issueId, actionId: action.id, status: "resolved", outcome: "restored" });
await vi.advanceTimersByTimeAsync(1); expect(wake).not.toHaveBeenCalled();
});
await settle(); expect(wake).toHaveBeenCalledTimes(1); await quiet(c);
const [saved] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, successor.id));
expect(saved?.retryOfRunId).toBe(run.id);
});
});