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

+28 -11
View File
@@ -9,6 +9,7 @@ import { executionProjectionsForRuns } from "./execution-projection.js";
import type { ExecutionProjection } from "@paperclipai/shared";
import { Buffer } from "node:buffer";
import { createHash, randomUUID } from "node:crypto";
import { isExplicitContinuationRetryClaim } from "./explicit-continuation-retry-claim.js";
import { markdownToPlainText, parseMarkdown } from "chat";
import {
and,
@@ -7497,6 +7498,7 @@ export function issueService(db: Db) {
const lockedIssue = await tx
.select({
id: issues.id,
companyId: issues.companyId,
status: issues.status,
assigneeAgentId: issues.assigneeAgentId,
checkoutRunId: issues.checkoutRunId,
@@ -7528,7 +7530,7 @@ export function issueService(db: Db) {
]);
const [existingRun, actorRun] = await Promise.all([
tx
.select({ status: heartbeatRuns.status })
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, input.expectedCheckoutRunId))
.then((rows) => rows[0] ?? null),
@@ -7540,6 +7542,17 @@ export function issueService(db: Db) {
]);
const stale =
!existingRun || TERMINAL_HEARTBEAT_RUN_STATUSES.has(existingRun.status);
if (isExplicitContinuationRetryClaim(lockedIssue, existingRun)) {
return { adopted: null, latest: lockedIssue };
}
if (lockedIssue.executionRunId && lockedIssue.executionRunId !== input.expectedCheckoutRunId) {
const executionRun = await tx.select().from(heartbeatRuns)
.where(eq(heartbeatRuns.id, lockedIssue.executionRunId)).for("update")
.then(rows => rows[0] ?? null);
if (isExplicitContinuationRetryClaim(lockedIssue, executionRun)) {
return { adopted: null, latest: lockedIssue };
}
}
const actorLive =
actorRun && !TERMINAL_HEARTBEAT_RUN_STATUSES.has(actorRun.status);
if (!stale || !actorLive) {
@@ -7649,7 +7662,7 @@ export function issueService(db: Db) {
sql`select ${issues.id} from ${issues} where ${issues.id} = ${issueId} for update`,
);
const issue = await tx
.select({ executionRunId: issues.executionRunId })
.select({ id: issues.id, companyId: issues.companyId, executionRunId: issues.executionRunId })
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
@@ -7659,11 +7672,12 @@ export function issueService(db: Db) {
sql`select ${heartbeatRuns.id} from ${heartbeatRuns} where ${heartbeatRuns.id} = ${issue.executionRunId} for update`,
);
const run = await tx
.select({ status: heartbeatRuns.status })
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, issue.executionRunId))
.then((rows) => rows[0] ?? null);
if (run && !TERMINAL_HEARTBEAT_RUN_STATUSES.has(run.status)) return false;
if (isExplicitContinuationRetryClaim(issue, run)) return false;
const updated = await tx
.update(issues)
@@ -7689,8 +7703,7 @@ export function issueService(db: Db) {
// Symmetric to clearExecutionRunIfTerminal. Clears checkoutRunId (and the
// bundled execution lock cols) when the row's checkoutRunId points at a
// heartbeat run that is terminal or no longer exists. No assignee/status
// precondition: a terminal run holds no real claim regardless of who is
// assigned or what status the issue is currently in.
// precondition. Explicit retry claims remain owned by queue-first settlement.
async function clearCheckoutRunIfTerminal(issueId: string): Promise<boolean> {
return db.transaction(async (tx) => {
await tx.execute(
@@ -7698,6 +7711,8 @@ export function issueService(db: Db) {
);
const issue = await tx
.select({
id: issues.id,
companyId: issues.companyId,
checkoutRunId: issues.checkoutRunId,
executionRunId: issues.executionRunId,
})
@@ -7710,11 +7725,12 @@ export function issueService(db: Db) {
sql`select ${heartbeatRuns.id} from ${heartbeatRuns} where ${heartbeatRuns.id} = ${issue.checkoutRunId} for update`,
);
const run = await tx
.select({ status: heartbeatRuns.status })
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, issue.checkoutRunId))
.then((rows) => rows[0] ?? null);
if (run && !TERMINAL_HEARTBEAT_RUN_STATUSES.has(run.status)) return false;
if (isExplicitContinuationRetryClaim(issue, run)) return false;
if (
issue.executionRunId &&
@@ -7724,7 +7740,7 @@ export function issueService(db: Db) {
sql`select ${heartbeatRuns.id} from ${heartbeatRuns} where ${heartbeatRuns.id} = ${issue.executionRunId} for update`,
);
const executionRun = await tx
.select({ status: heartbeatRuns.status })
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, issue.executionRunId))
.then((rows) => rows[0] ?? null);
@@ -7733,6 +7749,7 @@ export function issueService(db: Db) {
!TERMINAL_HEARTBEAT_RUN_STATUSES.has(executionRun.status)
)
return false;
if (isExplicitContinuationRetryClaim(issue, executionRun)) return false;
}
const updated = await tx
@@ -11600,10 +11617,10 @@ export function issueService(db: Db) {
current.executionRunId !== checkoutRunId &&
(current.assigneeAgentId === agentId || current.assigneeAgentId == null)
) {
const stale = await isTerminalOrMissingHeartbeatRun(
current.executionRunId,
);
if (stale) {
const executionRun = await db.select().from(heartbeatRuns)
.where(eq(heartbeatRuns.id, current.executionRunId)).then(rows => rows[0] ?? null);
const stale = !executionRun || TERMINAL_HEARTBEAT_RUN_STATUSES.has(executionRun.status);
if (stale && !isExplicitContinuationRetryClaim({ ...current, companyId: issueCompany.companyId }, executionRun)) {
const now = new Date();
const adoptionSet: Record<string, unknown> = {
assigneeAgentId: agentId,