fix(server): extend OpenCode event stream readiness timeout to match server startup budget
Closes #3571.
This commit is contained in:
@@ -26,6 +26,11 @@ import type {
|
||||
AgentTimelineItem,
|
||||
} from "../agent-sdk-types.js";
|
||||
|
||||
// Deliberately an independent literal rather than the production constant these tests
|
||||
// guard: deriving the boundary from OPENCODE_SERVER_STARTUP_TIMEOUT_MS would keep the
|
||||
// readiness tests green if that constant were shortened below the required budget.
|
||||
const EXPECTED_STARTUP_BUDGET_MS = 30_000;
|
||||
|
||||
function tmpCwd(): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "opencode-agent-test-"));
|
||||
try {
|
||||
@@ -3299,7 +3304,7 @@ describe("OpenCode adapter startTurn error handling", () => {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test("bounds dispatch readiness at ten seconds without sending a prompt", async () => {
|
||||
test("bounds dispatch readiness at the server startup budget without sending a prompt", async () => {
|
||||
vi.useFakeTimers();
|
||||
const openCode = new TestOpenCodeClient();
|
||||
const session = new __openCodeInternals.OpenCodeAgentSession(
|
||||
@@ -3316,10 +3321,21 @@ describe("OpenCode adapter startTurn error handling", () => {
|
||||
try {
|
||||
const dispatch = session.startTurn("wait for transport");
|
||||
const rejection = expect(dispatch).rejects.toThrow("OpenCode event stream first record");
|
||||
await vi.advanceTimersByTimeAsync(9_999);
|
||||
let settled = false;
|
||||
const markSettled = () => {
|
||||
settled = true;
|
||||
};
|
||||
void dispatch.then(markSettled, markSettled);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(EXPECTED_STARTUP_BUDGET_MS - 1);
|
||||
// Still waiting one tick before the budget: a shorter readiness timeout would have
|
||||
// already failed the dispatch here.
|
||||
expect(settled).toBe(false);
|
||||
expect(openCode.calls.sessionPromptAsync).toHaveLength(0);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await rejection;
|
||||
expect(settled).toBe(true);
|
||||
expect(openCode.calls.sessionPromptAsync).toHaveLength(0);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
@@ -3327,6 +3343,40 @@ describe("OpenCode adapter startTurn error handling", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("dispatches a prompt when the event stream needs more than ten seconds", async () => {
|
||||
vi.useFakeTimers();
|
||||
const openCode = new TestOpenCodeClient();
|
||||
openCode.sessionPromptAsyncEvents = [];
|
||||
const streamReady = createTestDeferred<void>();
|
||||
const session = new __openCodeInternals.OpenCodeAgentSession(
|
||||
{ provider: "opencode", cwd: "/workspace/repo" },
|
||||
openCode.asSdkClient(),
|
||||
"ses_readiness_slow_stream",
|
||||
createTestLogger(),
|
||||
new Map(),
|
||||
{
|
||||
ready: () => streamReady.promise,
|
||||
subscribe: () => () => undefined,
|
||||
},
|
||||
);
|
||||
try {
|
||||
const dispatch = session.startTurn("wait for a slow transport");
|
||||
// OpenCode installs with plugins have been observed taking well past ten seconds
|
||||
// to emit their first SSE record.
|
||||
await vi.advanceTimersByTimeAsync(20_000);
|
||||
expect(openCode.calls.sessionPromptAsync).toHaveLength(0);
|
||||
|
||||
streamReady.resolve();
|
||||
await dispatch;
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(openCode.calls.sessionPromptAsync).toHaveLength(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
streamReady.resolve();
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("close aborts every in-flight reconciliation request class", async () => {
|
||||
const cases = [
|
||||
"child discovery",
|
||||
|
||||
@@ -75,6 +75,7 @@ import { execCommand } from "../../../utils/spawn.js";
|
||||
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
||||
import {
|
||||
OpenCodeServerManager,
|
||||
OPENCODE_SERVER_STARTUP_TIMEOUT_MS,
|
||||
type OpenCodeServerAcquisition,
|
||||
type OpenCodeServerManagerLike,
|
||||
} from "./opencode/server-manager.js";
|
||||
@@ -3531,7 +3532,14 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
const effectiveMode = resolveOpenCodeRuntimeAgentId(this.currentMode);
|
||||
|
||||
try {
|
||||
await withTimeout(this.events.ready(), 10_000, "OpenCode event stream first record");
|
||||
// The stream cannot deliver its first record before OpenCode finished booting, so
|
||||
// this wait gets the same budget as server startup instead of a shorter one that
|
||||
// fails turns on slow (plugin-heavy or cold) starts.
|
||||
await withTimeout(
|
||||
this.events.ready(),
|
||||
OPENCODE_SERVER_STARTUP_TIMEOUT_MS,
|
||||
"OpenCode event stream first record",
|
||||
);
|
||||
} catch (error) {
|
||||
if (this.abortController === turnAbortController) {
|
||||
this.abortController = null;
|
||||
|
||||
@@ -21,6 +21,12 @@ import {
|
||||
type OpenCodeEventSource,
|
||||
} from "./event-consumer.js";
|
||||
|
||||
/**
|
||||
* Budget for an OpenCode server to become usable after spawn. Plugin-heavy installs
|
||||
* routinely need well over ten seconds on a cold start, so every wait that depends on
|
||||
* the server finishing its boot shares this budget.
|
||||
*/
|
||||
export const OPENCODE_SERVER_STARTUP_TIMEOUT_MS = 30_000;
|
||||
const OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5_000;
|
||||
const OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1_000;
|
||||
|
||||
@@ -388,7 +394,7 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike {
|
||||
if (!started) {
|
||||
failStartup(new Error(buildStartupErrorMessage("OpenCode server startup timeout")));
|
||||
}
|
||||
}, 30_000);
|
||||
}, OPENCODE_SERVER_STARTUP_TIMEOUT_MS);
|
||||
|
||||
serverProcess.stdout?.on("data", (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
|
||||
Reference in New Issue
Block a user