From 9b44e0db22753dcf993483401bb0c6bbdae1da3e Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 20:37:39 -0500 Subject: [PATCH] diagnostics: record native completion cancellation source --- .../services/heartbeat/run-completion.test.ts | 41 ++++++++++++++++++- .../src/services/heartbeat/run-completion.ts | 31 ++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/server/src/services/heartbeat/run-completion.test.ts b/server/src/services/heartbeat/run-completion.test.ts index 4388cd4e4e..8ccfffbde4 100644 --- a/server/src/services/heartbeat/run-completion.test.ts +++ b/server/src/services/heartbeat/run-completion.test.ts @@ -10,7 +10,7 @@ import { createHeartbeatLifecycle, type HeartbeatLifecycleDependencies } from ". import { createHeartbeatRunPreparation, ConfigurationIncompleteFailure } from "./run-preparation.js"; import { createHeartbeatRunState, getAdapterSessionCodec } from "./run-state.js"; import { buildEffectiveRunSessionConfigMetadata } from "./workspaces.js"; -import { createHeartbeatRunCompletion, type CompleteHeartbeatRunInput, type FailHeartbeatRunInput, type HeartbeatRunCompletionDependencies } from "./run-completion.js"; +import { createHeartbeatRunCompletion, nativeCompletionCancellationDiagnostic, type CompleteHeartbeatRunInput, type FailHeartbeatRunInput, type HeartbeatRunCompletionDependencies } from "./run-completion.js"; vi.mock("../live-events.js", () => ({ publishLiveEvent: vi.fn() })); @@ -72,6 +72,45 @@ it("constructs completion handlers without querying or starting work", () => { for (const callback of Object.values(deps)) if (vi.isMockFunction(callback)) expect(callback).not.toHaveBeenCalled(); }); +describe("native completion cancellation diagnostics", () => { + it.each([ + ["Legacy controller lease lost", "legacy_controller_lease_lost"], + ["Run stopped before provider startup", "stopped_before_provider_startup"], + ["private-token-do-not-log", "other"], + ])("classifies an aborted controller without exposing its message: %s", (message, abortReason) => { + const controller = new AbortController(); + controller.abort(new Error(message)); + const diagnostic = nativeCompletionCancellationDiagnostic({ outcome: "cancelled", nativeTerminal: "succeeded", + persistedRunStatus: "running", signal: controller.signal }); + expect(diagnostic).toEqual({ nativeTerminal: "succeeded", outcomeSource: "execution_signal", + persistedRunStatus: "running", signalAborted: true, abortReason }); + expect(JSON.stringify(diagnostic)).not.toContain(message); + }); + + it("reports a persisted cancellation ahead of an aborted controller", () => { + const controller = new AbortController(); + controller.abort(new Error("Legacy controller lease lost")); + expect(nativeCompletionCancellationDiagnostic({ outcome: "cancelled", nativeTerminal: "succeeded", + persistedRunStatus: "cancelled", signal: controller.signal })).toMatchObject({ + outcomeSource: "persisted_terminal", persistedRunStatus: "cancelled", abortReason: "legacy_controller_lease_lost", + }); + }); + + it("reports persisted cancellation without inferring a signal cause", () => { + expect(nativeCompletionCancellationDiagnostic({ outcome: "cancelled", nativeTerminal: "succeeded", + persistedRunStatus: "cancelled", signal: new AbortController().signal })).toMatchObject({ + outcomeSource: "persisted_terminal", signalAborted: false, abortReason: "not_aborted", + }); + }); + + it("omits diagnostics for successful completion and provider cancellation", () => { + const signal = new AbortController().signal; + expect(nativeCompletionCancellationDiagnostic({ outcome: "succeeded", nativeTerminal: "succeeded", persistedRunStatus: "running", signal })).toBeNull(); + expect(nativeCompletionCancellationDiagnostic({ outcome: "cancelled", nativeTerminal: "cancelled", persistedRunStatus: "running", signal })).toBeNull(); + expect(nativeCompletionCancellationDiagnostic({ outcome: "cancelled", nativeTerminal: undefined, persistedRunStatus: "cancelled", signal })).toBeNull(); + }); +}); + const support = await getEmbeddedPostgresTestSupport(); describe.skipIf(!support.supported)("heartbeat run completion boundary", () => { let database: Awaited>; diff --git a/server/src/services/heartbeat/run-completion.ts b/server/src/services/heartbeat/run-completion.ts index abda3c7f21..723d48ec27 100644 --- a/server/src/services/heartbeat/run-completion.ts +++ b/server/src/services/heartbeat/run-completion.ts @@ -50,6 +50,30 @@ type RunState = ReturnType; type IssueContext = Awaited["getIssueExecutionContext"]>> | null; type TaskSession = Awaited> | null; +/** Closed diagnostics for a host cancellation overriding a completed native turn. */ +export function nativeCompletionCancellationDiagnostic(input: { + outcome: RunSessionOutcome; + nativeTerminal: string | undefined; + persistedRunStatus: string | null; + signal: AbortSignal; +}) { + if (input.outcome !== "cancelled" || input.nativeTerminal !== "succeeded") return null; + const persistedCancellation = input.persistedRunStatus === "cancelled"; + if (!persistedCancellation && !input.signal.aborted) return null; + const abortMessage = input.signal.reason instanceof Error ? input.signal.reason.message : null; + return { + nativeTerminal: "succeeded", + outcomeSource: persistedCancellation ? "persisted_terminal" : "execution_signal", + persistedRunStatus: input.persistedRunStatus === null ? "missing" + : ["queued", "scheduled_retry", "running", "succeeded", "failed", "cancelled", "timed_out", "interrupted"].includes(input.persistedRunStatus) + ? input.persistedRunStatus : "unknown", + signalAborted: input.signal.aborted, + abortReason: !input.signal.aborted ? "not_aborted" + : abortMessage === "Legacy controller lease lost" ? "legacy_controller_lease_lost" + : abortMessage === "Run stopped before provider startup" ? "stopped_before_provider_startup" : "other", + }; +} + /** Completion persists terminal outcomes; the executor retains dispatch and cleanup ownership. */ export interface HeartbeatRunCompletionDependencies extends Pick, @@ -393,6 +417,12 @@ export function createHeartbeatRunCompletion(db: Db, dependencies: HeartbeatRunC } else { outcome = "failed"; } + const nativeCancellationDiagnostic = nativeCompletionCancellationDiagnostic({ + outcome, + nativeTerminal: adapterResult.nativeFinalization?.terminal.runTerminalState, + persistedRunStatus: latestRun?.status ?? null, + signal, + }); const nextSessionState = resolveNextSessionState({ adapterType: agent.adapterType, @@ -666,6 +696,7 @@ export function createHeartbeatRunCompletion(db: Db, dependencies: HeartbeatRunC payload: { status, exitCode: adapterResult.exitCode, + ...(nativeCancellationDiagnostic ? { nativeOutcomeOverride: nativeCancellationDiagnostic } : {}), ...(readRunCancellation(finalizedRun.resultJson) ? { cancellation: readRunCancellation(finalizedRun.resultJson) } : {}), }, });