fix(agent): complete opencode warmup before serving

The previous fire-and-forget startup only created the SDK client, leaving the first chat to await project and tool initialization. Warm the real session/tool path and gate port listening on completion.
This commit is contained in:
2026-08-04 15:25:00 +08:00
parent 94529cb141
commit 07016451d6
3 changed files with 90 additions and 12 deletions
+32
View File
@@ -64,6 +64,38 @@ export class OpencodeRuntimeAdapter {
return requireData(response.data, "global.health");
}
async warmup(): Promise<void> {
const client = await this.ensureClient();
const healthResponse = await client.global.health();
requireData(healthResponse.data, "global.health");
const sessionResponse = await client.session.create({
title: "tjwater-agent-warmup",
});
const session = requireData(sessionResponse.data, "session.create");
try {
const [provider, model] = config.OPENCODE_MODEL.split("/");
if (!provider || !model) {
throw new Error(
`invalid OPENCODE_MODEL; expected provider/model, received ${config.OPENCODE_MODEL}`,
);
}
const toolsResponse = await client.tool.list({ provider, model });
requireData(toolsResponse.data, "tool.list");
} finally {
await client.session.delete(
{ sessionID: session.id },
{ throwOnError: true },
).catch((error) => {
logger.warn(
{ err: error, sessionId: session.id },
"failed to remove opencode warmup session",
);
});
}
}
async createSession(title?: string) {
const client = await this.ensureClient();
const response = await client.session.create({
+13 -12
View File
@@ -488,23 +488,12 @@ const bootstrap = async () => {
resultReferenceStore.initialize(),
sessionTranscriptStore.initialize(),
]);
resultReferenceStore.startCleanupLoop();
};
await bootstrap();
const server = app.listen(config.PORT, config.HOST, () => {
logger.info(
{ host: config.HOST, port: config.PORT },
"TJWaterAgent listening",
);
void warmupOpencodeRuntime();
});
const warmupOpencodeRuntime = async () => {
const startedAt = Date.now();
try {
await opencodeRuntime.ensureClient();
await opencodeRuntime.warmup();
logger.info(
{
elapsedMs: Math.max(0, Date.now() - startedAt),
@@ -521,9 +510,21 @@ const warmupOpencodeRuntime = async () => {
},
"failed to warm up opencode runtime",
);
throw error;
}
};
await bootstrap();
await warmupOpencodeRuntime();
resultReferenceStore.startCleanupLoop();
const server = app.listen(config.PORT, config.HOST, () => {
logger.info(
{ host: config.HOST, port: config.PORT },
"TJWaterAgent listening",
);
});
const shutdown = async () => {
logger.info("shutting down TJWaterAgent");
server.close();
+45
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "bun:test";
import { type OpencodeClient } from "@opencode-ai/sdk/v2";
import { config } from "../../src/config.js";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
const createRuntimeAdapter = (
@@ -85,3 +86,47 @@ describe("OpencodeRuntimeAdapter.ensureClient", () => {
expect(attempts).toBe(2);
});
});
describe("OpencodeRuntimeAdapter.warmup", () => {
it("initializes the project session and model tools before reporting ready", async () => {
const calls: string[] = [];
const client = {
global: {
health: async () => {
calls.push("health");
return { data: { healthy: true, version: "test" } };
},
},
session: {
create: async () => {
calls.push("session.create");
return { data: { id: "warmup-session" } };
},
delete: async ({ sessionID }: { sessionID: string }) => {
calls.push(`session.delete:${sessionID}`);
return { data: true };
},
},
tool: {
list: async (model: { provider: string; model: string }) => {
calls.push(`tool.list:${model.provider}/${model.model}`);
return { data: [] };
},
},
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
clientPromise: null,
closeServer: null,
ensureClient: async () => client,
}) as OpencodeRuntimeAdapter;
await runtime.warmup();
expect(calls).toEqual([
"health",
"session.create",
`tool.list:${config.OPENCODE_MODEL}`,
"session.delete:warmup-session",
]);
});
});