fix(cli): make agent wait unlimited by default

This commit is contained in:
Mohamed Boudra
2026-02-23 20:51:42 +07:00
parent b167abfda5
commit 3f06063b4b
7 changed files with 98 additions and 25 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ export function createCli(): Command {
.command('wait') .command('wait')
.description('Wait for an agent to become idle') .description('Wait for an agent to become idle')
.argument('<id>', 'Agent ID (or prefix)') .argument('<id>', 'Agent ID (or prefix)')
.option('--timeout <seconds>', 'Maximum wait time (default: 600)') .option('--timeout <seconds>', 'Maximum wait time (default: no limit)')
.option('--json', 'Output in JSON format') .option('--json', 'Output in JSON format')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)') .option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runWaitCommand)) .action(withOutput(runWaitCommand))
+1 -1
View File
@@ -99,7 +99,7 @@ export function createAgentCommand(): Command {
.command('wait') .command('wait')
.description('Wait for an agent to become idle') .description('Wait for an agent to become idle')
.argument('<id>', 'Agent ID (or prefix)') .argument('<id>', 'Agent ID (or prefix)')
.option('--timeout <seconds>', 'Maximum wait time (default: 600)') .option('--timeout <seconds>', 'Maximum wait time (default: no limit)')
.option('--json', 'Output in JSON format') .option('--json', 'Output in JSON format')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)') .option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runWaitCommand)) .action(withOutput(runWaitCommand))
+9 -10
View File
@@ -108,14 +108,17 @@ export async function runWaitCommand(
throw error throw error
} }
// Parse timeout (default 10 minutes) // Parse timeout (no limit unless explicitly provided)
let timeoutMs: number let timeoutMs = 0
let timeoutLabel: string | null = null
if (options.timeout) { if (options.timeout) {
try { try {
timeoutMs = parseDuration(options.timeout) timeoutMs = parseDuration(options.timeout)
if (timeoutMs <= 0) { if (timeoutMs <= 0) {
throw new Error('Timeout must be positive') throw new Error('Timeout must be positive')
} }
const timeoutSeconds = Math.floor(timeoutMs / 1000)
timeoutLabel = `${timeoutSeconds} second${timeoutSeconds === 1 ? '' : 's'}`
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err) const message = err instanceof Error ? err.message : String(err)
const error: CommandError = { const error: CommandError = {
@@ -125,11 +128,7 @@ export async function runWaitCommand(
} }
throw error 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 let client
try { try {
@@ -156,15 +155,15 @@ export async function runWaitCommand(
await client.close() await client.close()
if (state.status === 'timeout') { 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 { return {
type: 'single', type: 'single',
data: { data: {
agentId: resolvedAgentId, agentId: resolvedAgentId,
status: 'timeout', status: 'timeout',
message: appendRecentActivity( message: appendRecentActivity(timeoutMessage, recentActivity),
`Agent did not finish within ${timeoutLabel}. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`,
recentActivity
),
}, },
schema: agentWaitSchema, schema: agentWaitSchema,
} }
+7 -6
View File
@@ -167,15 +167,16 @@ try {
console.log('wait command mentions ID\n') 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() const result = await $`npx paseo wait --help`.nothrow()
assert.strictEqual(result.exitCode, 0, 'wait --help should exit 0') assert.strictEqual(result.exitCode, 0, 'wait --help should exit 0')
const hasDefault = assert(
result.stdout.includes('600') || result.stdout.toLowerCase().includes('default') result.stdout.toLowerCase().includes('default: no limit'),
assert(hasDefault, 'help should mention timeout default value') 'help should mention timeout default is no limit'
console.log('timeout option has default value documented\n') )
console.log('timeout option documents no default limit\n')
} }
} finally { } finally {
// Clean up temp directory // Clean up temp directory
@@ -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()
}
})
}) })
+3 -2
View File
@@ -2426,17 +2426,18 @@ export class DaemonClient {
async waitForFinish(agentId: string, timeout = 60000): Promise<WaitForFinishResult> { async waitForFinish(agentId: string, timeout = 60000): Promise<WaitForFinishResult> {
const requestId = this.createRequestId() const requestId = this.createRequestId()
const hasTimeout = Number.isFinite(timeout) && timeout > 0
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: 'wait_for_finish_request', type: 'wait_for_finish_request',
requestId, requestId,
agentId, agentId,
timeoutMs: timeout, ...(hasTimeout ? { timeoutMs: timeout } : {}),
}) })
const payload = await this.sendCorrelatedRequest({ const payload = await this.sendCorrelatedRequest({
requestId, requestId,
message, message,
responseType: 'wait_for_finish_response', responseType: 'wait_for_finish_response',
timeout: timeout + 5000, timeout: hasTimeout ? timeout + 5000 : 0,
options: { skipQueue: true }, options: { skipQueue: true },
}) })
return { return {
+9 -5
View File
@@ -5497,10 +5497,12 @@ export class Session {
} }
const abortController = new AbortController() const abortController = new AbortController()
const effectiveTimeoutMs = timeoutMs ?? 600_000 // 10 minutes default const hasTimeout = typeof timeoutMs === 'number' && timeoutMs > 0
const timeoutHandle = setTimeout(() => { const timeoutHandle = hasTimeout
abortController.abort('timeout') ? setTimeout(() => {
}, effectiveTimeoutMs) abortController.abort('timeout')
}, timeoutMs)
: null
try { try {
let result = await this.agentManager.waitForAgentEvent(agentId, { let result = await this.agentManager.waitForAgentEvent(agentId, {
@@ -5553,7 +5555,9 @@ export class Session {
payload: { requestId, status: 'timeout', final, error: null, lastMessage: null }, payload: { requestId, status: 'timeout', final, error: null, lastMessage: null },
}) })
} finally { } finally {
clearTimeout(timeoutHandle) if (timeoutHandle) {
clearTimeout(timeoutHandle)
}
} }
} }