From cd28da13f46c5d96bbd9e39d787b59bf168ccbef Mon Sep 17 00:00:00 2001 From: Avery Bloom Date: Fri, 24 Jul 2026 15:25:53 +0800 Subject: [PATCH] fix(orchestration): treat ask as a long-poll so it survives the 30s socket idle wall (#9351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit orchestration.ask blocks server-side until a reply lands or its timeout (default 600s) elapses, holding the RPC open — but isLongPollRequest never classified it as a long-poll. So the keepalive that resets the 30s RUNTIME_RPC_SOCKET_IDLE_TIMEOUT_MS was never armed, and any ask left unanswered for 30s died with a misleading runtime_unavailable ("The Orca runtime closed the connection"), regardless of --timeout-ms. The same omission left the handler's abort signal unwired (it is only passed for long-polls), so the client-disconnect release path guarded by signal?.aborted was dead code. Add orchestration.ask to isLongPollRequest so it gets the keepalive, the abort signal, and long-poll admission. The client already extends its per-call socket timeout for ask (handlers/orchestration.ts passes timeoutMs + 5s at the call site), so only the server-side classification was missing. --- src/main/runtime/runtime-rpc.test.ts | 45 ++++++++++++++++++++++++++++ src/main/runtime/runtime-rpc.ts | 8 +++++ 2 files changed, 53 insertions(+) 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