Files
PaperClipAI/server/src/services/native-runtime/native-run-finalizer-telemetry.test.ts
T
DottaandClaude 6e524d8a69 fix(workspaces): stop retrying a copy-back that hit a full sandbox disk (#20678)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a native run, Paperclip copies the agent's sandbox workspace
back to durable storage. This is finalization.
> - The copy-back builds an archive inside the sandbox. When the sandbox
disk is full, the copy-back fails with
`remote_workspace_sandbox_disk_full`.
> - Finalization treated that failure as transient. It retried three
times. Each attempt took about 15 minutes for a large workspace, and
nothing freed space between attempts.
> - Saved-work recovery already reclaims the retained sandbox's inactive
provider cache when a save hits a full disk. It runs only after
finalization ends.
> - This pull request ends finalization on the first full-disk copy-back
failure, with the accepted result kept, so saved-work recovery starts at
once.
> - The benefit is that the task gets its files saved sooner, and the
user sees what happened instead of "failed after 3 attempts".

## Linked Issues or Issue Description

Refs #19619 (related: records why the read-only capture fallback failed;
this PR does not change that path)

**What happened?**
On staging, a native run finished its turn and its result was accepted.
The workspace copy-back then failed three times with
`remote_workspace_sandbox_disk_full` (about 7.5 GB of workspace, about 6
GB free). The three attempts spanned about one hour across deploys.
Finalization then ended as `native_workspace_sync_out_retry_exhausted`.
Saved-work recovery reclaimed the inactive provider cache (about 3 GB
freed) and saved the files.

**Expected behavior**
A full sandbox disk is recognised on the first failure. Paperclip does
not spend the finalization budget on attempts that cannot succeed. The
agent's result is kept. The user sees that the disk is full and that
Paperclip saves the files again without another provider turn.

**Steps to reproduce**
1. Run a native task on a remote sandbox whose workspace is larger than
the free sandbox space.
2. Let the provider turn finish.
3. Observe three copy-back attempts that each fail with
`remote_workspace_sandbox_disk_full`.

## What Changed

