diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index d1655712a..3e5b7c801 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -36,6 +36,14 @@ type DistributiveOmit = T extends unknown /** RpcCommand without the id field (for internal send) */ type RpcCommandBody = DistributiveOmit; +const DEFAULT_RPC_REQUEST_TIMEOUT_MS = 30_000; +const DEFAULT_RPC_INIT_TIMEOUT_MS = 120_000; + +export function resolveRpcInitTimeoutMs(): number { + const parsed = Number.parseInt(process.env.SF_RPC_INIT_TIMEOUT_MS ?? "", 10); + if (Number.isFinite(parsed) && parsed > 0) return parsed; + return DEFAULT_RPC_INIT_TIMEOUT_MS; +} export interface RpcClientOptions { /** Path to the CLI entry point (default: searches for dist/cli.js) */ @@ -635,11 +643,14 @@ export class RpcClient { * Returns the negotiated protocol version, session ID, and server capabilities. */ async init(options?: { clientId?: string }): Promise { - const response = await this.send({ - type: "init", - protocolVersion: 2, - clientId: options?.clientId, - }); + const response = await this.send( + { + type: "init", + protocolVersion: 2, + clientId: options?.clientId, + }, + resolveRpcInitTimeoutMs(), + ); return this.getData(response); } @@ -765,7 +776,10 @@ export class RpcClient { } } - private async send(command: RpcCommandBody): Promise { + private async send( + command: RpcCommandBody, + timeoutMs = DEFAULT_RPC_REQUEST_TIMEOUT_MS, + ): Promise { if (!this.process?.stdin) { throw new Error("Client not started"); } @@ -781,7 +795,7 @@ export class RpcClient { `Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`, ), ); - }, 30000); + }, timeoutMs); this.pendingRequests.set(id, { resolve: (response) => { diff --git a/packages/coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts b/packages/coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts index cf0ef411c..c2c16e63f 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts @@ -14,6 +14,7 @@ import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js"; import { buildRpcLaunchSpec, collectRpcDescendantPids, + resolveRpcInitTimeoutMs, shouldDetachRpcChild, signalRpcProcessTree, } from "./rpc-client.js"; @@ -451,6 +452,19 @@ describe("RpcClient command serialization", () => { assert.equal(parsed.clientId, "test-client"); }); + it("init timeout defaults longer than normal request timeout", () => { + assert.equal(resolveRpcInitTimeoutMs(), 120_000); + }); + + it("init timeout can be overridden by env", () => { + process.env.SF_RPC_INIT_TIMEOUT_MS = "45000"; + try { + assert.equal(resolveRpcInitTimeoutMs(), 45_000); + } finally { + delete process.env.SF_RPC_INIT_TIMEOUT_MS; + } + }); + it("shutdown command serializes correctly", () => { const cmd = { id: "req_2", type: "shutdown" }; const serialized = serializeJsonLine(cmd);