fix: renew explicit continuation authorization for bounded retries (#14987)

Preserve exact user-authorized continuation receipts across bounded retries and retain the task claim through owned retry admission. Stop, reassignment, superseding input, and active cleanup continue to block unsafe continuation.

Validation: exact-head Greptile 5/5, passing CI, no unresolved review threads, and a clean merge. Detailed verification and limitations are recorded in the pull request.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-10-05 10:57:57 -07:00
1 parent 7498705642
commit 2e67ea8dc7
11 files changed
+639 -16

No files matched your search

+16
View File
@@ -3,6 +3,7 @@ import { isNativeWorkspaceExportRepairCause } from "@paperclipai/shared";
import { settleSlackConversation } from "../slack-conversation-lifecycle.js";
import { externalConversationStateSql } from "../slack-conversation-state.js";
import { executionRetryAccounting } from "../execution-recovery-attempt.js";
import { isExplicitContinuationRetryClaim } from "../explicit-continuation-retry-claim.js";
import {
decideLegacyContinuation, legacyDispositionEpisode, legacyDispositionFingerprint,
LEGACY_DISPOSITION_REPAIR_INSTRUCTION, type LegacyDispositionEpisode,
@@ -912,6 +913,8 @@ export function recoveryService(
scheduleRecoveryRetry?: (
runId: string,
) => Promise<typeof heartbeatRuns.$inferSelect | null>;
/** Settle retained explicit retry claims through the queue-first release policy. */
settleExplicitContinuationRetry?: (run: typeof heartbeatRuns.$inferSelect) => Promise<void>;
/**
* Whether a failed or interrupted run has consumed every bounded
* transient retry, so `scheduleRecoveryRetry` can no longer produce a
@@ -6098,6 +6101,7 @@ export function recoveryService(
: [];
const runStatusById = new Map<string, string>();
for (const row of runRows) runStatusById.set(row.id, row.status);
const runById = new Map(runRows.map(row => [row.id, row]));
// Collect the runs that a non-terminal issue still references. Such a run is
// the live run of an active issue. A different, terminal issue can also hold
@@ -6159,6 +6163,18 @@ export function recoveryService(
};
for (const issue of candidates) {
const originalOwner = issue.executionRunId ? runById.get(issue.executionRunId) : undefined;
// The pre-pass can lose its terminal write to the executor and observe a
// newer terminal status. Do not test claim ownership using the old status.
const owner = originalOwner ? { ...originalOwner,
status: runStatusById.get(originalOwner.id) ?? originalOwner.status } : undefined;
if (owner && isExplicitContinuationRetryClaim(issue, owner)) {
// A terminal row can still own cleanup and a pending bounded retry.
// Re-enter the same policy rather than erasing its exact-owner proof.
// That policy handles pending cleanup, newer input, and final denial.
await deps.settleExplicitContinuationRetry?.(owner);
continue;
}
if (
!isCleanable(issue.checkoutRunId) ||
!isCleanable(issue.executionRunId)