From 5a28f7acf8a8c86536c4f5c5369bfeb49a46c41a Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sun, 25 Oct 2026 16:06:36 -0700 Subject: [PATCH] fix(server): include restored monitor proof in assessment identity (#17665) Include validated preserved monitor proof in the immutable assessment input so restored receipts or completion times can produce a new status decision at the same deadline. Preserve accepted results, earlier decisions, provider attempts, monitor deadlines, and idempotent replay. Validate two baseline regressions, 201 targeted tests on current master, workspace typecheck, server build, full CI, and Apex 5/5. Co-Authored-By: Paperclip --- doc/DATABASE.md | 5 ++++ .../native-preserved-monitor-wait.test.ts | 25 ++++++++++++++++--- .../native-runtime/native-run-finalizer.ts | 1 + .../native-runtime/work-assessments.ts | 4 ++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/doc/DATABASE.md b/doc/DATABASE.md index d5f970ac23d..cb341f8ea79 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -175,6 +175,11 @@ launch context does not travel with each policy or parent-report check. JSON types, task-key precedence, and company and agent identity checks are preserved. Each decision still reads current data; this is not an authorization cache. +Restoring a saved monitor's receipt or completion proof creates a new native +assessment identity even when its scheduled deadline is unchanged. The saved +proof is part of the assessment input. Prior decisions and accepted results stay +intact, and an unchanged replay reuses the same assessment and decision. + Native tool authority lookups select current run identity, status, invocation, responsible user, context, and receipt data. Saved profiles and output excerpts stay in storage and are omitted from that read. Task and agent binding, current diff --git a/server/src/services/native-runtime/native-preserved-monitor-wait.test.ts b/server/src/services/native-runtime/native-preserved-monitor-wait.test.ts index ad269c7467c..1cedcb28be8 100644 --- a/server/src/services/native-runtime/native-preserved-monitor-wait.test.ts +++ b/server/src/services/native-runtime/native-preserved-monitor-wait.test.ts @@ -126,21 +126,38 @@ describe.skipIf(!support.supported)("released native monitor results", () => { }, ); - it("supersedes an earlier authority error without replaying the accepted result", async () => { + it.each(["schedule", "receipt", "finished_time"] as const)("supersedes an earlier authority error after restoring %s without replaying the result", async state => { const f = await fixture(), scheduled = await f.readIssue(); - await db.update(issues).set({ executionPolicy: {}, monitorNextCheckAt: null }).where(eq(issues.id, f.issueId)); + const [source] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)); + await db.update(nativeRunFinalizations).set({ controllerBootId: randomUUID(), attempt: 1 }) + .where(eq(nativeRunFinalizations.runId, f.runId)); + if (state === "schedule") await db.update(issues).set({ executionPolicy: {}, monitorNextCheckAt: null }).where(eq(issues.id, f.issueId)); + if (state === "receipt") await db.update(heartbeatRuns).set({ resultJson: {} }).where(eq(heartbeatRuns.id, f.runId)); + if (state === "finished_time") await db.update(heartbeatRuns).set({ finishedAt: null }).where(eq(heartbeatRuns.id, f.runId)); await f.finalize(); const old = await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId)); expect(old.some(decision => decision.reasonCode === "monitor_wait_authority_lost")).toBe(true); - await db.update(issues).set({ executionPolicy: scheduled.executionPolicy, monitorNextCheckAt: scheduled.monitorNextCheckAt }) + if (state === "schedule") await db.update(issues).set({ executionPolicy: scheduled.executionPolicy, monitorNextCheckAt: scheduled.monitorNextCheckAt }) .where(eq(issues.id, f.issueId)); + if (state === "receipt") { + const [current] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)); + await db.update(heartbeatRuns).set({ resultJson: { ...current.resultJson, + semanticToolReceipts: source.resultJson?.semanticToolReceipts } }).where(eq(heartbeatRuns.id, f.runId)); + } + if (state === "finished_time") await db.update(heartbeatRuns).set({ finishedAt: f.finishedAt }).where(eq(heartbeatRuns.id, f.runId)); await f.finalize(); - expect((await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)))[0].phase).toBe("committed"); + const [settled] = await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)); + expect(settled).toMatchObject({ phase: "committed", nextAttemptAt: null, attempt: 1 }); const after = await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId)); for (const before of old) expect(after.find(decision => decision.id === before.id)).toEqual(before); expect(after.some(decision => decision.reasonCode === "scheduled_monitor_preserved")).toBe(true); expect((await db.select().from(nativeRunResults).where(eq(nativeRunResults.id, f.resultId)))[0].resultJson).toEqual(f.result); expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, f.companyId))).toEqual([]); + expect(await f.readIssue()).toMatchObject({ monitorNextCheckAt: scheduled.monitorNextCheckAt, + executionRunId: null, checkoutRunId: null }); + await f.finalize(); + expect(await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId))).toEqual(after); + expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)))[0].finishedAt).toEqual(f.finishedAt); }); it("notifies the existing monitor scheduler once when finalization stops blocking it", async () => { diff --git a/server/src/services/native-runtime/native-run-finalizer.ts b/server/src/services/native-runtime/native-run-finalizer.ts index 1f8b830ff03..5931662d7db 100644 --- a/server/src/services/native-runtime/native-run-finalizer.ts +++ b/server/src/services/native-runtime/native-run-finalizer.ts @@ -1488,6 +1488,7 @@ export async function finalizeNativeRun(input: { workspaceFinalizeStatus: input.workspaceFinalizeStatus, monitorDelivery, monitorWaitAt: assessment.continuation?.kind === "monitor" ? monitorWaitAt : null, + preservedMonitorWait, }); await input.db .update(nativeRunFinalizations) diff --git a/server/src/services/native-runtime/work-assessments.ts b/server/src/services/native-runtime/work-assessments.ts index 829b497040b..58975d83b6d 100644 --- a/server/src/services/native-runtime/work-assessments.ts +++ b/server/src/services/native-runtime/work-assessments.ts @@ -3,7 +3,7 @@ import type { Db } from "@paperclipai/db"; import { workAssessments } from "@paperclipai/db"; import type { NativeEvidenceAssessment } from "./evidence-classifier.js"; import { nativeSha256 } from "./canonical.js"; -import type { NativeMonitorDelivery } from "./native-monitor-delivery.js"; +import type { NativeMonitorDelivery, NativePreservedMonitorWait } from "./native-monitor-delivery.js"; export async function recordNativeWorkAssessment(input: { db: Db; @@ -26,12 +26,14 @@ export async function recordNativeWorkAssessment(input: { monitorDelivery?: NativeMonitorDelivery | null; /** A persisted schedule can regain settlement authority after a policy repair. */ monitorWaitAt?: string | null; + preservedMonitorWait?: NativePreservedMonitorWait | null; supersedesAssessmentId?: string | null; }) { const assessmentJson = { ...input.assessment, ...(input.monitorDelivery ? { monitorDelivery: input.monitorDelivery } : {}), ...(input.monitorWaitAt ? { monitorWaitAt: input.monitorWaitAt } : {}), + ...(input.preservedMonitorWait ? { preservedMonitorWait: input.preservedMonitorWait } : {}), } as unknown as Record; const inputDigest = nativeSha256({ contractId: input.contractId,