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,
|
AgentTimelineItem,
|
||||||
} from "../agent-sdk-types.js";
|
} 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 {
|
function tmpCwd(): string {
|
||||||
const dir = mkdtempSync(path.join(os.tmpdir(), "opencode-agent-test-"));
|
const dir = mkdtempSync(path.join(os.tmpdir(), "opencode-agent-test-"));
|
||||||
try {
|
try {
|
||||||
@@ -3299,7 +3304,7 @@ describe("OpenCode adapter startTurn error handling", () => {
|
|||||||
await session.close();
|
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();
|
vi.useFakeTimers();
|
||||||
const openCode = new TestOpenCodeClient();
|
const openCode = new TestOpenCodeClient();
|
||||||
const session = new __openCodeInternals.OpenCodeAgentSession(
|
const session = new __openCodeInternals.OpenCodeAgentSession(
|
||||||
@@ -3316,10 +3321,21 @@ describe("OpenCode adapter startTurn error handling", () => {
|
|||||||
try {
|
try {
|
||||||
const dispatch = session.startTurn("wait for transport");
|
const dispatch = session.startTurn("wait for transport");
|
||||||
const rejection = expect(dispatch).rejects.toThrow("OpenCode event stream first record");
|
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);
|
expect(openCode.calls.sessionPromptAsync).toHaveLength(0);
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
await rejection;
|
await rejection;
|
||||||
|
expect(settled).toBe(true);
|
||||||
expect(openCode.calls.sessionPromptAsync).toHaveLength(0);
|
expect(openCode.calls.sessionPromptAsync).toHaveLength(0);
|
||||||
} finally {
|
} finally {
|
||||||
vi.useRealTimers();
|
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 () => {
|
test("close aborts every in-flight reconciliation request class", async () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
"child discovery",
|
"child discovery",
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import { execCommand } from "../../../utils/spawn.js";
|
|||||||
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
||||||
import {
|
import {
|
||||||
OpenCodeServerManager,
|
OpenCodeServerManager,
|
||||||
|
OPENCODE_SERVER_STARTUP_TIMEOUT_MS,
|
||||||
type OpenCodeServerAcquisition,
|
type OpenCodeServerAcquisition,
|
||||||
type OpenCodeServerManagerLike,
|
type OpenCodeServerManagerLike,
|
||||||
} from "./opencode/server-manager.js";
|
} from "./opencode/server-manager.js";
|
||||||
@@ -3531,7 +3532,14 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
const effectiveMode = resolveOpenCodeRuntimeAgentId(this.currentMode);
|
const effectiveMode = resolveOpenCodeRuntimeAgentId(this.currentMode);
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
if (this.abortController === turnAbortController) {
|
if (this.abortController === turnAbortController) {
|
||||||
this.abortController = null;
|
this.abortController = null;
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ import {
|
|||||||
type OpenCodeEventSource,
|
type OpenCodeEventSource,
|
||||||
} from "./event-consumer.js";
|
} 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_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5_000;
|
||||||
const OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1_000;
|
const OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1_000;
|
||||||
|
|
||||||
@@ -388,7 +394,7 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike {
|
|||||||
if (!started) {
|
if (!started) {
|
||||||
failStartup(new Error(buildStartupErrorMessage("OpenCode server startup timeout")));
|
failStartup(new Error(buildStartupErrorMessage("OpenCode server startup timeout")));
|
||||||
}
|
}
|
||||||
}, 30_000);
|
}, OPENCODE_SERVER_STARTUP_TIMEOUT_MS);
|
||||||
|
|
||||||
serverProcess.stdout?.on("data", (data: Buffer) => {
|
serverProcess.stdout?.on("data", (data: Buffer) => {
|
||||||
const output = data.toString();
|
const output = data.toString();
|
||||||
|
|||||||
Reference in New Issue
Block a user