diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 3e24155fb87..8538a710687 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -2477,6 +2477,9 @@ function renderPaperclipWakePromptBody( : []; const wakeSummaryLines = [ `- reason: ${normalized.reason ?? "unknown"}`, + ...(normalized.executionContinuation?.trigger?.retriedReason + ? [`- retried wake: ${normalized.executionContinuation.trigger.retriedReason} (this Retry re-delivers the wake the failed run did not finish; handle it as that wake)`] + : []), `- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`, ...(hasWakeCommentBatch ? [ diff --git a/packages/shared/src/types/execution-continuation.ts b/packages/shared/src/types/execution-continuation.ts index 545998e33ab..5afe62afd22 100644 --- a/packages/shared/src/types/execution-continuation.ts +++ b/packages/shared/src/types/execution-continuation.ts @@ -7,6 +7,7 @@ export interface ExecutionContinuationEnvelope { reason: string; interactionId: string | null; sourceRunId: string | null; + retriedReason?: string; }; originCommentIds: string[]; objective: string; diff --git a/server/src/services/execution-continuation.test.ts b/server/src/services/execution-continuation.test.ts index 1c81337f4ce..24e108e18ac 100644 --- a/server/src/services/execution-continuation.test.ts +++ b/server/src/services/execution-continuation.test.ts @@ -366,6 +366,36 @@ const support = externalUrl ? { supported: true } : await getEmbeddedPostgresTes } }); + it("carries the failed run's wake reason into a Retry so a monitor check is not lost", async () => { + const [source] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)); + const retryRunId = randomUUID(); + await db.update(heartbeatRuns).set({ status: "failed", errorCode: "native_session_interrupted", + contextSnapshot: { issueId, wakeReason: "issue_monitor_due", monitorNotes: "Phase 2 check" } }) + .where(eq(heartbeatRuns.id, runId)); + try { + const envelope = await buildExecutionContinuation({ db, companyId, issueId, agentId, + context: { retryOfRunId: runId, wakeReason: "task_retry" }, summary: null, exposeLowTrustRaw: false }); + expect(envelope.trigger).toMatchObject({ reason: "task_retry", sourceRunId: runId, retriedReason: "issue_monitor_due" }); + await db.insert(heartbeatRuns).values({ id: retryRunId, companyId, agentId, status: "failed", + contextSnapshot: { issueId, wakeReason: "task_retry", executionContinuation: envelope } }); + const chained = await buildExecutionContinuation({ db, companyId, issueId, agentId, + context: { retryOfRunId: retryRunId, wakeReason: "task_retry" }, summary: null, exposeLowTrustRaw: false }); + expect(chained.trigger).toMatchObject({ sourceRunId: retryRunId, retriedReason: "issue_monitor_due" }); + for (const resumedSession of [false, true]) { + const prompt = renderPaperclipWakePrompt({ reason: "task_retry", executionContinuation: envelope }, { resumedSession }); + expect(prompt).toContain("- reason: task_retry"); + expect(prompt).toContain("- retried wake: issue_monitor_due"); + } + const fresh = await buildExecutionContinuation({ db, companyId, issueId, agentId, + context: { wakeReason: "issue_assigned" }, summary: null, exposeLowTrustRaw: false }); + expect(fresh.trigger).not.toHaveProperty("retriedReason"); + } finally { + await db.delete(heartbeatRuns).where(eq(heartbeatRuns.id, retryRunId)); + await db.update(heartbeatRuns).set({ status: "failed", errorCode: null, contextSnapshot: source.contextSnapshot }) + .where(eq(heartbeatRuns.id, runId)); + } + }); + it("keeps Local CLI run-authored comments as history without promoting them to human direction", async () => { const id = randomUUID(); await db.insert(issueComments).values({ id, companyId, issueId, authorType: "user", diff --git a/server/src/services/execution-continuation.ts b/server/src/services/execution-continuation.ts index 29a44f2e316..5e2dc7aa6c8 100644 --- a/server/src/services/execution-continuation.ts +++ b/server/src/services/execution-continuation.ts @@ -20,6 +20,8 @@ import { isCompletedOnboardingHandoffWake } from "./chat-completion-delivery.js" import { githubRecoveryReceipts } from "./chat-channels/github/recovery-receipts.js"; import { readTaskRetryIntent } from "./task-retry.js"; +const retryWakeReasons = new Set(["task_retry", "retry_failed_run"]); + const savedRecordOperations = new Set([ "PUT /api/issues/{id}/documents/{key}", "POST /api/issues/{id}/comments", @@ -534,6 +536,12 @@ export async function buildExecutionContinuation(input: { (hasConversationContinuationPolicy(lastTerminal.result) || lastTerminal.status === "interrupted" || lastTerminal.errorCode === "process_lost") ? lastTerminal.id : undefined); + const triggerReason = string(input.context.wakeReason) ?? "task_execution"; + const sourceContext = object(sourceRun?.context); + const retriedCandidate = retryWakeReasons.has(triggerReason) && sourceRun + ? string(object(object(sourceContext.executionContinuation).trigger).retriedReason) ?? string(sourceContext.wakeReason) + : null; + const retriedReason = retriedCandidate && !retryWakeReasons.has(retriedCandidate) ? retriedCandidate : null; return { ...(interruptedRunId ? { interruptedRunId } : {}), ...(resumeDelta ? { resumeDelta } : {}), @@ -547,9 +555,10 @@ export async function buildExecutionContinuation(input: { companyId, issueId, trigger: { - reason: string(input.context.wakeReason) ?? "task_execution", + reason: triggerReason, interactionId: triggerInteraction?.id ?? null, sourceRunId, + ...(retriedReason ? { retriedReason } : {}), }, originCommentIds, objective: latestRequest?.body ?? issue.description ?? issue.title, diff --git a/tests/runner-e2e/source-sdk-public-inputs.json b/tests/runner-e2e/source-sdk-public-inputs.json index 28b9b17be8c..b438afbecc3 100644 --- a/tests/runner-e2e/source-sdk-public-inputs.json +++ b/tests/runner-e2e/source-sdk-public-inputs.json @@ -387,7 +387,7 @@ "packages/shared/src/types/document-annotation.ts": "7769a42f1ccd707a2259774e7fe6aa536b5d183f", "packages/shared/src/types/email.ts": "556ff6a0dcefa01eb704d6d00d3b191de81ff4dc", "packages/shared/src/types/environment.ts": "a380b9ce9d2d1763011e9adaa58615a3ef1e56c5", - "packages/shared/src/types/execution-continuation.ts": "545998e33ab18d38268558d1a367f4aa12c2d5d3", + "packages/shared/src/types/execution-continuation.ts": "5afe62afd227efa03ff847d8f73291c14707ab5b", "packages/shared/src/types/execution-projection.ts": "e72e641602b296f6d3e16d07700629df3039a7b1", "packages/shared/src/types/external-object.ts": "7a225e0f5a6777d449019a30530c858b753eb4c1", "packages/shared/src/types/feedback.ts": "c5eb31545c7f67c04ad9afd9db45a6b4774bcb35",