fix(recovery): skip successful-run handoff for routine executions
Routine execution issues (originKind=routine_execution) stay in_progress between cron fires by design — one open issue is coalesced across dispatches via issues_open_routine_execution_uq. decideSuccessfulRunHandoff treated that steady state as a missing disposition, enqueued a corrective handoff that exhausted after one attempt, and left the issue blocked with a source_scoped_recovery_action — re-waking the agent on every successful run (observed at ~27 runs/hour on one orphan-check routine, which additionally tripped a false productivity review). Exempt routine_execution issues from the handoff, mirroring the existing issue-monitor maintenance skip; the routine lifecycle owns their disposition. Adds a regression test that fails without the guard.
This commit is contained in:
parent
2494a2a0fe
commit
ac3dbba406
|
|
@ -9235,6 +9235,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
|||
assigneeUserId: issues.assigneeUserId,
|
||||
executionState: issues.executionState,
|
||||
monitorNextCheckAt: issues.monitorNextCheckAt,
|
||||
originKind: issues.originKind,
|
||||
projectId: issues.projectId,
|
||||
})
|
||||
.from(issues)
|
||||
|
|
|
|||
|
|
@ -346,6 +346,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: {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,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<RunLivenessState>([
|
||||
"advanced",
|
||||
"completed",
|
||||
|
|
@ -61,6 +67,7 @@ type IssueRow = Pick<
|
|||
| "assigneeAgentId"
|
||||
| "assigneeUserId"
|
||||
| "executionState"
|
||||
| "originKind"
|
||||
>;
|
||||
type AgentRow = Pick<typeof agents.$inferSelect, "id" | "companyId" | "status">;
|
||||
type NoticeIssue = Pick<typeof issues.$inferSelect, "id" | "identifier" | "title" | "status">;
|
||||
|
|
@ -429,6 +436,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" };
|
||||
|
|
|
|||
Loading…
Reference in New Issue