Files
PaperClipAI/server/src/__tests__/retained-native-session-cleanup-work.test.ts
T
4c56702e06 fix(server): schedule retained native-session cleanup (#17717)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs can retain a local session after an interrupted durable
suspend.
> - The orphan reaper repeatedly scans completed history for that
cleanup duty.
> - Idle scans keep the database active. Removing them requires complete
wake and ownership paths.
> - This pull request moves retained-session cleanup to committed
signals, local release events and saved deadlines.
> - Existing physical proof checks still decide whether cleanup is safe.
The shared scheduler does not grant execution authority.
> - The benefit is quiet retained history with recoverable cleanup and
owned shutdown.

## Linked Issues or Issue Description

Refs #17709 and #18135. This is slice 51 of the existing
recurring-server sleep stack. Both referenced dependencies have merged.
The target is `master`.

**What happened?**

The periodic orphan pass scans retained native sessions even after their
cleanup is complete or no automatic action is safe. Some eligible
sessions also depend on a local session, environment lease or original
maintenance callback releasing ownership. An earlier decision
notification can be consumed before the run's later terminal projection
commits.

**Expected behavior**

Reconstruct existing work at startup. Wake from committed writes and
actual owner release. Register the saved claim expiry or a probe for a
specific live process. Stop recurring discovery when no obligation
remains. Preserve original callback ownership and all existing cleanup
proof checks.

**Steps to reproduce**

1. Retain completed native runs with a durable-suspend close failure.
2. Let the periodic orphan pass run with no new work.
3. Observe repeated cleanup discovery. Separately, delay terminal
projection after its decision notification is consumed and observe the
missing cleanup wake.

**Paperclip version or commit**

Parent #17709 at reviewed head
`3b75f4bc1da1370304f069fc287af3d693575716`, landed as
`df6156c2e37f542a69f393dde31e357b82b7cac1`. Current integrated master:
`31b927e3013a8a4b3266854b740cf40a24753788`.

## What Changed

- Move scoped, paged discovery to the retained cleanup owner. Preserve
explicit manual recovery.
- Register the duty with the shared scheduler and remove it from the
periodic orphan pass. Keep concrete lease expiry and live-process probe
obligations.
- Use typed outcomes for settled/refused work and process, session,
environment, maintenance or lease waits. Keep physical proof and
quarantine decisions in the executor.
- Wake from committed finalization and terminal projection writes,
durable process/lease release, and local session or callback settlement.
Routine company-only finalization hints leave dormant cleanup quiet.
- Join duplicate cleanup and serialize inspections on one database lane.
Keep original Runner callbacks owned until settlement and shutdown.
- Preserve all master profile fixtures and the branch's historical
identities. Save the colliding sleep profile 79 separately. Generate
combined Copilot profile 80 from its source and retain master's other
provider profiles.
- Add the owner audit and regression coverage. No new table, migration
or independent maintenance timer is added.

## Verification

- Server acceptance: **2,342 distinct passing cases in 18 complete
files**. The 14-file unaffected group contributes 1,532 passes. Full
startup contributes 76, process recovery 410, environment service 51,
and external-chat response 273. The 29-case cleanup worker suite and
977-case executor suite run in full. No case in these files is skipped.
- Controlled scope regression: removing only the ordinary finalization
issue/run guard wakes dormant retained cleanup; the same assertion
passes after restoring it. A real local owner release still resumes the
durable candidate.
- Preserve the first broad failure: 1,607 passed and one startup
shutdown case failed because the coordinator mock lacked `stop()`. Add
only that mock method. All 76 unchanged startup cases then pass. Do not
treat the first broad command as green.
- Dedicated PostgreSQL 17 databases use the existing migration 413
schema. Embedded-only fixtures use the existing recovery helper with
fresh per-call databases. All temporary imports are restored byte-exact.
No assertion or timeout is relaxed.
- Current-master integration preserves both complete source histories.
The server, UI and database trees are identical to the server-tested
source. All master historical profile fixtures remain byte-exact. The
prepared sleep profile 79 remains independently inspectable.
- Current runtime validation at
`d08f764e3efe75e96929173afe7ee8739121ad9f` passes all 12 commands (22
installed ACPX/Grok contract cases and 26 native-provider Rust cases):
`pnpm install --frozen-lockfile --ignore-scripts`; Copilot/Grok identity
and generated profile checks; Runner and server typechecking; server
build; Copilot/Pi installation, profile compatibility and transport
settlement files (130 passed; one opt-in closed Pi snapshot case skipped
because its distribution root is not configured); installed ACPX
model/reconnect package contract and Grok identity tests; Rust
native-provider backend tests; module boundaries and migration order.
- Earlier 94-case installation/settlement acceptance applies to the
prepared profile 79 source. It is retained as history and does not
substitute for the current profile 80 validation above.
- Failure history and duty-specific evidence remain in
`doc/plans/2026-10-26-retained-native-session-cleanup-sleep-audit.md`.
Exact-current-head acceptance: all 60 checks are clean, including
protected CI and security; Greptile gives 5/5 for
`281d579de0d08e907194b8b790a0597b07d16b7b` with no actionable findings.
No Ponytail review is present under the authorized missing-review
policy.
- These checks do not claim a full local repository suite, live Copilot
or Daytona qualification, deployed host/database suspension, or measured
cost savings.

## Risks

- Missing a supported producer could strand cleanup. Tests exercise real
finalizer, heartbeat, environment and process writers. Process-local
signals cover in-process writers; no multi-replica or external SQL
notification transport is added.
- A timeout can leave an original callback active. Cleanup and shutdown
must keep waiting for it. The existing global maintenance drain is
conservative across roots.
- A retained live process still has a scoped 30-second absence probe. A
saved lease keeps its actual deadline. Operational errors retain retry
work. Immutable proof refusals and operator-required histories do not
gain automatic retry authority.
- Cleanup keeps all existing artifact, identity, claim, no-launch and
quarantine checks. Terminal status alone does not authorize physical
cleanup.
- This PR does not expand Cloud alarm coverage or relax host sleep
eligibility. It proves worker behavior, not deployed app/database
suspension.
- The Copilot source identity has changed; these checks do not claim new
live provider qualification.
- The parent has merged. This PR is merged only after its current-head
protected CI and review gates pass under the existing campaign
authorization.

## Model Used

OpenAI Codex, GPT-6 family, with code execution and repository tools.
The exact serving variant and context-window size 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>
Co-authored-by: Codex <noreply@openai.com>
2026-10-29 04:24:57 -05:00

632 lines
31 KiB
TypeScript

import { heartbeatService } from "../services/heartbeat.js";
import * as nativeExecutor from "../services/native-runtime/native-session-executor.js";
import { finalizeNativeRun } from "../services/native-runtime/native-run-finalizer.js";
import { notifyNativeFinalizationWork } from "../services/native-finalization-notifications.js";
import { randomUUID } from "node:crypto";
import os from "node:os";
import { PROCESS_IDENTITY_RECORDED, releaseSettledNativeRunProcess } from "../services/native-local-process-stop.js";
import { setTimeout as delay } from "node:timers/promises";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { eq } from "drizzle-orm";
import { agents, companies, completionContracts, createDb, environmentLeases, heartbeatRunEvents, heartbeatRuns, issues, nativeRunFinalizations,
nativeRunResults, statusDecisions, workAssessments, type Db } from "@paperclipai/db";
import { settleAbsentSandbox, settleStopOnlyCleanup } from "../services/sandbox-stop-and-retain.js";
import { settleNativeWorkspaceExportResume } from "../services/native-runtime/native-workspace-export-resume.js";
import type { SandboxAbsenceProof } from "../services/sandbox-absence-proof.js";
import { environmentService } from "../services/environments.js";
import { createDeliveryWorkCoordinator } from "../services/delivery-work-coordinator.js";
import { idleWorkSnapshot } from "../services/task-admission.js";
import { notifyRetainedNativeSessionCleanup, releaseRetainedNativeSessionCleanup } from "../services/retained-native-session-cleanup-notifications.js";
import { retainedNativeSessionCleanupWork } from "../services/native-runtime/retained-native-session-cleanup-work.js";
import { reconcileRetainedNativeSessionCleanups, runRetainedNativeSessionCleanup, type NativeCleanupOutcome } from "../services/native-runtime/retained-native-session-cleanup.js";
import { reconcileRetainedNativeSessionCleanup } from "../services/native-runtime/native-session-executor.js";
import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import { resolveExternalTestDatabaseUrl } from "./helpers/external-test-database.js";
function deferred() {
let resolve!: () => void;
const promise = new Promise<void>(done => { resolve = done; });
return { promise, resolve };
}
describe("retained native cleanup scheduled lifecycle", () => {
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
let db: ReturnType<typeof createDb>;
let coordinator: ReturnType<typeof createDeliveryWorkCoordinator> | undefined;
let worker: ReturnType<ReturnType<typeof retainedNativeSessionCleanupWork>["register"]> | undefined;
const companyId = randomUUID(), agentId = randomUUID();
const closeError = "provider_transport_failed: runner did not durably suspend before checkpoint";
let admitted = true;
const errors: unknown[] = [];
async function settle(input: { runId: string; companyId: string }): Promise<NativeCleanupOutcome> {
await db.update(nativeRunFinalizations).set({ leaseOwner: null, leaseExpiresAt: null,
recoveryHistory: [{ kind: "native_cleanup_maintenance", phase: "settled" }] })
.where(eq(nativeRunFinalizations.runId, input.runId));
return { runId: input.runId, status: "settled" };
}
const cleanup = vi.fn(settle);
beforeAll(async () => {
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_RETAINED_CLEANUP_TEST_DATABASE_URL");
if (!external) temporary = await startEmbeddedPostgresTestDatabase("paperclip-retained-cleanup-");
db = createDb(external ?? temporary!.connectionString);
await db.insert(companies).values({ id: companyId, name: "Retained cleanup lifecycle", issuePrefix: `RC${randomUUID().slice(0, 8)}` });
await db.insert(agents).values({ id: agentId, companyId, name: "Cleanup agent", adapterType: "codex_local" });
}, 30_000);
beforeEach(() => {
admitted = true; errors.length = 0; cleanup.mockReset().mockImplementation(settle);
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
});
afterEach(async () => {
worker?.stop(); await coordinator?.stop(); worker = undefined; coordinator = undefined;
vi.useRealTimers(); vi.restoreAllMocks();
await db.delete(nativeRunFinalizations).where(eq(nativeRunFinalizations.companyId, companyId));
expect(errors).toEqual([]);
});
afterAll(async () => { await db?.$client.end({ timeout: 1 }); await temporary?.cleanup(); });
async function start() {
coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => admitted, canReconcile: () => true,
onError: error => errors.push(error) });
worker = retainedNativeSessionCleanupWork(db, cleanup).register(coordinator, () => admitted);
await worker.ready;
}
async function flush() {
await vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000);
expect(idleWorkSnapshot().active).toBe(0);
expect(coordinator!.nextWakeAt()).toBeNull();
}, { timeout: 15_000 });
}
async function quiet(ms = 120_000) {
const previous = db.$client.options.debug;
const queries: string[] = [];
db.$client.options.debug = (_id, query) => { queries.push(query); };
try { await vi.advanceTimersByTimeAsync(ms); await delay(10); expect(queries).toEqual([]); }
finally { db.$client.options.debug = previous; }
}
async function commitCandidate() {
const runId = randomUUID();
return db.transaction(async tx => {
await notifyNativeFinalizationWork(tx, { companyId, runId });
return candidate({ runId }, tx as unknown as Db);
});
}
async function candidate(
options: {
runId?: string;
owner?: { companyId: string; agentId: string };
run?: Partial<typeof heartbeatRuns.$inferInsert>;
coordinator?: Partial<typeof nativeRunFinalizations.$inferInsert>;
schemaStatus?: string;
issueId?: string;
revision?: number;
} = {},
writer: Db = db,
) {
const owner = options.owner ?? { companyId, agentId };
const runId = options.runId ?? randomUUID();
const issueId = options.issueId ?? randomUUID();
const contractId = randomUUID();
if (!options.issueId) await writer
.insert(issues)
.values({
id: issueId,
companyId: owner.companyId,
title: "Retained session",
assigneeAgentId: owner.agentId,
});
await writer.insert(completionContracts).values({
id: contractId,
companyId: owner.companyId,
issueId,
revision: options.revision ?? 1,
schemaVersion: "paperclip.completion-contract.v1",
policyVersion: "phase6-v3",
risk: "standard",
completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: {},
canonicalSha256: runId,
createdByActorType: "system",
createdByActorId: "test",
});
await writer.insert(heartbeatRuns).values({
id: runId,
companyId: owner.companyId,
agentId: owner.agentId,
runtimeMode: "native",
status: "succeeded",
nativeIssueId: issueId,
nativeSessionId: runId,
nativePhase: "terminal_failure",
completionContractId: contractId,
completionContractSha256: runId,
finishedAt: new Date(),
errorCode: "adapter_failed",
error: closeError,
...options.run,
});
const [result] = await writer
.insert(nativeRunResults)
.values({
companyId: owner.companyId,
issueId,
runId,
completionContractId: contractId,
serverFingerprint: runId,
canonicalSha256: runId,
schemaStatus: options.schemaStatus ?? "accepted",
resultJson: {},
})
.returning();
const [assessment] = await writer
.insert(workAssessments)
.values({
companyId: owner.companyId,
issueId,
runId,
contractId,
resultId: result!.id,
triggerKind: "native_result",
triggerActorCompanyId: owner.companyId,
priorIssueStatus: "in_progress",
priorStatusVersion: 0,
policyVersion: "phase6-v3",
assessmentJson: {},
inputDigest: runId,
})
.returning();
const [decision] = await writer
.insert(statusDecisions)
.values({
companyId: owner.companyId,
issueId,
runId,
assessmentId: assessment!.id,
decisionVersion: options.revision ?? 1,
policyVersion: "phase6-v3",
fromStatus: "in_progress",
toStatus: "in_review",
reasonCode: "prior_status_terminal_preserved",
decisionJson: {},
decisionDigest: runId,
})
.returning();
await writer.insert(nativeRunFinalizations).values({
companyId: owner.companyId,
issueId,
runId,
phase: "committed",
resultId: result!.id,
assessmentId: assessment!.id,
decisionId: decision!.id,
...options.coordinator,
});
return runId;
}
it("becomes SQL-quiet with no work, then processes the first committed candidate", async () => {
await start(); await flush(); await quiet();
const runId = await commitCandidate();
await flush();
expect(cleanup).toHaveBeenCalledExactlyOnceWith({ companyId, runId });
await quiet();
});
it("leaves retained unprovable history quiet through the real physical cleanup boundary", async () => {
await candidate();
cleanup.mockImplementation(input => reconcileRetainedNativeSessionCleanup(db, input));
await start(); await flush();
expect(cleanup).toHaveBeenCalledTimes(1);
await quiet();
});
it("keeps dormant retained cleanup quiet on unscoped finalization hints and resumes from local release", async () => {
const runId = await candidate();
cleanup.mockResolvedValue({ runId, status: "deferred", waitFor: "session_release" });
await start();
await flush();
await quiet();
expect(cleanup).toHaveBeenCalledTimes(1);
expect(await retainedNativeSessionCleanupWork(db, cleanup).load({ companyId, runId })).toHaveLength(1);
const debug = db.$client.options.debug;
const queries: string[] = [];
db.$client.options.debug = (_connection, query) => {
if (query.startsWith("select") && query.includes("native_run_finalizations")) queries.push(query);
};
try {
for (let i = 0; i < 3; i++) {
await db.transaction(tx => notifyNativeFinalizationWork(tx, { companyId }));
await flush();
}
expect(queries).toEqual([]);
expect(cleanup).toHaveBeenCalledTimes(1);
} finally { db.$client.options.debug = debug; }
cleanup.mockImplementation(settle);
releaseRetainedNativeSessionCleanup(db, { companyId, runId });
await flush();
expect(cleanup).toHaveBeenCalledTimes(2);
await quiet();
});
it.each(["session_release", "environment_release", "maintenance_settlement"] as const)(
"waits quietly for %s and then reactivates only the affected run", async waitFor => {
const runId = await candidate();
cleanup.mockResolvedValueOnce({ runId, status: "deferred", waitFor });
await start(); await flush(); await quiet();
releaseRetainedNativeSessionCleanup(db, { companyId, runId });
await flush();
expect(cleanup).toHaveBeenCalledTimes(2);
await quiet();
},
);
it.each(["single", "run"] as const)("reactivates after the real %s lease release commits, but not rollback", async method => {
const runId = await candidate();
const [lease] = await db.insert(environmentLeases).values({ companyId, heartbeatRunId: runId,
status: "active", provider: "local" }).returning();
cleanup.mockImplementation(async input => {
const [current] = await db.select().from(environmentLeases).where(eq(environmentLeases.id, lease!.id));
return current!.status === "active"
? { runId: input.runId, status: "deferred", waitFor: "environment_release" }
: settle(input);
});
const release = async (writer: Db) => {
const service = environmentService(writer);
if (method === "single") await service.releaseLease(lease!.id);
else await service.releaseLeasesForRun(runId);
};
await start(); await flush(); await quiet();
expect(cleanup).toHaveBeenCalledTimes(1);
await expect(db.transaction(async tx => {
await release(tx as unknown as Db);
await vi.advanceTimersByTimeAsync(1000);
expect(cleanup).toHaveBeenCalledTimes(1);
throw new Error("rollback lease release");
})).rejects.toThrow("rollback lease release");
await flush(); await quiet();
const afterRollback = cleanup.mock.calls.length;
await db.transaction(async tx => {
await release(tx as unknown as Db);
await vi.advanceTimersByTimeAsync(1000);
expect(cleanup).toHaveBeenCalledTimes(afterRollback);
});
await flush();
expect(cleanup).toHaveBeenCalledTimes(afterRollback + 1);
expect((await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, runId)))[0]!.recoveryHistory)
.toEqual([{ kind: "native_cleanup_maintenance", phase: "settled" }]);
await quiet();
});
it("keeps pending cleanup quiet through metadata updates and stale release attempts", async () => {
const runId = await candidate(), attemptId = randomUUID();
const [lease] = await db.insert(environmentLeases).values({ companyId, heartbeatRunId: runId,
status: "pending_cleanup", provider: "local", metadata: { pendingCleanupAttemptId: attemptId } }).returning();
cleanup.mockResolvedValueOnce({ runId, status: "deferred", waitFor: "environment_release" });
await start(); await flush();
const service = environmentService(db);
await service.updateLeaseMetadata(lease!.id, { pendingCleanupAttemptId: attemptId, pendingCleanupInFlight: true });
await service.releaseLease(lease!.id, "pending_cleanup", { expectedPendingCleanupAttemptId: attemptId, cleanupStatus: "failed" });
expect(await service.releaseLease(lease!.id, "released", { expectedPendingCleanupAttemptId: randomUUID(), cleanupStatus: "success" })).toBeNull();
await flush(); await quiet();
expect(cleanup).toHaveBeenCalledTimes(1);
expect(await service.releaseLease(lease!.id, "released", { expectedPendingCleanupAttemptId: attemptId, cleanupStatus: "success" })).not.toBeNull();
await flush();
expect(cleanup).toHaveBeenCalledTimes(2);
await quiet();
});
it.each(["stop-only", "export-resume", "absent"] as const)("reactivates after real %s settlement with outer transaction ownership", async method => {
const ownerToken = randomUUID();
const runId = await candidate({ run: { runnerProfileJson: { nativeWorkspaceFinalizationOwner: { token: ownerToken } } } });
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
const leaseId = randomUUID(), providerLeaseId = randomUUID(), attemptId = randomUUID(), pluginId = randomUUID();
const binding = { companyId, runId, leaseId, provider: "daytona" as const, providerLeaseId };
const marker = { ...binding, requestId: attemptId, pluginId,
schema: method === "export-resume" ? "paperclip.workspace-export-resume.v2" : "paperclip.sandbox-stop-and-retain.v1",
resultId: randomUUID() };
const [lease] = await db.insert(environmentLeases).values({ id: leaseId, companyId, heartbeatRunId: runId,
issueId: run!.nativeIssueId, provider: "daytona", providerLeaseId, status: "pending_cleanup", cleanupStatus: "failed",
metadata: { pluginId, pendingCleanupAttemptId: attemptId,
[method === "export-resume" ? "nativeWorkspaceExportResume" : "sandboxStopAndRetain"]: marker } }).returning();
const proof: SandboxAbsenceProof = { ...binding, issueId: run!.nativeIssueId!, ownerToken, v: 1,
iss: "paperclip-cloud", aud: "paperclip-sandbox-absence/v1", sub: "fixture", absent: true,
iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 60 };
cleanup.mockImplementation(async input => {
const [current] = await db.select().from(environmentLeases).where(eq(environmentLeases.id, leaseId));
return current!.status === "pending_cleanup"
? { runId: input.runId, status: "deferred", waitFor: "environment_release" }
: settle(input);
});
const release = async (writer: Db) => {
const options = { attemptId, receipt: { providerLeaseId, state: "stopped" } };
if (method === "absent") expect(await settleAbsentSandbox(writer, lease!, { proof, receipt: "verified-upstream" })).toBe(true);
else if (method === "export-resume") expect(await settleNativeWorkspaceExportResume(writer, lease!, options)).not.toBeNull();
else expect(await settleStopOnlyCleanup(writer, lease!, options)).not.toBeNull();
};
await start(); await flush(); await quiet();
expect(cleanup).toHaveBeenCalledTimes(1);
await expect(db.transaction(async tx => {
await release(tx as unknown as Db);
await vi.advanceTimersByTimeAsync(1000);
expect(cleanup).toHaveBeenCalledTimes(1);
throw new Error("rollback settlement");
})).rejects.toThrow("rollback settlement");
await flush(); await quiet();
const afterRollback = cleanup.mock.calls.length;
await db.transaction(async tx => {
await release(tx as unknown as Db);
await vi.advanceTimersByTimeAsync(1000);
expect(cleanup).toHaveBeenCalledTimes(afterRollback);
});
await flush();
expect(cleanup).toHaveBeenCalledTimes(afterRollback + 1);
await quiet();
});
it("reactivates immediately after verified process release commits without waiting for its probe", async () => {
const pid = 2_000_000_000, startedAt = new Date();
const runId = await candidate({ run: { status: "failed", processPid: pid, processGroupId: pid,
processStartedAt: startedAt, runnerProfileJson: { sessionCheckpoint: {} } } });
await db.insert(heartbeatRunEvents).values({ companyId, runId, agentId, seq: 0,
eventType: PROCESS_IDENTITY_RECORDED, payload: { targetKind: "local", hostname: os.hostname(),
processPid: pid, processGroupId: pid, processStartedAt: startedAt.toISOString() } });
await db.update(heartbeatRuns).set({ nextEventSeq: 1 }).where(eq(heartbeatRuns.id, runId));
const originalKill = process.kill.bind(process);
vi.spyOn(process, "kill").mockImplementation((id, signal) => {
if ((id === pid || id === -pid) && signal === 0) throw Object.assign(new Error("gone"), { code: "ESRCH" });
return originalKill(id, signal);
});
cleanup.mockImplementation(async input => {
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, input.runId));
return run!.processPid !== null
? { runId: input.runId, status: "deferred", waitFor: "process_exit" }
: settle(input);
});
await start();
expect(cleanup).toHaveBeenCalledTimes(1);
expect(coordinator!.nextWakeAt()).not.toBeNull();
await expect(db.transaction(async tx => {
expect(await releaseSettledNativeRunProcess(tx as unknown as Db, runId)).toBe("released");
await vi.advanceTimersByTimeAsync(1000);
expect(cleanup).toHaveBeenCalledTimes(1);
throw new Error("rollback process release");
})).rejects.toThrow("rollback process release");
await vi.waitFor(async () => {
await vi.advanceTimersByTimeAsync(1000);
expect(cleanup).toHaveBeenCalledTimes(2);
expect(idleWorkSnapshot().active).toBe(0);
});
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)))[0]!.processPid).toBe(pid);
const afterRollback = cleanup.mock.calls.length;
await db.transaction(async tx => {
expect(await releaseSettledNativeRunProcess(tx as unknown as Db, runId)).toBe("released");
await vi.advanceTimersByTimeAsync(1000);
expect(cleanup).toHaveBeenCalledTimes(afterRollback);
});
await vi.advanceTimersByTimeAsync(1000);
await vi.waitFor(() => expect(cleanup).toHaveBeenCalledTimes(afterRollback + 1));
await flush(); await quiet();
});
it("reactivates when the real finalizer projects an already committed result to a terminal run", async () => {
const runId = await candidate({ run: { status: "running", finishedAt: null } });
await db.update(nativeRunResults).set({ resultJson: { terminal: { runTerminalState: "succeeded" } } })
.where(eq(nativeRunResults.runId, runId));
await start(); await flush(); await quiet();
expect(cleanup).not.toHaveBeenCalled();
const finalize = (writer: Db) => finalizeNativeRun({ db: writer, runId, workspaceFinalizeStatus: "succeeded",
projectRunStatus: true, skipNativeResponsePresentation: true, skipNativeReviewResponse: true });
await expect(db.transaction(async tx => {
await finalize(tx as unknown as Db);
await vi.advanceTimersByTimeAsync(1000);
expect(cleanup).not.toHaveBeenCalled();
throw new Error("rollback final projection");
})).rejects.toThrow("rollback final projection");
await flush(); await quiet();
expect(cleanup).not.toHaveBeenCalled();
await finalize(db); await flush();
expect(cleanup).toHaveBeenCalledExactlyOnceWith({ companyId, runId });
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
expect(run!.status).toBe("succeeded");
expect(run!.error).toBeNull();
expect(run!.resultJson?.recoveredExecutionFailure).toMatchObject({ errorCode: "adapter_failed", error: closeError });
await quiet();
});
it("reconstructs a lease deadline across restart and does no SQL before it", async () => {
const leaseExpiresAt = new Date(Date.now() + 60_000);
const runId = await candidate({ coordinator: { leaseOwner: "other-owner", leaseExpiresAt } });
await start();
expect(cleanup).not.toHaveBeenCalled();
expect(coordinator!.nextWakeAt()).toBe(leaseExpiresAt.getTime());
await quiet(10_000);
worker!.stop(); await coordinator!.stop();
await start();
expect(coordinator!.nextWakeAt()).toBe(leaseExpiresAt.getTime());
await quiet(10_000);
await vi.advanceTimersByTimeAsync(40_000); await flush();
expect(cleanup).toHaveBeenCalledExactlyOnceWith({ companyId, runId });
await quiet();
});
it("retains only the concrete process-exit probe and disarms it after settlement", async () => {
const runId = await candidate();
cleanup.mockResolvedValueOnce({ runId, status: "deferred", waitFor: "process_exit" });
await start();
expect(cleanup).toHaveBeenCalledTimes(1);
await quiet(20_000);
await vi.advanceTimersByTimeAsync(10_000); await flush();
expect(cleanup).toHaveBeenCalledTimes(2);
await quiet();
});
it("does not run before outer commit and clears rolled-back intent", async () => {
await start(); await flush();
const entered = deferred(), release = deferred();
const transaction = db.transaction(async outer => {
await outer.transaction(async tx => {
await notifyRetainedNativeSessionCleanup(tx, { companyId });
await candidate({}, tx as unknown as Db);
});
entered.resolve(); await release.promise;
throw new Error("rollback fixture");
});
const rolledBack = expect(transaction).rejects.toThrow("rollback fixture");
await entered.promise;
await vi.advanceTimersByTimeAsync(10_000);
expect(cleanup).not.toHaveBeenCalled();
release.resolve(); await rolledBack; await flush(); await quiet();
const runId = await commitCandidate(); await flush();
expect(cleanup).toHaveBeenCalledExactlyOnceWith({ companyId, runId });
await quiet();
});
it("retains the first write during drain without passive SQL until resume", async () => {
await start(); await flush(); admitted = false;
const runId = await commitCandidate();
await vi.advanceTimersByTimeAsync(1000); await delay(10);
expect(cleanup).not.toHaveBeenCalled();
await quiet();
admitted = true; await vi.advanceTimersByTimeAsync(30_000); await flush();
expect(cleanup).toHaveBeenCalledExactlyOnceWith({ companyId, runId });
await quiet();
});
it("retains an operational failure across drain and retries after resume", async () => {
await candidate(); cleanup.mockRejectedValueOnce(new Error("connection lost"));
await start(); expect(errors).toHaveLength(1); errors.length = 0;
admitted = false; await quiet();
expect(cleanup).toHaveBeenCalledTimes(1);
admitted = true; await vi.advanceTimersByTimeAsync(30_000); await flush();
expect(cleanup).toHaveBeenCalledTimes(2);
await quiet();
});
it("keeps a committed wake arriving during cleanup and never overlaps worker dispatch", async () => {
await candidate();
const entered = deferred(), release = deferred();
let active = 0, maximum = 0;
cleanup.mockImplementation(async input => {
maximum = Math.max(maximum, ++active);
try { entered.resolve(); await release.promise; return await settle(input); }
finally { active--; }
});
const starting = start(); await entered.promise;
await commitCandidate(); release.resolve(); await starting; await flush();
expect(cleanup).toHaveBeenCalledTimes(2); expect(maximum).toBe(1);
await quiet();
});
it("joins the original manual startup operation instead of performing cleanup twice", async () => {
await candidate();
const entered = deferred(), release = deferred();
cleanup.mockImplementation(async input => { entered.resolve(); await release.promise; return settle(input); });
const manual = reconcileRetainedNativeSessionCleanups(db, { cleanup });
await entered.promise;
const work = retainedNativeSessionCleanupWork(db, cleanup);
const [entry] = await work.load();
const joined = work.check(entry);
try { expect(cleanup).toHaveBeenCalledTimes(1); }
finally { release.resolve(); }
await expect(joined).resolves.toBeNull(); await manual;
expect(cleanup).toHaveBeenCalledTimes(1);
});
it("serializes different cleanup inspections sharing a database lane", async () => {
const first = await candidate(), second = await candidate();
const entered = deferred(), release = deferred();
cleanup.mockImplementationOnce(async input => { entered.resolve(); await release.promise; return settle(input); });
const active = runRetainedNativeSessionCleanup(db, { companyId, runId: first }, cleanup);
await entered.promise;
const queued = runRetainedNativeSessionCleanup(db, { companyId, runId: second }, cleanup);
try { await delay(10); expect(cleanup).toHaveBeenCalledTimes(1); }
finally { release.resolve(); }
await Promise.all([active, queued]);
expect(cleanup.mock.calls.map(([input]) => input.runId)).toEqual([first, second]);
});
it("joins scheduled physical cleanup through the assembled heartbeat shutdown owner", async () => {
const runId = await candidate();
const entered = deferred(), release = deferred();
const physical = vi.spyOn(nativeExecutor, "reconcileRetainedNativeSessionCleanup").mockImplementation(async (_db, input) => {
entered.resolve(); await release.promise; return settle(input);
});
const heartbeat = heartbeatService(db);
coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError: error => errors.push(error) });
worker = heartbeat.registerRetainedNativeSessionCleanupWork(coordinator, () => true);
await entered.promise;
let drained = false;
const draining = heartbeat.drainActiveRunExecutions().then(() => { drained = true; });
try {
await delay(10);
expect(drained).toBe(false);
expect(physical).toHaveBeenCalledExactlyOnceWith(db, { companyId, runId });
} finally { release.resolve(); }
await worker.ready; await draining; await flush();
expect(drained).toBe(true); await quiet();
});
it("waits on the original in-flight cleanup when shutdown begins", async () => {
await candidate();
const entered = deferred(), release = deferred();
cleanup.mockImplementation(async input => { entered.resolve(); await release.promise; return settle(input); });
const starting = start(); await entered.promise;
let stopped = false;
worker!.stop(); const stopping = coordinator!.stop().then(() => { stopped = true; });
try { await delay(10); expect(stopped).toBe(false); }
finally { release.resolve(); }
await Promise.all([starting, stopping]);
expect(stopped).toBe(true); expect(cleanup).toHaveBeenCalledTimes(1);
releaseRetainedNativeSessionCleanup(db, { companyId });
await quiet();
});
it("replaces an old lease alarm when a committed change releases it early", async () => {
const runId = await candidate({ coordinator: { leaseOwner: "old-owner", leaseExpiresAt: new Date(Date.now() + 60_000) } });
await start(); expect(cleanup).not.toHaveBeenCalled();
await db.transaction(async tx => {
await notifyNativeFinalizationWork(tx, { companyId, runId });
await tx.update(nativeRunFinalizations).set({ leaseOwner: null, leaseExpiresAt: null }).where(eq(nativeRunFinalizations.runId, runId));
});
await flush(); expect(cleanup).toHaveBeenCalledExactlyOnceWith({ companyId, runId });
await quiet();
});
it("cancels a saved alarm when a committed owner marks the candidate operator-required", async () => {
const runId = await candidate({ coordinator: { leaseOwner: "old-owner", leaseExpiresAt: new Date(Date.now() + 60_000) } });
await start();
await db.transaction(async tx => {
await notifyNativeFinalizationWork(tx, { companyId, runId });
await tx.update(nativeRunFinalizations).set({ recoveryHistory: [{ kind: "native_cleanup_source_archive", phase: "operator_required" }] })
.where(eq(nativeRunFinalizations.runId, runId));
});
await flush(); expect(cleanup).not.toHaveBeenCalled(); await quiet();
});
it("retains startup discovery during drain and reconstructs only after admission resumes", async () => {
const runId = await candidate(); admitted = false;
await start(); await quiet(); expect(cleanup).not.toHaveBeenCalled();
admitted = true; await vi.advanceTimersByTimeAsync(30_000); await flush();
expect(cleanup).toHaveBeenCalledExactlyOnceWith({ companyId, runId }); await quiet();
});
it("drains a backlog across discovery pages and becomes quiet with the completed history retained", async () => {
const runIds: string[] = [];
for (let index = 0; index < 103; index++) runIds.push(await candidate());
await start(); await flush();
expect(cleanup.mock.calls.map(([input]) => input.runId).sort()).toEqual(runIds.sort());
expect(await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.companyId, companyId))).toHaveLength(103);
await quiet();
});
it("retries failed startup discovery without treating an unavailable database as empty", async () => {
const runId = await candidate();
vi.spyOn(db, "select").mockImplementationOnce(() => { throw new Error("discovery connection lost"); });
await start(); expect(errors).toHaveLength(1); errors.length = 0;
expect(cleanup).not.toHaveBeenCalled();
await quiet(20_000); await vi.advanceTimersByTimeAsync(10_000); await flush();
expect(cleanup).toHaveBeenCalledExactlyOnceWith({ companyId, runId }); await quiet();
});
});