fix(headless): disable overall timeout for auto-mode, fix lock-guard auto-select (#2586)
Auto-mode sessions are long-running (minutes to hours) with their own internal per-unit timeout via auto-supervisor. The 300s overall timeout was killing active sessions mid-execution, triggering wasteful restart cycles. Changes: - Disable overall timeout for auto-mode when using the default 300s (user can still set --timeout explicitly, including --timeout 0) - Guard timeout timer creation for null when timeout is 0 - Cancel overall timeout when new-milestone --auto chains into auto-mode - Fix headless auto-responder to pick "Force start" for lock-guard prompts instead of "View status" (which silently blocked auto-mode) - Allow --timeout 0 to explicitly disable timeout for any command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5f8bbbc6e1
commit
fde0be6979
2 changed files with 36 additions and 15 deletions
|
|
@ -40,9 +40,18 @@ export function handleExtensionUIRequest(
|
|||
let response: Record<string, unknown>
|
||||
|
||||
switch (method) {
|
||||
case 'select':
|
||||
response = { type: 'extension_ui_response', id, value: event.options?.[0] ?? '' }
|
||||
case 'select': {
|
||||
// Lock-guard prompts list "View status" first, but headless needs "Force start"
|
||||
// to proceed. Detect by title and pick the force option.
|
||||
const title = String(event.title ?? '')
|
||||
let selected = event.options?.[0] ?? ''
|
||||
if (title.includes('Auto-mode is running') && event.options) {
|
||||
const forceOption = event.options.find(o => o.toLowerCase().includes('force start'))
|
||||
if (forceOption) selected = forceOption
|
||||
}
|
||||
response = { type: 'extension_ui_response', id, value: selected }
|
||||
break
|
||||
}
|
||||
case 'confirm':
|
||||
response = { type: 'extension_ui_response', id, confirmed: true }
|
||||
break
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
if (!positionalStarted && arg.startsWith('--')) {
|
||||
if (arg === '--timeout' && i + 1 < args.length) {
|
||||
options.timeout = parseInt(args[++i], 10)
|
||||
if (Number.isNaN(options.timeout) || options.timeout <= 0) {
|
||||
process.stderr.write('[headless] Error: --timeout must be a positive integer (milliseconds)\n')
|
||||
if (Number.isNaN(options.timeout) || options.timeout < 0) {
|
||||
process.stderr.write('[headless] Error: --timeout must be a non-negative integer (milliseconds, 0 to disable)\n')
|
||||
process.exit(1)
|
||||
}
|
||||
} else if (arg === '--json') {
|
||||
|
|
@ -183,6 +183,14 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
|
|||
options.timeout = 600_000 // 10 minutes
|
||||
}
|
||||
|
||||
// auto-mode sessions are long-running (minutes to hours) with their own internal
|
||||
// per-unit timeout via auto-supervisor. Disable the overall timeout unless the
|
||||
// user explicitly set --timeout.
|
||||
const isAutoMode = options.command === 'auto'
|
||||
if (isAutoMode && options.timeout === 300_000) {
|
||||
options.timeout = 0
|
||||
}
|
||||
|
||||
// Supervised mode cannot share stdin with --context -
|
||||
if (options.supervised && options.context === '-') {
|
||||
process.stderr.write('[headless] Error: --supervised cannot be used with --context - (both require stdin)\n')
|
||||
|
|
@ -337,12 +345,14 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
|
|||
// Precompute supervised response timeout
|
||||
const responseTimeout = options.responseTimeout ?? 30_000
|
||||
|
||||
// Overall timeout
|
||||
const timeoutTimer = setTimeout(() => {
|
||||
process.stderr.write(`[headless] Timeout after ${options.timeout / 1000}s\n`)
|
||||
exitCode = 1
|
||||
resolveCompletion()
|
||||
}, options.timeout)
|
||||
// Overall timeout (disabled when options.timeout === 0, e.g. auto-mode)
|
||||
const timeoutTimer = options.timeout > 0
|
||||
? setTimeout(() => {
|
||||
process.stderr.write(`[headless] Timeout after ${options.timeout / 1000}s\n`)
|
||||
exitCode = 1
|
||||
resolveCompletion()
|
||||
}, options.timeout)
|
||||
: null
|
||||
|
||||
// Event handler
|
||||
client.onEvent((event) => {
|
||||
|
|
@ -434,7 +444,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
|
|||
interrupted = true
|
||||
exitCode = 1
|
||||
client.stop().finally(() => {
|
||||
clearTimeout(timeoutTimer)
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
if (idleTimer) clearTimeout(idleTimer)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
|
@ -447,7 +457,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
|
|||
await client.start()
|
||||
} catch (err) {
|
||||
process.stderr.write(`[headless] Error: Failed to start RPC session: ${err instanceof Error ? err.message : String(err)}\n`)
|
||||
clearTimeout(timeoutTimer)
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -456,7 +466,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
|
|||
if (!internalProcess?.stdin) {
|
||||
process.stderr.write('[headless] Error: Cannot access child process stdin\n')
|
||||
await client.stop()
|
||||
clearTimeout(timeoutTimer)
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -511,7 +521,9 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
|
|||
process.stderr.write('[headless] Milestone ready — chaining into auto-mode...\n')
|
||||
}
|
||||
|
||||
// Reset completion state for the auto-mode phase
|
||||
// Reset completion state for the auto-mode phase.
|
||||
// Disable the overall timeout — auto-mode has its own internal supervisor.
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
completed = false
|
||||
milestoneReady = false
|
||||
blocked = false
|
||||
|
|
@ -532,7 +544,7 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number):
|
|||
}
|
||||
|
||||
// Cleanup
|
||||
clearTimeout(timeoutTimer)
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
if (idleTimer) clearTimeout(idleTimer)
|
||||
pendingResponseTimers.forEach((timer) => clearTimeout(timer))
|
||||
pendingResponseTimers.clear()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue