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

  • Provide agents agents.provideMediumAdds agents to the app.Provide the Codex agent and pass its tool calls to plugin tools
  • Run named programs processMediumStarts the listed programs.Run the Codex CLI, and ask or tell the installer that owns it (npm or Homebrew) about a newer versionPrograms: codexnpmbrew
  • Environment variables envMediumReads the listed environment variables.Find each account's Codex home, and pass extra app-server arguments set in CODEX_ARGSVariables: HOMECODEX_HOMECODEX_ARGS
  • Read files fs.readMediumReads files in the listed places.Read, once, the accounts the previous Codex provider keptPlaces: its own data folder

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

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <378.1 KB4 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.