diff --git a/src/resources/extensions/ask-user-questions.ts b/src/resources/extensions/ask-user-questions.ts index ab7ff280a..3cb7e2ae1 100644 --- a/src/resources/extensions/ask-user-questions.ts +++ b/src/resources/extensions/ask-user-questions.ts @@ -254,7 +254,7 @@ export default function AskUserQuestions(pi: ExtensionAPI) { const raceResult = await raceRemoteAndLocal( () => tryRemoteQuestions(params.questions, raceSignal), - () => showInterviewRound(params.questions, {}, ctx as any), + () => showInterviewRound(params.questions, { signal: raceSignal }, ctx as any), raceController, params.questions, ); diff --git a/src/resources/extensions/gsd/auto/phases.ts b/src/resources/extensions/gsd/auto/phases.ts index f9ff192fe..d313053fe 100644 --- a/src/resources/extensions/gsd/auto/phases.ts +++ b/src/resources/extensions/gsd/auto/phases.ts @@ -1309,8 +1309,8 @@ export async function runUnitPhase( return { action: "break", reason: "provider-pause" }; } // Session creation timeout (not a structural error): pause auto-mode - // and let the provider-error-resume timer handle recovery. This matches - // the provider-pause path — break out cleanly, don't hard-stop. + // and let the provider-error-resume timer handle recovery (#3767). This + // matches the provider-pause path — break out cleanly, don't hard-stop. // Structural errors (TypeError, is not a function) are NOT transient // and must hard-stop to avoid infinite retry loops. if ( @@ -1318,7 +1318,7 @@ export async function runUnitPhase( unitResult.errorContext?.category === "timeout" ) { ctx.ui.notify( - `Session creation timed out for ${unitType} ${unitId}. Will retry.`, + `Session creation timed out for ${unitType} ${unitId}. Pausing auto-mode (recoverable).`, "warning", ); debugLog("autoLoop", { phase: "session-timeout-pause", unitType, unitId }); diff --git a/src/resources/extensions/gsd/auto/session.ts b/src/resources/extensions/gsd/auto/session.ts index b191d8591..5f822a51f 100644 --- a/src/resources/extensions/gsd/auto/session.ts +++ b/src/resources/extensions/gsd/auto/session.ts @@ -67,7 +67,7 @@ export interface SidecarItem { export const MAX_UNIT_DISPATCHES = 3; export const STUB_RECOVERY_THRESHOLD = 2; export const MAX_LIFETIME_DISPATCHES = 6; -export const NEW_SESSION_TIMEOUT_MS = 30_000; +export const NEW_SESSION_TIMEOUT_MS = 120_000; // ─── AutoSession ───────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts b/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts index 1c0eadf49..beba08221 100644 --- a/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +++ b/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts @@ -360,8 +360,8 @@ describe("session management", () => { assert.equal(s.unitRecoveryCount.size, 0, "recovery counts cleared"); }); - test("NEW_SESSION_TIMEOUT_MS is 30 seconds", () => { - assert.equal(NEW_SESSION_TIMEOUT_MS, 30_000, "session timeout should be 30s"); + test("NEW_SESSION_TIMEOUT_MS is 120 seconds", () => { + assert.equal(NEW_SESSION_TIMEOUT_MS, 120_000, "session timeout should be 120s"); }); test("MAX_UNIT_DISPATCHES limits retries for a single unit", () => { diff --git a/src/resources/extensions/shared/interview-ui.ts b/src/resources/extensions/shared/interview-ui.ts index c38833761..66771bc84 100644 --- a/src/resources/extensions/shared/interview-ui.ts +++ b/src/resources/extensions/shared/interview-ui.ts @@ -80,6 +80,12 @@ export interface InterviewRoundOptions { * Label for the Esc-confirm overlay header. Defaults to "End interview?". */ exitHeadline?: string; + /** + * Optional AbortSignal to cancel the interview externally (e.g. when racing + * against a remote question channel). When aborted, the TUI closes and the + * promise resolves with an empty answers object. + */ + signal?: AbortSignal; /** * Text for the "exit" hint shown in the review screen footer and exit confirm overlay. * Defaults to "end interview". @@ -207,6 +213,13 @@ export async function showInterviewRound( let exitCursor = 0; // 0 = keep going (default), 1 = end interview let cachedLines: string[] | undefined; + // External cancellation (e.g. remote channel won the race) + if (opts.signal) { + const onAbort = () => done({ endInterview: false, answers: {} }); + if (opts.signal.aborted) { onAbort(); } + else { opts.signal.addEventListener("abort", onAbort, { once: true }); } + } + // Editor is created once; editorTheme comes from the design system const editorRef = { current: null as Editor | null };