Merge pull request #3819 from jeremymcs/fix/auto-session-timeout-3767

fix(auto): increase session timeout to 120s and treat timeout as recoverable pause
This commit is contained in:
Jeremy McSpadden 2026-04-08 20:50:40 -05:00 committed by GitHub
commit 3e32c02851
5 changed files with 20 additions and 7 deletions

View file

@ -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,
);

View file

@ -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 });

View file

@ -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 ─────────────────────────────────────────────────────────────

View file

@ -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", () => {

View file

@ -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 };