diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 3de2209b3c..83cb9f25ac 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -13107,6 +13107,7 @@ export function heartbeatService( assigneeUserId: issues.assigneeUserId, executionState: issues.executionState, monitorNextCheckAt: issues.monitorNextCheckAt, + originKind: issues.originKind, projectId: issues.projectId, originKind: issues.originKind, }) diff --git a/server/src/services/recovery/successful-run-handoff.test.ts b/server/src/services/recovery/successful-run-handoff.test.ts index 67baafc129..24cb4b8f70 100644 --- a/server/src/services/recovery/successful-run-handoff.test.ts +++ b/server/src/services/recovery/successful-run-handoff.test.ts @@ -420,6 +420,18 @@ describe("successful run handoff decision", () => { }); }); + it("does not queue for routine execution issues left in_progress between fires", () => { + // A successful orphan-check routine run left its coalesced routine_execution + // issue in `in_progress`, and the handoff wrongly treated that steady state + // as a missing disposition -> blocked + recovery loop. + expect(decide({ + issue: { ...issue, originKind: "routine_execution" } as any, + })).toEqual({ + kind: "skip", + reason: "routine execution owns its own disposition", + }); + }); + it("does not queue for successful comment-driven wakes", () => { expect(decide({ run: { diff --git a/server/src/services/recovery/successful-run-handoff.ts b/server/src/services/recovery/successful-run-handoff.ts index cf33b61cf5..5c199f73cf 100644 --- a/server/src/services/recovery/successful-run-handoff.ts +++ b/server/src/services/recovery/successful-run-handoff.ts @@ -35,6 +35,12 @@ export const SUCCESSFUL_RUN_HANDOFF_OPTIONS = [ "delegate_or_continue_from_checkpoint", ] as const; +// Routine execution issues stay `in_progress` between cron fires by design — one +// open issue is coalesced across dispatches (see issues_open_routine_execution_uq). +// Their disposition is owned by the routine lifecycle, not by the missing-disposition +// handoff, so a successful routine run must not be treated as a missing next step. +const ROUTINE_EXECUTION_ORIGIN_KIND = "routine_execution"; + const PRODUCTIVE_SUCCESS_LIVENESS_STATES = new Set([ "advanced", "completed", @@ -493,6 +499,9 @@ export function decideSuccessfulRunHandoff(input: { return { kind: "skip", reason: "missing issue comment retry owns the next action" }; } if (!issue) return { kind: "skip", reason: "issue not found" }; + if (issue.originKind === ROUTINE_EXECUTION_ORIGIN_KIND) { + return { kind: "skip", reason: "routine execution owns its own disposition" }; + } if (!agent) return { kind: "skip", reason: "agent not found" }; if (issue.companyId !== run.companyId || agent.companyId !== run.companyId) { return { kind: "skip", reason: "company scope mismatch" };