- `recordRetryableFailure` treats a workspace failure whose cause starts
with `remote_workspace_sandbox_disk_full:` as an exhausted budget on the
first attempt. The code stays
`native_workspace_sync_out_retry_exhausted`, so the existing export
hold, saved-work recovery, and export Retry apply unchanged.
- The run error for that case says that the sandbox disk is full and
that Paperclip saves the files again without repeating the agent's work.
It no longer says "after 3 attempts".
- Other copy-back failures keep their bounded retry.
- `captureRemoteWorkspaceArchive` rethrows the read-only fallback's own
failure when that fallback only lost its connection (`transport_closed`,
`provider_unavailable`, `provider_rate_limited`,
`sandbox_state_transition`). That failure is not disk-full, so
finalization keeps its bounded retry for it. Other fallback failures
keep the disk-full code.
- `remoteWorkspaceFailureCause` records the server-built read-only
capture failure, so a failed save names where the fallback stopped.
- The transport exports `REMOTE_WORKSPACE_SANDBOX_DISK_FULL_CODE`. The
finalizer and tests use it.
- `doc/execution-semantics.md` describes the new rule.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-run-finalizer-telemetry.test.ts
src/services/remote-workspace-transport.test.ts` (195 pass).
- New test "hands a full sandbox disk to saved-work recovery on the
first copy-back failure" fails on `origin/master` (the failure stays
`retryable_failure`) and passes with this change. It completes an
accepted result first and checks that the result stays linked and that
no agent wakeup is created.
- New transport test "keeps a full disk only when the read-only fallback
did not just lose its connection": the two dropped-connection cases fail
on `origin/master` and pass with this change.
- New test "keeps other copy-back failures on their bounded retry"
passes before and after.
- `cd server && npx tsc --noEmit` passes. `pnpm verify` selected 2504
test files because the finalizer is widely imported; its first selected
batch passed, and required CI supplies the full run.

## Risks

- Low risk. The terminal state is the same state that three failed
attempts produce today. Only the timing changes.
- A sandbox whose disk frees space by itself between attempts now waits
for saved-work recovery instead of the next finalizer attempt.
Saved-work recovery repeats the save with its own bounded attempts, so
this does not lose work.
- Upgrade safety: a run saved by an older release in `retryable_failure`
with this cause ends on its next full-disk failure. No migration is
needed.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell,
file edits) in Claude Code. Extended reasoning was not configured.

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 19:31:33 -06:00

1225 lines
57 KiB
TypeScript

import { resolveExternalTestDatabaseUrl } from "../../__tests__/helpers/external-test-database.js";
import { enableRunFailureReporting } from "../../__tests__/helpers/run-failure-reporting.js";
import { randomUUID } from "node:crypto";
import { verifyCommittedReplayPayloadReads } from "./__tests__/helpers/committed-replay.js";
import { eq, sql } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import {
agents,
companies,
completionContracts,
createDb,
heartbeatRuns,
heartbeatRunEvents,
issues,
nativeRunFinalizations,
nativeRunResults,
workAssessments,
statusDecisions,
issueRecoveryActions,
agentWakeupRequests,
workspaceOperations,
activityLog,
} from "@paperclipai/db";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "../../__tests__/helpers/embedded-postgres.js";
import {
CONTROL_PLANE_CONFORMANCE_RESULT,
CONTROL_PLANE_CONFORMANCE_TERMINAL,
} from "../../vendor/paperclip-runner/testing.js";
const mockTelemetryClient = vi.hoisted(() => ({
track: vi.fn(),
hashPrivateRef: vi.fn((value: string) => `hashed:${value}`),
}));
vi.mock("../../telemetry.ts", () => ({ getTelemetryClient: () => mockTelemetryClient }));
const reconcileMuseAssignments = vi.hoisted(() => vi.fn(async () => undefined));
vi.mock("../muse-runner-broker.js", async importOriginal => ({
...await importOriginal<typeof import("../muse-runner-broker.js")>(),
museRunnerBroker: () => ({ reconcileTerminalAssignments: reconcileMuseAssignments }),
}));
const mockCaptureRunFailure = vi.hoisted(() => vi.fn());
vi.mock("../../sentry.ts", async () => {
const actual = await vi.importActual<typeof import("../../sentry.ts")>("../../sentry.ts");
return {
...actual,
captureRunFailure: mockCaptureRunFailure,
};
});
function agentTaskRunCalls(fromIndex: number) {
return mockTelemetryClient.track.mock.calls
.slice(fromIndex)
.filter((call) => call[0] === "agent.task_run");
}
function captureRunFailureCallsFrom(fromIndex: number) {
return mockCaptureRunFailure.mock.calls.slice(fromIndex);
}
import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js";
import { restoreNativeWorkspaceBestEffort } from "./native-workspace-best-effort.js";
import { reconcileNativeFinalizations } from "./native-finalization-reconciler.js";
import { recoverLegacyUnsafeWorkspaceExports } from "./native-workspace-export-recovery.js";
import { finalizeNativeRun, recordNativeFinalizationFailure } from "./native-run-finalizer.js";
import { REMOTE_WORKSPACE_SANDBOX_DISK_FULL } from "../remote-workspace-transport.js";
import { waitForPendingRunFailureReports } from "../run-failure-report.js";
import { deliverExecutionStatuses } from "../execution-status-delivery.js";
import { commitNativeStatusDecision } from "./status-decision-committer.js";
import { NATIVE_STATUS_ARBITER_POLICY_VERSION, type NativeStatusDecision } from "./status-arbiter.js";
import { DELIVERY_QUEUES, subscribeDeliveryWork } from "../delivery-work-notifications.js";
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_NATIVE_FINALIZER_TEST_DATABASE_URL");
const embeddedPostgresSupport = external ? { supported: true, reason: undefined } : await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres native-finalizer telemetry tests on this host: ${
embeddedPostgresSupport.reason ?? "unsupported environment"
}`,
);
}
describeEmbeddedPostgres("native run finalizer / status decision committer — agent.task_run emission", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
let companyId: string;
let agentId: string;
beforeAll(async () => {
if (!external) tempDb = await startEmbeddedPostgresTestDatabase("paperclip-native-finalizer-telemetry-");
db = createDb(external ?? tempDb!.connectionString);
companyId = randomUUID();
agentId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Telemetry", issuePrefix: "TLM" });
await db.insert(agents).values({
id: agentId,
companyId,
name: "Native worker",
adapterType: "codex_local",
status: "running",
});
}, 30_000);
afterAll(async () => {
await db?.$client.end({ timeout: 1 });
await tempDb?.cleanup();
});
async function seedNativeRun() {
const issueId = randomUUID();
const contractId = randomUUID();
const runId = randomUUID();
const sessionId = randomUUID();
const runnerInstanceId = randomUUID();
const contractSha256 = `contract-${contractId}`;
await db.insert(issues).values({
id: issueId,
companyId,
title: "Native telemetry fixture",
status: "in_progress",
assigneeAgentId: agentId,
workMode: "standard",
});
await db.insert(completionContracts).values({
id: contractId,
companyId,
issueId,
revision: 1,
schemaVersion: "paperclip.completion-contract.v1",
policyVersion: "telemetry-v1",
risk: "standard",
completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: {
revision: "telemetry-v1",
objective: "Emit telemetry",
criteria: [{ id: "objective", requirement: "Complete the task" }],
},
canonicalSha256: contractSha256,
createdByActorType: "system",
createdByActorId: "test",
});
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId,
status: "running",
runtimeMode: "native",
nativeIssueId: issueId,
nativeSessionId: sessionId,
runnerInstanceId,
completionContractId: contractId,
completionContractSha256: contractSha256,
contextSnapshot: { issueId },
});
return { issueId, contractId, contractSha256, runId, sessionId, runnerInstanceId };
}
function newPort(fixture: Awaited<ReturnType<typeof seedNativeRun>>) {
return new PaperclipControlPlanePort(db, {
companyId,
issueId: fixture.issueId,
runId: fixture.runId,
agentId,
sessionId: fixture.sessionId,
completionContractId: fixture.contractId,
completionContractSha256: fixture.contractSha256,
sourceInstanceId: fixture.runnerInstanceId,
controlPlaneSourceInstanceId: "telemetry-test",
});
}
async function driveToCompleteResult(
fixture: Awaited<ReturnType<typeof seedNativeRun>>,
terminal: typeof CONTROL_PLANE_CONFORMANCE_TERMINAL = CONTROL_PLANE_CONFORMANCE_TERMINAL,
result = CONTROL_PLANE_CONFORMANCE_RESULT,
) {
const port = newPort(fixture);
await port.openRun({
identity: {
runId: fixture.runId,
sessionId: fixture.sessionId,
companyId,
issueId: fixture.issueId,
agentId,
},
backendKind: "mock",
sourceInstanceId: fixture.runnerInstanceId,
});
await port.completeRun({
result,
terminal,
callerResultId: `${fixture.runId}:result`,
});
}
it("finishes an accepted result after shutdown failed and workspace repair succeeded", async () => {
const fixture = await seedNativeRun();
await db.update(completionContracts).set({ risk: "low", completionAuthority: "agent_claim_policy",
contractJson: { revision: CONTROL_PLANE_CONFORMANCE_RESULT.completionClaim.contractRevision,
objective: "Finish the work", criteria: [{ id: "objective", requirement: "Complete the task" }] } })
.where(eq(completionContracts.id, fixture.contractId));
await driveToCompleteResult(fixture);
await db.update(heartbeatRuns).set({ status: "failed", finishedAt: new Date(),
errorCode: "provider_transport_failed", error: "provider_transport_failed: runner did not durably suspend before checkpoint" })
.where(eq(heartbeatRuns.id, fixture.runId));
await db.update(nativeRunFinalizations).set({ phase: "retryable_failure", leaseOwner: null,
leaseExpiresAt: null, nextAttemptAt: null }).where(eq(nativeRunFinalizations.runId, fixture.runId));
const failed = await finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "failed" });
// An unchanged failed workspace must replay its existing decision.
await finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "failed" });
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.runId, fixture.runId))).toHaveLength(1);
await db.insert(workspaceOperations).values({ companyId, issueId: fixture.issueId,
heartbeatRunId: fixture.runId, phase: "workspace_finalize", status: "succeeded" });
await db.update(nativeRunFinalizations).set({ nextAttemptAt: null }).where(eq(nativeRunFinalizations.runId, fixture.runId));
await reconcileNativeFinalizations(db, [fixture.runId]);
expect((await db.select().from(issues).where(eq(issues.id, fixture.issueId)))[0]).toMatchObject({
status: "done", executionRunId: null, checkoutRunId: null,
});
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId)))[0]).toMatchObject({
status: "succeeded", errorCode: null, error: null,
resultJson: { recoveredExecutionFailure: { errorCode: "provider_transport_failed" } },
});
const assessments = await db.select().from(workAssessments).where(eq(workAssessments.runId, fixture.runId));
expect(assessments).toHaveLength(2);
expect(assessments.find(row => row.id !== failed.assessmentId)?.supersedesAssessmentId).toBe(failed.assessmentId);
await reconcileNativeFinalizations(db, [fixture.runId]);
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.runId, fixture.runId))).toHaveLength(2);
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, fixture.runId))).toHaveLength(1);
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId))).toHaveLength(0);
});
it("emits exactly one agent.task_run event when the native finalizer commits a terminal status", async () => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture);
const callsBefore = mockTelemetryClient.track.mock.calls.length;
const captureCallsBefore = mockCaptureRunFailure.mock.calls.length;
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
const newCalls = agentTaskRunCalls(callsBefore);
expect(newCalls).toHaveLength(1);
expect(newCalls[0]?.[1]).toMatchObject({ agent_id: agentId, state: "succeeded" });
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId))
.then((rows) => rows[0]);
expect(run?.status).toBe("succeeded");
// "succeeded" is not a failure status, so it never reports to Sentry.
expect(captureRunFailureCallsFrom(captureCallsBefore)).toHaveLength(0);
});
it("emits zero events when a repeat finalize call preserves the succeeded terminal state", async () => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture);
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
await verifyCommittedReplayPayloadReads(db, { runId: fixture.runId, companyId, agentId }, async () => {
const callsBefore = mockTelemetryClient.track.mock.calls.length;
reconcileMuseAssignments.mockClear();
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
expect(agentTaskRunCalls(callsBefore)).toHaveLength(0);
expect(reconcileMuseAssignments).toHaveBeenCalledExactlyOnceWith(companyId, agentId, fixture.runId);
await db.insert(workspaceOperations).values({ companyId, issueId: fixture.issueId,
heartbeatRunId: fixture.runId, phase: "workspace_finalize", status: "succeeded" });
const reconciled = await reconcileNativeFinalizations(db, [fixture.runId]);
expect(reconciled).toContainEqual(expect.objectContaining({ runId: fixture.runId, phase: "committed" }));
expect(agentTaskRunCalls(callsBefore)).toHaveLength(0);
});
});
it("repairs a stale succeeded-run error once without duplicate telemetry or destroying retained owner evidence", async () => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture);
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
await db
.update(heartbeatRuns)
.set({
errorCode: "adapter_failed",
error: "provider_transport_failed: retained cleanup evidence",
processPid: 987654,
runnerProfileJson: { sessionCheckpoint: { retainedEvidence: "keep" } },
})
.where(eq(heartbeatRuns.id, fixture.runId));
const callsBefore = mockTelemetryClient.track.mock.calls.length;
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
const [recovered] = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId));
expect(recovered).toMatchObject({
status: "succeeded",
error: null,
errorCode: null,
processPid: 987654,
runnerProfileJson: { sessionCheckpoint: { retainedEvidence: "keep" } },
resultJson: {
recoveredExecutionFailure: {
schema: "paperclip.recovered_execution_failure.v1",
errorCode: "adapter_failed",
error: "provider_transport_failed: retained cleanup evidence",
},
},
});
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
const [replayed] = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId));
expect(replayed.resultJson?.recoveredExecutionFailure).toEqual(
recovered.resultJson?.recoveredExecutionFailure,
);
expect(agentTaskRunCalls(callsBefore)).toHaveLength(0);
});
it.each([
"native_execution_ownership_unverified",
"native_adopted_runner_authentication_timeout",
])(
"does not project through retained ownership guard %s",
async (errorCode) => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture);
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
const [held] = await db
.update(heartbeatRuns)
.set({
status: "running",
finishedAt: null,
nativePhase: "terminal_failure",
errorCode,
error: "Retained owner must remain fenced",
processPid: 987654,
})
.where(eq(heartbeatRuns.id, fixture.runId))
.returning();
const callsBefore = mockTelemetryClient.track.mock.calls.length;
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
const [after] = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId));
expect(after).toEqual(held);
expect(agentTaskRunCalls(callsBefore)).toHaveLength(0);
},
);
it.each([null, "Earlier diagnostic"])(
"preserves the current-row diagnostic when it changes after finalizer admission (prior=%s)",
async (priorError) => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture);
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
await db
.update(heartbeatRuns)
.set({
error: priorError,
errorCode: priorError ? "adapter_failed" : null,
})
.where(eq(heartbeatRuns.id, fixture.runId));
const select = db.select.bind(db);
let changed = false;
// Hold the persisted-result read inside projectCommittedRun, after its
// caller captured the run but before the terminal UPDATE. This models
// a late heartbeat cleanup diagnostic without changing production APIs.
const wrap = (query: any): any =>
new Proxy(query, {
get(target, key) {
const value = Reflect.get(target, key, target);
if (key === "then")
return async (fulfilled: any, rejected: any) => {
if (!changed) {
changed = true;
await db
.update(heartbeatRuns)
.set({
error: "Latest cleanup diagnostic",
errorCode: "provider_transport_failed",
resultJson: {
concurrentMarker: "keep",
nativeCommittedChatResponse: {
resultId: "retain-selector",
},
},
})
.where(eq(heartbeatRuns.id, fixture.runId));
}
return value.call(target, fulfilled, rejected);
};
return typeof value === "function"
? (...args: any[]) => wrap(value.apply(target, args))
: value;
},
});
const spy = vi.spyOn(db, "select").mockImplementation(((
selection: any,
) => {
const query = select(selection);
return selection?.terminalState
? wrap(query)
: query;
}) as typeof db.select);
try {
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
} finally {
spy.mockRestore();
}
expect(changed).toBe(true);
const [after] = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId));
expect(after).toMatchObject({
error: null,
errorCode: null,
resultJson: {
concurrentMarker: "keep",
nativeCommittedChatResponse: { resultId: "retain-selector" },
recoveredExecutionFailure: {
error: "Latest cleanup diagnostic",
errorCode: "provider_transport_failed",
},
},
});
},
);
it.each(["in_progress", "cancelled"])("does not redeliver a committed failure during reconciliation of a %s task", async (issueStatus) => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture, {
...CONTROL_PLANE_CONFORMANCE_TERMINAL,
turnTerminalState: "failed",
runTerminalState: "failed",
});
// workspaceFinalizeStatus reports whether the workspace finalization
// step itself succeeded, independent of the run's own terminal state
// (runTerminalState below), which is what actually failed here.
const captureCallsBeforeFirstFinalize = mockCaptureRunFailure.mock.calls.length;
await finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId))
.then((rows) => rows[0]);
expect(run?.status).toBe("failed");
// The first write is a genuine transition into "failed": it reports
// exactly one Sentry event.
await vi.waitFor(() => {
expect(captureRunFailureCallsFrom(captureCallsBeforeFirstFinalize)).toHaveLength(1);
}, { timeout: 5_000 });
const firstFinalizeCaptures = captureRunFailureCallsFrom(captureCallsBeforeFirstFinalize);
expect(firstFinalizeCaptures[0]?.[0]).toMatchObject({
runId: fixture.runId,
runStatus: "failed",
});
await db.update(issues).set({ status: issueStatus }).where(eq(issues.id, fixture.issueId));
const publish = vi.fn();
await deliverExecutionStatuses(db, { publish });
expect(publish.mock.calls.filter(([event]) => event.payload.runId === fixture.runId)).toHaveLength(1);
const [beforeReplay] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
expect(beforeReplay.executionStatusDeliveryId).toBeNull();
publish.mockClear();
const callsBefore = mockTelemetryClient.track.mock.calls.length;
const captureCallsBeforeReplay = mockCaptureRunFailure.mock.calls.length;
// Simulate several periodic sweeps, including overlapping reconciliation.
for (let sweep = 0; sweep < 3; sweep += 1) {
await Promise.all([0, 1].map(() => finalizeNativeRun({
db,
runId: fixture.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
})));
await deliverExecutionStatuses(db, { publish });
}
expect(publish.mock.calls.filter(([event]) => event.payload.runId === fixture.runId)).toHaveLength(0);
const [afterReplay] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
expect(afterReplay).toMatchObject({
executionStatusDeliveryId: null,
updatedAt: beforeReplay.updatedAt,
nativePhaseUpdatedAt: beforeReplay.nativePhaseUpdatedAt,
finishedAt: beforeReplay.finishedAt,
});
expect(agentTaskRunCalls(callsBefore)).toHaveLength(0);
expect(captureRunFailureCallsFrom(captureCallsBeforeReplay)).toHaveLength(0);
});
it("repairs an incomplete committed failure once and preserves its pending delivery on replay", async () => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture, {
...CONTROL_PLANE_CONFORMANCE_TERMINAL,
turnTerminalState: "failed",
runTerminalState: "failed",
});
const finalize = () => finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
await finalize();
await db.update(heartbeatRuns).set({
finishedAt: null,
nativePhase: "arbitrating",
executionStatusDeliveryId: null,
}).where(eq(heartbeatRuns.id, fixture.runId));
await finalize();
const [repaired] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
expect(repaired.status).toBe("failed");
expect(repaired.nativePhase).toBe("committed");
expect(repaired.finishedAt).toBeInstanceOf(Date);
expect(repaired.executionStatusDeliveryId).toBeTypeOf("string");
await finalize();
const [replayed] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
expect(replayed).toEqual(repaired);
});
it("reports a committed failure once when the run row was an unreported pending retry", async () => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture, {
...CONTROL_PLANE_CONFORMANCE_TERMINAL,
turnTerminalState: "failed",
runTerminalState: "failed",
});
const finalize = () => finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
await finalize();
// The executor stores a scheduled same-run retry as failed + retryable_failure,
// and reportRunFailure skips that write.
await db.update(heartbeatRuns).set({
nativePhase: "retryable_failure",
}).where(eq(heartbeatRuns.id, fixture.runId));
await waitForPendingRunFailureReports();
const captureCallsBefore = mockCaptureRunFailure.mock.calls.length;
await finalize();
await waitForPendingRunFailureReports();
expect(captureRunFailureCallsFrom(captureCallsBefore)).toEqual([
[expect.objectContaining({ runId: fixture.runId, runStatus: "failed" })],
]);
await finalize();
await waitForPendingRunFailureReports();
expect(captureRunFailureCallsFrom(captureCallsBefore)).toHaveLength(1);
});
it("retires this run's scheduled finalization retry after successful commit", async () => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture, CONTROL_PLANE_CONFORMANCE_TERMINAL);
await recordNativeFinalizationFailure({ db, runId: fixture.runId,
error: new Error("native_workspace_sync_out_failed"), failureScope: "workspace", projectRunStatus: true });
const [pending] = await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, fixture.issueId));
expect(pending).toMatchObject({ status: "active", evidence: { runId: fixture.runId } });
await finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
const [settled] = await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.id, pending.id));
expect(settled.status).toBe("resolved");
});
it("repairs stale retry metadata on an already committed successful run", async () => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture, CONTROL_PLANE_CONFORMANCE_TERMINAL);
await finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
const [before] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
await db.update(heartbeatRuns).set({ resultJson: {
...before.resultJson, finalizationPhase: "retryable_failure", failureCode: "native_finalization_invalid",
originalFailureCode: "native_finalization_invalid", nextAttemptAt: new Date().toISOString(),
} }).where(eq(heartbeatRuns.id, fixture.runId));
await finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
const [after] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
expect(after).toMatchObject({ status: "succeeded", nativePhase: "committed", resultJson: {
finalizationPhase: "committed", failureCode: null, originalFailureCode: null, nextAttemptAt: null,
} });
});
it("ignores a stale workspace failure after the same run committed successfully", async () => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture, CONTROL_PLANE_CONFORMANCE_TERMINAL);
await finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
const [before] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
const late = await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_workspace_sync_out_failed"),
failureScope: "workspace", projectRunStatus: true,
});
expect(late.phase).toBe("committed");
const [after] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
expect(after).toEqual(before);
expect(await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, fixture.issueId))).toEqual([]);
});
it("materializes recovery for an agent-owned invalid-result outcome", async () => {
const fixture = await seedNativeRun();
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId,
phase: "terminal_failure", failureCode: "native_finalization_invalid",
failureDetail: { recoveryOwner: { kind: "agent", agentId } },
});
const outcome = await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_finalization_invalid"),
});
expect(outcome.phase).toBe("retryable_failure");
expect(await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, fixture.issueId)))
.toEqual([expect.objectContaining({ status: "active", ownerType: "agent", cause: "native_finalization_invalid" })]);
});
it.each(["native_workspace_sync_out_unrecoverable", "native_workspace_sync_out_unsafe_archive"])(
"does not reopen board-owned %s repair for a late failure", async (failureCode) => {
const fixture = await seedNativeRun();
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "workspace_finalizing",
});
await recordNativeFinalizationFailure({ db, runId: fixture.runId,
error: new Error(failureCode), failureScope: "workspace", permanent: true });
const [before] = await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, fixture.runId));
const recoveryBefore = await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, fixture.issueId));
for (const failureScope of [undefined, "workspace"] as const) {
await recordNativeFinalizationFailure({ db, runId: fixture.runId,
error: new Error("native_workspace_sync_out_failed"), failureScope });
}
const [after] = await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, fixture.runId));
expect(after).toEqual(before);
expect(await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, fixture.issueId))).toEqual(recoveryBefore);
});
it("emits zero events when a retryable-failure write's conditional status spread is omitted", async () => {
const fixture = await seedNativeRun();
// recordNativeFinalizationFailure only needs the run and coordinator rows
// to exist — no persisted native result is required.
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId,
companyId,
issueId: fixture.issueId,
phase: "observed",
attempt: 0,
});
const callsBefore = mockTelemetryClient.track.mock.calls.length;
// projectRunStatus is omitted (falsy): the conditional spread in
// recordRetryableFailure never includes `status`, so the write never sets
// a terminal status, regardless of the attempt count.
await recordNativeFinalizationFailure({
db,
runId: fixture.runId,
error: new Error("native_test_failure"),
});
expect(agentTaskRunCalls(callsBefore)).toHaveLength(0);
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId))
.then((rows) => rows[0]);
expect(run?.status).toBe("running");
});
it("bounds workspace-only retries without consuming the provider attempt", async () => {
const fixture = await seedNativeRun();
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId,
companyId,
issueId: fixture.issueId,
phase: "observed",
attempt: 1,
});
for (
let expectedAttempt = 1;
expectedAttempt <= 3;
expectedAttempt += 1
) {
await recordNativeFinalizationFailure({
db,
runId: fixture.runId,
error: new Error("native_workspace_sync_out_failed"),
projectRunStatus: true,
failureScope: "workspace",
});
const coordinator = await db
.select()
.from(nativeRunFinalizations)
.where(eq(nativeRunFinalizations.runId, fixture.runId))
.then((rows) => rows[0]!);
expect(coordinator.attempt).toBe(1);
expect(coordinator.failureDetail).toMatchObject({
workspaceFinalizeAttempt: expectedAttempt,
});
expect(coordinator.phase).toBe(
expectedAttempt === 3 ? "terminal_failure" : "retryable_failure",
);
}
await expect(
db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId))
.then((rows) => rows[0]?.status),
).resolves.toBe("failed");
// A failed run must say why it failed. The retry-exhausted code is the
// terminal cause, so it is also the run's error code.
const failedRun = await readRunError(fixture.runId);
expect(failedRun).toMatchObject({
status: "failed",
errorCode: "native_workspace_sync_out_retry_exhausted",
});
expect(failedRun?.error).toMatch(/workspace/i);
});
async function readRunError(runId: string) {
return db
.select({ status: heartbeatRuns.status, errorCode: heartbeatRuns.errorCode, error: heartbeatRuns.error })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, runId))
.then((rows) => rows[0]);
}
async function exhaustWorkspaceRetries(runId: string, beforeLastAttempt?: () => Promise<unknown>) {
for (let attempt = 1; attempt <= 3; attempt += 1) {
if (attempt === 3) await beforeLastAttempt?.();
await recordNativeFinalizationFailure({
db, runId, error: new Error("native_workspace_sync_out_failed"),
projectRunStatus: true, failureScope: "workspace",
});
}
}
it("keeps an earlier root error code when workspace retries are exhausted", async () => {
const fixture = await seedNativeRun();
await db.update(heartbeatRuns)
.set({ errorCode: "native_provider_terminal_failed", error: "Provider failed earlier." })
.where(eq(heartbeatRuns.id, fixture.runId));
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 1,
});
await exhaustWorkspaceRetries(fixture.runId);
await expect(readRunError(fixture.runId)).resolves.toEqual({
status: "failed",
errorCode: "native_provider_terminal_failed",
error: "Provider failed earlier.",
});
});
it("does not record an error on a run cancelled before workspace retries were exhausted", async () => {
const fixture = await seedNativeRun();
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 1,
});
await exhaustWorkspaceRetries(fixture.runId, () => db.update(heartbeatRuns)
.set({ status: "cancelled", finishedAt: new Date() })
.where(eq(heartbeatRuns.id, fixture.runId)));
await expect(readRunError(fixture.runId)).resolves.toEqual({ status: "cancelled", errorCode: null, error: null });
});
it("does not record an error when Stop wins after the finalizer read the running run", async () => {
const fixture = await seedNativeRun();
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 1,
});
for (let attempt = 1; attempt <= 2; attempt += 1) {
await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_workspace_sync_out_failed"),
projectRunStatus: true, failureScope: "workspace",
});
}
// Stop holds the run row. The finalizer reads the still-running run, then
// its terminal UPDATE waits for that lock. Stop commits the cancellation,
// and the UPDATE must evaluate the cancelled row, not its earlier read.
let finalizer: Promise<unknown> | null = null;
await db.transaction(async (stop) => {
await stop.select({ id: heartbeatRuns.id }).from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId)).for("update");
finalizer = recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_workspace_sync_out_failed"),
projectRunStatus: true, failureScope: "workspace",
});
const deadline = Date.now() + 10_000;
for (;;) {
const [waiting] = await db.execute<{ count: number }>(
sql`select count(*)::int as count from pg_locks where not granted and locktype in ('tuple', 'transactionid')`,
);
if (Number(waiting?.count) > 0) break;
if (Date.now() > deadline) throw new Error("finalizer never waited for the Stop lock");
await new Promise((resolve) => setTimeout(resolve, 20));
}
await stop.update(heartbeatRuns).set({ status: "cancelled", finishedAt: new Date() })
.where(eq(heartbeatRuns.id, fixture.runId));
});
await finalizer;
await expect(readRunError(fixture.runId)).resolves.toEqual({ status: "cancelled", errorCode: null, error: null });
});
it("leaves a run without an error while workspace retries remain", async () => {
const fixture = await seedNativeRun();
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 1,
});
await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_workspace_sync_out_failed"),
projectRunStatus: true, failureScope: "workspace",
});
await expect(readRunError(fixture.runId)).resolves.toEqual({ status: "running", errorCode: null, error: null });
});
it("records the finalization error code when provider-scope retries fail an unsucceeded run", async () => {
const fixture = await seedNativeRun();
// Two earlier finalizations of this result failed; this is the third.
await db.update(heartbeatRuns).set({ resultJson: { finalizationFailureCount: 2 } })
.where(eq(heartbeatRuns.id, fixture.runId));
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 3,
});
await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_finalization_invalid"), projectRunStatus: true,
});
const run = await readRunError(fixture.runId);
expect(run).toMatchObject({ status: "failed", errorCode: "native_finalization_retry_exhausted" });
expect(run?.error).toBeTruthy();
});
it("keeps a provider-succeeded run's first finalization failure retryable after three provider attempts", async () => {
const fixture = await seedNativeRun();
await db.update(heartbeatRuns).set({ resultJson: { prpRunTerminalState: "succeeded" } })
.where(eq(heartbeatRuns.id, fixture.runId));
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 3,
});
const failure = await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_finalization_invalid"), projectRunStatus: true,
});
expect(failure).toMatchObject({ phase: "retryable_failure", failureCode: "native_finalization_invalid" });
await expect(readRunError(fixture.runId)).resolves.toEqual({ status: "running", errorCode: null, error: null });
});
it("fails a provider-succeeded run with an error code when workspace retries are exhausted", async () => {
// The reported case: the provider turn succeeded, then the workspace
// copy-back failed three times. Workspace scope fails the run regardless
// of the provider outcome, so the run must name the cause.
const fixture = await seedNativeRun();
await db.update(heartbeatRuns).set({ resultJson: { prpRunTerminalState: "succeeded" } })
.where(eq(heartbeatRuns.id, fixture.runId));
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 1,
});
await exhaustWorkspaceRetries(fixture.runId);
const run = await readRunError(fixture.runId);
expect(run).toMatchObject({ status: "failed", errorCode: "native_workspace_sync_out_retry_exhausted" });
expect(run?.error).toMatch(/recovery action/);
});
it("hands a full sandbox disk to saved-work recovery on the first copy-back failure", async () => {
// PAP-4253: three copy-back attempts each hit the same full disk (about 15
// minutes apiece across deploys). Nothing frees the sandbox between them;
// saved-work recovery reclaims the inactive provider cache, then saves.
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture);
const [accepted] = await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, fixture.runId));
expect(accepted).toBeDefined();
const wakeupsBefore = await db.select({ id: agentWakeupRequests.id }).from(agentWakeupRequests)
.where(eq(agentWakeupRequests.agentId, agentId));
const failure = await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_workspace_sync_out_failed"),
projectRunStatus: true, failureScope: "workspace", failureCause: REMOTE_WORKSPACE_SANDBOX_DISK_FULL,
});
expect(failure).toMatchObject({ phase: "terminal_failure", failureCode: "native_workspace_sync_out_retry_exhausted",
nextAttemptAt: null });
const [coordinator] = await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, fixture.runId));
expect(coordinator.failureDetail).toMatchObject({ recoveryOwner: { kind: "board" }, workspaceFinalizeAttempt: 1,
failureCause: REMOTE_WORKSPACE_SANDBOX_DISK_FULL });
// The accepted result stays linked for the export recovery.
expect(coordinator.resultId).toBe(accepted.id);
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, fixture.runId))).toEqual([accepted]);
const run = await readRunError(fixture.runId);
expect(run).toMatchObject({ status: "failed", errorCode: "native_workspace_sync_out_retry_exhausted" });
expect(run?.error).toMatch(/sandbox disk is full/);
expect(run?.error).not.toMatch(/after 3 attempts/);
// The export hold owns the next step; no provider turn is requested.
expect(await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, fixture.issueId)))
.toEqual([expect.objectContaining({ status: "active", ownerType: "board", cause: "native_workspace_sync_out_retry_exhausted" })]);
expect(await db.select({ id: agentWakeupRequests.id }).from(agentWakeupRequests)
.where(eq(agentWakeupRequests.agentId, agentId))).toEqual(wakeupsBefore);
});
it("keeps other copy-back failures on their bounded retry", async () => {
const fixture = await seedNativeRun();
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 1,
});
const failure = await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_workspace_sync_out_failed"),
projectRunStatus: true, failureScope: "workspace",
failureCause: "remote_workspace_capture_failed: stage=archive code=ECONNRESET",
});
expect(failure).toMatchObject({ phase: "retryable_failure", failureCode: "native_workspace_sync_out_failed" });
});
it("does not attach a finalization error to a provider-succeeded run when provider retries are exhausted", async () => {
const fixture = await seedNativeRun();
await db.update(heartbeatRuns).set({ resultJson: { prpRunTerminalState: "succeeded", finalizationFailureCount: 2 } })
.where(eq(heartbeatRuns.id, fixture.runId));
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId, companyId, issueId: fixture.issueId, phase: "observed", attempt: 3,
});
await recordNativeFinalizationFailure({
db, runId: fixture.runId, error: new Error("native_finalization_invalid"), projectRunStatus: true,
});
await expect(readRunError(fixture.runId)).resolves.toEqual({ status: "succeeded", errorCode: null, error: null });
});
it("blocks immediately when the sandbox with unexported changes is gone", async () => {
const fixture = await seedNativeRun();
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId,
companyId,
issueId: fixture.issueId,
phase: "workspace_finalizing",
attempt: 1,
resultId: null,
});
const failure = await recordNativeFinalizationFailure({
db,
runId: fixture.runId,
error: new Error("native_workspace_sync_out_unrecoverable"),
projectRunStatus: true,
failureScope: "workspace",
permanent: true,
});
expect(failure).toMatchObject({
phase: "terminal_failure",
failureCode: "native_workspace_sync_out_unrecoverable",
nextAttemptAt: null,
attempt: 1,
});
await expect(
db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId))
.then((rows) => rows[0]?.status),
).resolves.toBe("failed");
await expect(
db
.select({ errorCode: heartbeatRuns.errorCode })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId))
.then((rows) => rows[0]?.errorCode),
).resolves.toBe("native_workspace_sync_out_unrecoverable");
await expect(
db
.select({ status: issues.status })
.from(issues)
.where(eq(issues.id, fixture.issueId))
.then((rows) => rows[0]?.status),
).resolves.toBe("blocked");
});
it("commits the saved result when unsafe workspace export is omitted, with only a run log", async () => {
const fixture = await seedNativeRun();
await db.update(completionContracts).set({ risk: "low", completionAuthority: "agent_claim_policy" })
.where(eq(completionContracts.id, fixture.contractId));
await driveToCompleteResult(fixture, CONTROL_PLANE_CONFORMANCE_TERMINAL, {
...CONTROL_PLANE_CONFORMANCE_RESULT,
completionClaim: { ...CONTROL_PLANE_CONFORMANCE_RESULT.completionClaim!, contractRevision: "telemetry-v1" },
});
const [original] = await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, fixture.runId));
const restore = vi.fn(async () => {
throw new Error("Daytona syncOut refusing tarball link whose target escapes the extraction dir: tools/pnpm -> /private/tool");
});
const assertOwnership = vi.fn(async () => {});
await restoreNativeWorkspaceBestEffort({ db, runId: fixture.runId, restore, assertOwnership });
const outcome = await finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true, preserveProviderAttempt: true });
expect(outcome.phase).toBe("committed");
expect(restore).toHaveBeenCalledOnce();
expect(assertOwnership).toHaveBeenCalledOnce();
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
const [issue] = await db.select().from(issues).where(eq(issues.id, fixture.issueId));
expect(run).toMatchObject({ status: "succeeded", error: null, errorCode: null });
expect(issue.status).toBe("done");
const results = await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, fixture.runId));
expect(results).toEqual([original]);
const actions = await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, fixture.issueId));
expect(actions).toEqual([]);
const events = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, fixture.runId));
expect(events.filter(event => event.eventType === "workspace_export_omitted")).toMatchObject([
{ level: "info", payload: { reason: "restore_unsafe_archive" } },
]);
expect(JSON.stringify(events)).not.toContain("/private/tool");
});
it.each(["active", "resolved", "missing"])("automatically completes a legacy unsafe result with a %s repair action and no sandbox", async actionState => {
const fixture = await seedNativeRun();
await db.update(completionContracts).set({ risk: "low", completionAuthority: "agent_claim_policy" }).where(eq(completionContracts.id, fixture.contractId));
await driveToCompleteResult(fixture, CONTROL_PLANE_CONFORMANCE_TERMINAL, {
...CONTROL_PLANE_CONFORMANCE_RESULT,
completionClaim: { ...CONTROL_PLANE_CONFORMANCE_RESULT.completionClaim!, contractRevision: "telemetry-v1" },
});
const originalResults = await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, fixture.runId));
await db.update(nativeRunFinalizations).set({ phase: "terminal_failure", failureCode: "native_workspace_sync_out_unsafe_archive" }).where(eq(nativeRunFinalizations.runId, fixture.runId));
await db.update(heartbeatRuns).set({ status: "failed", nativePhase: "terminal_failure", errorCode: "native_workspace_sync_out_unsafe_archive",
runnerProfileJson: { nativeWorkspaceSync: { schema: "paperclip.native-workspace-sync/v1", state: "prepared",
descriptorSha256: "a".repeat(64), baselineSha256: "b".repeat(64), finalHostSha256: null,
workspaceId: randomUUID(), leaseId: randomUUID(), providerLeaseId: "missing-old-sandbox", remoteCwd: "/work", resourceDisposition: "destroy" } },
}).where(eq(heartbeatRuns.id, fixture.runId));
await db.update(issues).set({ status: "blocked" }).where(eq(issues.id, fixture.issueId));
if (actionState !== "missing") await db.insert(issueRecoveryActions).values({ companyId, sourceIssueId: fixture.issueId,
kind: "active_run_watchdog", ownerType: "board", cause: "native_workspace_sync_out_unsafe_archive", fingerprint: fixture.runId,
evidence: { runId: fixture.runId }, nextAction: "Repair the unsafe link manually", status: actionState,
...(actionState === "resolved" ? { outcome: "restored", resolutionNote: "new_source_execution_path", resolvedAt: new Date() } : {}),
});
const priorWakeups = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId));
const priorActivity = await db.select().from(activityLog).where(eq(activityLog.entityId, fixture.issueId));
await recoverLegacyUnsafeWorkspaceExports(db, [fixture.runId]);
expect(await db.select().from(activityLog).where(eq(activityLog.entityId, fixture.issueId))).toEqual(priorActivity);
// No environment runtime is supplied: a stopped, deleted, or unavailable
// provider cannot prevent omission and accepted-result commitment.
await reconcileNativeFinalizations(db, [fixture.runId]);
await reconcileNativeFinalizations(db, [fixture.runId]);
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId)))[0]).toMatchObject({ status: "succeeded", nativePhase: "committed", error: null, errorCode: null });
expect((await db.select().from(issues).where(eq(issues.id, fixture.issueId)))[0].status).toBe("done");
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, fixture.runId))).toEqual(originalResults);
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId))).toEqual(priorWakeups);
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.nativeIssueId, fixture.issueId))).toHaveLength(1);
const actions = await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, fixture.issueId));
expect(actions.every(action => action.status === "resolved" && action.nextAction === "")).toBe(true);
expect((await db.select().from(workspaceOperations).where(eq(workspaceOperations.heartbeatRunId, fixture.runId)))[0]).toMatchObject({ status: "succeeded", metadata: { workspaceSync: { omitted: true, legacy: true } } });
const events = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, fixture.runId));
expect(events.filter(event => event.eventType === "workspace_export_omitted")).toMatchObject([{ level: "info", payload: { reason: "restore_unsafe_archive", legacy: true } }]);
expect(events.filter(event => event.eventType === "workspace_export_omitted")).toHaveLength(1);
});
it("emits exactly one event for a cancel_continuations write (trap 2: :685/:518 overlap)", async () => {
const fixture = await seedNativeRun();
// Build the minimal real rows commitNativeStatusDecision's foreign keys
// require, without going through the evidence-classifier pipeline.
const resultId = randomUUID();
await db.insert(nativeRunResults).values({
id: resultId,
companyId,
issueId: fixture.issueId,
runId: fixture.runId,
completionContractId: fixture.contractId,
serverFingerprint: `fp-${resultId}`,
schemaStatus: "accepted",
resultJson: {},
canonicalSha256: `sha-${resultId}`,
});
const assessmentId = randomUUID();
await db.insert(workAssessments).values({
id: assessmentId,
companyId,
issueId: fixture.issueId,
runId: fixture.runId,
contractId: fixture.contractId,
resultId,
triggerKind: "native_result",
triggerActorCompanyId: companyId,
priorIssueStatus: "in_progress",
priorStatusVersion: 0,
policyVersion: "telemetry-v1",
assessmentJson: {},
inputDigest: `digest-${assessmentId}`,
});
await db.insert(nativeRunFinalizations).values({
runId: fixture.runId,
companyId,
issueId: fixture.issueId,
phase: "arbitrating",
attempt: 0,
resultId,
});
// Shaped exactly like resolveNativeCancellationStatus("issue", ...) in
// native-session-executor.ts — the only place cancel_continuations is
// produced today.
const decision: NativeStatusDecision = {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
statusAction: "cancelled",
toStatus: "cancelled",
reasonCode: "cancellation_issue_authorized",
unblockDescriptor: null,
effects: [
{ kind: "release_checkout" },
{ kind: "cancel_continuations" },
],
};
const callsBefore = mockTelemetryClient.track.mock.calls.length;
await commitNativeStatusDecision({
db,
companyId,
issueId: fixture.issueId,
runId: fixture.runId,
assessmentId,
priorStatus: "in_progress",
priorStatusVersion: 0,
priorDecisionId: null,
decision,
});
const afterCommit = agentTaskRunCalls(callsBefore);
// status-decision-committer.ts:685 (the cancel_continuations effect)
// wrote this run's terminal status and emitted for it.
expect(afterCommit).toHaveLength(1);
expect(afterCommit[0]?.[1]).toMatchObject({ agent_id: agentId, state: "cancelled" });
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId))
.then((rows) => rows[0]);
expect(run?.status).toBe("cancelled");
// native-run-finalizer.ts:518's own guard: it skips its emit whenever the
// decision it just committed already included cancel_continuations,
// because :685 above already wrote and emitted for this run. Reproduce
// the exact guard predicate on the same decision object to prove it
// resolves to "skip", so the run's total event count for this commit
// stays at exactly one even after the finalizer's own write runs.
const alreadyEmittedByCommittedDecision = decision.effects.some(
(effect) => effect.kind === "cancel_continuations",
);
expect(alreadyEmittedByCommittedDecision).toBe(true);
expect(agentTaskRunCalls(callsBefore)).toHaveLength(1);
});
it.each([true, false])("signals instruction collection only when native failure projects terminal status (%s)", async projectRunStatus => {
const fixture = await seedNativeRun();
await driveToCompleteResult(fixture);
const settled = vi.fn();
const unsubscribe = subscribeDeliveryWork(db, DELIVERY_QUEUES.instructionCollection, settled);
try {
await recordNativeFinalizationFailure({ db, runId: fixture.runId,
error: new Error("native_fixture_permanent_failure"), permanent: true, projectRunStatus });
expect((await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, fixture.runId)))[0].phase).toBe("terminal_failure");
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
if (projectRunStatus) {
expect(run.status).not.toBe("running");
expect(settled).toHaveBeenCalled();
} else {
expect(run.status).toBe("running");
expect(settled).not.toHaveBeenCalled();
}
} finally { unsubscribe(); }
});
it("signals durable accounting when native commitment or permanent failure makes a receipt actionable", async () => {
const settled = vi.fn();
const unsubscribe = subscribeDeliveryWork(db, DELIVERY_QUEUES.accounting, settled);
try {
const committed = await seedNativeRun();
await driveToCompleteResult(committed);
const beforeCommit = settled.mock.calls.length;
await finalizeNativeRun({ db, runId: committed.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
expect((await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, committed.runId)))[0].phase).toBe("committed");
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, committed.runId)))[0].status).toBe("succeeded");
expect(settled.mock.calls.length).toBeGreaterThan(beforeCommit);
const failed = await seedNativeRun();
await driveToCompleteResult(failed);
const beforeFailure = settled.mock.calls.length;
await recordNativeFinalizationFailure({ db, runId: failed.runId, error: new Error("native_fixture_permanent_failure"), permanent: true });
expect((await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, failed.runId)))[0].phase).toBe("terminal_failure");
expect(settled.mock.calls.length).toBeGreaterThan(beforeFailure);
} finally { unsubscribe(); }
});
});
enableRunFailureReporting();