Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 41 additions & 9 deletions src/__tests__/backend-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,11 @@ describe("backend conformance — finalizePartsIntoState", () => {
// ── Conformance: delivery routing ──────────────────────────────────────────

describe("backend conformance — routeDelivery", () => {
it("returns the same route for both backends given equivalent state", async () => {
// Text-part route
it("returns the same route for all three remote-server backends given equivalent state", async () => {
// Text-part route — Kilo / OpenCode / Codex all use shared routeDelivery
const stateA = createStreamState();
const stateB = createStreamState();
const stateC = createStreamState();
const kiloDecision = await routeDelivery({
backendLabel: "Kilo",
chatId: "c",
Expand All @@ -361,38 +362,58 @@ describe("backend conformance — routeDelivery", () => {
state: stateB,
responseText: "Hello",
});
const codexDecision = await routeDelivery({
backendLabel: "Codex",
chatId: "c",
state: stateC,
responseText: "Hello",
});
expect(kiloDecision.route).toBe(opencodeDecision.route);
expect(kiloDecision.route).toBe(codexDecision.route);
expect(kiloDecision.chars).toBe(opencodeDecision.chars);
expect(kiloDecision.chars).toBe(codexDecision.chars);

// Tool route
const stateC = createStreamState();
Object.assign(stateC, { deliveredTextNorms: ["delivered"] });
const stateD = createStreamState();
Object.assign(stateD, { deliveredTextNorms: ["delivered"] });
const toolStateA = createStreamState();
Object.assign(toolStateA, { deliveredTextNorms: ["delivered"] });
const toolStateB = createStreamState();
Object.assign(toolStateB, { deliveredTextNorms: ["delivered"] });
const toolStateC = createStreamState();
Object.assign(toolStateC, { deliveredTextNorms: ["delivered"] });
const kT = await routeDelivery({
backendLabel: "Kilo",
chatId: "c",
state: stateC,
state: toolStateA,
responseText: "ignored",
});
const oT = await routeDelivery({
backendLabel: "OpenCode",
chatId: "c",
state: stateD,
state: toolStateB,
responseText: "ignored",
});
const cT = await routeDelivery({
backendLabel: "Codex",
chatId: "c",
state: toolStateC,
responseText: "ignored",
});
expect(kT.route).toBe(oT.route);
expect(kT.route).toBe(cT.route);
expect(kT.route).toBe("tool");
});

it("uses backend label in synthetic-error message prefix", async () => {
it("uses backend label in synthetic-error message prefix (all three remote-server backends)", async () => {
const kiloMessages: string[] = [];
const opencodeMessages: string[] = [];
const codexMessages: string[] = [];

const stateA = createStreamState();
Object.assign(stateA, { syntheticError: "boom" });
const stateB = createStreamState();
Object.assign(stateB, { syntheticError: "boom" });
const stateC = createStreamState();
Object.assign(stateC, { syntheticError: "boom" });

await routeDelivery({
backendLabel: "Kilo",
Expand All @@ -414,8 +435,19 @@ describe("backend conformance — routeDelivery", () => {
},
});

await routeDelivery({
backendLabel: "Codex",
chatId: "c",
state: stateC,
responseText: "",
onTextBlock: async (text) => {
codexMessages.push(text);
},
});

expect(kiloMessages[0]).toBe("⚠️ Kilo: boom");
expect(opencodeMessages[0]).toBe("⚠️ OpenCode: boom");
expect(codexMessages[0]).toBe("⚠️ Codex: boom");
});
});

Expand Down
14 changes: 13 additions & 1 deletion src/backend/codex/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { log } from "../../util/log.js";
import { initCodexAgent } from "./init.js";
import { handleMessage as codexHandleMessage } from "./handler.js";
import { runOneShotAgent as codexRunOneShotAgent } from "./one-shot.js";
import { resetState as resetCodexState } from "./state.js";
import {
resolveModel,
getModelInfo,
Expand Down Expand Up @@ -48,7 +49,18 @@ const codexFactory: BackendFactory = {
backendLabel: "Codex",
};

return { backend };
return {
backend,
// Cleanup: drop the cached Codex instance + state. The Codex
// SDK's instances are stateless from our perspective (each
// `runStreamed` spawns a fresh subprocess), so there's no
// long-running server to stop — clearing module-scoped state
// is enough for hot-reload + test isolation.
cleanup: () => {
resetCodexState();
log("bot", "Codex backend cleaned up");
},
};
},
};

Expand Down
1 change: 1 addition & 0 deletions src/backend/codex/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export async function handleMessage(
// pass through whatever the chat settings hold (default `gpt-5-codex`).
const chatSettings = getChatSettings(chatId);
const activeModel = chatSettings.model ?? config.model ?? CODEX_DEFAULT_MODEL;
log("agent", `[${chatId}] Codex model resolved: ${activeModel}`);

// First-turn system-prompt rebuild + Codex-specific delivery suffix.
const systemPrompt = prepareSystemPrompt({
Expand Down
Loading