From af99a031c5c0dd11a2e48bb1802a3c90042f7f2c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:36:06 -0700 Subject: [PATCH] fix(orchestration): re-press Enter when dsh's launchpad refuses a worker's first dispatch (#33499) dsh worker dispatches (and other dsh agent-prompt sends) now get #33316's bounded Enter retry: dsh paints its launchpad before its session opens and refuses the first Enter with "still starting", so a fresh worker's task sat unsent. The retry only re-presses while the launchpad still holds the same prompt and stops once a turn starts. Fixes #30778 Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> --- .../dsh-launch-prompt-resubmit.test.ts | 31 ++++++++++++++----- .../terminal/input/terminal-agent-prompt.ts | 3 +- src/shared/agent-prompt-injection.ts | 9 +++--- src/shared/tui-agent-config-types.ts | 2 +- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/main/runtime/dsh-launch-prompt-resubmit.test.ts b/src/main/runtime/dsh-launch-prompt-resubmit.test.ts index 4d5cc43bd1c..6768e4c6c10 100644 --- a/src/main/runtime/dsh-launch-prompt-resubmit.test.ts +++ b/src/main/runtime/dsh-launch-prompt-resubmit.test.ts @@ -3,6 +3,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { AGENT_PROMPT_BRACKETED_PASTE_END } from '../../shared/agent-prompt-injection' import { createAgentPromptSubmissionRuntime } from './agent-prompt-submission-runtime-test-fixture' +import { dispatchPreambleSendOptions } from './orchestration/preamble' vi.mock('../git/worktree', () => { const worktrees = [ @@ -30,7 +31,7 @@ const DSH_TURN_STARTED = '\x1b]0;⠂ 🐋 \x07\x1b]7501;state=working:app=dsh-tu describe('a dsh launch prompt whose first Enter the launchpad refused', () => { afterEach(() => vi.useRealTimers()) - async function launchDsh(screen: string, acceptingEnter: number) { + async function launchDsh(screen: string, acceptingEnter: number, dispatch = false) { vi.useFakeTimers() const { runtime, handle, writes } = await createAgentPromptSubmissionRuntime( (runtime, data) => { @@ -45,13 +46,19 @@ describe('a dsh launch prompt whose first Enter the launchpad refused', () => { ) // The captures were painted on a 120x40 grid. vi.spyOn(runtime, 'getTerminalSize').mockReturnValue({ cols: 120, rows: 40 }) - const submission = runtime.sendTerminalAgentPrompt(handle, 'say hi in one word', { - inputKind: 'launch', - composerReady: true, - acceptQueued: true, - requestId: 'dsh-launch', - observationTimeoutMs: 0 - }) + const submission = runtime.sendTerminalAgentPrompt( + handle, + 'say hi in one word', + dispatch + ? dispatchPreambleSendOptions('dsh-dispatch') + : { + inputKind: 'launch', + composerReady: true, + acceptQueued: true, + requestId: 'dsh-launch', + observationTimeoutMs: 0 + } + ) await vi.advanceTimersByTimeAsync(40_000) await expect(submission).resolves.toMatchObject({ accepted: true }) return { @@ -80,4 +87,12 @@ describe('a dsh launch prompt whose first Enter the launchpad refused', () => { pastes: 1 }) }) + + // #30778: a fresh worker's tui-idle settles on dsh 0.17's boot `OSC 7501 idle`, before its session opens. + it('presses Enter again for a worker dispatch the launchpad refused', async () => { + await expect(launchDsh(fixture('launchpad-draft-starting'), 3, true)).resolves.toEqual({ + enters: 3, + pastes: 1 + }) + }) }) diff --git a/src/main/runtime/terminal/input/terminal-agent-prompt.ts b/src/main/runtime/terminal/input/terminal-agent-prompt.ts index c5582ba66cc..329ab67ecd7 100644 --- a/src/main/runtime/terminal/input/terminal-agent-prompt.ts +++ b/src/main/runtime/terminal/input/terminal-agent-prompt.ts @@ -182,8 +182,7 @@ export abstract class OrcaRuntimeWithWriteTerminalAgentPrompt extends OrcaRuntim heldBudgetMs: getAgentPromptHeldSubmitBudgetMs( writeHostPlatform, this.getPtyAgent(ptyId), - pastePayload, - options.composerReady === true + pastePayload ) })) const effectTimeoutMs = resolveAgentPromptEffectTimeoutMs(this.getPtyAgent(ptyId)) diff --git a/src/shared/agent-prompt-injection.ts b/src/shared/agent-prompt-injection.ts index 6d7fa3659ae..b3a7a01886d 100644 --- a/src/shared/agent-prompt-injection.ts +++ b/src/shared/agent-prompt-injection.ts @@ -76,18 +76,17 @@ export function getMaxTerminalPasteBytesForIngestMs( // Codex 0.162.1 on a low-spec Win11 host took 5.1 s for 1,849 chars and 21.1 s for 9,249 (~2.3 ms/char). const WINDOWS_INPUT_RECORD_PASTE_MS_PER_CHAR = 3 -/** How long a held prompt may still need Enter: an input-record TUI's paste burst, or a launch composer not yet submit-ready. */ +/** How long a held prompt may still need Enter: an input-record TUI's paste burst, or a composer not yet submit-ready. */ export function getAgentPromptHeldSubmitBudgetMs( platform: NodeJS.Platform, agent: TerminalAgent | null | undefined, - pastePayload: string, - composerReady: boolean + pastePayload: string ): number { if (!isTuiAgent(agent)) { return 0 } const config = TUI_AGENT_CONFIG[agent] - const launchMs = composerReady ? (config.submitRetryBudgetMs ?? 0) : 0 + const startupMs = config.submitRetryBudgetMs ?? 0 const pasteBurstMs = platform === 'win32' && config.windowsInputRecordPasteNewline ? pastePayload.length * WINDOWS_INPUT_RECORD_PASTE_MS_PER_CHAR @@ -97,7 +96,7 @@ export function getAgentPromptHeldSubmitBudgetMs( const capMs = AGENT_PROMPT_EFFECT_TIMEOUT_MS - 2 * getTerminalPasteIngestMs(platform, Buffer.byteLength(pastePayload, 'utf8')) - return Math.max(0, Math.min(Math.max(launchMs, pasteBurstMs), capMs)) + return Math.max(0, Math.min(Math.max(startupMs, pasteBurstMs), capMs)) } // Codex dedupes a repeated placeholder label as `[Pasted Content N chars] #2`. diff --git a/src/shared/tui-agent-config-types.ts b/src/shared/tui-agent-config-types.ts index 3a138df3fab..5bddcc3126f 100644 --- a/src/shared/tui-agent-config-types.ts +++ b/src/shared/tui-agent-config-types.ts @@ -64,7 +64,7 @@ export type TuiAgentConfig = { composerReadyCaptures?: readonly string[] /** Delay before one extra blind submit Enter, for agents that render their composer before Enter is live (codex); a no-op if the first Enter landed. */ submitRetryDelayMs?: number - /** How long a launch keeps re-pressing Enter at `submitRetryDelayMs` while its composer holds the prompt, for TUIs that paint it before their session can submit (dsh). */ + /** How long an agent-prompt send keeps re-pressing Enter at `submitRetryDelayMs` while its composer holds the prompt, for TUIs that paint it before their session can submit (dsh). */ submitRetryBudgetMs?: number /** Extra ms per logical prompt line before Enter, for TUIs that expand multiline paste slowly (antigravity). */ submitLineSettleMsPerLine?: number