From d02e0f45e75dcbb7f48455d3ce4da485f33b0ec7 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 27 Sep 2026 18:21:56 -0500 Subject: [PATCH] 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 --- .../native-run-finalizer-telemetry.test.ts | 30 +++++++++++++++++++ .../native-runtime/native-run-finalizer.ts | 10 ++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/server/src/services/native-runtime/native-run-finalizer-telemetry.test.ts b/server/src/services/native-runtime/native-run-finalizer-telemetry.test.ts index a4f47f143bb..0d925d7ffad 100644 --- a/server/src/services/native-runtime/native-run-finalizer-telemetry.test.ts +++ b/server/src/services/native-runtime/native-run-finalizer-telemetry.test.ts @@ -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 diff --git a/server/src/services/native-runtime/native-run-finalizer.ts b/server/src/services/native-runtime/native-run-finalizer.ts index b49ab168988..50fe7a32608 100644 --- a/server/src/services/native-runtime/native-run-finalizer.ts +++ b/server/src/services/native-runtime/native-run-finalizer.ts @@ -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");