fix(agent-codex): add jsonrpc 2.0 field and fix resume flag ordering

- Add "jsonrpc": "2.0" to all JSON-RPC requests, notifications, and
  approval responses for spec compliance
- Place flags before positional threadId in resume command for CLI
  parser compatibility (codex resume --flags <threadId>)
- Add test assertions for jsonrpc field and flag ordering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
suraj-markup 2026-02-25 01:41:47 +05:30
parent e7033048f2
commit 3f76338bb4
4 changed files with 35 additions and 4 deletions

View File

@ -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);
});

View File

@ -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<string, unknown>;
@ -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<Record<string, unknown>>((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 } }));
}
// ---------------------------------------------------------------------------

View File

@ -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" },

View File

@ -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(" ");
},