diff --git a/src/main/runtime/runtime-rpc.test.ts b/src/main/runtime/runtime-rpc.test.ts index 802956d1e..213d66deb 100644 --- a/src/main/runtime/runtime-rpc.test.ts +++ b/src/main/runtime/runtime-rpc.test.ts @@ -3909,6 +3909,51 @@ describe('OrcaRuntimeRpcServer', () => { } }) + it('emits keepalive frames while orchestration.ask blocks for a reply', async () => { + const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) + const runtime = new OrcaRuntimeService() + const db = new OrchestrationDb(':memory:') + runtime.setOrchestrationDb(db) + const server = new OrcaRuntimeRpcServer({ + runtime, + userDataPath, + keepaliveIntervalMs: 50 + }) + await server.start() + + try { + const metadata = readRuntimeMetadata(userDataPath) + // Why: no reply is ever sent, so ask blocks the full window on the same + // hold-the-socket path check --wait uses. Without ask in the long-poll + // set the 30s idle timer would tear this down before it keepalives. + const session = openFramedSession(metadata!.transports[0]!.endpoint, { + id: 'req_ask', + authToken: metadata!.authToken, + method: 'orchestration.ask', + params: { + to: 'term_nobody', + from: 'term_asker', + question: 'ping?', + timeoutMs: 300 + } + }) + await session.done + + const keepalives = session.frames.filter((f) => f._keepalive === true) + const terminals = session.frames.filter((f) => f.ok !== undefined) + expect(terminals).toHaveLength(1) + expect(terminals[0]).toMatchObject({ + id: 'req_ask', + ok: true, + result: { timedOut: true } + }) + expect(keepalives.length).toBeGreaterThanOrEqual(3) + } finally { + db.close() + await server.stop() + } + }) + it('emits keepalive frames while terminal.wait blocks and returns its structured timeout', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() diff --git a/src/main/runtime/runtime-rpc.ts b/src/main/runtime/runtime-rpc.ts index 726981e98..503bc9c31 100644 --- a/src/main/runtime/runtime-rpc.ts +++ b/src/main/runtime/runtime-rpc.ts @@ -397,6 +397,14 @@ function isLongPollRequest(request: RpcRequest): boolean { if (request.method === 'terminal.wait') { return true } + // Why: orchestration.ask blocks unconditionally (default 600 s) holding the + // RPC open until a reply lands or the deadline passes, so it needs the same + // keepalive as check --wait or the 30 s socket idle timer tears it down. It + // also relies on the abort signal (only wired for long-polls) to release the + // waiter when the asking client disconnects. + if (request.method === 'orchestration.ask') { + return true + } if (request.method === 'orchestration.check') { const params = request.params as { wait?: unknown } | undefined return params?.wait === true