Honor operator cancellation before immediate task recovery

Reuse the periodic recovery policy so an operator-cancelled run does not immediately dispatch a replacement and resume its sandbox. Keep explicit deferred wake promotion intact.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-10 02:50:57 -05:00
1 parent 1caad1bf9e
commit 5acb4274cb
4 files changed
+29 -1

No files matched your search

@@ -6009,6 +6009,25 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
expect(runningProcesses.has(runId)).toBe(false);
});
it.each(["user", "board", "interrupt"])("does not immediately recover an operator-cancelled run (%s)", async (actor) => {
const { companyId, agentId, runId, issueId, wakeupRequestId } = await seedRunFixture({
agentStatus: "running", includeIssue: true,
});
const heartbeat = heartbeatService(db);
try {
const cancelled = await heartbeat.cancelRun(runId, "Operator stopped this run", actor === "interrupt"
? { errorCode: "operator_interrupted" }
: { resultJson: { cancelledByActorType: actor } });
expect(cancelled?.status).toBe("cancelled");
const wakeups = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId));
expect(wakeups.map((wake) => wake.id)).toEqual([wakeupRequestId]);
const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, agentId));
expect(runs.map((run) => run.id)).toEqual([runId]);
const [issue] = await db.select().from(issues).where(eq(issues.id, issueId));
expect(issue).toMatchObject({ status: "in_progress", executionRunId: null, checkoutRunId: null });
} finally { await heartbeat.drainActiveRunExecutions(); }
});
it("records manual cancellation stop metadata", async () => {
const { runId } = await seedRunFixture({
agentStatus: "running",
+6
View File
@@ -371,6 +371,7 @@ import {
import { withRecoveryContext } from "./recovery/status-only-context.js";
import {
ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS as RECOVERY_ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS,
isOperatorCancelledRun,
recoveryService,
} from "./recovery/service.js";
import { collectDispositionRepairSourceState } from "./recovery/disposition-repair.js";
@@ -23689,6 +23690,11 @@ export function heartbeatService(
};
}
// Match the periodic recovery policy: an operator's stop must not create
// a fresh continuation that immediately resumes the cancelled sandbox.
// Explicit deferred wakes above remain eligible for normal promotion.
if (isOperatorCancelledRun(run)) return { kind: "released" as const };
const findExistingExecutionPath = (agentId?: string | null) =>
tx
.select({ id: heartbeatRuns.id })
+1 -1
View File
@@ -608,7 +608,7 @@ function isStrandedIssueRecoveryIssue(issue: Pick<typeof issues.$inferSelect, "o
* stopped the agent, and re-waking it — or escalating "stranding" — would
* fight the human. Any newer run or wake supersedes the exemption.
*/
function isOperatorCancelledRun(latestRun: LatestIssueRun): boolean {
export function isOperatorCancelledRun(latestRun: LatestIssueRun): boolean {
if (!latestRun || latestRun.status !== "cancelled") return false;
if (latestRun.errorCode === "operator_interrupted") return true;
const result = parseObject(latestRun.resultJson);