diff --git a/packages/plugins/agent-codex/src/app-server-client.test.ts b/packages/plugins/agent-codex/src/app-server-client.test.ts index 1e58489c0..47eb7e991 100644 --- a/packages/plugins/agent-codex/src/app-server-client.test.ts +++ b/packages/plugins/agent-codex/src/app-server-client.test.ts @@ -137,8 +137,10 @@ describe("CodexAppServerClient", () => { // Should have sent initialize request and initialized notification const initReq = findRequest(proc, "initialize"); expect(initReq).toBeDefined(); + expect(initReq!["jsonrpc"]).toBe("2.0"); const initializedNotif = findRequest(proc, "initialized"); expect(initializedNotif).toBeDefined(); + expect(initializedNotif!["jsonrpc"]).toBe("2.0"); await closeClient(client, proc); }); diff --git a/packages/plugins/agent-codex/src/app-server-client.ts b/packages/plugins/agent-codex/src/app-server-client.ts index 74bd5f4ce..5d5a753c2 100644 --- a/packages/plugins/agent-codex/src/app-server-client.ts +++ b/packages/plugins/agent-codex/src/app-server-client.ts @@ -19,6 +19,7 @@ import { EventEmitter } from "node:events"; /** JSON-RPC request sent from client to server */ export interface JsonRpcRequest { + jsonrpc: "2.0"; id: string; method: string; params: Record; @@ -339,7 +340,7 @@ export class CodexAppServerClient extends EventEmitter { } const id = randomUUID(); - const request: JsonRpcRequest = { id, method, params }; + const request: JsonRpcRequest = { jsonrpc: "2.0", id, method, params }; return new Promise>((resolve, reject) => { const timer = setTimeout(() => { @@ -357,7 +358,7 @@ export class CodexAppServerClient extends EventEmitter { if (this.closed) return; if (!this.process?.stdin?.writable) return; - this.writeLine(JSON.stringify({ method, params })); + this.writeLine(JSON.stringify({ jsonrpc: "2.0", method, params })); } /** Respond to an approval request from the server */ @@ -365,7 +366,7 @@ export class CodexAppServerClient extends EventEmitter { if (this.closed) return; if (!this.process?.stdin?.writable) return; - this.writeLine(JSON.stringify({ id, result: { decision } })); + this.writeLine(JSON.stringify({ jsonrpc: "2.0", id, result: { decision } })); } // --------------------------------------------------------------------------- diff --git a/packages/plugins/agent-codex/src/index.test.ts b/packages/plugins/agent-codex/src/index.test.ts index 9bf4d11fd..5ddbc5057 100644 --- a/packages/plugins/agent-codex/src/index.test.ts +++ b/packages/plugins/agent-codex/src/index.test.ts @@ -942,6 +942,30 @@ describe("getRestoreCommand", () => { expect(cmd).toContain("--ask-for-approval untrusted"); }); + it("places flags before positional threadId in resume command", async () => { + const content = jsonl( + { type: "session_meta", cwd: "/workspace/test", model: "o3-mini" }, + { threadId: "thread-order-test" }, + ); + mockReaddir.mockResolvedValue(["sess.jsonl"]); + setupMockOpen(content); + mockReadFile.mockResolvedValue(content); + mockStat.mockResolvedValue({ mtimeMs: 1000 }); + + const session = makeSession({ workspacePath: "/workspace/test" }); + const cmd = await agent.getRestoreCommand!(session, makeProjectConfig({ + agentConfig: { permissions: "auto-edit", model: "o3-mini" }, + })); + + expect(cmd).not.toBeNull(); + // threadId should come after all flags + const threadIdIdx = cmd!.indexOf("thread-order-test"); + const flagIdx = cmd!.indexOf("--ask-for-approval"); + const modelIdx = cmd!.indexOf("--model"); + expect(flagIdx).toBeLessThan(threadIdIdx); + expect(modelIdx).toBeLessThan(threadIdIdx); + }); + it("includes model from project config (overrides session model)", async () => { const content = jsonl( { type: "session_meta", cwd: "/workspace/test", model: "gpt-4o" }, diff --git a/packages/plugins/agent-codex/src/index.ts b/packages/plugins/agent-codex/src/index.ts index ced06398b..25a9c681a 100644 --- a/packages/plugins/agent-codex/src/index.ts +++ b/packages/plugins/agent-codex/src/index.ts @@ -732,8 +732,9 @@ function createCodexAgent(): Agent { // Use Codex's native `resume` subcommand for proper conversation resume. // This restores the full thread state, not just a text prompt re-injection. + // Flags are placed before the positional threadId for CLI parser compatibility. const binary = resolvedBinary ?? "codex"; - const parts: string[] = [binary, "resume", shellEscape(threadId)]; + const parts: string[] = [binary, "resume"]; // Add approval policy flags from project config const perms = project.agentConfig?.permissions as string | undefined; @@ -754,6 +755,9 @@ function createCodexAgent(): Agent { } } + // Positional threadId goes last, after all flags + parts.push(shellEscape(threadId)); + return parts.join(" "); },