diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md index ba7286241be..333cb8697b1 100644 --- a/doc/execution-semantics.md +++ b/doc/execution-semantics.md @@ -1373,6 +1373,8 @@ settling their usage and terminal evidence before detaching. Recovery verifies the trigger against the retained event ledger and settles that same turn, even if the user has already answered the interaction. It replays retained accounting through the ordinary control-plane observers without starting another model turn. +When a later heartbeat run reuses the provider session, it clears that prior +run's wait marker along with its terminal, result and pending-request authority. A timeout, missing accounting, or an unproven provider terminal remains a failure; the saved wait does not certify successful provider execution. diff --git a/server/src/services/native-runtime/native-completion-feedback.test.ts b/server/src/services/native-runtime/native-completion-feedback.test.ts index a8490f5b954..d49a5cc1339 100644 --- a/server/src/services/native-runtime/native-completion-feedback.test.ts +++ b/server/src/services/native-runtime/native-completion-feedback.test.ts @@ -77,6 +77,11 @@ describe("native final-response feedback", () => { completionClaim: { ...report.completionClaim, criteria: [{ criterionId: "objective", status: "not_satisfied", evidenceRefs: [`approval:${actionRequestId}`] }] }, }; await expect(nativeCompletionFeedback(db, value.runId, criterionOnly)).rejects.toThrow("already resolved"); + // A provider can mark the approval criterion unknown while citing the exact + // declined interaction; that still is not a new review target. + await expect(nativeCompletionFeedback(db, value.runId, { + ...criterionOnly, completionClaim: { ...criterionOnly.completionClaim, criteria: [{ criterionId: "objective", status: "unknown", evidenceRefs: [`interaction:${interactionId}`] }] }, + })).rejects.toThrow("already resolved"); // Satisfied historical evidence does not bind the independent review target. if (disposition !== "yielded") await expect(nativeCompletionFeedback(db, value.runId, { ...criterionOnly, completionClaim: { ...criterionOnly.completionClaim, criteria: [{ criterionId: "past-action", status: "satisfied", evidenceRefs: [`approval:${actionRequestId}`] }] }, diff --git a/server/src/services/native-runtime/native-completion-feedback.ts b/server/src/services/native-runtime/native-completion-feedback.ts index c319fc79a89..9bbb9d39d26 100644 --- a/server/src/services/native-runtime/native-completion-feedback.ts +++ b/server/src/services/native-runtime/native-completion-feedback.ts @@ -186,7 +186,7 @@ export async function nativeCompletionFeedback( blocker: result.blocker, // An unmet criterion can name the action still being requested. Completed // evidence elsewhere in the report is not authority to suppress a review. - unmetCriteria: result.completionClaim.criteria.filter(criterion => criterion.status === "not_satisfied"), + unmetCriteria: result.completionClaim.criteria.filter(criterion => criterion.status !== "satisfied"), continuation: result.continuation, // The existing response-wake contract also names its card through evidence. evidence: result.reportedWorkDisposition === "yielded" && result.continuation?.kind === "response_wake" ? result.evidence : [], diff --git a/server/src/services/native-runtime/native-session-resume.test.ts b/server/src/services/native-runtime/native-session-resume.test.ts index 60290408caf..574e0dda15e 100644 --- a/server/src/services/native-runtime/native-session-resume.test.ts +++ b/server/src/services/native-runtime/native-session-resume.test.ts @@ -2125,10 +2125,17 @@ describe("rebindNativeSessionCheckpoint", () => { }); it("retains provider identity but clears prior turn and event state", () => { + const prior = previousRun(); + const profile = prior.runnerProfileJson as Record; + const checkpoint = profile.sessionCheckpoint as Record; + checkpoint.governedWait = { sourceEvent: { runId: previousRunId, turnId: "old-turn" }, result: { reportedWorkDisposition: "yielded" } }; + const before = structuredClone(prior); const rebound = rebindNativeSessionCheckpoint({ - previousRun: previousRun(), + previousRun: prior, currentExecution: execution(currentRunId), }); + expect(rebound?.governedWait).toBeUndefined(); + expect(prior).toEqual(before); expect(rebound).toMatchObject({ sessionId: "provider-thread-123", providerSessionId: "provider-thread-123", diff --git a/server/src/services/native-runtime/native-session-resume.ts b/server/src/services/native-runtime/native-session-resume.ts index c7090ea8639..051fbc0cf22 100644 --- a/server/src/services/native-runtime/native-session-resume.ts +++ b/server/src/services/native-runtime/native-session-resume.ts @@ -568,6 +568,9 @@ export function rebindNativeSessionCheckpoint(input: { activeTurnId: null, terminalTurns: [], pendingRuntimeRequests: [], + // Settlement evidence belongs to the prior heartbeat run, even when the + // provider session is reused for the user's response. + governedWait: undefined, providerRecoveryPolicy, }; }