From 869d2073f61d2f764bdcd243511469a90e0ef6bb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 03:19:04 -0700 Subject: [PATCH] fix(onboard): scope hatch timeout to initial turn (#130010) Amp-Thread-ID: https://ampcode.com/threads/T-01a021f6-157a-707a-aab4-df4ff9e0ae98 Co-authored-by: Amp --- src/tui/tui-command-handlers.test.ts | 21 +++++++++++++++++++-- src/tui/tui-command-handlers.ts | 4 ++-- src/tui/tui-types.ts | 2 ++ src/tui/tui.ts | 2 +- src/wizard/setup.finalize.test.ts | 13 ++++++++----- src/wizard/setup.finalize.ts | 2 +- 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/tui/tui-command-handlers.test.ts b/src/tui/tui-command-handlers.test.ts index fc595123c49c..af59a6a0ae9c 100644 --- a/src/tui/tui-command-handlers.test.ts +++ b/src/tui/tui-command-handlers.test.ts @@ -24,7 +24,7 @@ import { type TuiPendingSubmit, } from "./tui-submit-state.js"; import { createEditorSubmitHandler, createSubmitBurstCoalescer } from "./tui-submit.js"; -import type { SessionInfo } from "./tui-types.js"; +import type { SessionInfo, TuiOptions } from "./tui-types.js"; type LoadHistoryMock = ReturnType & (() => Promise); type RunAuthFlow = NonNullable[0]["runAuthFlow"]>; @@ -115,7 +115,7 @@ function createHarness(params?: { activeChatRunId?: string | null; pendingSubmit?: TuiPendingSubmit | null; activityStatus?: string; - opts?: { local?: boolean }; + opts?: Pick; currentSessionId?: string | null; sessionGeneration?: number; currentAgentId?: string; @@ -582,6 +582,23 @@ describe("tui command handlers", () => { expect(requestRender).toHaveBeenCalled(); }); + it("scopes an explicit timeout override to one message", async () => { + const { handleCommand, sendMessage, sendChat, state } = createHarness(); + + await sendMessage("automatic hatch", 300_000); + state.pendingSubmit = null; + await handleCommand("later interactive turn"); + + expect(sendChat).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ message: "automatic hatch", timeoutMs: 300_000 }), + ); + expect(sendChat).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ message: "later interactive turn", timeoutMs: undefined }), + ); + }); + it("projects the canonical pending user before chat.send is acknowledged", async () => { const deferred = createDeferred<{ runId: string }>(); const sendChat = vi.fn(() => deferred.promise); diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index 14914ef3be8f..dc2ff15404ba 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -886,7 +886,7 @@ export function createCommandHandlers(context: CommandHandlerContext) { tui.requestRender(); }; - const sendMessage = async (text: string) => { + const sendMessage = async (text: string, timeoutMs = opts.timeoutMs) => { const admission = resolveMessageAdmission(text); if (admission.status === "blocked") { reportBlockedMessageSubmit(text, admission); @@ -942,7 +942,7 @@ export function createCommandHandlers(context: CommandHandlerContext) { message: text, thinking: opts.thinking, deliver: deliverDefault, - timeoutMs: opts.timeoutMs, + timeoutMs, runId, }); const acceptedRunId = sendResult.runId || runId; diff --git a/src/tui/tui-types.ts b/src/tui/tui-types.ts index f9ac6a081aa8..0a058713c3ef 100644 --- a/src/tui/tui-types.ts +++ b/src/tui/tui-types.ts @@ -17,6 +17,8 @@ export type TuiOptions = { timeoutMs?: number; historyLimit?: number; message?: string; + /** Overrides timeoutMs only for the message sent automatically at startup. */ + initialMessageTimeoutMs?: number; /** * Internal CLI guard: after the standalone TUI returns, force the child * process out if imported runtime handles keep the event loop alive. diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 5ca9cdd5488a..4b4fab6917ae 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -1858,7 +1858,7 @@ async function runTuiUnlocked(opts: RunTuiOptions): Promise { scheduleDynamicSlashCommandsRefresh(); if (!state.autoMessageSent && autoMessage) { state.autoMessageSent = true; - await sendMessage(autoMessage); + await sendMessage(autoMessage, opts.initialMessageTimeoutMs); if (!ownsConnection()) { return; } diff --git a/src/wizard/setup.finalize.test.ts b/src/wizard/setup.finalize.test.ts index 4c11a7d5e0e9..cd5f863f5786 100644 --- a/src/wizard/setup.finalize.test.ts +++ b/src/wizard/setup.finalize.test.ts @@ -604,7 +604,7 @@ describe("finalizeSetupWizard", () => { local: true, deliver: false, message: undefined, - timeoutMs: 300_000, + initialMessageTimeoutMs: 300_000, }); }); @@ -669,6 +669,8 @@ describe("finalizeSetupWizard", () => { const tuiOptions = runTui.mock.calls.at(-1)?.[0] as Record; expect(tuiOptions).not.toHaveProperty("url"); expect(tuiOptions).not.toHaveProperty("token"); + expect(tuiOptions).toMatchObject({ initialMessageTimeoutMs: 300_000 }); + expect(tuiOptions).not.toHaveProperty("timeoutMs"); }); it.each([ @@ -867,7 +869,7 @@ describe("finalizeSetupWizard", () => { local: true, deliver: false, message: "Wake up, my friend!", - timeoutMs: 300_000, + initialMessageTimeoutMs: 300_000, }); }); @@ -1026,7 +1028,7 @@ describe("finalizeSetupWizard", () => { local: true, deliver: false, message: undefined, - timeoutMs: 300_000, + initialMessageTimeoutMs: 300_000, }); }); @@ -1073,7 +1075,7 @@ describe("finalizeSetupWizard", () => { local: true, deliver: false, message: "醒醒,我的朋友!", - timeoutMs: 300_000, + initialMessageTimeoutMs: 300_000, }); } finally { if (previousLocale === undefined) { @@ -2013,9 +2015,10 @@ describe("finalizeSetupWizard", () => { }, deliver: false, message: undefined, - timeoutMs: 300_000, + initialMessageTimeoutMs: 300_000, }), ); + expect(runTui.mock.calls.at(-1)?.[0]).not.toHaveProperty("timeoutMs"); expect(sessionGateway.close).toHaveBeenCalledWith({ reason: "onboarding tui exited" }); expect(cancelProcessExitAfterTuiReturn).toHaveBeenCalledWith(setupCleanupExitTimer); expect(scheduleProcessExitAfterTuiReturn).toHaveBeenCalledTimes(2); diff --git a/src/wizard/setup.finalize.ts b/src/wizard/setup.finalize.ts index 6757fd2ce188..055416c48145 100644 --- a/src/wizard/setup.finalize.ts +++ b/src/wizard/setup.finalize.ts @@ -1038,7 +1038,7 @@ export async function finalizeSetupWizard( message: shouldSeedBootstrapHatch ? t("wizard.finalize.bootstrapHatchMessage") : undefined, - timeoutMs: HATCH_TUI_TIMEOUT_MS, + initialMessageTimeoutMs: HATCH_TUI_TIMEOUT_MS, }); } finally { restoreTerminalState("post-setup tui", { resumeStdinIfPaused: false });