diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index 50b620ca..7682b01c 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -804,6 +804,66 @@ test("Code Agent PC gateway prompt reaches Codex stdio instead of internal hardw } }); +test("Codex stdio manager rebuilds app-server client when provider profile config changes", async () => { + const calls = []; + const createdModels = []; + const closedModels = []; + const fakeCodex = await createFakeCodexCommand(); + const codexHome = await prepareFakeCodexHome(); + const baseEnv = { + PATH: process.env.PATH, + OPENAI_API_KEY: "test-openai-key-material", + CODEX_HOME: codexHome, + HWLAB_CODE_AGENT_PROVIDER: "codex-stdio", + HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command, + HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1", + HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned", + HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access", + HWLAB_CODE_AGENT_WORKSPACE: process.cwd() + }; + const manager = createCodexStdioSessionManager({ + idFactory: () => `ses_profile_${createdModels.length + 1}`, + createRpcClient: async ({ env }) => { + const model = env.HWLAB_CODE_AGENT_MODEL; + createdModels.push(model); + const client = createFakeAppServerClient({ calls, responses: [`reply from ${model}`] }); + client.close = () => closedModels.push(model); + return client; + } + }); + + try { + await manager.chat({ + conversationId: "cnv_profile_deepseek", + traceId: "trc_profile_deepseek", + message: "first", + env: { + ...baseEnv, + HWLAB_CODE_AGENT_MODEL: "deepseek-chat", + HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses" + } + }); + await manager.chat({ + conversationId: "cnv_profile_codex", + traceId: "trc_profile_codex", + message: "second", + env: { + ...baseEnv, + HWLAB_CODE_AGENT_MODEL: "gpt-5.5", + HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses" + } + }); + + assert.deepEqual(createdModels, ["deepseek-chat", "gpt-5.5"]); + assert.deepEqual(closedModels, ["deepseek-chat"]); + assert.equal(calls.filter((call) => call.method === "initialize").length, 2); + assert.deepEqual(calls.filter((call) => call.method === "turn/start").map((call) => call.args.model), ["deepseek-chat", "gpt-5.5"]); + } finally { + await rm(fakeCodex.root, { recursive: true, force: true }); + await rm(codexHome, { recursive: true, force: true }); + } +}); + test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", async () => { const calls = []; let providerCalled = false; diff --git a/internal/cloud/codex-stdio-session.mjs b/internal/cloud/codex-stdio-session.mjs index 568ab6e2..b2002938 100644 --- a/internal/cloud/codex-stdio-session.mjs +++ b/internal/cloud/codex-stdio-session.mjs @@ -112,6 +112,7 @@ export function createCodexStdioSessionManager(options = {}) { const sessions = new Map(); const conversations = new Map(); let rpcClient = options.rpcClient ?? null; + let rpcClientSignature = options.rpcClient ? "injected" : null; let rpcStartedAt = null; let rpcInitialized = false; let protocolProbe = null; @@ -964,18 +965,30 @@ export function createCodexStdioSessionManager(options = {}) { } async function ensureRpcClient({ env, availability, timeoutMs } = {}) { - if (rpcClient && typeof rpcClient.startTurn === "function") return rpcClient; + const args = codexAppServerArgs(env); + const childEnv = childProcessEnv(env); + const nextSignature = rpcClientConfigSignature({ availability, args, childEnv }); + if (rpcClient && typeof rpcClient.startTurn === "function" && rpcClientSignature === nextSignature) return rpcClient; + if (rpcClient && rpcClientSignature !== nextSignature && typeof rpcClient.close === "function") { + rpcClient.close(); + } + if (rpcClientSignature !== nextSignature) { + rpcClient = null; + rpcInitialized = false; + rpcClientSignature = null; + } rpcClient = options.createRpcClient ? await options.createRpcClient({ env, availability }) : createCodexAppServerJsonLineClient({ command: availability.command, - args: codexAppServerArgs(env), - env: childProcessEnv(env), + args, + env: childEnv, cwd: availability.workspace }); if (typeof rpcClient.initialize === "function") { await rpcClient.initialize(effectiveTimeout(timeoutMs)); } + rpcClientSignature = nextSignature; rpcInitialized = true; rpcStartedAt = timestampFor(nowDefault); return rpcClient; @@ -3550,6 +3563,15 @@ function codexProviderNoProxyEntries(env = process.env) { } } +function rpcClientConfigSignature({ availability, args = [], childEnv = {} } = {}) { + return JSON.stringify({ + command: availability?.command ?? null, + workspace: availability?.workspace ?? null, + codexHome: childEnv.CODEX_HOME ?? null, + args + }); +} + function mergeNoProxy(...parts) { const entries = []; const seen = new Set();