From 3f06063b4bda98983dc5dd519510d5e2a5ebff87 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 23 Feb 2026 20:51:42 +0700 Subject: [PATCH] fix(cli): make agent wait unlimited by default --- packages/cli/src/cli.ts | 2 +- packages/cli/src/commands/agent/index.ts | 2 +- packages/cli/src/commands/agent/wait.ts | 19 +++--- packages/cli/tests/11-agent-wait.test.ts | 13 ++-- .../server/src/client/daemon-client.test.ts | 68 +++++++++++++++++++ packages/server/src/client/daemon-client.ts | 5 +- packages/server/src/server/session.ts | 14 ++-- 7 files changed, 98 insertions(+), 25 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index fede55476..3864401f0 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -140,7 +140,7 @@ export function createCli(): Command { .command('wait') .description('Wait for an agent to become idle') .argument('', 'Agent ID (or prefix)') - .option('--timeout ', 'Maximum wait time (default: 600)') + .option('--timeout ', 'Maximum wait time (default: no limit)') .option('--json', 'Output in JSON format') .option('--host ', 'Daemon host:port (default: localhost:6767)') .action(withOutput(runWaitCommand)) diff --git a/packages/cli/src/commands/agent/index.ts b/packages/cli/src/commands/agent/index.ts index 6eb47d968..9b0b7afad 100644 --- a/packages/cli/src/commands/agent/index.ts +++ b/packages/cli/src/commands/agent/index.ts @@ -99,7 +99,7 @@ export function createAgentCommand(): Command { .command('wait') .description('Wait for an agent to become idle') .argument('', 'Agent ID (or prefix)') - .option('--timeout ', 'Maximum wait time (default: 600)') + .option('--timeout ', 'Maximum wait time (default: no limit)') .option('--json', 'Output in JSON format') .option('--host ', 'Daemon host:port (default: localhost:6767)') .action(withOutput(runWaitCommand)) diff --git a/packages/cli/src/commands/agent/wait.ts b/packages/cli/src/commands/agent/wait.ts index dfd799a4b..73ec33095 100644 --- a/packages/cli/src/commands/agent/wait.ts +++ b/packages/cli/src/commands/agent/wait.ts @@ -108,14 +108,17 @@ export async function runWaitCommand( throw error } - // Parse timeout (default 10 minutes) - let timeoutMs: number + // Parse timeout (no limit unless explicitly provided) + let timeoutMs = 0 + let timeoutLabel: string | null = null if (options.timeout) { try { timeoutMs = parseDuration(options.timeout) if (timeoutMs <= 0) { throw new Error('Timeout must be positive') } + const timeoutSeconds = Math.floor(timeoutMs / 1000) + timeoutLabel = `${timeoutSeconds} second${timeoutSeconds === 1 ? '' : 's'}` } catch (err) { const message = err instanceof Error ? err.message : String(err) const error: CommandError = { @@ -125,11 +128,7 @@ export async function runWaitCommand( } throw error } - } else { - timeoutMs = 10 * 60 * 1000 // default 10 minutes } - const timeoutSeconds = Math.floor(timeoutMs / 1000) - const timeoutLabel = `${timeoutSeconds} second${timeoutSeconds === 1 ? '' : 's'}` let client try { @@ -156,15 +155,15 @@ export async function runWaitCommand( await client.close() if (state.status === 'timeout') { + const timeoutMessage = timeoutLabel + ? `Agent did not finish within ${timeoutLabel}. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.` + : `Agent wait timed out. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.` return { type: 'single', data: { agentId: resolvedAgentId, status: 'timeout', - message: appendRecentActivity( - `Agent did not finish within ${timeoutLabel}. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`, - recentActivity - ), + message: appendRecentActivity(timeoutMessage, recentActivity), }, schema: agentWaitSchema, } diff --git a/packages/cli/tests/11-agent-wait.test.ts b/packages/cli/tests/11-agent-wait.test.ts index 574d766bb..bf822b2a7 100644 --- a/packages/cli/tests/11-agent-wait.test.ts +++ b/packages/cli/tests/11-agent-wait.test.ts @@ -167,15 +167,16 @@ try { console.log('wait command mentions ID\n') } - // Test 12: timeout option has default value documented + // Test 12: timeout option documents no default limit { - console.log('Test 12: timeout option has default value documented') + console.log('Test 12: timeout option documents no default limit') const result = await $`npx paseo wait --help`.nothrow() assert.strictEqual(result.exitCode, 0, 'wait --help should exit 0') - const hasDefault = - result.stdout.includes('600') || result.stdout.toLowerCase().includes('default') - assert(hasDefault, 'help should mention timeout default value') - console.log('timeout option has default value documented\n') + assert( + result.stdout.toLowerCase().includes('default: no limit'), + 'help should mention timeout default is no limit' + ) + console.log('timeout option documents no default limit\n') } } finally { // Clean up temp directory diff --git a/packages/server/src/client/daemon-client.test.ts b/packages/server/src/client/daemon-client.test.ts index 54e9d815d..501ca4340 100644 --- a/packages/server/src/client/daemon-client.test.ts +++ b/packages/server/src/client/daemon-client.test.ts @@ -1740,4 +1740,72 @@ describe('DaemonClient', () => { }, ]) }) + + test('waitForFinish with timeout=0 omits timeoutMs and has no client deadline', async () => { + vi.useFakeTimers() + try { + const logger = createMockLogger() + const mock = createMockTransport() + + const client = new DaemonClient({ + url: 'ws://test', + clientId: 'clsk_unit_test', + logger, + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }) + clients.push(client) + + const connectPromise = client.connect() + mock.triggerOpen() + await connectPromise + + const waitPromise = client.waitForFinish('agent-wait-zero-timeout', 0) + + expect(mock.sent).toHaveLength(1) + const request = JSON.parse(String(mock.sent[0])) as { + type: 'session' + message: { + type: 'wait_for_finish_request' + requestId: string + agentId: string + timeoutMs?: number + } + } + expect(request.message.type).toBe('wait_for_finish_request') + expect(request.message.agentId).toBe('agent-wait-zero-timeout') + expect(request.message).not.toHaveProperty('timeoutMs') + + const settled = vi.fn() + void waitPromise.then( + () => settled('resolved'), + () => settled('rejected') + ) + + await vi.advanceTimersByTimeAsync(5 * 60 * 1000) + expect(settled).not.toHaveBeenCalled() + + mock.triggerMessage( + wrapSessionMessage({ + type: 'wait_for_finish_response', + payload: { + requestId: request.message.requestId, + status: 'idle', + final: null, + error: null, + lastMessage: null, + }, + }) + ) + + await expect(waitPromise).resolves.toEqual({ + status: 'idle', + final: null, + error: null, + lastMessage: null, + }) + } finally { + vi.useRealTimers() + } + }) }) diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index 9ca019849..1c7882424 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -2426,17 +2426,18 @@ export class DaemonClient { async waitForFinish(agentId: string, timeout = 60000): Promise { const requestId = this.createRequestId() + const hasTimeout = Number.isFinite(timeout) && timeout > 0 const message = SessionInboundMessageSchema.parse({ type: 'wait_for_finish_request', requestId, agentId, - timeoutMs: timeout, + ...(hasTimeout ? { timeoutMs: timeout } : {}), }) const payload = await this.sendCorrelatedRequest({ requestId, message, responseType: 'wait_for_finish_response', - timeout: timeout + 5000, + timeout: hasTimeout ? timeout + 5000 : 0, options: { skipQueue: true }, }) return { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index cb067ddd5..e6ca8ca9d 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -5497,10 +5497,12 @@ export class Session { } const abortController = new AbortController() - const effectiveTimeoutMs = timeoutMs ?? 600_000 // 10 minutes default - const timeoutHandle = setTimeout(() => { - abortController.abort('timeout') - }, effectiveTimeoutMs) + const hasTimeout = typeof timeoutMs === 'number' && timeoutMs > 0 + const timeoutHandle = hasTimeout + ? setTimeout(() => { + abortController.abort('timeout') + }, timeoutMs) + : null try { let result = await this.agentManager.waitForAgentEvent(agentId, { @@ -5553,7 +5555,9 @@ export class Session { payload: { requestId, status: 'timeout', final, error: null, lastMessage: null }, }) } finally { - clearTimeout(timeoutHandle) + if (timeoutHandle) { + clearTimeout(timeoutHandle) + } } }