mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
fix: preserve agent-owned native result recovery
Keep settled and foreign-owner failures fenced while allowing audit-only malformed-result outcomes to materialize their existing bounded recovery action. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c5b2a16a28
commit
d02e0f45e7
2 files changed
+39
-1
No files matched your search
@@ -553,6 +553,36 @@ describeEmbeddedPostgres("native run finalizer / status decision committer — a
|
||||
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("does not reopen a board-owned terminal workspace repair for a late generic failure", async () => {
|
||||
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("native_workspace_sync_out_unrecoverable"), 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));
|
||||
await recordNativeFinalizationFailure({ db, runId: fixture.runId, error: new Error("native_finalization_invalid") });
|
||||
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
|
||||
|
||||
@@ -291,7 +291,15 @@ async function recordRetryableFailure(input: {
|
||||
eq(nativeRunFinalizations.companyId, input.run.companyId),
|
||||
)).for("update").limit(1).then((rows) => rows[0] ?? null);
|
||||
if (!current) throw new Error("native_finalization_missing");
|
||||
if (current.phase === "committed" || current.phase === "terminal_failure"
|
||||
// Audit-only attention first records an agent-owned invalid-result outcome;
|
||||
// its caller still needs to materialize the normal bounded recovery action.
|
||||
// Board-owned terminal repairs and late snapshots remain settled.
|
||||
const recoveryOwner = record(record(current.failureDetail).recoveryOwner);
|
||||
const pendingAgentRecovery = current.phase === "terminal_failure"
|
||||
&& input.coordinator.phase === "terminal_failure"
|
||||
&& recoveryOwner.kind === "agent" && recoveryOwner.agentId === input.run.agentId;
|
||||
if (current.phase === "committed"
|
||||
|| (current.phase === "terminal_failure" && !pendingAgentRecovery)
|
||||
|| current.leaseOwner !== input.coordinator.leaseOwner) return current;
|
||||
const [currentRun] = await tx.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, input.run.id)).limit(1);
|
||||
if (!currentRun) throw new Error("native_finalization_run_missing");
|
||||
|
||||
Reference in new issue
Block a user