mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 01:57:31 +01:00
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:
1 parent
7498705642
commit
2e67ea8dc7
11 files changed
+639
-16
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user