Official
codex
Codex agent provider: runs the Codex CLI's app-server for each account.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/codex@0.2.0
Permissions in 0.2.0
Files
agent.test.ts83.6 KB
// Ported from the Rust plugin's agent.rs tests: notifications are routed
// straight into the agent, and the tests that drove a real RpcClient
// against a scripted Python peer drive the SDK's process transport
// against a scripted fake peer instead.
import * as Effect from "effect/Effect";
import { test } from "node:test";
import assert from "node:assert/strict";
import { testAgent } from "./testing.test.ts";
import { CodexAgent, newSession, versionOf } from "./agent.ts";
import { defaultAccount } from "./instances.ts";
import type { FakePeer, TestMessage, HostScript, PeerScript } from "../sdk/testing.ts";
import type { Event } from "./types.ts";
import type * as wire from "./wire.ts";
import * as z from "zod";
import { fakeApi, settle } from "../sdk/testing.ts";
import { clearCache } from "../sdk/maintenance.ts";
const SESSION = "thread-1";
const RUN = "run-1";
/// A scripted app-server: answers `initialize`, every other request with
/// `handlers[method](params, message, peer)` (no answer when that returns
/// `undefined`), and a method it has no handler for with an error, as the
/// real one does.
type Handlers = Record<string, (params: Record<string, unknown>, message: TestMessage, peer: FakePeer) => unknown>;
type Keys<T> = T extends unknown ? keyof T : never;
type Field<T, K> = T extends unknown ? (K extends keyof T ? T[K] : never) : never;
type ObservedEvent = Event & { [K in Keys<Event>]?: Field<Event, K> };
function appServer(handlers: Handlers = {}): PeerScript {
return (message, peer) => {
if (message.method === undefined) return;
if (message.method === "initialize") {
peer.reply(message, { userAgent: "convergence/0.155.1 (Mac OS 26.5.0; arm64) iTerm.app" });
return;
}
const handler = handlers[message.method];
if (!handler) {
if (message.id !== undefined)
peer.send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32601, message: `unknown method ${message.method}` },
});
return;
}
const result = handler(message.params, message, peer);
if (result !== undefined) peer.reply(message, result);
};
}
/// An agent with one registered session and an active run.
function harness({
handlers,
onHost,
env = { HOME: "/Users/me" },
}: { handlers?: Handlers; onHost?: HostScript; env?: Record<string, string> } = {}) {
const api = fakeApi({ onSpawn: (program) => (program === "codex" ? appServer(handlers) : () => {}), onHost });
const agent = testAgent(api, env);
const events: ObservedEvent[] = [];
agent.emit = (event) => events.push(event);
const state = newSession("/w");
state.run = RUN;
state.turn = "turn-1";
agent.sessions.set(SESSION, state);
return { agent, events, api };
}
function notify(agent: ReturnType<typeof testAgent>, method: string, params: wire.Notification) {
agent.routeNotification(method, params);
}
const tasks = (events: ObservedEvent[]) => events.filter((event) => event.event === "task");
function spawnItem(id: string, sender: string, child: string, prompt: string) {
return {
type: "collabAgentToolCall",
id,
tool: "spawnAgent",
status: "completed",
senderThreadId: sender,
receiverThreadIds: [child],
prompt,
model: "gpt-5.5",
reasoningEffort: "low",
agentsStates: { [child]: { status: "pendingInit", message: null } },
};
}
test("text deltas accumulate under one item id", () => {
const { agent, events } = harness();
for (const delta of ["P", "ONG", " "]) {
notify(agent, "item/agentMessage/delta", { threadId: SESSION, turnId: "turn-1", itemId: "msg_1", delta });
}
// The completed item must not repeat text that already streamed.
notify(agent, "item/completed", {
threadId: SESSION,
turnId: "turn-1",
completedAtMs: 0,
item: { type: "agentMessage", id: "msg_1", text: "PONG " },
});
assert.deepEqual(
events.map((event) => [event.event, event.itemId, event.text, event.mode, event.runId, event.sessionId]),
[
["text_delta", "msg_1", "P", "append", RUN, SESSION],
["text_delta", "msg_1", "ONG", "append", RUN, SESSION],
["text_delta", "msg_1", " ", "append", RUN, SESSION],
],
);
});
test("unstreamed messages and reasoning arrive as replacements", () => {
const { agent, events } = harness();
notify(agent, "item/completed", {
threadId: SESSION,
turnId: "turn-1",
item: { type: "reasoning", id: "rs_1", summary: ["Weighing options"], content: [] },
});
notify(agent, "item/completed", {
threadId: SESSION,
turnId: "turn-1",
item: { type: "agentMessage", id: "msg_1", text: "done" },
});
assert.deepEqual(events[0], {
sessionId: SESSION,
runId: RUN,
event: "reasoning_delta",
itemId: "rs_1",
text: "Weighing options",
mode: "replace",
});
assert.deepEqual(events[1], {
sessionId: SESSION,
runId: RUN,
event: "text_delta",
itemId: "msg_1",
text: "done",
mode: "replace",
});
});
test("reasoning summary parts are separated, and whitespace deltas are kept", () => {
const { agent, events } = harness();
notify(agent, "item/reasoning/summaryPartAdded", { threadId: SESSION, itemId: "rs_1" });
assert.equal(events.length, 0, "no break before anything streamed");
notify(agent, "item/reasoning/summaryTextDelta", { threadId: SESSION, itemId: "rs_1", delta: "**Plan**" });
notify(agent, "item/reasoning/summaryPartAdded", { threadId: SESSION, itemId: "rs_1" });
notify(agent, "item/reasoning/textDelta", { threadId: SESSION, itemId: "rs_1", delta: " " });
notify(agent, "item/completed", {
threadId: SESSION,
item: { type: "reasoning", id: "rs_1", summary: ["**Plan**"], content: [] },
});
assert.deepEqual(
events.map((event) => event.text),
["**Plan**", "\n\n", " "],
);
assert.ok(events.every((event) => event.mode === "append"));
});
test("reasoning that is only the stdin notice of codex exec is left out", () => {
const { agent, events } = harness();
for (const delta of ["Reading additional ", "input from stdin..."]) {
notify(agent, "item/reasoning/textDelta", { threadId: SESSION, itemId: "rs_stdin", delta });
}
notify(agent, "item/completed", {
threadId: SESSION,
item: { type: "reasoning", id: "rs_stdin", summary: [], content: ["Reading additional input from stdin..."] },
});
// Unstreamed, the completed item alone is dropped too.
notify(agent, "item/completed", {
threadId: SESSION,
item: { type: "reasoning", id: "rs_2", summary: ["Reading additional input from stdin..."], content: [] },
});
assert.deepEqual(Array.from(events), []);
// Text that starts the same way but goes on is sent whole.
notify(agent, "item/reasoning/textDelta", { threadId: SESSION, itemId: "rs_3", delta: "Reading " });
notify(agent, "item/reasoning/textDelta", { threadId: SESSION, itemId: "rs_3", delta: "the parser" });
notify(agent, "item/reasoning/textDelta", { threadId: SESSION, itemId: "rs_3", delta: " first" });
// A held prefix that ends the item is not lost.
notify(agent, "item/reasoning/textDelta", { threadId: SESSION, itemId: "rs_4", delta: "Read" });
notify(agent, "item/completed", {
threadId: SESSION,
item: { type: "reasoning", id: "rs_4", summary: ["Read"], content: [] },
});
assert.deepEqual(
events.map((event) => [event.itemId, event.text, event.mode]),
[
["rs_3", "Reading the parser", "append"],
["rs_3", " first", "append"],
["rs_4", "Read", "append"],
],
);
});
test("a command streams its output, then completes", () => {
const { agent, events } = harness();
const item = (status: string, output: string | null, exitCode: number | null) => ({
type: "commandExecution",
id: "exec-1",
command: "ls -a",
cwd: "/w",
status,
commandActions: [{ type: "listFiles", command: "ls -a", path: "/w" }],
aggregatedOutput: output,
exitCode,
});
notify(agent, "item/started", {
threadId: SESSION,
turnId: "turn-1",
startedAtMs: 0,
item: item("inProgress", null, null),
});
notify(agent, "item/commandExecution/outputDelta", {
threadId: SESSION,
turnId: "turn-1",
itemId: "exec-1",
delta: ".\n..\n",
});
notify(agent, "item/completed", {
threadId: SESSION,
turnId: "turn-1",
completedAtMs: 0,
item: item("completed", ".\n..\n", 0),
});
const [started, output, completed] = events;
assert.equal(started.event, "tool_call_started");
assert.deepEqual([started.id, started.kind, started.status], ["exec-1", "search", "running"]);
assert.deepEqual(output, {
sessionId: SESSION,
runId: RUN,
event: "tool_call_updated",
id: "exec-1",
outputDelta: ".\n..\n",
});
assert.equal(completed.event, "tool_call_updated");
assert.equal(completed.status, "completed");
assert.deepEqual(completed.content, [
{ type: "terminal", command: "ls -a", cwd: "/w", output: ".\n..\n", exitCode: 0 },
]);
assert.deepEqual(completed.locations, [{ path: "/w" }]);
});
test("a tool that completes without starting is reported once, as started", () => {
const { agent, events } = harness();
notify(agent, "item/completed", { threadId: SESSION, item: { type: "webSearch", id: "ws-1", query: "rust" } });
assert.deepEqual(
events.map((event) => event.event),
["tool_call_started"],
);
});
test("a file edit reports a unified diff, and patch updates replace it", () => {
const { agent, events } = harness();
notify(agent, "item/started", {
threadId: SESSION,
turnId: "turn-1",
startedAtMs: 0,
item: {
type: "fileChange",
id: "fc-1",
status: "inProgress",
changes: [{ path: "/w/a.rs", kind: { type: "update", move_path: null }, diff: "@@ -1 +1 @@\n-a\n+b\n" }],
},
});
notify(agent, "item/fileChange/patchUpdated", {
threadId: SESSION,
itemId: "fc-1",
changes: [{ path: "/w/a.rs", diff: "@@ -1 +1 @@\n-a\n+c\n" }],
});
const [started, patched] = events;
assert.equal(started.kind, "edit");
assert.equal(present(started.locations)[0].path, "/w/a.rs");
assert.deepEqual(started.content, [{ type: "diff", path: "/w/a.rs", diff: "@@ -1 +1 @@\n-a\n+b\n" }]);
assert.deepEqual(patched.content, [{ type: "diff", path: "/w/a.rs", diff: "@@ -1 +1 @@\n-a\n+c\n" }]);
});
test("a completed turn finishes the run exactly once", () => {
const { agent, events } = harness();
const completed = {
threadId: SESSION,
turn: { id: "turn-1", items: [], itemsView: "summary", status: "completed", error: null },
};
notify(agent, "turn/completed", completed);
notify(agent, "turn/completed", completed);
assert.deepEqual(events, [
{ sessionId: SESSION, runId: RUN, event: "run_finished", outcome: { status: "completed" } },
]);
assert.equal(present(agent.sessions.get(SESSION)).turn, null);
});
test("an interrupted turn is cancelled and a failed one reports the codex message", () => {
const { agent, events } = harness();
notify(agent, "turn/completed", {
threadId: SESSION,
turn: { id: "turn-1", items: [], status: "failed", error: { message: "model overloaded" } },
});
assert.deepEqual(events[0].outcome, { status: "failed", message: "model overloaded" });
const other = harness();
notify(other.agent, "turn/completed", {
threadId: SESSION,
turn: { id: "turn-1", items: [], status: "interrupted" },
});
assert.deepEqual(other.events[0].outcome, { status: "cancelled" });
const third = harness();
notify(third.agent, "turn/completed", { threadId: SESSION, turn: { id: "turn-1", status: "inProgress" } });
assert.equal(third.events.length, 0, "a turn still in progress ends nothing");
});
test("error notifications leave notices but only a terminal turn fails the run", () => {
const { agent, events } = harness();
notify(agent, "error", { threadId: SESSION, turnId: "turn-1", willRetry: true, error: { message: "retrying" } });
notify(agent, "error", {
threadId: SESSION,
turnId: "turn-1",
willRetry: false,
error: { message: "stream closed" },
});
assert.equal(agent.runOf(SESSION), RUN);
assert.equal(events.filter((event) => event.event === "run_finished").length, 0);
notify(agent, "turn/completed", {
threadId: SESSION,
turn: { id: "turn-1", status: "failed", error: { message: "stream closed" } },
});
assert.deepEqual(
events.map((event) => [
event.event,
event.level ?? event.outcome?.status,
event.message ?? (event.outcome?.status === "failed" ? event.outcome.message : undefined),
]),
[
["notice", "error", "retrying"],
["notice", "error", "stream closed"],
["run_finished", "failed", "stream closed"],
],
);
});
test("token usage and thread names reach the host", () => {
const { agent, events } = harness();
const breakdown = (totalTokens: number) => ({
totalTokens,
inputTokens: 0,
cachedInputTokens: 0,
cacheWriteInputTokens: 0,
outputTokens: 0,
reasoningOutputTokens: 0,
});
notify(agent, "thread/tokenUsage/updated", {
threadId: SESSION,
turnId: "turn-1",
tokenUsage: { total: breakdown(100), last: breakdown(40), modelContextWindow: 1000 },
});
notify(agent, "thread/name/updated", { threadId: SESSION, threadName: "Fix the parser" });
assert.deepEqual(events[0], { sessionId: SESSION, runId: RUN, event: "usage", usedTokens: 40, contextWindow: 1000 });
assert.deepEqual(events[1], { sessionId: SESSION, runId: RUN, event: "session_info", title: "Fix the parser" });
});
test("plans, warnings, reroutes and rate limits reach the host", () => {
const { agent, events } = harness();
notify(agent, "turn/plan/updated", { threadId: SESSION, plan: [{ step: "one", status: "inProgress" }] });
notify(agent, "warning", { threadId: SESSION, message: "slow" });
notify(agent, "warning", { message: "no thread" });
notify(agent, "configWarning", { message: "bad key" });
notify(agent, "model/rerouted", { threadId: SESSION, fromModel: "a", toModel: "b" });
notify(agent, "item/mcpToolCall/progress", { threadId: SESSION, itemId: "m1", message: "halfway" });
notify(agent, "account/rateLimits/updated", { rateLimits: { primary: { usedPercent: 5, windowDurationMins: 300 } } });
notify(agent, "item/completed", { threadId: SESSION, item: { type: "contextCompaction", id: "c1" } });
assert.deepEqual(
events.map((event) => event.event),
["plan", "notice", "notice", "notice", "tool_call_updated", "usage_limits", "compacted"],
);
assert.deepEqual(events[0].entries, [{ content: "one", status: "in_progress" }]);
assert.deepEqual([events[2].sessionId, events[2].level, events[2].message], ["", "warning", "bad key"]);
assert.equal(events[3].message, "Codex switched from a to b.");
assert.equal(events[4].outputDelta, "halfway\n");
assert.equal(events[5].sessionId, SESSION);
assert.equal(present(events[5].windows)[0].label, "5 hours");
});
test("an approval holds the server request until the user answers", async () => {
const { agent, events, api } = harness();
await agent.client();
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 7,
method: "item/commandExecution/requestApproval",
params: {
kind: "command",
threadId: SESSION,
turnId: "turn-1",
itemId: "exec-9",
startedAtMs: 0,
environmentId: null,
command: "rm -rf build",
cwd: "/w",
commandActions: [{ type: "unknown", command: "rm -rf build" }],
},
});
await settle();
const request = present(events.find((event) => event.event === "approval"));
assert.equal(request.title, "Run a command");
assert.equal(present(request.toolCall).id, "exec-9");
assert.equal(present(request.toolCall).kind, "execute");
assert.equal(present(request.toolCall).status, "pending");
assert.deepEqual(
present(request.options).map((option) => option.kind),
["allow_once", "allow_always", "reject_once", "reject_always"],
);
assert.equal(
peer.received.some((message) => message.id === 7),
false,
"nothing is answered before the user",
);
await agent.respondToApproval({ approvalId: request.id, optionId: "acceptForSession" });
const reply = await peer.waitFor((message) => message.id === 7);
assert.deepEqual(reply.result, { decision: "acceptForSession" });
assert.equal(agent.approvals.size, 0);
await assert.rejects(
agent.respondToApproval({ approvalId: request.id, optionId: "accept" }),
/no codex approval is waiting/,
);
});
test("an unknown approval option declines, and a file change asks with its reason", async () => {
const { agent, events, api } = harness();
await agent.client();
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 8,
method: "item/fileChange/requestApproval",
params: { threadId: SESSION, turnId: "t", itemId: "fc-2", grantRoot: "/w/out" },
});
await settle();
const request = present(events.find((event) => event.event === "approval"));
assert.equal(request.title, "Allow writes under /w/out");
assert.deepEqual([present(request.toolCall).kind, present(request.toolCall).name], ["edit", "apply_patch"]);
await agent.respondToApproval({ approvalId: request.id, optionId: "maybe" });
assert.deepEqual((await peer.waitFor((message) => message.id === 8)).result, { decision: "decline" });
});
// Codex's file change approval names the item only: the card shows the
// changes the item started with.
test("a file change approval carries the item's diffs", async () => {
const { agent, events, api } = harness();
await agent.client();
const peer = api.peers[0];
notify(agent, "item/started", {
threadId: SESSION,
turnId: "t",
startedAtMs: 0,
item: {
type: "fileChange",
id: "fc-3",
status: "inProgress",
changes: [{ path: "/w/new.js", kind: { type: "add" }, diff: "x;\n" }],
},
});
peer.send({
jsonrpc: "2.0",
id: 9,
method: "item/fileChange/requestApproval",
params: { threadId: SESSION, turnId: "t", itemId: "fc-3", startedAtMs: 0 },
});
await settle();
const request = present(events.find((event) => event.event === "approval"));
assert.equal(request.title, "Apply file changes");
assert.deepEqual(present(request.toolCall).content, [
{ type: "diff", path: "/w/new.js", oldText: "", newText: "x;\n" },
]);
});
// An approval answered in another Codex client used to leave our card
// waiting for an answer that could never arrive.
test("an approval answered elsewhere is withdrawn and never answered", async () => {
const { agent, events, api } = harness();
await agent.client();
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 11,
method: "item/commandExecution/requestApproval",
params: { threadId: SESSION, turnId: "turn-1", itemId: "exec-9", command: "rm -rf build" },
});
await settle();
const approval = present(events.find((event) => event.event === "approval"));
peer.send({ jsonrpc: "2.0", method: "serverRequest/resolved", params: { threadId: SESSION, requestId: 11 } });
await settle();
const resolved = present(events.find((event) => event.event === "approval_resolved"));
assert.equal(resolved.id, approval.id);
assert.equal(agent.approvals.size, 0);
await settle();
assert.equal(
peer.received.some((message) => message.id === 11),
false,
"a late reply was sent for a resolved request",
);
});
test("a question is answered with codex's answer map, or withdrawn", async () => {
const { agent, events, api } = harness();
await agent.client();
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: "q-1",
method: "item/tool/requestUserInput",
params: {
threadId: SESSION,
turnId: "t",
itemId: "i",
questions: [
{
id: "target",
header: "Target",
question: "Which one?",
isOther: true,
options: [
{ label: "Desktop", description: "The app" },
{ label: "Mobile", description: "" },
],
},
{ id: "notes", header: "", question: "Anything else?", isOther: false, options: null },
{ id: "flag", header: "Flag", question: "", options: null },
],
},
});
await settle();
const question = present(events.find((event) => event.event === "question"));
assert.equal(question.responseMode, "tool");
assert.deepEqual(question.fields[0], {
id: "target",
label: "Target",
description: "Which one?",
kind: "select",
allowOther: true,
required: true,
options: [
{ value: "Desktop", label: "Desktop", description: "The app" },
{ value: "Mobile", label: "Mobile" },
],
});
assert.deepEqual(
[question.fields[1].label, question.fields[1].kind, question.fields[1].allowOther],
["Anything else?", "text", false],
);
await agent.respondToQuestion({
questionId: question.id,
answer: { values: { target: "Mobile", notes: ["a", "b"], flag: true }, cancelled: false },
});
const reply = await peer.waitFor((message) => message.id === "q-1");
assert.deepEqual(reply.result, {
answers: { target: { answers: ["Mobile"] }, notes: { answers: ["a", "b"] }, flag: { answers: ["true"] } },
});
peer.send({
jsonrpc: "2.0",
id: 12,
method: "item/tool/requestUserInput",
params: { threadId: SESSION, questions: [{ id: "x", question: "?" }] },
});
await settle();
peer.send({ jsonrpc: "2.0", method: "serverRequest/resolved", params: { requestId: 12 } });
await settle();
assert.equal(events.filter((event) => event.event === "question_resolved").length, 1);
assert.equal(agent.questions.size, 0);
});
test("server requests nobody here answers are refused at once", async () => {
const { agent, api } = harness();
await agent.client();
const peer = api.peers[0];
peer.send({ jsonrpc: "2.0", id: 21, method: "item/permissions/requestApproval", params: {} });
const reply = await peer.waitFor((message) => message.id === 21);
assert.equal(z.object({ code: z.number() }).parse(reply.error).code, -32601);
// An elicitation in a mode nothing here renders is refused the same way.
peer.send({
jsonrpc: "2.0",
id: 22,
method: "mcpServer/elicitation/request",
params: { threadId: SESSION, mode: "telepathy", message: "?" },
});
assert.equal(
z.object({ code: z.number() }).parse((await peer.waitFor((message) => message.id === 22)).error).code,
-32601,
);
});
// Computer Use and other MCP tools ask through an elicitation rather than
// either of Codex's command/file approval methods. The advertised
// persistence scopes must become choices, then return in response
// metadata instead of being copied into ordinary form content.
test("an MCP tool elicitation becomes an approval with persistence", async () => {
const { agent, events, api } = harness();
await agent.client();
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 8,
method: "mcpServer/elicitation/request",
params: {
threadId: SESSION,
turnId: "turn-1",
serverName: "cua_repl",
mode: "form",
_meta: {
codex_approval_kind: "mcp_tool_call",
persist: ["session", "always"],
tool_name: "get_app_state",
tool_params_display: [{ display_name: "App", value: "Convergence Dev" }],
},
message: 'Allow Computer Use to use "Convergence Dev"?',
requestedSchema: { type: "object", properties: {} },
},
});
await settle();
const request = present(events.find((event) => event.event === "approval"));
assert.equal(request.title, 'Allow Computer Use to use "Convergence Dev"?');
assert.deepEqual(
present(request.options).map((option) => [option.id, option.name]),
[
["accept", "Allow once"],
["acceptForSession", "Allow for this session"],
["acceptAlways", "Always allow"],
["cancel", "Cancel"],
],
);
assert.equal(
peer.received.some((message) => message.id === 8),
false,
"nothing is answered before the user",
);
await agent.respondToApproval({ approvalId: request.id, optionId: "acceptAlways" });
const reply = await peer.waitFor((message) => message.id === 8);
assert.deepEqual(reply.result, { action: "accept", content: {}, _meta: { persist: "always" } });
assert.equal(agent.approvals.size, 0);
// A single advertised scope offers only that scope.
peer.send({
jsonrpc: "2.0",
id: 18,
method: "mcpServer/elicitation/request",
params: {
threadId: SESSION,
mode: "form",
_meta: { codex_approval_kind: "mcp_tool_call", persist: "session" },
message: "Allow?",
},
});
await settle();
const second = events.filter((event) => event.event === "approval")[1];
assert.deepEqual(
present(second.options).map((option) => option.id),
["accept", "acceptForSession", "cancel"],
);
await agent.respondToApproval({ approvalId: second.id, optionId: "cancel" });
assert.deepEqual((await peer.waitFor((message) => message.id === 18)).result, {
action: "cancel",
content: null,
_meta: null,
});
});
test("an MCP form elicitation round-trips structured answers", async () => {
const { agent, events, api } = harness();
await agent.client();
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 9,
method: "mcpServer/elicitation/request",
params: {
threadId: SESSION,
turnId: "turn-1",
serverName: "deploy",
mode: "form",
_meta: null,
message: "Configure the deployment",
requestedSchema: {
type: "object",
properties: {
branch: { type: "string", title: "Branch", description: "Git branch to deploy" },
force: { type: "boolean", title: "Force" },
target: {
type: "string",
title: "Target",
oneOf: [
{ const: "main", title: "Production" },
{ const: "dev", title: "Staging" },
],
},
reviewers: { type: "array", title: "Reviewers", items: { type: "string", enum: ["ada", "grace"] } },
attempts: { type: "integer", title: "Attempts" },
},
required: ["branch", "target"],
},
},
});
await settle();
const request = present(events.find((event) => event.event === "question"));
assert.equal(request.message, "Configure the deployment");
const field = (id: string) => present(request.fields.find((candidate) => candidate.id === id));
assert.equal(field("branch").label, "Branch");
assert.equal(field("branch").description, "Git branch to deploy");
assert.equal(field("branch").kind, "text");
assert.equal(field("branch").required, true);
assert.equal(field("force").kind, "boolean");
assert.equal(field("force").required, false);
assert.equal(field("target").kind, "select");
assert.equal(present(field("target").options)[0].label, "Production");
assert.equal(field("reviewers").kind, "multi_select");
assert.deepEqual(
present(field("reviewers").options).map((option) => option.value),
["ada", "grace"],
);
assert.equal(field("attempts").kind, "text");
await agent.respondToQuestion({
questionId: request.id,
answer: {
values: { branch: "feature/cua", force: true, target: "dev", reviewers: ["ada", "grace"], attempts: "3" },
cancelled: false,
},
});
const reply = await peer.waitFor((message) => message.id === 9);
assert.deepEqual(reply.result, {
action: "accept",
content: { branch: "feature/cua", force: true, target: "dev", reviewers: ["ada", "grace"], attempts: 3 },
_meta: null,
});
assert.equal(agent.questions.size, 0);
});
test("an MCP URL elicitation becomes an external link question", async () => {
const { agent, events, api } = harness();
await agent.client();
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 10,
method: "mcpServer/elicitation/request",
params: {
threadId: SESSION,
turnId: "turn-1",
serverName: "github",
mode: "url",
_meta: null,
message: "Sign in to GitHub to continue",
url: "https://example.com/oauth/start",
elicitationId: "oauth-1",
},
});
await settle();
const request = present(events.find((event) => event.event === "question"));
assert.equal(request.message, "Sign in to GitHub to continue");
assert.equal(request.url, "https://example.com/oauth/start");
assert.deepEqual(request.fields, []);
await agent.respondToQuestion({ questionId: request.id, answer: { values: {}, cancelled: false } });
assert.deepEqual((await peer.waitFor((message) => message.id === 10)).result, {
action: "accept",
content: null,
_meta: null,
});
// Anything but an HTTP(S) page is refused with a notice, never shown.
peer.send({
jsonrpc: "2.0",
id: 11,
method: "mcpServer/elicitation/request",
params: { threadId: SESSION, mode: "url", message: "Open", url: "file:///etc/passwd" },
});
assert.deepEqual((await peer.waitFor((message) => message.id === 11)).result, {
action: "cancel",
content: null,
_meta: null,
});
assert.equal(events.filter((event) => event.event === "question").length, 1);
assert.ok(
events.some(
(event) => event.event === "notice" && event.level === "error" && /invalid external URL/.test(event.message),
),
);
});
// `item/tool/call` goes to the host with the agent's id, the session and
// Codex's call id, and the host's result goes back as content items.
test("a dynamic tool call is answered with the host's result", async () => {
const { agent, api } = harness({
onHost: (method, params) => {
if (method === "host/tools.call") return { content: [{ type: "text", text: "12:00" }] };
return {};
},
});
await agent.client();
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 5,
method: "item/tool/call",
params: {
threadId: SESSION,
turnId: "turn-1",
callId: "call-7",
namespace: null,
tool: "now",
arguments: { zone: "UTC" },
},
});
const reply = await peer.waitFor((message) => message.id === 5);
assert.deepEqual(reply.result, { contentItems: [{ type: "inputText", text: "12:00" }], success: true });
assert.deepEqual(present(api.hostCalls.find((call) => call.method === "host/tools.call")).params, {
agentId: "codex",
sessionId: SESSION,
name: "now",
input: { zone: "UTC" },
callId: "call-7",
});
});
test("a subagent's tool call belongs to the chat that spawned it, and a failure is a failed result", async () => {
const { agent, api } = harness({
onHost: (method) => {
if (method === "host/tools.call") throw new Error("no tool named now");
return {};
},
});
await agent.client();
notify(agent, "item/started", { threadId: SESSION, item: spawnItem("call-1", SESSION, "child-1", "One") });
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 6,
method: "item/tool/call",
params: { threadId: "child-1", callId: "c", tool: "now", arguments: {} },
});
const reply = await peer.waitFor((message) => message.id === 6);
assert.deepEqual(reply.result, { contentItems: [{ type: "inputText", text: "no tool named now" }], success: false });
assert.equal(present(api.hostCalls.find((call) => call.method === "host/tools.call")).params.sessionId, SESSION);
});
test("a dynamic tool call item shows its result", () => {
const { agent, events } = harness();
const item = (status: string, contentItems: wire.Item["contentItems"], success: boolean | null) => ({
threadId: SESSION,
turnId: "turn-1",
item: {
type: "dynamicToolCall",
id: "call-7",
namespace: null,
tool: "execute",
arguments: { code: "return await now()" },
status,
contentItems,
success,
durationMs: null,
},
});
notify(agent, "item/started", item("inProgress", null, null));
notify(agent, "item/completed", item("failed", [{ type: "inputText", text: "no clock" }], false));
assert.equal(events[0].event, "tool_call_started");
assert.equal(events[0].title, "execute");
assert.equal(events[0].status, "running");
assert.equal(z.object({ code: z.string() }).parse(events[0].input).code, "return await now()");
assert.equal(events[1].event, "tool_call_updated");
assert.equal(events[1].status, "failed");
assert.deepEqual(events[1].content, [{ type: "text", text: "no clock" }]);
});
// Codex runs each subagent in its own thread. Its events must reach the
// parent chat tagged with the task.
test("a spawned subagent becomes a task", () => {
const { agent, events } = harness();
notify(agent, "item/started", {
threadId: SESSION,
turnId: "turn-1",
item: {
type: "collabAgentToolCall",
id: "collab-1",
tool: "spawnAgent",
senderThreadId: SESSION,
receiverThreadIds: ["child-1"],
agentsStates: {},
prompt: "Search the tests\nand report",
},
});
const task = present(events.shift());
assert.equal(task.event, "task");
assert.deepEqual(
[task.id, task.title, task.status, task.sessionId, task.taskId],
["child-1", "Search the tests", "running", SESSION, undefined],
);
notify(agent, "item/agentMessage/delta", {
threadId: "child-1",
turnId: "turn-9",
itemId: "msg_1",
delta: "found it",
});
const text = present(events.shift());
assert.equal(text.sessionId, SESSION, "the child's events belong to the parent chat");
assert.equal(text.taskId, "child-1");
});
// Multi-agent v1: a `spawnAgent` call names the child, and `wait` reports
// its final answer in `agentsStates`. The answer is the result; the title
// stays the prompt's first line.
test("a v1 subagent keeps its prompt title and shows its answer", () => {
const { agent, events } = harness();
notify(agent, "item/completed", {
threadId: SESSION,
turnId: "turn-1",
item: spawnItem("call-spawn", SESSION, "child-1", "Count the tests\nthen report"),
});
let task = present(events.pop());
assert.equal(task.title, "Count the tests");
assert.equal(task.prompt, "Count the tests\nthen report");
assert.equal(task.toolCallId, "call-spawn");
assert.deepEqual([task.model, task.effort], ["gpt-5.5", "low"]);
assert.equal(task.status, "running");
assert.equal(task.parentTaskId, undefined);
// The child's own record names it.
notify(agent, "thread/started", {
thread: {
id: "child-1",
preview: "Count the tests\nthen report",
createdAt: 1,
updatedAt: 1,
parentThreadId: SESSION,
agentNickname: "Euler",
agentRole: "explorer",
source: {
subAgent: {
thread_spawn: {
parent_thread_id: SESSION,
depth: 1,
agent_path: null,
agent_nickname: "Euler",
agent_role: "explorer",
},
},
},
},
});
task = present(events.pop());
assert.equal(task.name, "Euler");
assert.equal(task.title, "Count the tests");
assert.equal(task.startedAt, new Date(1000).toISOString(), "the earlier start wins");
// Its turn ends: done for now, but it can be given more.
notify(agent, "turn/started", { threadId: "child-1", turn: { id: "turn-c1", items: [], status: "inProgress" } });
notify(agent, "item/completed", {
threadId: "child-1",
turnId: "turn-c1",
item: { type: "agentMessage", id: "msg-c1", text: "There are 42 tests." },
});
notify(agent, "turn/completed", { threadId: "child-1", turn: { id: "turn-c1", items: [], status: "completed" } });
const last = present(tasks(events).pop());
assert.equal(last.status, "idle");
assert.equal(last.summary, "There are 42 tests.");
assert.ok(last.endedAt);
assert.equal(
events.some((event) => event.event === "run_finished"),
false,
"a child's turn is not the chat's run",
);
// `wait` returns the final answer and closes it for good.
events.length = 0;
notify(agent, "item/completed", {
threadId: SESSION,
turnId: "turn-1",
item: {
type: "collabAgentToolCall",
id: "call-wait",
tool: "wait",
status: "completed",
senderThreadId: SESSION,
receiverThreadIds: ["child-1"],
prompt: null,
agentsStates: { "child-1": { status: "completed", message: "Final: 42 tests." } },
},
});
task = present(events.pop());
assert.equal(task.status, "completed");
assert.equal(task.summary, "Final: 42 tests.");
assert.equal(task.title, "Count the tests", "the answer never becomes the title");
// A later finished turn does not reopen it.
notify(agent, "turn/completed", { threadId: "child-1", turn: { id: "turn-c2", status: "completed" } });
assert.equal(present(agent.subagents.get("child-1")).task.status, "completed");
});
// A grandchild reports into the same chat, inside the middle child's task;
// its usage stays on its task.
test("a grandchild reaches the chat inside its parent task", () => {
const { agent, events } = harness();
notify(agent, "item/started", {
threadId: SESSION,
turnId: "turn-1",
item: spawnItem("call-1", SESSION, "child-1", "Plan the work"),
});
notify(agent, "item/started", {
threadId: "child-1",
turnId: "turn-c1",
item: spawnItem("call-2", "child-1", "grandchild-1", "Read the parser"),
});
const grandchild = present(events.at(-1));
assert.equal(grandchild.id, "grandchild-1");
assert.equal(grandchild.parentTaskId, "child-1");
assert.ok(events.every((event) => event.sessionId === SESSION));
assert.equal(grandchild.taskId, undefined, "a task event itself is placed by parentTaskId");
notify(agent, "item/agentMessage/delta", {
threadId: "grandchild-1",
turnId: "turn-g1",
itemId: "msg-g",
delta: "parsed",
});
const text = present(events.at(-1));
assert.equal(text.sessionId, SESSION);
assert.equal(text.taskId, "grandchild-1");
const breakdown = (totalTokens: number) => ({
totalTokens,
inputTokens: 0,
cachedInputTokens: 0,
outputTokens: 0,
reasoningOutputTokens: 0,
});
notify(agent, "thread/tokenUsage/updated", {
threadId: "grandchild-1",
turnId: "turn-g1",
tokenUsage: { total: breakdown(900), last: breakdown(300), modelContextWindow: 1000 },
});
const usage = present(events.at(-1));
assert.deepEqual(usage, { sessionId: SESSION, runId: RUN, taskId: "grandchild-1", event: "usage", usedTokens: 900 });
assert.deepEqual(present(agent.subagents.get("grandchild-1")).task.usage, { usedTokens: 900 });
});
test("a subagent's tools count and show as its activity", () => {
const { agent, events } = harness();
notify(agent, "item/started", { threadId: SESSION, item: spawnItem("call-1", SESSION, "child-1", "One") });
const command = (status: string) => ({
type: "commandExecution",
id: "exec-c",
command: "cargo test",
status,
commandActions: [],
});
notify(agent, "item/started", { threadId: "child-1", item: command("inProgress") });
let task = present(tasks(events).at(-1));
assert.deepEqual([task.toolUses, task.activity], [1, "cargo test"]);
notify(agent, "item/completed", { threadId: "child-1", item: command("completed") });
const update = present(events.at(-1));
assert.equal(update.event, "tool_call_updated", "a started tool is updated, not started again");
assert.equal(update.taskId, "child-1");
notify(agent, "turn/completed", { threadId: "child-1", turn: { id: "t", status: "completed" } });
task = present(tasks(events).at(-1));
assert.equal(task.activity, undefined, "an ended subagent has no activity");
});
// Multi-agent v2 spawns appear only as `subAgentActivity`. Inside a child,
// an activity naming `/root` is a message to the chat, not a subagent.
test("a v2 activity starts and ends a subagent", async () => {
const { agent, events } = harness();
const activity = (thread: string, kind: string, target: string, path: string) => ({
threadId: thread,
turnId: "t",
item: { type: "subAgentActivity", id: `act-${kind}-${target}`, kind, agentThreadId: target, agentPath: path },
});
notify(agent, "item/completed", activity(SESSION, "started", "child-2", "/root/scout"));
const task = present(events.shift());
assert.deepEqual(
[task.id, task.title, task.status],
["child-2", "scout", "running"],
"the task name, since v2 encrypts the instruction",
);
notify(agent, "item/completed", activity("child-2", "interacted", SESSION, "/root"));
assert.equal(events.length, 0, "a message to the chat changes nothing");
assert.equal(agent.subagents.has(SESSION), false);
notify(agent, "item/completed", activity(SESSION, "completed", "child-2", "/root/scout"));
assert.equal(present(events.shift()).status, "completed");
});
test("late child activity survives retirement of its parent turn", () => {
const { agent, events } = harness();
notify(agent, "item/started", { threadId: SESSION, item: spawnItem("spawn", SESSION, "child", "Scout") });
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "turn-1", status: "completed" } });
const state = present(agent.sessions.get(SESSION));
state.run = "successor";
state.turn = "turn-2";
notify(agent, "item/completed", {
threadId: SESSION,
turnId: "turn-1",
item: {
type: "subAgentActivity",
id: "activity",
kind: "completed",
agentThreadId: "child",
agentPath: "/root/scout",
},
});
assert.equal(tasks(events).at(-1)?.status, "completed");
assert.equal(state.run, "successor");
});
test("a v2 child without a prompt or name is described from its own thread record", async () => {
const { agent, events } = harness({
handlers: {
"thread/read": (params) => ({
thread: { id: params.threadId, preview: "", agentNickname: "Scout", createdAt: 1 },
}),
},
});
await agent.client();
notify(agent, "item/completed", {
threadId: SESSION,
item: {
type: "subAgentActivity",
id: "a",
kind: "started",
agentThreadId: "child-3",
agentPath: "/root/find_todos",
},
});
await settle();
const task = present(tasks(events).at(-1));
assert.equal(task.name, "Scout");
assert.equal(task.title, "find todos");
});
test("a child spawned before a restart is adopted when the chat addresses it", async () => {
const { agent, events } = harness({
handlers: {
"thread/read": () => ({
thread: { id: "old-child", preview: "Audit the build\nplease", agentNickname: "Kepler" },
}),
},
});
await agent.client();
notify(agent, "item/completed", {
threadId: SESSION,
item: {
type: "collabAgentToolCall",
id: "w",
tool: "wait",
senderThreadId: SESSION,
receiverThreadIds: ["old-child"],
agentsStates: { "old-child": { status: "completed", message: "Done." } },
},
});
await settle();
const task = present(tasks(events).at(-1));
assert.deepEqual(
[task.id, task.status, task.summary, task.title, task.name],
["old-child", "completed", "Done.", "Audit the build", "Kepler"],
);
// `listAgents` adopts nobody.
notify(agent, "item/completed", {
threadId: SESSION,
item: {
type: "collabAgentToolCall",
id: "l",
tool: "listAgents",
receiverThreadIds: ["stranger"],
agentsStates: {},
},
});
assert.equal(agent.subagents.has("stranger"), false);
});
// `thread/started` reports every thread of the app-server; a review or a
// stranger's subagent is not a task of this chat.
test("only spawned threads of known chats become tasks", () => {
const { agent, events } = harness();
notify(agent, "thread/started", {
thread: { id: "review-1", preview: "", createdAt: 1, updatedAt: 1, source: { subAgent: "review" } },
});
notify(agent, "thread/started", {
thread: {
id: "other-child",
preview: "x",
createdAt: 1,
updatedAt: 1,
parentThreadId: "someone-else",
source: { subAgent: { thread_spawn: { parent_thread_id: "someone-else", depth: 1 } } },
},
});
assert.equal(events.length, 0);
assert.equal(agent.subagents.size, 0);
});
// A subagent's approval flips it to waiting.
test("a subagent waiting on the user is waiting", () => {
const { agent, events } = harness();
notify(agent, "item/started", {
threadId: SESSION,
turnId: "turn-1",
item: spawnItem("call-1", SESSION, "child-1", "Edit the file"),
});
events.length = 0;
notify(agent, "thread/status/changed", {
threadId: "child-1",
status: { type: "active", activeFlags: ["waitingOnApproval"] },
});
assert.equal(present(events.shift()).status, "waiting");
notify(agent, "thread/status/changed", { threadId: "child-1", status: { type: "active", activeFlags: [] } });
assert.equal(present(events.shift()).status, "running");
notify(agent, "thread/status/changed", { threadId: "child-1", status: { type: "idle" } });
assert.equal(events.length, 0);
});
// Stop interrupts every live subagent turn before the chat's own, and
// releases the approvals the subagents wait on.
test("stop interrupts subagents first and releases their approvals", async () => {
const interrupts: unknown[][] = [];
const { agent, events, api } = harness({
handlers: {
"turn/interrupt": (params) => {
interrupts.push([params.threadId, params.turnId]);
return {};
},
},
});
await agent.client();
notify(agent, "item/started", {
threadId: SESSION,
turnId: "turn-1",
item: spawnItem("call-1", SESSION, "child-1", "One"),
});
notify(agent, "item/started", {
threadId: "child-1",
turnId: "turn-c1",
item: spawnItem("call-2", "child-1", "grandchild-1", "Two"),
});
for (const [thread, turn] of [
["child-1", "turn-c1"],
["grandchild-1", "turn-g1"],
]) {
notify(agent, "turn/started", { threadId: thread, turn: { id: turn, items: [], status: "inProgress" } });
}
const peer = api.peers[0];
peer.send({
jsonrpc: "2.0",
id: 9,
method: "item/commandExecution/requestApproval",
params: { threadId: "grandchild-1", itemId: "x", command: "rm" },
});
await settle();
const approval = present(events.find((event) => event.event === "approval"));
assert.equal(approval.taskId, "grandchild-1", "a subagent's approval is tagged with its task");
await agent.cancel({ sessionId: SESSION });
assert.deepEqual(interrupts.at(-1), [SESSION, "turn-1"], "the chat last");
assert.deepEqual(interrupts.slice(0, 2).sort(), [
["child-1", "turn-c1"],
["grandchild-1", "turn-g1"],
]);
const released = await peer.waitFor((message) => message.id === 9);
assert.deepEqual(released.result, { decision: "cancel" });
assert.ok(events.some((event) => event.event === "run_finished" && event.outcome.status === "cancelled"));
});
// `cancel_task` stops one subagent (and what it started) and leaves the
// chat's run alone.
test("cancel_task interrupts only that subagent", async () => {
const interrupts: Record<string, unknown>[] = [];
const { agent } = harness({
handlers: {
"turn/interrupt": (params) => {
interrupts.push(params);
return {};
},
},
});
notify(agent, "item/started", {
threadId: SESSION,
turnId: "turn-1",
item: spawnItem("call-1", SESSION, "child-1", "One"),
});
await assert.rejects(agent.cancelTask({ sessionId: SESSION, taskId: "child-1" }), /not running/, "no live turn yet");
notify(agent, "turn/started", { threadId: "child-1", turn: { id: "turn-c1", items: [], status: "inProgress" } });
await assert.rejects(
agent.cancelTask({ sessionId: "another-chat", taskId: "child-1" }),
/no codex subagent/,
"not this chat's",
);
await agent.cancelTask({ sessionId: SESSION, taskId: "child-1" });
assert.deepEqual(interrupts, [{ threadId: "child-1", turnId: "turn-c1" }]);
assert.equal(present(agent.sessions.get(SESSION)).run, RUN, "the chat keeps running");
});
test("versions come from the app-server's user agent", () => {
assert.equal(versionOf("convergence/0.155.1 (Mac OS 26.5.0; arm64) iTerm.app"), "0.155.1");
assert.equal(versionOf("nothing"), null);
assert.equal(versionOf(null), null);
});
// A deadlock inside `initialize` once left every agent listing empty; here
// initialize must answer with the app-server's facts.
test("initialize starts the app-server once and describes the agent", async () => {
clearCache();
const { agent, api } = harness({
handlers: {
"account/read": () => ({ account: { type: "chatgpt", email: "me@example.com" }, requiresOpenaiAuth: true }),
"account/rateLimits/read": () => ({ rateLimits: { primary: { usedPercent: 10, windowDurationMins: 300 } } }),
},
onHost: (method) => {
if (method === "host/process.which") return { path: "/usr/local/bin/codex", realPath: "/usr/local/bin/codex" };
return {};
},
});
const [info] = await Promise.all([agent.initialize(), agent.client()]);
assert.equal(api.peers.filter((peer) => peer.program === "codex").length, 1, "one app-server for concurrent callers");
const peer = api.peers[0];
assert.deepEqual(peer.args, ["app-server"]);
assert.deepEqual(peer.options.env, { CODEX_HOME: "/Users/me/.codex" });
const init = present(peer.received.find((message) => message.method === "initialize"));
assert.ok(init);
assert.deepEqual(init.params.capabilities, { experimentalApi: true, requestAttestation: false });
assert.equal(z.object({ name: z.string() }).parse(init.params.clientInfo).name, "convergence");
assert.ok(peer.received.some((message) => message.method === "initialized" && message.id === undefined));
assert.equal(info.id, "codex");
assert.equal(info.version, "0.155.1");
assert.deepEqual(info.status, { state: "ready" });
assert.equal(present(info.capabilities).subagents, true);
assert.equal(present(info.capabilities).slashCommands, false);
assert.equal(info.family, "codex");
assert.equal(info.continuationKey, "/Users/me/.codex");
assert.ok(present(info.icon).startsWith("<svg"));
assert.equal(present(present(info.usageLimits).windows)[0].label, "5 hours");
assert.deepEqual(
info.maintenance,
{ canUpdate: false, installedVersion: "0.155.1" },
"an unproven installer stays manual",
);
});
test("a signed-out account needs a login, and a missing CLI is an error", async () => {
const { agent } = harness({ handlers: { "account/read": () => ({ account: null, requiresOpenaiAuth: true }) } });
const info = await agent.initialize();
assert.deepEqual(info.status, { state: "auth_required", message: "Sign in with `codex login`" });
const api = fakeApi({ onSpawn: () => new Error("codex is not on the PATH") });
const missing = testAgent(api);
await assert.rejects(missing.initialize(), /codex CLI could not be started.*not on the PATH/);
});
test("the maintenance check asks the installer that owns the CLI", async () => {
clearCache();
const npmReal = "/Users/me/.nvm/v24/lib/node_modules/@openai/codex/bin/codex.js";
const { agent, api } = harness({
handlers: { "account/read": () => ({ account: {} }) },
onHost: (method) =>
method === "host/process.which" ? { path: "/Users/me/.nvm/v24/bin/codex", realPath: npmReal } : {},
});
const original = api.process.spawn;
api.process.spawn = async (program, args, options) => {
const child = await original(program, args, options);
if (program === "npm") {
queueMicrotask(() => {
const peer = present(api.peers.at(-1));
peer.stdout.push("0.160.0\n");
peer.exit(0);
});
}
return child;
};
const info = await agent.initialize();
assert.deepEqual(info.maintenance, {
canUpdate: true,
installedVersion: "0.155.1",
latestVersion: "0.160.0",
manager: "npm",
});
const npm = present(api.peers.find((peer) => peer.program === "npm"));
assert.deepEqual(npm.args, ["view", "@openai/codex", "version", "--prefix", "/Users/me/.nvm/v24"]);
});
test("options come from the live catalog and the user's config", async () => {
const { agent } = harness({
handlers: {
"model/list": (params) =>
params.cursor === null
? {
data: [
{
id: "gpt-5.5",
displayName: "GPT-5.5",
isDefault: true,
supportedReasoningEfforts: [{ reasoningEffort: "high", description: "" }],
defaultReasoningEffort: "high",
},
],
nextCursor: "p2",
}
: {
data: [
{ id: "hidden", hidden: true },
{ id: "gpt-mini", supportedReasoningEfforts: [{ reasoningEffort: "low", description: "" }] },
],
nextCursor: null,
},
"config/read": (params) => {
assert.equal(params.cwd, "/w");
return { config: { approval_policy: "on-request", sandbox_mode: "workspace-write" } };
},
},
});
const { options } = await agent.listOptions({ workspace: "/w" });
const model = present(options.find((option) => option.id === "model"));
assert.deepEqual(
present(model.choices).map((choice) => choice.value),
["gpt-5.5", "gpt-mini"],
"hidden models are left out, all pages read",
);
assert.equal(present(options.find((option) => option.id === "permission_mode")).value, "auto_edits");
// Changing the model clears the reasoning chosen for the old one.
await agent.setOption({ sessionId: SESSION, optionId: "reasoning", value: "high" });
const { options: after } = await agent.setOption({ sessionId: SESSION, optionId: "model", value: "gpt-mini" });
assert.equal(present(agent.sessions.get(SESSION)).options.reasoning, undefined);
assert.equal(present(after.find((option) => option.id === "reasoning")).value, "low");
});
test("a prompt starts a turn with the session's settings, and one during a turn steers it", async () => {
const started: Record<string, unknown>[] = [];
const { agent, events, api } = harness({
handlers: {
"thread/start": (params) => {
started.push(params);
return { thread: { id: "thread-new" } };
},
"turn/start": (params) => ({
turn: {
id: `turn-for-${z.array(z.object({ text: z.string() })).parse(params.input)[0].text}`,
items: [],
status: "inProgress",
},
}),
"turn/steer": () => ({ turnId: "x" }),
},
});
const { sessionId } = await agent.createSession({
workspace: "/w2",
options: { model: "gpt-5.5", reasoning: "high", permission_mode: "full" },
tools: [{ name: "now", description: "Time", inputSchema: { type: "object", properties: {} } }],
instructions: "Be brief.",
});
assert.equal(sessionId, "thread-new");
assert.equal(started[0].cwd, "/w2");
assert.equal(started[0].developerInstructions, "Be brief.");
assert.equal(z.array(z.object({ name: z.string() })).parse(started[0].dynamicTools)[0].name, "now");
const { runId } = await agent.prompt({
sessionId,
input: { blocks: [{ type: "text", text: "hi" }], itemId: "host-item-1" },
});
assert.match(runId, /^codex-run-/);
const peer = api.peers[0];
const turn = await peer.waitFor((message) => message.method === "turn/start");
assert.deepEqual(
[
turn.params.threadId,
turn.params.model,
turn.params.effort,
turn.params.approvalPolicy,
z.object({ type: z.string() }).parse(turn.params.sandboxPolicy).type,
],
["thread-new", "gpt-5.5", "high", "never", "dangerFullAccess"],
);
await settle();
assert.equal(present(agent.sessions.get(sessionId)).turn, "turn-for-hi");
assert.equal(
present(agent.sessions.get(sessionId)).turns.get("host-item-1"),
"turn-for-hi",
"the host's message id names its turn",
);
const steered = await agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "also this" }] } });
assert.equal(steered.runId, runId, "a steer shares the run");
const steer = await peer.waitFor((message) => message.method === "turn/steer");
assert.deepEqual(steer.params, {
threadId: sessionId,
input: [{ type: "text", text: "also this", text_elements: [] }],
expectedTurnId: "turn-for-hi",
});
peer.send({
jsonrpc: "2.0",
method: "turn/completed",
params: { threadId: sessionId, turn: { id: "turn-for-hi", status: "completed" } },
});
await settle();
assert.deepEqual(
events.filter((event) => event.event === "run_finished").map((event) => event.runId),
[runId],
);
});
test("steering waits for start-in-flight without a second start or a replacement run", async () => {
const { agent, api } = harness({
handlers: { "turn/start": () => undefined, "turn/steer": () => ({ turnId: "turn-new" }) },
});
const state = present(agent.sessions.get(SESSION));
state.run = null;
state.turn = null;
const first = agent.prompt({
sessionId: SESSION,
input: {
delivery: { inputId: "first", attemptId: "start-attempt", intent: "send" },
blocks: [{ type: "text", text: "Inspect alpha" }],
},
});
await settle();
const peer = api.peers[0];
const start = await peer.waitFor((message) => message.method === "turn/start");
const originalRun = state.run;
const second = agent.prompt({
sessionId: SESSION,
input: {
delivery: { inputId: "second", attemptId: "steer-attempt", intent: "steer" },
blocks: [{ type: "text", text: "Do not change beta" }],
},
});
await settle();
assert.deepEqual(
peer.received.filter((message) => message.method?.startsWith("turn/")).map((message) => message.method),
["turn/start"],
);
peer.send({ method: "turn/started", params: { threadId: SESSION, turn: { id: "turn-new", status: "inProgress" } } });
const steer = await peer.waitFor((message) => message.method === "turn/steer");
assert.equal(steer.params.expectedTurnId, "turn-new");
assert.equal(steer.params.clientUserMessageId, "steer-attempt");
assert.equal(start.params.clientUserMessageId, "start-attempt");
peer.reply(start, { turn: { id: "turn-new" } });
const [started, steered] = await Promise.all([first, second]);
assert.equal(started.runId, originalRun);
assert.equal(steered.runId, originalRun);
assert.equal(state.run, originalRun);
assert.equal(peer.received.filter((message) => message.method === "turn/start").length, 1);
assert.deepEqual(steered.receipt, { evidence: "native_admission", nativeInputId: "steer-attempt" });
});
test("a turn that ends before its start reply positively rejects a waiting steer", async () => {
const { agent, events, api } = harness({ handlers: { "turn/start": () => undefined } });
const state = present(agent.sessions.get(SESSION));
state.run = null;
state.turn = null;
const { runId } = await agent.prompt({ sessionId: SESSION, input: { blocks: [{ type: "text", text: "hi" }] } });
const peer = api.peers[0];
const start = await peer.waitFor((message) => message.method === "turn/start");
const steer = agent.prompt({
sessionId: SESSION,
input: {
delivery: { inputId: "second", attemptId: "attempt", intent: "steer" },
blocks: [{ type: "text", text: "also" }],
},
});
const rejected = assert.rejects(steer, /ended before steering/);
await settle();
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "turn-new", status: "completed" } });
await rejected;
peer.reply(start, { turn: { id: "turn-new" } });
await settle();
assert.equal(state.run, null);
assert.equal(state.turn, null);
assert.equal(peer.received.filter((message) => message.method === "turn/start").length, 1);
assert.equal(peer.received.filter((message) => message.method === "turn/steer").length, 0);
assert.equal(events.find((event) => event.event === "input_rejected")?.attemptId, "attempt");
assert.deepEqual(
events.filter((event) => event.event === "run_finished").map((event) => event.runId),
[runId],
);
});
for (const failure of ["precondition", "transport", "malformed", "server"] as const) {
test(`steer ${failure} failure distinguishes rejection from uncertain admission`, async () => {
const { agent, events, api } = harness({
handlers: {
"turn/steer": (_params, message, peer) => {
if (failure === "transport") {
present(agent.proc).connection.close("lost the steer reply");
} else if (failure === "malformed") {
peer.reply(message, {});
} else {
peer.send({
id: message.id,
error: {
code: failure === "precondition" ? -32600 : -32603,
message: "expected active turn id `turn-1` but found `turn-2`",
},
});
}
},
},
});
await assert.rejects(
agent.prompt({
sessionId: SESSION,
input: {
delivery: { inputId: "followup", attemptId: "attempt", intent: "steer" },
blocks: [{ type: "text", text: "also" }],
},
}),
/turn\/steer failed/,
);
const rejection = events.find((event) => event.event === "input_rejected");
assert.equal(Boolean(rejection), failure === "precondition");
if (rejection) assert.deepEqual([rejection.inputId, rejection.attemptId], ["followup", "attempt"]);
assert.equal(present(agent.sessions.get(SESSION)).inputs.has("attempt"), failure !== "precondition");
assert.equal(agent.runOf(SESSION), RUN);
assert.equal(
events.some((event) => event.event === "run_finished"),
false,
);
assert.equal(api.peers[0].received.filter((message) => message.method === "turn/start").length, 0);
});
}
test("clientUserMessageId consumption is correlated, one-shot and retains its original run", async () => {
const { agent, events, api } = harness({ handlers: { "turn/steer": () => ({ turnId: "turn-1" }) } });
const result = await agent.prompt({
sessionId: SESSION,
input: {
delivery: { inputId: "followup", attemptId: "attempt", intent: "steer" },
blocks: [{ type: "text", text: "same text" }],
},
});
assert.equal(result.receipt?.evidence, "native_admission");
assert.equal(
events.some((event) => event.event === "input_consumed"),
false,
"a native reply alone is not consumption",
);
const peer = api.peers[0];
assert.equal(
(await peer.waitFor((message) => message.method === "turn/steer")).params.clientUserMessageId,
"attempt",
);
const item = {
type: "userMessage",
id: "native-user",
clientId: "attempt",
content: [{ type: "text", text: "same text" }],
};
notify(agent, "item/completed", { threadId: SESSION, item: { ...item, clientId: "unrelated" } });
notify(agent, "item/completed", { threadId: "other-thread", item });
assert.equal(
events.some((event) => event.event === "input_consumed"),
false,
);
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "turn-1", status: "completed" } });
const state = present(agent.sessions.get(SESSION));
state.run = "successor";
state.turn = "turn-2";
notify(agent, "item/started", { threadId: SESSION, turnId: "turn-1", item });
notify(agent, "item/completed", { threadId: SESSION, turnId: "turn-1", item });
assert.deepEqual(
events.filter((event) => event.event === "input_consumed"),
[{ sessionId: SESSION, runId: RUN, event: "input_consumed", inputId: "followup", nativeInputId: "native-user" }],
);
assert.equal(state.run, "successor");
});
test("old terminal events and late start replies cannot finish or retarget a successor", async () => {
const { agent, events, api } = harness({ handlers: { "turn/start": () => undefined } });
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "turn-1", status: "completed" } });
const { runId } = await agent.prompt({ sessionId: SESSION, input: { blocks: [{ type: "text", text: "new work" }] } });
const start = await api.peers[0].waitFor((message) => message.method === "turn/start");
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "turn-1", status: "failed" } });
notify(agent, "turn/started", { threadId: SESSION, turn: { id: "turn-1", status: "inProgress" } });
notify(agent, "error", {
threadId: SESSION,
turnId: "turn-1",
error: { message: "old quota", codexErrorInfo: "usageLimitExceeded" },
});
assert.equal(agent.runOf(SESSION), runId);
notify(agent, "turn/started", { threadId: SESSION, turn: { id: "turn-2", status: "inProgress" } });
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "unrecognized-old", status: "completed" } });
assert.equal(agent.runOf(SESSION), runId);
agent.finishRun(SESSION, { status: "cancelled" });
notify(agent, "turn/started", { threadId: SESSION, turn: { id: "turn-3", status: "inProgress" } });
const successor = agent.runOf(SESSION);
assert.ok(successor);
api.peers[0].reply(start, { turn: { id: "turn-2" } });
await settle();
assert.equal(present(agent.sessions.get(SESSION)).turn, "turn-3");
assert.equal(agent.runOf(SESSION), successor);
assert.equal(events.filter((event) => event.event === "run_finished").length, 2);
assert.equal(
events.some((event) => event.event === "usage_blocked"),
false,
);
});
test("autonomous turns start and finish once and a changed native turn is not filtered", () => {
const { agent, events } = harness();
notify(agent, "turn/started", { threadId: SESSION, turn: { id: "turn-next", status: "inProgress" } });
assert.equal(present(agent.sessions.get(SESSION)).turn, "turn-next");
assert.equal(agent.runOf(SESSION), RUN, "the active response keeps its run identity");
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "turn-1", status: "completed" } });
assert.equal(agent.runOf(SESSION), RUN);
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "turn-next", status: "completed" } });
notify(agent, "turn/started", { threadId: SESSION, turn: { id: "autonomous", status: "inProgress" } });
notify(agent, "turn/started", { threadId: SESSION, turn: { id: "autonomous", status: "inProgress" } });
const run = agent.runOf(SESSION);
assert.ok(run);
assert.notEqual(run, RUN);
notify(agent, "item/agentMessage/delta", {
threadId: SESSION,
turnId: "autonomous",
itemId: "answer",
delta: "done",
});
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "autonomous", status: "completed" } });
notify(agent, "turn/completed", { threadId: SESSION, turn: { id: "autonomous", status: "completed" } });
assert.deepEqual(
events.filter((event) => event.event === "run_started").map((event) => event.runId),
[run],
);
assert.deepEqual(
events.filter((event) => event.event === "run_finished").map((event) => event.runId),
[RUN, run],
);
assert.equal(events.find((event) => event.event === "text_delta")?.runId, run);
});
test("native quota warnings and retries leave tools and the run alive until turn/completed", async () => {
const { agent, events } = harness({
handlers: {
"account/rateLimits/read": () => ({
ordinaryUsageAllowed: false,
accountId: "account-one",
rateLimits: {
limitId: "codex",
rateLimitReachedType: "rate_limit_reached",
primary: { usedPercent: 100, resetsAt: 1_900_000_000 },
},
}),
},
});
await agent.limits();
notify(agent, "item/started", {
threadId: SESSION,
turnId: "turn-1",
item: { type: "commandExecution", id: "tool", command: "sleep 1", status: "inProgress" },
});
notify(agent, "warning", { threadId: SESSION, message: "limit nearly reached" });
for (const codexErrorInfo of [
"rateLimitExceeded",
"unauthorized",
"contextWindowExceeded",
{ httpConnectionFailed: { httpStatusCode: 429 } },
]) {
notify(agent, "error", {
threadId: SESSION,
turnId: "turn-1",
willRetry: true,
error: { message: "not quota evidence", codexErrorInfo },
});
}
assert.equal(
events.some((event) => event.event === "usage_blocked"),
false,
);
for (const willRetry of [true, false]) {
notify(agent, "error", {
threadId: SESSION,
turnId: "turn-1",
willRetry,
error: { message: "quota exhausted", codexErrorInfo: "usageLimitExceeded" },
});
assert.equal(agent.runOf(SESSION), RUN);
assert.equal(
events.some((event) => event.event === "run_finished"),
false,
);
}
notify(agent, "item/commandExecution/outputDelta", {
threadId: SESSION,
turnId: "turn-1",
itemId: "tool",
delta: "still running",
});
assert.equal(events.at(-1)?.outputDelta, "still running");
const recovery = present(events.find((event) => event.event === "usage_blocked")?.recovery);
assert.deepEqual(
[recovery.identity, recovery.scope, recovery.resetsAt],
["account-one", undefined, new Date(1_900_000_000_000).toISOString()],
);
notify(agent, "turn/completed", {
threadId: SESSION,
turn: {
id: "turn-1",
status: "failed",
error: { message: "quota exhausted", codexErrorInfo: "usageLimitExceeded" },
},
});
assert.equal(agent.runOf(SESSION), null);
assert.deepEqual(events.at(-1)?.outcome, { status: "failed", message: "quota exhausted" });
});
test("usage reads return cached native identity, latest reset and real availability", async () => {
let accountId = "account-one";
let snapshotAccount: string | null = null;
let allowed: boolean | null = false;
const { agent, events } = harness({
handlers: {
"account/read": () => ({ account: { type: "chatgpt" }, workspaceRouting: { chatgptAccountId: accountId } }),
"account/rateLimits/read": () => ({
accountId: snapshotAccount,
ordinaryUsageAllowed: allowed,
rateLimits: {
limitId: "codex",
rateLimitReachedType: "rate_limit_reached",
primary: { usedPercent: 100, resetsAt: 1_900_000_000 },
secondary: { usedPercent: 100, resetsAt: 1_900_100_000 },
},
}),
},
});
const limits = present(await agent.limits());
const recovery = present(limits.recovery);
assert.deepEqual(
[recovery.identity, recovery.availability, recovery.resetsAt],
["account-one", "blocked", new Date(1_900_100_000_000).toISOString()],
);
assert.deepEqual(agent.quotaFacts, recovery);
agent.onNotification("account/rateLimits/updated", { rateLimits: { primary: { usedPercent: 1 } } });
assert.equal(events.at(-1)?.recovery?.availability, "unknown", "a sparse meter cannot grant usage");
assert.equal(agent.quotaFacts?.identity, "account-one");
allowed = null;
assert.equal((await agent.limits())?.recovery?.availability, "unknown");
allowed = true;
assert.equal(
(await agent.limits())?.recovery?.availability,
"allowed",
"only native permission grants usage, even at 100%",
);
accountId = "account-two";
snapshotAccount = "account-one";
const mismatch = present((await agent.limits())?.recovery);
assert.equal(mismatch.availability, "unknown");
assert.equal(mismatch.resetsAt, undefined);
notify(agent, "account/updated", {});
assert.equal(agent.quotaFacts, null);
snapshotAccount = null;
allowed = false;
assert.equal((await agent.limits())?.recovery?.identity, "account-two");
});
test("an account change during a native usage read invalidates the in-flight evidence", async () => {
const { agent, api } = harness({
handlers: {
"account/read": () => ({ workspaceRouting: { chatgptAccountId: "account-one" } }),
"account/rateLimits/read": () => undefined,
},
});
const reading = agent.limits();
await settle();
const peer = api.peers[0];
const request = await peer.waitFor((message) => message.method === "account/rateLimits/read");
notify(agent, "account/updated", {});
peer.reply(request, { accountId: "account-one", ordinaryUsageAllowed: true });
assert.equal(await reading, null);
assert.equal(agent.quotaFacts, null);
});
test("a turn codex refuses fails the run with its message", async () => {
const { agent, events, api } = harness();
await agent.client();
api.peers[0].onMessage = (message, peer) => {
if (message.method === "turn/start")
peer.send({ jsonrpc: "2.0", id: message.id, error: { code: -32000, message: "model not found" } });
};
present(agent.sessions.get(SESSION)).run = null;
const { runId } = await agent.prompt({ sessionId: SESSION, input: { blocks: [{ type: "text", text: "hi" }] } });
await settle();
const finished = present(events.find((event) => event.event === "run_finished"));
assert.equal(finished.runId, runId);
assert.deepEqual(finished.outcome, { status: "failed", message: "turn/start failed: model not found" });
assert.equal(present(events.find((event) => event.event === "notice")).level, "error");
});
test("an app-server that exits fails every run and every live subagent", async () => {
const { agent, events, api } = harness();
await agent.client();
notify(agent, "item/started", { threadId: SESSION, item: spawnItem("call-1", SESSION, "child-1", "One") });
api.peers[0].exit(1);
await settle(10);
assert.equal(present(agent.subagents.get("child-1")).task.status, "failed");
assert.equal(present(agent.subagents.get("child-1")).task.summary, "The codex app-server exited");
const finished = present(events.find((event) => event.event === "run_finished"));
assert.deepEqual(finished.outcome, { status: "failed", message: "the codex app-server exited" });
// The next call starts a new one.
await agent.client();
assert.equal(api.peers.length, 2);
});
test("after the app-server restarts, an open thread is resumed before its next turn", async () => {
const { agent, api } = harness({
handlers: {
"thread/resume": () => ({ thread: { id: SESSION } }),
"turn/start": () => ({ turn: { id: "turn-2" } }),
},
});
await agent.client();
api.peers[0].exit(1);
await settle(10);
await agent.prompt({ sessionId: SESSION, input: { blocks: [{ type: "text", text: "again" }] } });
const peer = api.peers[1];
await peer.waitFor((message) => message.method === "turn/start");
const methods = peer.received.map((message) => message.method).filter((method) => method?.startsWith("t"));
assert.deepEqual(methods, ["thread/resume", "turn/start"]);
const resume = present(peer.received.find((message) => message.method === "thread/resume"));
assert.equal(resume.params.threadId, SESSION);
assert.equal(resume.params.cwd, "/w");
// Loaded now: the next turn goes straight out.
await settle();
present(agent.sessions.get(SESSION)).run = null;
present(agent.sessions.get(SESSION)).turn = null;
await agent.prompt({ sessionId: SESSION, input: { blocks: [{ type: "text", text: "more" }] } });
await settle();
assert.equal(peer.received.filter((message) => message.method === "thread/resume").length, 1);
});
test("sessions are listed for the workspace, without subagents", async () => {
const { agent } = harness({
handlers: {
"thread/list": (params) => {
assert.deepEqual(params, { cwd: "/w", limit: 100 });
return {
data: [
{
id: "a",
name: null,
preview: "Fix the parser\nplease",
createdAt: 1_700_000_000,
updatedAt: 1_700_000_100,
},
{ id: "b", preview: "x", parentThreadId: "a" },
],
};
},
},
});
const { sessions } = await agent.listSessions({ workspace: "/w" });
assert.deepEqual(sessions, [
{
id: "a",
title: "Fix the parser",
createdAt: new Date(1_700_000_000_000).toISOString(),
updatedAt: new Date(1_700_000_100_000).toISOString(),
},
]);
});
test("reading a session pages its turns, rebuilds its subagents and names it", async () => {
const { agent, events } = harness({
handlers: {
"thread/read": (params) =>
params.threadId === "root"
? { thread: { id: "root", name: null, preview: "Count the tests", historyMode: "paginated" } }
: { thread: { id: params.threadId, preview: "Child work", agentNickname: "Euler" } },
"thread/turns/list": (params) => {
if (params.threadId === "child-1") {
return {
data: [{ id: "tc", status: "completed", items: [{ type: "agentMessage", id: "cm", text: "42." }] }],
nextCursor: null,
};
}
assert.equal(params.itemsView, "full");
assert.equal(params.sortDirection, "asc");
return params.cursor === null
? {
data: [
{
id: "t1",
status: "completed",
items: [{ type: "userMessage", id: "u1", content: [{ type: "text", text: "Count the tests" }] }],
},
],
nextCursor: "c2",
}
: {
data: [
{
id: "t2",
status: "completed",
items: [
spawnItem("s", "root", "child-1", "Child work"),
{ type: "agentMessage", id: "m", text: "Done" },
],
},
],
nextCursor: null,
};
},
},
});
const { items } = await agent.readSession({ workspace: "/w", sessionId: "root" });
assert.deepEqual(
items.map((item) => item.role),
["user", "task", "assistant"],
);
assert.ok(items[1].role === "task");
assert.equal(items[1].task.name, "Euler");
assert.deepEqual(
present(items[1].items).map((item) => {
assert.ok(item.role === "assistant");
return item.text;
}),
["42."],
);
assert.deepEqual(present(events.at(-1)), { sessionId: "root", event: "session_info", title: "Count the tests" });
});
test("a legacy thread without paged turns falls back to thread/read", async () => {
const { agent } = harness({
handlers: {
"thread/read": (params) =>
params.includeTurns
? {
thread: {
id: "old",
turns: [
{ id: "t", status: "completed", items: [{ type: "agentMessage", id: "m", text: "old reply" }] },
],
},
}
: { thread: { id: "old", preview: "" } },
"thread/turns/list": () => ({ data: [], nextCursor: null }),
},
});
const { items } = await agent.readSession({ workspace: "/w", sessionId: "old" });
assert.deepEqual(
items.map((item) => {
assert.ok(item.role === "assistant");
return item.text;
}),
["old reply"],
);
});
test("skills are listed per workspace and their paths kept for prompts", async () => {
const { agent, api } = harness({
handlers: {
"skills/list": (params) => {
assert.deepEqual(params, { cwds: ["/w"] });
return {
data: [
{
skills: [
{
name: "review",
description: "Long",
shortDescription: "Short",
path: "/s/review",
scope: "user",
enabled: true,
},
{ name: "off", description: "", path: "/s/off", enabled: false },
],
},
],
};
},
"turn/start": () => ({ turn: { id: "t" } }),
},
});
const { skills } = await agent.listSkills({ workspace: "/w" });
assert.deepEqual(skills, [{ name: "review", description: "Short", source: "user", manualOnly: false }]);
present(agent.sessions.get(SESSION)).run = null;
await agent.prompt({ sessionId: SESSION, input: { blocks: [{ type: "skill", name: "review", input: "" }] } });
const turn = await api.peers[0].waitFor((message) => message.method === "turn/start");
assert.deepEqual(turn.params.input, [{ type: "skill", name: "review", path: "/s/review" }]);
});
test("rollback reverts from the turn the host's message started", async () => {
const reverts: Record<string, unknown>[] = [];
const { agent } = harness({
handlers: {
"thread/revert": (params) => {
reverts.push(params);
return {};
},
"thread/turns/list": () => ({
data: [{ id: "turn-a", items: [{ type: "userMessage", id: "codex-item", content: [] }] }],
nextCursor: null,
}),
},
});
present(agent.sessions.get(SESSION)).turns.set("host-item", "turn-7");
await agent.rollback({ sessionId: SESSION, itemId: "host-item" });
await agent.rollback({ sessionId: SESSION, itemId: "codex-item" });
assert.deepEqual(reverts, [
{ threadId: SESSION, beforeTurnId: "turn-7" },
{ threadId: SESSION, beforeTurnId: "turn-a" },
]);
await assert.rejects(agent.rollback({ sessionId: SESSION, itemId: "nobody" }), /codex does not know message nobody/);
});
test("fork, compact, close, logout and sign-in go to their codex methods", async () => {
const seen: [string | undefined, Record<string, unknown>][] = [];
let opened = null;
const record = (result: unknown) => (params: Record<string, unknown>, message: TestMessage) => {
seen.push([message.method, params]);
return result;
};
const { agent, api } = harness({
handlers: {
"thread/fork": record({ thread: { id: "forked" } }),
"thread/compact/start": record({}),
"thread/unsubscribe": record({}),
"account/logout": record({}),
"account/login/start": record({ type: "chatgpt", loginId: "l1", authUrl: "https://auth.openai.com/oauth?x=1" }),
},
});
api.openUrl = async (url) => {
opened = url;
return { opened: true };
};
assert.deepEqual(
await agent.forkSession({ sessionId: SESSION, workspace: "/w", options: {}, instructions: "Rules" }),
{ sessionId: "forked" },
);
assert.ok(agent.sessions.has("forked"));
await agent.compact({ sessionId: SESSION });
await agent.logout();
await agent.authenticate({ method: "chatgpt" });
await agent.closeSession({ sessionId: SESSION });
assert.equal(agent.sessions.has(SESSION), false);
assert.deepEqual(
seen.map(([method]) => method),
["thread/fork", "thread/compact/start", "account/logout", "account/login/start", "thread/unsubscribe"],
);
assert.equal(seen[0][1].developerInstructions, "Rules");
assert.deepEqual(seen[3][1], { type: "chatgpt" });
assert.equal(opened, "https://auth.openai.com/oauth?x=1");
// A link the runtime did not open is the sign-in's error, with its reason.
api.openUrl = async () => ({ opened: false, reason: "only web and mail links open" });
await assert.rejects(
agent.authenticate({ method: "chatgpt" }),
/open https:\/\/auth\.openai\.com\/oauth\?x=1 in a browser to sign in \(only web and mail links open\)/,
);
});
test("resume re-attaches with the chat's settings", async () => {
const resumedCalls: Record<string, unknown>[] = [];
const { agent } = harness({
handlers: {
"thread/resume": (params) => {
resumedCalls.push(params);
return { thread: { id: params.threadId } };
},
},
});
await agent.resumeSession({
sessionId: "thread-9",
workspace: "/w9",
options: { permission_mode: "auto" },
instructions: "Rules",
});
const resumed = resumedCalls[0];
assert.deepEqual(
[resumed.threadId, resumed.cwd, resumed.excludeTurns, resumed.approvalsReviewer, resumed.developerInstructions],
["thread-9", "/w9", true, "auto_review", "Rules"],
);
assert.equal(present(agent.sessions.get("thread-9")).workspace, "/w9");
});
test("the handlers the host calls are the agent protocol's methods", () => {
const { agent } = harness();
const definition = agent.definition();
for (const method of [
"initialize",
"list_options",
"list_commands",
"list_sessions",
"read_session",
"list_skills",
"create_session",
"resume_session",
"close_session",
"fork_session",
"prompt",
"cancel",
"cancel_task",
"set_option",
"respond_to_approval",
"respond_to_question",
"rollback",
"compact",
"usage_limits",
"update",
"authenticate",
"logout",
]) {
assert.equal(typeof Reflect.get(definition, method), "function", method);
}
assert.deepEqual([definition.id, definition.name], ["codex", "Codex"]);
});
function present<T>(value: T | null | undefined): T {
assert.ok(value != null);
return value;
}
test("concurrent first calls share one scoped app-server", async () => {
const { agent, api } = harness();
const peers = await Promise.all([agent.client(), agent.client(), agent.client()]);
assert.equal(api.peers.length, 1);
assert.ok(peers.every((peer) => peer === peers[0]));
assert.equal(api.peers[0].received.filter((message) => message.method === "initialize").length, 1);
});
test("background jobs offered before the consumer runs are retained in order", async () => {
const { agent } = harness();
const seen: number[] = [];
// Synchronous notification routing can enqueue work before the forked
// consumer gets its first turn; a subscription stream would lose it.
agent.enqueue(
Effect.sync(() => {
seen.push(1);
}),
);
agent.enqueue(
Effect.sync(() => {
seen.push(2);
}),
);
await settle();
assert.deepEqual(seen, [1, 2]);
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 78.1 KB | 4 permissions | Listed |
No comments yet.