From 2b4ae5d9e2f22c6b87c1877c4956b354435f2b91 Mon Sep 17 00:00:00 2001 From: kot4ri <20045613+kot4ri@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:29:25 +0800 Subject: [PATCH 1/2] chore: upgrade Codex CLI to 0.151.0 --- .github/workflows/docker-publish.yml | 4 +- Dockerfile | 4 +- README.en.md | 4 +- README.md | 4 +- docker-compose.yml | 2 +- docs/docker.md | 6 +- docs/upstream/README.md | 2 +- ...0.149.1.md => codex-app-server-0.151.0.md} | 336 +++++++++++++++--- package.json | 2 +- pnpm-lock.yaml | 63 ++-- src/threads/thread-resume-registry.service.ts | 19 +- 11 files changed, 345 insertions(+), 101 deletions(-) rename docs/upstream/{codex-app-server-0.149.1.md => codex-app-server-0.151.0.md} (82%) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index ccc564d..a98a771 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -83,7 +83,7 @@ jobs: file: ./Dockerfile platforms: linux/${{ matrix.arch }} build-args: | - CODEX_CLI_VERSION=${{ steps.meta.outputs.codex_version || '0.149.1' }} + CODEX_CLI_VERSION=${{ steps.meta.outputs.codex_version || '0.151.0' }} tags: | ${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}:${{ matrix.arch }}-${{ steps.meta.outputs.version_tag }} push: true @@ -104,7 +104,7 @@ jobs: file: ./Dockerfile platforms: linux/${{ matrix.arch }} build-args: | - CODEX_CLI_VERSION=${{ steps.meta.outputs.codex_version || '0.149.1' }} + CODEX_CLI_VERSION=${{ steps.meta.outputs.codex_version || '0.151.0' }} tags: | ${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}:${{ matrix.arch }}-${{ steps.meta.outputs.version_tag }} push: true diff --git a/Dockerfile b/Dockerfile index 023331b..5451739 100644 --- a/Dockerfile +++ b/Dockerfile @@ -91,7 +91,7 @@ RUN node --version \ && mise --version # Install global npm tools (codex + MCP utilities) -ARG CODEX_CLI_VERSION=0.149.1 +ARG CODEX_CLI_VERSION=0.151.0 ENV CODEX_CLI_VERSION=${CODEX_CLI_VERSION} RUN npm install -g \ @openai/codex@${CODEX_CLI_VERSION} \ @@ -119,7 +119,7 @@ RUN npx --yes node-gyp rebuild --directory=node_modules/node-pty || true \ # ── Stage 6: Runtime ───────────────────────────────────────────────── FROM runtime-base AS runtime -ARG CODEX_CLI_VERSION=0.149.1 +ARG CODEX_CLI_VERSION=0.151.0 ENV CODEX_CLI_VERSION=${CODEX_CLI_VERSION} WORKDIR /app diff --git a/README.en.md b/README.en.md index 1eb3a16..0f99646 100644 --- a/README.en.md +++ b/README.en.md @@ -249,8 +249,8 @@ The same image can serve both a domain root and a proxy subpath (for example, `h ```bash docker build \ - --build-arg CODEX_CLI_VERSION=0.149.1 \ - -t codex-webui:0.149.1 . + --build-arg CODEX_CLI_VERSION=0.151.0 \ + -t codex-webui:0.151.0 . ``` Nginx must retain `/codex/` in browser-facing URLs and strip it when proxying to the backend. The trailing slashes on both `location` and `proxy_pass` are required: diff --git a/README.md b/README.md index ce4a470..589b5f0 100644 --- a/README.md +++ b/README.md @@ -249,8 +249,8 @@ Docker Compose 中使用时,`proxy_pass` 改为 `http://codex-webui:8172`, ```bash docker build \ - --build-arg CODEX_CLI_VERSION=0.149.1 \ - -t codex-webui:0.149.1 . + --build-arg CODEX_CLI_VERSION=0.151.0 \ + -t codex-webui:0.151.0 . ``` Nginx 必须保留浏览器侧的 `/codex/` 前缀,并在转发到后端时将它移除。`location` 和 `proxy_pass` 末尾的 `/` 均不可省略: diff --git a/docker-compose.yml b/docker-compose.yml index e9c40f8..531106a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: # build: # context: . # args: - # CODEX_CLI_VERSION: "0.149.1" + # CODEX_CLI_VERSION: "0.151.0" ports: - "${PORT:-8172}:8172" environment: diff --git a/docs/docker.md b/docs/docker.md index 99066a7..6df7a8a 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -114,16 +114,16 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ | ARG | 默认值 | 说明 | |-----|--------|------| -| `CODEX_CLI_VERSION` | `0.149.1` | **运行时**镜像内全局安装的 codex npm 包版本 | +| `CODEX_CLI_VERSION` | `0.151.0` | **运行时**镜像内全局安装的 codex npm 包版本 | 构建阶段生成协议类型用的是 `@openai/codex` devDependency(由 `pnpm-lock.yaml` 锁定),**不受本 ARG 控制**。改 `CODEX_CLI_VERSION` 时须同步更新 `package.json` 里的 devDependency,否则会出现「类型按 A 版本生成、运行时跑 B 版本」的错配。版本的唯一真相源是 `package.json`,Dockerfile / docker-compose / CI fallback 均跟随它。 本地构建: ```bash -docker compose build --build-arg CODEX_CLI_VERSION=0.149.1 +docker compose build --build-arg CODEX_CLI_VERSION=0.151.0 ``` -CI 由 `codex-*` 格式的 tag 触发,tag 名会被解析成本 ARG:`codex-0.149.1` → `0.149.1`。同一 codex 版本重发镜像用 `codex-0.149.1-2` 这类后缀。tag 名不合该格式会导致解析失败、构建报错。 +CI 由 `codex-*` 格式的 tag 触发,tag 名会被解析成本 ARG:`codex-0.151.0` → `0.151.0`。同一 codex 版本重发镜像用 `codex-0.151.0-2` 这类后缀。tag 名不合该格式会导致解析失败、构建报错。 ## 反向代理与子目录部署 diff --git a/docs/upstream/README.md b/docs/upstream/README.md index b4e5150..5257709 100644 --- a/docs/upstream/README.md +++ b/docs/upstream/README.md @@ -8,7 +8,7 @@ Do not edit them. Fix anything wrong by refreshing from upstream. | File | Upstream path | Tag | |---|---|---| -| `codex-app-server-0.149.1.md` | `codex-rs/app-server/README.md` | `rust-v0.149.1` | +| `codex-app-server-0.151.0.md` | `codex-rs/app-server/README.md` | `rust-v0.151.0` | The tag matches the `@openai/codex` version pinned in the root `package.json`. Refresh after bumping that dependency — a protocol migration is exactly when a diff --git a/docs/upstream/codex-app-server-0.149.1.md b/docs/upstream/codex-app-server-0.151.0.md similarity index 82% rename from docs/upstream/codex-app-server-0.149.1.md rename to docs/upstream/codex-app-server-0.151.0.md index 483924a..4957690 100644 --- a/docs/upstream/codex-app-server-0.149.1.md +++ b/docs/upstream/codex-app-server-0.151.0.md @@ -36,7 +36,7 @@ When running with `--listen ws://IP:PORT`, the same listener also serves basic H Websocket transport is currently experimental and unsupported. Do not rely on it for production workloads. -Pass `--code-mode-host URL` to connect this app-server process to a remote code-mode host instead of starting a local host. Use `ws://` or `wss://` for the WebSocket protocol, or a root `http://` or `https://` URL without a path or query for gRPC. Remote hosts require the `code_mode_host` feature. This outbound connection is independent of `--listen` and is shared by the process's threads. +Pass `--code-mode-host URL` to connect this app-server process to a remote code-mode host instead of starting a local host. Use a root `http://` or `https://` URL without a path or query for gRPC. Remote hosts require the `code_mode_host` feature. This outbound connection is independent of `--listen` and is shared by the process's threads. The unix socket transport is intended for local app-server control-plane clients. `codex app-server proxy` opens exactly one raw stream connection to `$CODEX_HOME/app-server-control/app-server-control.sock` @@ -161,9 +161,9 @@ Example with notification opt-out: ## API Overview - `server/diagnostics` — experimental; read process-local memory measurements and registered diagnostic gauges. -- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. Experimental `projectId` assigns a durable thread to an existing project; ephemeral threads expose the same project identity in live responses without creating a stored/listable assignment. Experimental `historyMode: "paginated"` selects projection-backed durable history. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `allowProviderModelFallback` lets providers backed by an authoritative static model catalog replace an unavailable requested `model` with the catalog default; dynamic or cached catalogs preserve the requested model. Experimental `runtimeWorkspaceRoots` supplies the runtime workspace roots used when app-server creates default environment selections; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd` and optional environment-native `runtimeWorkspaceRoots`. Explicit environments ignore the top-level roots; omitted per-environment roots default to that environment's `cwd`, while an empty list explicitly selects no roots. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are started in that environment, and HTTP MCP connections use that environment's HTTP client. +- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. Experimental `projectId` assigns a durable thread to an existing project; ephemeral threads expose the same project identity in live responses without creating a stored/listable assignment. Experimental `historyMode` selects the persisted history contract: when omitted, durable threads use `"paginated"` if the active thread store supports `thread/turns/list` and `thread/items/list`, while ephemeral threads and stores without that support use `"legacy"`. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `allowProviderModelFallback` lets providers backed by an authoritative static model catalog replace an unavailable requested `model` with the catalog default; dynamic or cached catalogs preserve the requested model. Experimental `runtimeWorkspaceRoots` supplies the runtime workspace roots used when app-server creates default environment selections; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd` and optional environment-native `runtimeWorkspaceRoots`. Explicit environments ignore the top-level roots; omitted per-environment roots default to that environment's `cwd`, while an empty list explicitly selects no roots. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are started in that environment, and HTTP MCP connections use that environment's HTTP client. - `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. -- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` boundary is rejected. Experimental `beforeTurnId` instead copies history strictly before the referenced turn, including when that turn is in progress, and cannot be combined with `lastTurnId`. If both boundaries are null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately, or `deferGoalContinuation: true` to carry the source thread's current goal into the fork and run an explicit turn before automatic continuation resumes. Deferred goal continuation is persisted until that turn starts and cannot be combined with `ephemeral: true`. Accepts the same permission override rules as `thread/start`. +- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` boundary is rejected. Experimental `beforeTurnId` instead copies history strictly before the referenced turn, including when that turn is in progress, and cannot be combined with `lastTurnId`. If both boundaries are null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Experimental `deferGoalContinuation: true` carries the source thread's current goal into the fork and runs an explicit turn before automatic continuation resumes. Deferred goal continuation is persisted until that turn starts and cannot be combined with `ephemeral: true`. Accepts the same permission override rules as `thread/start`. - `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior. - `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `sectionId`, `cwd`, and `searchTerm` filters. Experimental `projectId` filters one project, while `null` selects unassigned threads. Set `sortKey` to `"section_position"` when listing a section in its persisted manual order. Experimental clients can use `parentThreadId` for direct spawned children or `ancestorThreadId` for spawned descendants at any depth; the two filters are mutually exclusive. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known. - `project/list`, `project/read`, `project/create`, `project/import`, `project/update`, `project/move`, and `project/delete` — experimental SQLite-backed project APIs. Projects have canonical server-generated IDs, persisted manual positions, ordered absolute roots, and an opaque string metadata bag. `project/move` places a project before another project or appends it when `beforeProjectId` is `null`. Create and import require an opaque `idempotencyKey`; clients should generate a UUID for ordinary creates and may use a stable namespaced legacy ID for migration. Reusing a key returns the original project without emitting notifications or repeating thread assignments, and keys remain reserved after deletion. Import can atomically assign existing thread IDs. Delete clears assignments but never deletes threads, directories, or files. @@ -173,18 +173,18 @@ Example with notification opt-out: - `threadSection/update` — rename an existing custom section and optionally replace its `appearance`; omit appearance to preserve it or pass `null` to clear it. The built-in pinned section cannot be updated. - `threadSection/delete` — delete an existing custom section and atomically return its member threads to the unsectioned list; returns `{}`. The built-in pinned section cannot be deleted. - `thread/loaded/list` — list the thread ids currently loaded in memory. -- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. For loaded threads, experimental clients can use `canAcceptDirectInput` to determine whether `turn/start` and `turn/steer` are accepted; unloaded stored threads report `null` when that capability is unavailable. -- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`. -- `thread/items/list` — experimental; page through persisted thread items without resuming the thread. Pass `turnId` to restrict results to one turn, or omit it to page items across the thread. The active thread store must support item pagination. +- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. For loaded threads, experimental clients can use `canAcceptDirectInput` to determine whether `turn/start` and `turn/steer` are accepted (`false` for parent-owned Multi-Agent V2 subagents); unloaded stored threads report `null` when that capability is unavailable. +- `thread/turns/list` — page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`. +- `thread/items/list` — page through persisted thread items without resuming the thread. Pass `turnId` to restrict results to one turn, or omit it to page items across the thread. The active thread store must support item pagination. - `thread/searchOccurrences` — experimental; find literal, case-insensitive matches in visible user messages and summary-selected final assistant messages within one paginated thread. - `thread/metadata/update` — patch stored thread metadata in sqlite; supports updating persisted `gitInfo` fields and experimental `projectId`, then returns the refreshed `thread`. Omit `projectId` to preserve assignment and pass an empty string to clear it. - `thread/section/move` — atomically move a thread into the section identified by `sectionId`, before another thread or at the end when `beforeThreadId` is `null`. Reordering within the same section preserves `sectionEnteredAt`; entering a different section resets it. Set `sectionId` to `null` to remove the thread from its section. Returns `{}` on success. -- `thread/settings/update` — experimental; queue a partial update to a loaded thread’s next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; deprecated `multiAgentMode` is ignored, while Ultra reasoning effort enables proactive multi-agent behavior; `sandboxPolicy` and `permissions` cannot be combined. Returns `{}` when the update is accepted and emits `thread/settings/updated` with the full effective settings only if they actually change. `turn/start` settings overrides emit the same notification when they change the stored settings. +- `thread/settings/update` — experimental; queue a partial update to a loaded thread’s next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; deprecated `multiAgentMode` is ignored, while Ultra reasoning effort enables proactive multi-agent behavior; `sandboxPolicy` and `permissions` cannot be combined. Parent-owned Multi-Agent V2 subagents reject direct settings updates. Returns `{}` when the update is accepted and emits `thread/settings/updated` with the full effective settings only if they actually change. `turn/start` settings overrides emit the same notification when they change the stored settings. - `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success. - `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success. -- `thread/goal/set` — create or update the single persisted goal for a materialized thread; returns the current goal and emits `thread/goal/updated`. -- `thread/goal/get` — fetch the current persisted goal for a materialized thread; returns `goal: null` when no goal exists. -- `thread/goal/clear` — clear the current persisted goal for a materialized thread; returns whether a goal was removed and emits `thread/goal/cleared` when state changes. +- `thread/goal/set` — create or update the single persisted goal for a materialized thread; returns the current goal and emits `thread/goal/updated`. Parent-owned Multi-Agent V2 subagents reject goal updates, including while unloaded. +- `thread/goal/get` — fetch the current persisted goal for a materialized thread; returns `goal: null` when no goal exists. Available even for parent-owned Multi-Agent V2 subagents. +- `thread/goal/clear` — clear the current persisted goal for a materialized thread; returns whether a goal was removed and emits `thread/goal/cleared` when state changes. Parent-owned Multi-Agent V2 subagents reject goal clearing, including while unloaded. - `thread/goal/updated` — notification emitted whenever a thread goal changes; includes the full current goal. - `thread/goal/cleared` — notification emitted whenever a thread goal is removed. - `thread/queue/add` — experimental; persist a user turn for automatic FIFO submission when the thread next becomes idle. @@ -201,23 +201,26 @@ Example with notification opt-out: - `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server keeps the thread loaded and unloads it only after it has had no subscribers and no thread activity for 30 minutes, runs `SessionEnd` hooks, then emits `thread/closed`. - `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success and emits `thread/name/updated` to initialized, opted-in clients. Thread names are not required to be unique; name lookups resolve to the most recently updated thread. - `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`. -- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. -- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. +- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. Parent-owned Multi-Agent V2 subagents reject direct compaction requests. +- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Parent-owned Multi-Agent V2 subagents reject direct shell commands. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. +- `thread/approveGuardianDeniedAction` — manually approve a previously denied Guardian action; parent-owned Multi-Agent V2 subagents reject direct approvals. Replies to pending server-issued approval requests are unaffected. - `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted. - `thread/backgroundTerminals/list` — list running background terminals for a loaded thread (experimental; requires `capabilities.experimentalApi`); returns `data` with the running terminal ids. - `thread/backgroundTerminals/terminate` — terminate one running background terminal by app-server `processId` (experimental; requires `capabilities.experimentalApi`); returns whether a process was terminated. -- `thread/rollback` — deprecated and will be removed soon. Drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. Paginated threads do not support rollback. -- `thread/revert` — experimental. Replace a loaded paginated thread's durable history with the prefix strictly before `beforeTurnId` while preserving its thread id. The operation interrupts an active turn if needed, leaves older rollout files immutable, reloads the thread, returns updated thread metadata with empty `turns` plus pagination cursors, and emits `thread/reverted`. It does not revert local file changes. -- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` supplies the default roots for newly resolved environment selections. Explicit `environments[].runtimeWorkspaceRoots` override that fallback with environment-native absolute paths. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Deprecated experimental `multiAgentMode` is ignored; Ultra reasoning effort selects proactive behavior. -- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success. -- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`. -- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. -- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, optionally pass `model` and `version` to override configured realtime selection for this session only, pass `includeStartupContext: false` to omit Codex's generated startup context, and optionally pass `initialItems` to seed V3 with complete role-bearing text messages at session creation. Pass `realtimeStartInstructions` and `realtimeEndInstructions` to control the developer instructions given to the backing Codex model when this session starts and ends. Version `"v1"` uses legacy Bidi `conversation.handoff.*`, `"v2"` uses the Realtime Voice API, and `"v3"` preserves V1 Codex Voice behavior while using Frameless Bidi `delegation.*`. For V3 automatic Codex text, `codexResponseHandoffMode` accepts `"thinking"` (the default; all output uses channel-less thinking appends), `"commentary"` (all output uses the commentary channel), or `"bemTags"` (the raw BEM envelope selects the API channel: BEM `analysis` and `commentary` use `commentary`, while BEM `final` and unparsable output use `speakable`). The BEM envelope remains in the appended text for the frontend model to interpret. V1 and V2 ignore this setting. For V3, pass `delegationAckFiller: false` to suppress the Realtime API's delegation acknowledgement filler or `true` to restore it; omitting the field preserves the Realtime API's default. V1 and V2 ignore `delegationAckFiller`. V3 handoffs do not prepend the legacy `"Agent Final Message"` label. Pass `clientManagedHandoffs: true` to disable automatic Codex response delivery so only the client's explicit append calls produce handoffs. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a Bidi WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. Conversation `version: "v2"` requests remain unsupported for WebRTC. -- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`. -- `thread/realtime/appendText` — append text input to the active realtime session with a required `role` of `user`, `developer`, or `assistant` (experimental); returns `{}`. Older clients that omit `role` default to `user`. -- `thread/realtime/appendSpeech` — append text that the realtime model should speak to the user (experimental); returns `{}`. -- `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`. -- `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start`. Inline reviews emit `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review. Detached reviews stream ordinary turn items on the new review thread. +- `thread/rollback` — deprecated and will be removed soon. Drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. Paginated threads do not support rollback. Parent-owned Multi-Agent V2 subagents reject direct rollback requests. +- `thread/revert` — replace a loaded paginated thread's durable history with the prefix strictly before `beforeTurnId` while preserving its thread id. The operation interrupts an active turn if needed, leaves older rollout files immutable, reloads the thread, returns updated thread metadata with empty `turns` plus pagination cursors, and emits `thread/reverted`. It does not revert local file changes. Parent-owned Multi-Agent V2 subagents reject direct revert requests. +- `turn/start` — add user input or a named standalone function-call output to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For standalone outputs, provide `toolOutput` with an empty `input` array. Optional `turnTrigger` classifies who or what started a new turn and is sent as `turn_trigger` in Responses request metadata; it is ignored if the request steers an active turn. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` supplies the default roots for newly resolved environment selections. Explicit `environments[].runtimeWorkspaceRoots` override that fallback with environment-native absolute paths. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Deprecated experimental `multiAgentMode` is ignored; Ultra reasoning effort selects proactive behavior. Parent-owned Multi-Agent V2 subagents reject direct turns. +- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a turn; returns `{}` on success. Parent-owned Multi-Agent V2 subagents reject direct item injection. +- `turn/settings/update` — experimental; publish a narrow model-settings patch to the exact live task identified by `threadId` and `turnId`, regardless of task kind. Requires `step_model_switching`; returns `status: "applied"` or `status: "targetUnavailable"`, or a request error if rejected. Future-thread settings and already captured steps are unchanged. Parent-owned Multi-Agent V2 subagents reject direct settings updates. +- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`. Parent-owned Multi-Agent V2 subagents reject direct steering. +- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. Also available for parent-owned Multi-Agent V2 subagents. +- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, optionally pass `model` and `version` to override configured realtime selection for this session only, pass `includeStartupContext: false` to omit Codex's generated startup context, and optionally pass `initialItems` to seed V3 with complete role-bearing text messages at session creation. Pass `realtimeStartInstructions` and `realtimeEndInstructions` to control the developer instructions given to the backing Codex model when this session starts and ends. Version `"v1"` uses legacy Bidi `conversation.handoff.*`, `"v2"` uses the Realtime Voice API, and `"v3"` preserves V1 Codex Voice behavior while using Frameless Bidi `delegation.*`. For V3 automatic Codex text, `codexResponseHandoffMode` accepts `"thinking"` (the default; all output uses channel-less thinking appends), `"commentary"` (all output uses the commentary channel), or `"bemTags"` (the raw BEM envelope selects the API channel: BEM `analysis` and `commentary` use `commentary`, while BEM `final` and unparsable output use `speakable`). The BEM envelope remains in the appended text for the frontend model to interpret. V1 and V2 ignore this setting. For V3, pass `delegationAckFiller: false` to suppress the Realtime API's delegation acknowledgement filler or `true` to restore it; omitting the field preserves the Realtime API's default. V1 and V2 ignore `delegationAckFiller`. V3 handoffs do not prepend the legacy `"Agent Final Message"` label. Pass `clientManagedHandoffs: true` to disable automatic Codex response delivery so only the client's explicit append calls produce handoffs. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a Bidi WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. Conversation `version: "v2"` requests remain unsupported for WebRTC. Parent-owned Multi-Agent V2 subagents reject this request. +- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`. Parent-owned Multi-Agent V2 subagents reject this request. +- `thread/realtime/appendText` — append text input to the active realtime session with a required `role` of `user`, `developer`, or `assistant` (experimental); returns `{}`. Older clients that omit `role` default to `user`. Parent-owned Multi-Agent V2 subagents reject this request. +- `thread/realtime/appendSpeech` — append text that the realtime model should speak to the user (experimental); returns `{}`. Parent-owned Multi-Agent V2 subagents reject this request. +- `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`. Parent-owned Multi-Agent V2 subagents reject this request. +- `thread/timeline/list` — page ordinary turn items, durable realtime facts, and turn boundaries together in rollout order (experimental). Entries are tagged `item`, `realtime`, `turnStarted`, or `turnCompleted`. Turn boundaries carry lifecycle metadata without duplicating the turn's items; completed boundaries also cover interrupted and failed turns. Each response contains an opaque continuation cursor and `activeRealtimeSessionAtPageStart`, allowing clients to render any bounded page without loading earlier thread history. Entries at the same rollout position have stable ordering and can span pages. Existing `thread/items/list` remains unchanged. +- `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start`. Inline reviews emit `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review. Detached reviews stream ordinary turn items on the new review thread. Parent-owned Multi-Agent V2 subagents reject both inline and detached reviews. - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). - `command/exec/write` — write base64-decoded stdin bytes to a running `command/exec` session or close stdin; returns `{}`. - `command/exec/resize` — resize a running PTY-backed `command/exec` session by `processId`; returns `{}`. @@ -277,9 +280,11 @@ Example with notification opt-out: - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, optionally pass `clientRegistration` (`auto`, `cimd`, or `dcr`) to override client registration for this login only, and receive an `authorization_url` followed by `mcpServer/oauthLogin/completed` once the browser flow finishes. Omitting `clientRegistration` automatically discovers the authorization server's supported registration methods; the override is never persisted in server configuration. - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. -- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, owning `pluginId` (`null` for servers not contributed by a plugin), plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. An `unknown` auth status means OAuth support could not be determined; `unsupported` means OAuth is known not to be supported. +- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, owning `pluginId` (`null` for servers not contributed by a plugin), and nullable `runtimeStatus` from the current thread’s published connections, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly and `runtimeStatus` is `null`. Runtime status is also `null` when the latest server registration differs from the thread’s published configuration. Runtime status is observed without starting or reconnecting the thread’s servers; it can be `notStarted`, `starting`, `connected`, `authenticationRequired`, `failed`, `cancelled`, or `disabled`. Inventory may be cached or collected separately and does not prove that the thread is connected. Older servers omit `runtimeStatus`; clients should treat that as unknown. If `detail` is omitted, the server defaults to `full`. An `unknown` auth status means OAuth support could not be determined; `unsupported` means OAuth is known not to be supported. - `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. Pass `originCallId` with `threadId` to scope a Codex app widget to the app and account of the completed tool call that produced it; successful scoped reads return the same `originCallId`. Optional `connectorId` restricts other hosted app resources to their originating connector. If `threadId` is omitted, the server reads from the latest MCP config directly. -- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result. +- `mcpServer/event/stream/start` (experimental) — subscribe to an MCP event by `threadId`, `server`, `subscriptionId`, event `name`, `arguments`, and optional `_meta`. +- `mcpServer/event/stream/stop` (experimental) — stop the caller's event subscription by `subscriptionId`. +- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result. Parent-owned Multi-Agent V2 subagents reject direct tool calls. - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. - `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id. - `config/read` — fetch the runtime-effective config after resolving config layering and managed requirements, including opaque `desktop` values stored in `config.toml`. When configured, the `packagedDefaults` layer has the lowest precedence. @@ -288,7 +293,36 @@ Example with notification opt-out: - `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Successful session entries include the original imported title when one was available. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection. - `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. Writes that overlap a managed requirement are rejected with `configRequirementReadonly`. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults do not reload existing threads. -- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`cliAuthCredentialsStore`, `chatgptBaseUrl`, `sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), Browser Use policy (`browserUse.disableAutoReview`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including command handlers with optional `additionalContextLimit` and `mcp_tool` handlers with `server`, `tool`, `input`, `timeoutSec`, and `statusMessage`), `enforceResidency`, managed automatic review (`autoReview.requiredOnModels` and `autoReview.ignoreRules`), model defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`cliAuthCredentialsStore`, `chatgptBaseUrl`, `sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), requirements-only developer instructions (`additionalDeveloperInstructions`, supplied independently of ordinary developer instructions), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), the Browser/Computer Use umbrella policy (`allowBrowserAndComputerUse`), computer use policy (`computerUse`, including persistent approval, default application access, and per-platform application rules), Browser Use policy (`browserUse`, including history access, origin rules, auto-review, and approval controls), interactive browser import policy (`inAppBrowser.allowExternalBrowserSettingsImport`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including command handlers with optional `additionalContextLimit` and `mcp_tool` handlers with `server`, `tool`, `input`, `timeoutSec`, and `statusMessage`), `enforceResidency`, managed automatic review (`autoReview.requiredOnModels` and `autoReview.ignoreRules`), model defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. + +`mcpServer/resource/read` and `mcpServer/tool/call` preserve MCP protocol errors +with their original `code`, `message`, and `data`, including authentication +metadata in `data._meta`. Other operation failures retain the existing +internal-error response. Tool results with `isError: true` remain results, +including their `_meta`. + +### Plugin configuration scope + +Plugin activation and MCP settings use the existing merged configuration, including +system settings and trusted project overrides. `skills/list` resolves plugin skills +independently for each requested working directory. + +For local `plugin/list` and `plugin/installed` results, each requested cwd supplies +its effective plugin state and plugin feature flag. When a plugin appears in multiple +contexts, the first source wins and installed/enabled state is merged across contexts. +Invalid project configurations are reported in `marketplaceLoadErrors` without hiding +other projects or remote plugins. Omitted or empty `cwds` exclude project +configuration, including the app-server process's project. `forceRefetch` refreshes the selected local plugin +sources before returning; ordinary listing schedules the same work in the background. +Remote catalog settings and feature gating remain request-wide rather than being +selected from the requested repos. Search continues to report `enabled: false`. + +Marketplace definitions can come from system configuration, but configured Git +marketplaces currently require an existing downloaded snapshot. + +`marketplace/remove` rejects removal when the marketplace name is defined in another +enabled layer of the operation's loaded config stack. Otherwise it removes the +snapshot and any base-user entry; a base-user entry is not required for cleanup. ### Example: Start or resume a thread @@ -360,7 +394,9 @@ Valid `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. When ` To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`. When the stored session includes persisted token usage, the server emits `thread/tokenUsage/updated` immediately after the response so clients can render restored usage before the next turn starts. You can also pass the same configuration overrides supported by `thread/start`, including `approvalsReviewer`. On cold resume, approval policy and the active permission-profile ID select a source in this order: request override, latest persisted thread setting, current configured default. The persisted profile ID is resolved through the same config and requirements path as a `permissions` override. Threads without an active profile ID use current config instead of restoring their concrete historical permissions. -By default, `thread/resume` includes the reconstructed turn history in `thread.turns`. Experimental clients can pass `excludeTurns: true` to return only thread metadata and live resume state, then call `thread/turns/list` separately if they want to page the turn history over the network. A cold paginated resume can still replay persisted `thread/tokenUsage/updated` when it can identify the corresponding stored turn; resuming an already-loaded thread waits for the next live update. +Parent-owned Multi-Agent V2 children are an exception: `thread/resume` ignores configuration overrides and reattaches to the existing child. An unloaded child is reloaded through its actual, currently loaded parent using parent-derived configuration. If that owner-controlled reload cannot be performed, the request returns JSON-RPC error `-32600`; resume the parent first, or use `thread/read` or `thread/turns/list` to inspect the child's stored history without loading it. This policy follows the child's multi-agent runtime, including leaf workers whose models cannot delegate further. + +By default, `thread/resume` includes the reconstructed turn history in `thread.turns`. Full-history hydration is deprecated for paginated threads and emits `deprecationNotice`; clients should pass `excludeTurns: true` to return only thread metadata and live resume state, then page with `thread/turns/list` and `thread/items/list`. A cold paginated resume can still replay persisted `thread/tokenUsage/updated` when it can identify the corresponding stored turn; resuming an already-loaded thread waits for the next live update. Paginated threads keep the same resume contract as legacy threads. A default resume materializes the full projected history into `thread.turns`; `excludeTurns: true` keeps that array empty and includes `turnsBackwardsCursor` and `itemsBackwardsCursor` for the durable history visible at the resume boundary. Pass each cursor directly to its matching list API with `sortDirection: "desc"`; the first page includes the row identified by the cursor, while newer records arrive through live notifications. Either cursor is `null` when there is no durable row yet. @@ -416,7 +452,7 @@ To branch from a stored session, call `thread/fork` with the `thread.id`. This c { "method": "thread/started", "params": { "thread": { … } } } ``` -Like `thread/resume`, experimental clients can pass `excludeTurns: true` to `thread/fork` to return only thread metadata in `thread.turns` and page history with `thread/turns/list`. Metadata-only forks do not replay restored `thread/tokenUsage/updated`. Ephemeral forks of paginated threads require `excludeTurns: true`. +Like `thread/resume`, full-history hydration is deprecated for paginated `thread/fork` and emits `deprecationNotice`. Clients should pass `excludeTurns: true` to return only thread metadata in `thread.turns` and page history with `thread/turns/list` and `thread/items/list`. Metadata-only forks do not replay restored `thread/tokenUsage/updated`. Ephemeral forks of paginated threads require `excludeTurns: true`. ### Example: List threads (with pagination & filters) @@ -567,8 +603,10 @@ Later, after the idle unload timeout: Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want thread history loaded into `thread.turns`. The returned thread includes `parentThreadId`, `agentNickname`, and `agentRole` for subagent threads when available. -Paginated threads can also use `includeTurns: true`, but clients should prefer -`thread/turns/list` and `thread/items/list` for incremental history loading. +Paginated threads can also use `includeTurns: true`, but full-history hydration +is deprecated and emits `deprecationNotice`. Clients should omit `includeTurns` +(or set it to `false`), then use `thread/turns/list` and `thread/items/list` for +incremental history loading. ```json { "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } } @@ -584,9 +622,9 @@ Paginated threads can also use `includeTurns: true`, but clients should prefer } } ``` -### Example: List thread turns (experimental) +### Example: List thread turns -Use `thread/turns/list` with `capabilities.experimentalApi = true` to page a stored thread’s turn history without resuming it. By default, results are sorted descending so clients can start at the present and fetch older turns with `nextCursor`. The response also includes `backwardsCursor`; pass it as `cursor` on a later request with `sortDirection: "asc"` to fetch turns newer than the first item from the earlier page. +Use `thread/turns/list` to page a stored thread’s turn history without resuming it. By default, results are sorted descending so clients can start at the present and fetch older turns with `nextCursor`. The response also includes `backwardsCursor`; pass it as `cursor` on a later request with `sortDirection: "asc"` to fetch turns newer than the first item from the earlier page. Every returned `Turn` includes `itemsView`, which tells clients whether the `items` array was omitted intentionally (`notLoaded`), contains only summary items (`summary`), or contains every item available from persisted app-server history (`full`). Pass `itemsView` to choose the returned detail level; omitted `itemsView` defaults to `"summary"`. @@ -897,6 +935,12 @@ The `audio` variant accepts data URLs. Other URL schemes are rejected. `localAud You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread. `outputSchema` applies only to the current turn. Experimental `environments` is turn-scoped: omit it to inherit the thread's sticky environments, pass `[]` to run the turn with no environments, or pass explicit environment ids to override the sticky selection for this turn only. +`serviceTierForTurn` overrides the tier only when the request starts a new turn, without changing the thread's saved tier. Use `"default"` for standard speed, or omit it (or pass `null`) to inherit the thread's tier. It is ignored when the request steers an active turn. The existing `serviceTier` field still changes the tier for subsequent turns, including when both fields are supplied. + +Experimental `cyberAccessProgram` also applies only to the new turn. It accepts `standard`, `daybreakBlue`, or `daybreakRed`; omission preserves automatic backend behavior. For ChatGPT-authenticated requests through the built-in OpenAI provider, Codex sends the corresponding `standard`, `daybreak_blue`, or `daybreak_red` value in `access_programs.cyber` on Responses and remote-compaction requests. WebSocket `response.create` messages carry the choice per request, so changing it does not require reconnecting. The server still enforces workspace authorization and model restrictions. API-key and custom-provider requests omit this field. This field does not change the saved model or grant access. + +Child agents use the invoking turn's choice when spawned or started on a new follow-up, including after a reload. Input delivered into an already-running child turn does not change that turn's choice. + `approvalsReviewer` accepts: - `"user"` — default. Review approval requests directly in the client. @@ -1014,22 +1058,45 @@ Invoke a plugin by including a UI mention token such as `@sample` in the text in } } } ``` +### Example: Start a turn (standalone tool output) + +Provide a named `toolOutput` with an empty `input` array to start a real turn or join an active regular turn. `namespace` is nullable, and `output` can be text or structured content items. The output retains tool-tier authority and appears as a `functionCallOutput` item in durable history and standard item notifications; clients decide whether to display it. + +```json +{ "method": "turn/start", "id": 36, "params": { + "threadId": "thr_123", + "input": [], + "toolOutput": { + "name": "send_message_to_thread", + "namespace": "codex_app", + "output": "Another agent delegated this task." + } +} } +{ "id": 36, "result": { "turn": { "id": "turn_460", "status": "inProgress", "items": [], "error": null } } } +``` + ### Example: Inject raw history items -Use `thread/inject_items` to append prebuilt Responses API items to a loaded thread’s prompt history without starting a user turn. These items are persisted to the rollout and included in subsequent model requests. Any `input_image` items must use inline data URLs; remote HTTP(S) image URLs are rejected. +Use `thread/inject_items` to append prebuilt Responses API items to a loaded thread’s prompt history without starting a turn. These items are persisted to the rollout and included in subsequent model requests. A standalone `function_call_output` can omit `call_id` when it has a nonempty `name`; `namespace` is optional, and the output retains tool-tier authority. Any `input_image` items must use inline data URLs; remote HTTP(S) image URLs are rejected. History-only outputs are not exposed as thread items. ```json -{ "method": "thread/inject_items", "id": 36, "params": { +{ "method": "thread/inject_items", "id": 37, "params": { "threadId": "thr_123", "items": [ { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "Previously computed context." }] + }, + { + "type": "function_call_output", + "name": "send_message_to_thread", + "namespace": "codex_app", + "output": "Another agent delegated this task." } ] } } -{ "id": 36, "result": {} } +{ "id": 37, "result": {} } ``` ### Example: Start realtime with WebRTC @@ -1071,6 +1138,27 @@ Then send `offer.sdp` to app-server. Core uses `experimental_realtime_ws_backend } } ``` +Clients that create and negotiate the realtime call themselves can instead pass its call ID: + +```json +{ "method": "thread/realtime/start", "id": 41, "params": { + "threadId": "thr_123", + "outputModality": "audio", + "version": "v3", + "realtimeSessionId": "sess_123", + "transport": { "type": "existingCall", "callId": "rtc_123" } +} } +{ "id": 41, "result": {} } +``` + +The existing-call transport attaches Codex to the call over its sideband WebSocket without creating +another call or emitting `thread/realtime/sdp`. The client owns the SDP negotiation and the initial +realtime session configuration. Codex startup context is disabled by default for existing calls; +`includeStartupContext: true`, `prompt`, nonempty `initialItems`, `model`, `voice`, and +`delegationAckFiller` are rejected because they would change the client-owned session. Supply +`realtimeSessionId` when the upstream session ID is known; otherwise the +`thread/realtime/started` notification reports `realtimeSessionId: null`. + Omit `prompt` to use Codex's default realtime backend prompt. Send `prompt: null` or `prompt: ""` when the session should start without that default backend prompt. Pass `realtimeStartInstructions` to provide the developer instructions given to @@ -1203,6 +1291,46 @@ Use `thread/backgroundTerminals/terminate` to terminate one running background t { "id": 37, "result": { "terminated": true } } ``` +### Example: Update a running turn's settings (experimental) + +Enable `capabilities.experimentalApi` and the disabled-by-default `step_model_switching` +feature. Supply the exact turn ID from `turn/start`, `turn/started`, `thread/read` with +`includeTurns: true`, or `thread/turns/list`: + +```json +{ "method": "turn/settings/update", "id": 42, "params": { + "threadId": "thr_123", "turnId": "turn_456", "model": "gpt-5.4" +} } +{ "id": 42, "result": { "status": "applied" } } +``` + +Only `model`, `effort`, `summary`, and `serviceTier` may change. Unknown fields are +rejected. Omitted fields leave settings unchanged; `serviceTier: null` clears the +requested tier, while `null` for model, effort, or summary leaves it unchanged. + +The response waits for core: `status: "applied"` means a settings snapshot was published +for subsequent captures, even if its values were unchanged. Normal defaults and tier +filtering still apply; publication does not guarantee another inference will run or use +every preference. Existing captured steps keep their settings. + +Any live task kind may accept publication. Updating a parent review context does not +update its child session; shell tasks do not sample, and unmigrated compaction consumers +may still use initial settings. + +`status: "targetUnavailable"` means the exact live task was absent or lost before +publication. Validation, feature, and safety rejections return a JSON-RPC request error +with the explanation in `error.message`. Neither case retries or retargets another turn. + +This never updates future-thread settings. To change those too, send a separate +`thread/settings/update` and handle its queued acknowledgement separately. An older +server rejects the unknown turn method; clients must not fall back to a thread update. +No new step-state inspection API is provided. + +This diagnostic path retains live authorization and temporary safety checks. Most +consumers, including model-specific world-state instructions, still use initial-turn +settings. Saved threads are supported, but complete model-instruction correctness, +model attribution, and resume behavior for these switches are not guaranteed. + ### Example: Steer an active turn Use `turn/steer` to append additional user input to the currently active regular turn. This does @@ -1533,7 +1661,9 @@ All filesystem paths in this section must be absolute. Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `thread/archived`, `thread/unarchived`, `thread/closed`, `turn/*`, and `item/*` notifications. -Thread realtime uses a separate thread-scoped notification surface. `thread/realtime/*` notifications are ephemeral transport events, not `ThreadItem`s, and are not returned by `thread/read`, `thread/resume`, or `thread/fork`. +Thread realtime publishes thread-scoped timeline item lifecycle notifications for paginated threads alongside its existing realtime notifications. Completed timeline items are durably interleaved with ordinary turn items by `thread/timeline/list`. Neither surface changes `ThreadItem`, `thread/read`, `thread/resume`, or `thread/fork`; clients ignore notification methods they do not recognize. + +Each realtime item has an `id`, a `realtimeSessionId`, and one of four types: `realtimeSessionStarted`, `transcriptSegment`, `bemItemPromoted`, or `realtimeSessionClosed`. A `bemItemPromoted` item references an existing backing-agent item by `turnId` and `itemId`; its `presentation` is `wholeItem`, `inlineMarkdown`, or `inlineVisualization` with an `index`. Recoverable configuration and initialization warnings use the existing `configWarning` notification: `{ summary, details?, path?, range? }`. App-server may emit it during initialization for config parsing and related setup diagnostics, or to the requesting connection during `thread/start` when that thread's exec-policy rules fail to parse. @@ -1568,6 +1698,9 @@ The thread realtime API emits thread-scoped notifications for session lifecycle - `thread/realtime/itemAdded` — `{ threadId, item }` for raw non-audio realtime items that do not have a dedicated typed app-server notification, including `handoff_request` (experimental). `item` is forwarded as raw JSON while the upstream websocket item schema remains unstable. - `thread/realtime/transcript/delta` — `{ threadId, role, delta }` for live realtime transcript deltas (experimental). - `thread/realtime/transcript/done` — `{ threadId, role, text }` when realtime emits the final full text for a transcript part (experimental). +- `thread/realtime/item/started` — `{ threadId, item }` when a realtime item begins. Session boundaries and artifacts complete immediately; transcript segment IDs remain stable through streaming and persistence (experimental). +- `thread/realtime/item/transcript/delta` — `{ threadId, itemId, delta }` for text appended to a started transcript segment (experimental). +- `thread/realtime/item/completed` — `{ threadId, item }` after a session boundary, transcript segment, or promoted backing-agent artifact has been durably committed (experimental). - `thread/realtime/outputAudio/delta` — `{ threadId, audio }` for streamed output audio chunks (experimental). `audio` uses camelCase fields (`data`, `sampleRate`, `numChannels`, `samplesPerChannel`). - `thread/realtime/error` — `{ threadId, message }` when realtime encounters a transport or backend error (experimental). - `thread/realtime/closed` — `{ threadId, reason }` when the realtime transport closes (experimental). @@ -1587,7 +1720,7 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o The app-server streams JSON-RPC notifications while a turn is running. Each turn emits `turn/started` when it begins running and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`. - `turn/started` — `{ turn }` with the turn id, empty `items`, and `status: "inProgress"`. -- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; successful turns include their final agent message when available, and failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`. +- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; successful turns include their final agent message when available, and failures carry `{ error: { message, codexErrorInfo?, additionalDetails?, misalignment? } }`. - `turn/diff/updated` — `{ threadId, turnId, diff }` represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item. `diff` is the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individual `fileChange` items. - `turn/plan/updated` — `{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`. - `rawResponse/completed` — internal-only; when `thread/start.experimentalRawEvents` is enabled, emits `{ threadId, turnId, responseId, usage }` once for each upstream Responses API completion. `usage` is the exact upstream usage payload mapped to the app-server token breakdown shape and is `null` when the upstream completion omitted usage. Unlike `thread/tokenUsage/updated`, this notification is not accumulated, estimated, persisted, or replayed. @@ -1603,6 +1736,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn `ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items: - `userMessage` — `{id, clientId, content}` where `clientId` is the optional `clientUserMessageId` supplied to `turn/start` or `turn/steer`, and `content` is a list of user inputs (`text`, `image`, `localImage`, `audio`, or `localAudio`). +- `functionCallOutput` — `{id, name, namespace, output}` for a standalone function-call output without a `call_id`. `namespace` is nullable, and `output` is either a string or structured content items. Clients decide whether to render these tool-authority items; ordinary paired function-call outputs are not emitted separately. - `agentMessage` — `{id, text, phase, memoryCitation, delivery}` containing the accumulated agent reply. `delivery: "async"` identifies a user-visible message sent without ending the current turn; ordinary agent messages have `delivery: null`. - `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental). - `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models). @@ -1611,6 +1745,13 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn - `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`. - `mcpToolCall` — `{id, server, tool, status, arguments, appContext, mcpAppResourceUri?, pluginId, readOnlyHint, result?, error?}` describing MCP calls; `appContext` is `{connectorId, linkId, resourceUri, appName, actionName}` for calls through a trusted MCP app, where `connectorId` identifies the connector that owns the tool, `linkId` identifies the app link, `resourceUri` points to the widget template, `appName` is the connector's display name, and `actionName` is the stable connector `Action.name`. `readOnlyHint` is `true` for read-only tools, `false` for write-capable tools, and `null` when the annotation is unavailable, including older rollout entries. The hint describes tool capability, not whether an invocation succeeded or performed a write; use `status`, `result`, and `error` to determine the execution outcome. `appName` and `actionName` may be null for older rollout entries. The top-level `mcpAppResourceUri` is deprecated and temporarily duplicated for client migration. `tool` identifies the raw MCP tool. `status` is `inProgress`, `completed`, or `failed`. - `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`. +- `subAgentActivity` — `{id, kind, agentThreadId, agentPath}` describing Multi-Agent V2 lifecycle activity; `kind` is `started`, `interacted`, `interrupted`, or `completed`. A successful child completion is attributed to the parent turn that spawned it, so its `item/completed` notification may arrive after that turn's `turn/completed` notification and is included with that turn when history is read. + + The `CollabAgentTool` schema also includes `sendMessage`, `followupTask`, `interruptAgent`, and + `listAgents` for private Multi-Agent V2 analytics. These calls do not emit public collaborator tool + items; their existing `subAgentActivity` notifications are unchanged, and `list_agents` emits no + activity item. Calls cancelled during handler execution are recorded privately with status + `interrupted`, distinct from tool failures. - `webSearch` — `{id, query, action?, results?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion. For standalone web search, `results` contains the out-of-band structured result DTOs returned by `/v1/alpha/search`; clients should ignore result types and fields they do not understand. - `imageGeneration` — `{id, status, revisedPrompt, result, transparentBackground, savedPath?}` for a generated image. `transparentBackground` is `true` when the Images API reports a transparent background, `false` when it reports an opaque background, and `null` when the background is automatic, unavailable, or the item has not completed. The field is always present on v2 item payloads, including persisted and resumed items. - `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool. @@ -1628,7 +1769,7 @@ All items emit shared lifecycle events: - `item/autoApprovalReview/completed` — [UNSTABLE] temporary auto-review notification carrying `{threadId, turnId, targetItemId, review, action}` when approval auto-review resolves. This shape is expected to change soon. - `autoApprovalReview/strictReviewRequired` — experimental notification carrying `{threadId, turnId, startedAtMs}` whenever elevated or stale Guardian v2 risk requires synchronous approval review. -`review` is [UNSTABLE] and currently has `{status, riskLevel?, userAuthorization?, rationale?}`, where `status` is one of `inProgress`, `approved`, `denied`, or `aborted`. `riskLevel` is one of `"low"`, `"medium"`, `"high"`, or `"critical"` when present. `userAuthorization` is one of `"unknown"`, `"low"`, `"medium"`, or `"high"` when present. `action` is a tagged union with `type: "command" | "execve" | "applyPatch" | "networkAccess" | "mcpToolCall"`. Command-like actions include a `source` discriminator (`"shell"` or `"unifiedExec"`). These notifications are separate from the target item's own `item/completed` lifecycle and are intentionally temporary while the auto-review app protocol is still being designed. +`review` is [UNSTABLE] and currently has `{status, riskLevel?, userAuthorization?, rationale?}`, where `status` is one of `inProgress`, `approved`, `denied`, or `aborted`. `riskLevel` is one of `"low"`, `"medium"`, `"high"`, or `"critical"` when present. `userAuthorization` is one of `"unknown"`, `"low"`, `"medium"`, or `"high"` when present. `action` is a tagged union with `type: "command" | "execve" | "writeStdin" | "applyPatch" | "networkAccess" | "mcpToolCall" | "requestPermissions"`. Command-like actions include a `source` discriminator (`"shell"` or `"unifiedExec"`). A `writeStdin` action carries `approvalId`, `processId`, `stdin`, and `cwd`; it reviews input to an existing command item without changing that parent item's lifecycle. These notifications are separate from the target item's own `item/completed` lifecycle and are intentionally temporary while the auto-review app protocol is still being designed. There are additional item-specific events: @@ -1658,13 +1799,16 @@ There are additional item-specific events: ### Errors -`error` event is emitted whenever the server hits an error mid-turn (for example, upstream model errors or quota limits). Carries the same `{ error: { message, codexErrorInfo?, additionalDetails? } }` payload as `turn.status: "failed"` and may precede that terminal notification. +Ownership rejections for parent-owned Multi-Agent V2 subagents return JSON-RPC error code `-32600` with message `direct app-server input is not allowed for multi-agent v2 sub-agents`. + +`error` event is emitted whenever the server hits an error mid-turn (for example, upstream model errors or quota limits). Carries the same `{ error: { message, codexErrorInfo?, additionalDetails?, misalignment? } }` payload as `turn.status: "failed"` and may precede that terminal notification. `codexErrorInfo` maps to the `CodexErrorInfo` enum. Common values: - `ContextWindowExceeded` - `SessionBudgetExceeded` - `UsageLimitExceeded` +- `rateLimitExceeded`: an upstream rate limit received inside a streaming response; the turn fails with this category only after its existing stream retry budget is exhausted - `misalignmentPolicyViolation`: a non-retryable request blocked by the misalignment policy - `HttpConnectionFailed { httpStatusCode? }`: upstream HTTP failures including 4xx/5xx - `ResponseStreamConnectionFailed { httpStatusCode? }`: failure to connect to the response SSE stream @@ -1680,6 +1824,16 @@ There are additional item-specific events: When an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant. +For `misalignmentPolicyViolation`, optional `misalignment` details contain `errorType`, +`detailedExplanation`, and `steer: { message }`. Error categories are open-ended. A category alone +remains a terminal block; clients may offer continuation only when both a substantive explanation +and a steering message are present. To continue after user confirmation, submit the steering +message with the existing `turn/start` method and include +`responsesapiClientMetadata: { misalignment_override: JSON.stringify({ timestamp, feedback }) }`, +where `timestamp` is the confirmation time in Unix milliseconds and `feedback` is the user's +explanation. Misalignment explanation and steering details are delivered live but excluded from +persisted rollout errors, so unavailable details after a restart remain a terminal block. + ## Approvals Certain actions (shell commands or modifying files) may require explicit user approval depending on the user's config. When `turn/start` is used, the app-server drives an approval flow by sending a server-initiated JSON-RPC request to the client. The client must respond to tell Codex whether to proceed. UIs should present these requests inline with the active turn so users can review the proposed command or diff before choosing. @@ -1692,11 +1846,17 @@ Certain actions (shell commands or modifying files) may require explicit user ap Order of messages: 1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action. -2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, the nullable `environmentId` where the command will run, optionally `approvalId` (for subcommand callbacks), and `reason`. New shell and unified-exec approvals set `environmentId`; older events that do not provide one are exposed as `null`. For normal command approvals, the request also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire, and network access is represented as `additionalPermissions.network.enabled`. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted. +2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, the nullable `environmentId` where the command will run, `kind` (`command` or `writeStdin`), optionally `approvalId` (for subcommand callbacks or stdin writes), and `reason`. New shell and unified-exec approvals set `environmentId`; older events that do not provide one are exposed as `null`. For normal command approvals, the request also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire, and network access is represented as `additionalPermissions.network.enabled`. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted. 3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`. 4. `serverRequest/resolved` — `{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt. 5. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result. +`kind` distinguishes command approvals from writes to an existing terminal. Requests from older servers without `kind` retain `command` semantics; `approvalId` alone does not distinguish stdin writes from execve interception. + +When stdin approvals are enabled, a `write_stdin` approval sets `kind: "writeStdin"`, references the original terminal command's `itemId`, and has its own `approvalId`. The request belongs to the current turn, which may differ from the turn that opened the terminal. With `approvalsReviewer: "auto_review"`, the `item/autoApprovalReview/*` notifications likewise target the original command item and carry an action of type `writeStdin` with `approvalId`, `processId`, `stdin`, and `cwd`. For stdin approvals, `cwd` is the terminal’s launch directory, not its current working directory. Approving or denying a stdin write does not start, complete, or change the status of the parent command-execution item. + +For reviewed stdin, the complete formatted action and approval reason must fit within 8,000 bytes. Oversized or truncated actions are rejected before any bytes reach the terminal, rather than reviewing a shortened input and executing the full input. + ### File change approvals Order of messages: @@ -1880,6 +2040,7 @@ $skill-creator Add a new skill for triaging flaky CI and include step-by-step us ``` Use `skills/list` to fetch the available skills (optionally scoped by `cwds`, with `forceReload`). +Each skill includes a nullable `pluginId` matching its owning plugin's `id` in `plugin/list`, when known. Clients can use it to group plugin-owned skills without inferring ownership from names or paths. Older servers may omit this field. `skills/list` might reuse a cached skills result per `cwd`; setting `forceReload` to `true` refreshes the result from disk. The server also emits `skills/changed` notifications when watched local skill files change. Treat this as an invalidation signal and re-run `skills/list` with your current params when needed. Use `skills/extraRoots/set` to replace additional standalone skill roots for the current app-server process. These roots use the same layout as other standalone skill roots: each root contains skill directories, and each skill directory contains `SKILL.md`. Missing roots are accepted and load no skills until they exist. This setting is lost when app-server exits. @@ -1897,6 +2058,7 @@ Use `skills/extraRoots/set` to replace additional standalone skill roots for the "name": "skill-creator", "description": "Create or update a Codex skill", "enabled": true, + "pluginId": null, "interface": { "displayName": "Skill Creator", "shortDescription": "Create or update a Codex skill", @@ -2236,17 +2398,19 @@ Codex supports these authentication modes. The current mode is surfaced in `acco - **API key (`apiKey`)**: Caller supplies an OpenAI API key via `account/login/start` with `type: "apiKey"`. The API key is saved and used for API requests. - **ChatGPT managed (`chatgpt`)** (recommended): Codex owns the ChatGPT OAuth flow and refresh tokens. Start via `account/login/start` with `type: "chatgpt"` for the browser flow or `type: "chatgptDeviceCode"` for device code; Codex persists tokens to disk and refreshes them automatically. -- **Codex managed Amazon Bedrock auth (`amazonBedrock`, experimental)**: Caller supplies an Amazon Bedrock API key and region via `account/login/start` with `type: "amazonBedrock"`. The client must enable the `experimentalApi` initialization capability for Codex-managed Amazon Bedrock login. Codex replaces the current primary auth with the Bedrock credential and writes `model_provider = "amazon-bedrock"` to the user config. +- **Codex managed Amazon Bedrock auth (experimental)**: Caller supplies an Amazon Bedrock API key using `type: "amazonBedrock"` or AWS access keys using `type: "amazonBedrockAccessKeys"` via `account/login/start`. The client must enable the `experimentalApi` initialization capability. Codex replaces the current primary auth with the Bedrock credential and writes `model_provider = "amazon-bedrock"` to the user config. - **Personal access token (`personalAccessToken`)**: Codex uses a ChatGPT-backed personal access token loaded outside the app-server login RPCs, such as with `codex login --with-access-token` or `CODEX_ACCESS_TOKEN`. ### API Overview - `account/read` — fetch current account info; optionally refresh tokens. -- `account/login/start` — begin login (`apiKey`, `chatgpt`, `chatgptDeviceCode`, `amazonBedrock`). +- `account/login/start` — begin login (`apiKey`, `chatgpt`, `chatgptDeviceCode`, `amazonBedrock`, `amazonBedrockAccessKeys`). +- `account/bedrock/discover` — experimental; list available AWS profiles and identify AWS access keys or Amazon Bedrock API keys visible in the app-server environment. +- `account/bedrock/setup` — experimental; validate a selected AWS profile or existing environment credentials, then persist the Amazon Bedrock provider configuration. - `account/login/completed` (notify) — emitted when a login attempt finishes (success or error). - `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`. - `account/logout` — sign out; triggers `account/updated` on success. -- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `bedrockApiKey`, `chatgpt`, `personalAccessToken`, or `null`) and includes the current ChatGPT `planType` when available. +- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `bedrockApiKey`, `bedrockAccessKeys`, `chatgpt`, `personalAccessToken`, or `null`) and includes the current ChatGPT `planType` when available. - `account/rateLimits/read` — fetch ChatGPT rate limits, an optional effective monthly credit limit, whether spend control has been reached, and the earned rate-limit resets currently available, including expiry details when provided by the backend. Rate-limit updates arrive via `account/rateLimits/updated` (notify); reset-credit data is snapshot-only. - `account/rateLimitResetCredit/consume` — consume one earned reset using a caller-provided idempotency key, optionally selecting a reset-credit ID returned by `account/rateLimits/read`. - `account/usage/read` — fetch ChatGPT account token-activity summary and daily buckets, or pass a valid thread UUID as `threadId` to read estimated credits, optional cost, and usage breakdowns for one thread using the app-server's active account. The optional `threadUsage` response field is absent on older servers and `null` when the billing route is unavailable. @@ -2256,6 +2420,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco - `account/sendAddCreditsNudgeEmail` — ask ChatGPT to email the workspace owner about depleted credits or a reached usage limit. - `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, threadId, success, error? }`. - `mcpServer/startupStatus/updated` (notify) — emitted when a configured MCP server's startup status changes; payload includes `{ threadId, name, status, error, failureReason }`, where `threadId` is the owning thread when startup is thread-scoped and `null` when it is app-scoped, and `status` is `starting`, `ready`, `failed`, or `cancelled`. `failureReason` is `reauthenticationRequired` when stored OAuth credentials have expired and cannot be refreshed, so clients can prompt the user to reconnect the named server. +- `mcpServer/event/stream/notification` (experimental, notify) — forwards `{ subscriptionId, notification: { method, params } }` to the connection that owns the subscription. ### 1) Check auth state @@ -2277,7 +2442,7 @@ Field notes: - `refreshToken` (bool): set `true` to force a token refresh. - `email` is `null` when the ChatGPT account does not have an email address. - `requiresOpenaiAuth` reflects the active provider; when `false`, Codex can run without OpenAI credentials. -- Amazon Bedrock reports `usesCodexManagedCredentials: true` when it uses a Bedrock API key managed by Codex. It reports `false` for external credential paths, including the AWS credential chain and configured command auth. This identifies whether Codex-managed credentials are selected; it does not validate that the credential source can resolve credentials. +- Amazon Bedrock reports `usesCodexManagedCredentials: true` when it uses a Bedrock API key or AWS access keys managed by Codex. It reports `false` for external credential paths, including the AWS credential chain and configured command auth. This identifies whether Codex-managed credentials are selected; it does not validate that the credential source can resolve credentials. ### 2) Log in with an API key @@ -2320,7 +2485,7 @@ Field notes: `onboardingEntrypoint` is optional and is only emitted when the OAuth callback carries a recognized onboarding hint. -### 3) Log in with an Amazon Bedrock API key +### 3) Log in with Amazon Bedrock credentials This experimental flow requires the client to initialize with `experimentalApi: true`. @@ -2342,7 +2507,77 @@ This experimental flow requires the client to initialize with `experimentalApi: { "method": "account/updated", "params": { "authMode": "bedrockApiKey", "planType": null } } ``` -Codex stores the key and region as the primary Codex auth, replacing any previously stored login, and writes `model_provider = "amazon-bedrock"` to the active user config. Existing loaded sessions keep their current provider selection, so clients should restart the app-server before sending more model requests. This limitation will be addressed in a follow-up. +To log in with AWS access keys instead: + +```json +{ + "method": "account/login/start", + "id": 30, + "params": { + "type": "amazonBedrockAccessKeys", + "accessKeyId": "...", + "secretAccessKey": "...", + "sessionToken": "...", + "region": "us-west-2" + } +} +{ "id": 30, "result": { "type": "amazonBedrock" } } +{ "method": "account/login/completed", "params": { "loginId": null, "success": true, "error": null } } +{ "method": "account/updated", "params": { "authMode": "bedrockAccessKeys", "planType": null } } +``` + +The session token is optional. Both flows store credentials in the configured auth backend +(`auth.json` or keyring), replace any previously stored login, and select +`model_provider = "amazon-bedrock"`; access-key login also writes the selected AWS region to the +active user config. Neither flow changes `$CODEX_HOME/.env`. Existing loaded sessions keep their +current provider selection, so clients should restart the app-server before sending more model +requests. This limitation will be addressed in a follow-up. + +### Discover and configure AWS-managed Amazon Bedrock credentials + +These experimental methods require the client to initialize with `experimentalApi: true`. + +Discover AWS profiles and credentials already visible to the app-server process: + +```json +{ "method": "account/bedrock/discover", "id": 31, "params": {} } +{ + "id": 31, + "result": { + "profiles": [{ "name": "engineering", "region": "us-west-2" }], + "environmentCredentials": [ + { "type": "accessKeys", "region": "us-west-2" }, + { "type": "bedrockApiKey", "region": "us-west-2" } + ] + } +} +``` + +Discovery returns credential metadata only; it never includes access keys, secret access keys, +session tokens, or Bedrock API keys. A profile or environment credential's `region` is `null` +when no profile region or explicit `AWS_REGION` is available from that source. + +Set up a named AWS profile: + +```json +{ + "method": "account/bedrock/setup", + "id": 32, + "params": { "type": "profile", "profile": "engineering", "region": "us-west-2" } +} +{ "id": 32, "result": {} } +``` + +To select credentials already visible in the environment, use +`{ "type": "environment", "region": "us-west-2" }`. The provider +resolves available environment credentials through its normal authentication chain. Selecting +profile or environment credentials leaves existing keys in `$CODEX_HOME/.env` unchanged. + +Successful setup writes `model_provider = "amazon-bedrock"` and the selected AWS region to the +active user config, and additionally writes the selected profile for profile-based setup. Clients +should restart the app-server before sending more model requests. Logging out while an Amazon +Bedrock provider is selected clears the user-configured provider, profile, and region, removes +any Codex-managed credentials, and leaves AWS-managed credentials and `$CODEX_HOME/.env` unchanged. ### 4) Log in with ChatGPT (device code flow) @@ -2373,7 +2608,12 @@ Codex stores the key and region as the primary Codex auth, replacing any previou { "method": "account/updated", "params": { "authMode": null, "planType": null } } ``` -When using a Codex-managed Bedrock key, logout removes the key and clears `model_provider` if it is still set to `"amazon-bedrock"`. When using AWS-managed credentials, manage them through AWS or switch providers before logging out. +When `model_provider` is `"amazon-bedrock"` or `"amazon-bedrock-runtime"`, logout clears that +provider selection and its configured AWS profile and region, regardless of whether the +credentials are Codex-managed or AWS-managed. If the selected model is Bedrock-specific, logout +also clears `model`; `model_reasoning_effort` and other generic settings are preserved. +Codex-managed credentials are removed; AWS profiles, environment credentials, and +`$CODEX_HOME/.env` are left untouched. ### 7) Rate limits (ChatGPT) diff --git a/package.json b/package.json index 8b23089..753b717 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", - "@openai/codex": "0.149.1", + "@openai/codex": "0.151.0", "@swc/core": "^1.16.1", "@types/better-sqlite3": "^7.6.13", "@types/jsonwebtoken": "^9.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18ba553..fc86aaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,8 +112,8 @@ importers: specifier: ^11.0.1 version: 11.1.19(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-express@11.1.19) '@openai/codex': - specifier: 0.149.1 - version: 0.149.1 + specifier: 0.151.0 + version: 0.151.0 '@swc/core': specifier: ^1.16.1 version: 1.16.1 @@ -249,11 +249,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} @@ -1141,43 +1141,43 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true - '@openai/codex@0.149.1': - resolution: {integrity: sha512-6q5pbcpFbJbqOpkubSDBwXmktQ55aD8eUzGzBF1zASob2DjwhBKDSNGtdZKalfrNJUdTDTPDMmzCXEXs5tMBYA==} + '@openai/codex@0.151.0': + resolution: {integrity: sha512-mhtWmOZRdmWD1jPbLDnQb59BsaVP/V+lXe/OFNR9ZcLZU0UCiBwn98Fcav1ss7sDIlHkuqj6nWd44IPeXoOhJA==} engines: {node: '>=16'} hasBin: true - '@openai/codex@0.149.1-darwin-arm64': - resolution: {integrity: sha512-6X84kTCbnTgPIJ2EdcPsrvwS0Wxsqpa+bCswGmRf4BjhcQ5nPMnBC6yCAaCMj+vrbXQHj+L6sa9FaR4QkmA1qw==} + '@openai/codex@0.151.0-darwin-arm64': + resolution: {integrity: sha512-g7YzpaCZGCw19R/gly3vRPjnLqaW7JcBAu2WQQ6e8PIlvBPmS/gMplIUURMgNO6gi8LsPzdlQtLqkwoeOOlIdg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@openai/codex@0.149.1-darwin-x64': - resolution: {integrity: sha512-MfLBQLfcElJL9tvj6y45qVHHMGSXCPnQOixuD3/Zq0g1BW/eFizkrGLdn48cFpc+l8cK+gt5nYG5pQYwVs6g4A==} + '@openai/codex@0.151.0-darwin-x64': + resolution: {integrity: sha512-0y+g8TVpP+Fn10mjoKYXER6qYjn29w7xBUsbPXJ6Accu/FoM4Qp4WbKXQPmE0G0yUACTQVZRjzTSsdWUezNgkg==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@openai/codex@0.149.1-linux-arm64': - resolution: {integrity: sha512-OqxUfZ1TVvHd18zHPKK/8ZRlpk8Vy11mg5CMHaLxNWldTbwVImDKtSLWT+m8m4NM5Sz4PbjtZMrVT/RfpBW/mQ==} + '@openai/codex@0.151.0-linux-arm64': + resolution: {integrity: sha512-CsLgFeX4TQ6I2Gdrxd2r5UbgIbDLCdtcLAlnMYjr06bCL057MTNGec7Ewb3+Z2DBiMuXCljdTBGqLOePkMV0sQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@openai/codex@0.149.1-linux-x64': - resolution: {integrity: sha512-Of5fGYgr7tAMsyj6vhXb4/RM/UoA3Zq8BLegUBDC09UNy1XTLGYP/2XD+UX8z3qh0NDwxYdCjFIWdDNijKZggQ==} + '@openai/codex@0.151.0-linux-x64': + resolution: {integrity: sha512-xcVyY1FtwvVYhh2JBmz8fX8CQqFAxO/lxJ2IXsh8x5uwxZVHVl5fZHFHf8JdRaOGG0vpkYmu/DKKVoLd56/DDQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@openai/codex@0.149.1-win32-arm64': - resolution: {integrity: sha512-5K0DmOKGK9Bos627p8sK8ATHjovPK0sDyT6h9Cb+4v+5CW5SGw1HLgjGxoLfJ8g3cg6mtg/pRCXXo2L/j71UVA==} + '@openai/codex@0.151.0-win32-arm64': + resolution: {integrity: sha512-zDWzOoh9wHm+Om1Nhn7os47rAVeSGPh0SnM3YOttdq6iPJz2zn4vBnbGUZjeih1qW/3mvNF3Oyd4owlaHmphmg==} engines: {node: '>=16'} cpu: [arm64] os: [win32] - '@openai/codex@0.149.1-win32-x64': - resolution: {integrity: sha512-G3QXGAg7nyyhqOeooAMUekBCeHd8a1QByhKcVAFyzNBaI06t6Ft7nsF+1SzFS0spuIdU4YyMi5YD26ukADBQUQ==} + '@openai/codex@0.151.0-win32-x64': + resolution: {integrity: sha512-sLT7xvID3jhU6tkzcwRPnMEclKRwUPbpo0mtfxIF9KpdZH3VJV7sM2/kXWXyvUM7Zt/YeyOaeATTEysbRz8Yog==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -2240,6 +2240,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -4554,31 +4555,31 @@ snapshots: dependencies: consola: 3.4.2 - '@openai/codex@0.149.1': + '@openai/codex@0.151.0': optionalDependencies: - '@openai/codex-darwin-arm64': '@openai/codex@0.149.1-darwin-arm64' - '@openai/codex-darwin-x64': '@openai/codex@0.149.1-darwin-x64' - '@openai/codex-linux-arm64': '@openai/codex@0.149.1-linux-arm64' - '@openai/codex-linux-x64': '@openai/codex@0.149.1-linux-x64' - '@openai/codex-win32-arm64': '@openai/codex@0.149.1-win32-arm64' - '@openai/codex-win32-x64': '@openai/codex@0.149.1-win32-x64' + '@openai/codex-darwin-arm64': '@openai/codex@0.151.0-darwin-arm64' + '@openai/codex-darwin-x64': '@openai/codex@0.151.0-darwin-x64' + '@openai/codex-linux-arm64': '@openai/codex@0.151.0-linux-arm64' + '@openai/codex-linux-x64': '@openai/codex@0.151.0-linux-x64' + '@openai/codex-win32-arm64': '@openai/codex@0.151.0-win32-arm64' + '@openai/codex-win32-x64': '@openai/codex@0.151.0-win32-x64' - '@openai/codex@0.149.1-darwin-arm64': + '@openai/codex@0.151.0-darwin-arm64': optional: true - '@openai/codex@0.149.1-darwin-x64': + '@openai/codex@0.151.0-darwin-x64': optional: true - '@openai/codex@0.149.1-linux-arm64': + '@openai/codex@0.151.0-linux-arm64': optional: true - '@openai/codex@0.149.1-linux-x64': + '@openai/codex@0.151.0-linux-x64': optional: true - '@openai/codex@0.149.1-win32-arm64': + '@openai/codex@0.151.0-win32-arm64': optional: true - '@openai/codex@0.149.1-win32-x64': + '@openai/codex@0.151.0-win32-x64': optional: true '@oxc-project/types@0.146.0': {} diff --git a/src/threads/thread-resume-registry.service.ts b/src/threads/thread-resume-registry.service.ts index 0462364..324b2fc 100644 --- a/src/threads/thread-resume-registry.service.ts +++ b/src/threads/thread-resume-registry.service.ts @@ -13,6 +13,12 @@ import { type TurnsPage, } from './thread-history.service'; +type CachedThreadResponse = + | v2.ThreadStartResponse + | v2.ThreadForkResponse + | v2.ThreadResumeResponse + | MetadataFirstResumeResponse; + /** Prevents duplicate app-server resume calls for the same thread generation. */ @Injectable() export class ThreadResumeRegistryService { @@ -27,10 +33,7 @@ export class ThreadResumeRegistryService { * Used by `readAsResume` to return a complete `ThreadResumeResponse` * even though `thread/read` doesn't include resolved settings. */ - private readonly responseCache = new Map< - string, - v2.ThreadResumeResponse | MetadataFirstResumeResponse - >(); + private readonly responseCache = new Map(); constructor( private readonly history: ThreadHistoryService, @@ -109,7 +112,7 @@ export class ThreadResumeRegistryService { */ cacheResponse( threadId: string, - response: v2.ThreadResumeResponse | MetadataFirstResumeResponse, + response: CachedThreadResponse, ): void { this.responseCache.set(threadId, response); } @@ -200,7 +203,7 @@ export class ThreadResumeRegistryService { } private toWritableOpen( - response: v2.ThreadResumeResponse | MetadataFirstResumeResponse, + response: CachedThreadResponse, ): ThreadOpenResponseDto { const initialTurnsPage = this.readEmbeddedTurnsPage(response); return { @@ -229,7 +232,7 @@ export class ThreadResumeRegistryService { } private readEmbeddedTurnsPage( - response: v2.ThreadResumeResponse | MetadataFirstResumeResponse, + response: CachedThreadResponse, ): TurnsPage { const candidate = (response as MetadataFirstResumeResponse) .initialTurnsPage; @@ -263,7 +266,7 @@ export class ThreadResumeRegistryService { } private readNullableString( - response: v2.ThreadResumeResponse | MetadataFirstResumeResponse, + response: CachedThreadResponse, key: 'turnsBackwardsCursor' | 'itemsBackwardsCursor', ): string | null { const value = (response as Record)[key]; From 1b595e49efa635e45c8d07f23de4ec5e84087fa3 Mon Sep 17 00:00:00 2001 From: kot4ri <20045613+kot4ri@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:43:42 +0800 Subject: [PATCH 2/2] fix: fall back when thread item paging is unavailable --- src/threads/thread-history.service.spec.ts | 37 +++++++++++++++++ src/threads/thread-history.service.ts | 46 ++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/threads/thread-history.service.spec.ts b/src/threads/thread-history.service.spec.ts index af39209..62bc885 100644 --- a/src/threads/thread-history.service.spec.ts +++ b/src/threads/thread-history.service.spec.ts @@ -1,4 +1,5 @@ import { CodexService } from '../codex/codex.service'; +import { CodexRpcError } from '../codex/codex-errors'; import { ThreadHistoryService } from './thread-history.service'; describe('ThreadHistoryService', () => { @@ -94,4 +95,40 @@ describe('ThreadHistoryService', () => { { threadId: 'missing', count: null, errorMessage: 'gone' }, ]); }); + + it('falls back to full turn pages when thread/items/list is unsupported', async () => { + mockCodex.request + .mockRejectedValueOnce( + new CodexRpcError( + { code: -32601, message: 'thread/items/list is not supported yet' }, + { method: 'thread/items/list' }, + ), + ) + .mockResolvedValueOnce({ + data: [ + { + id: 'turn-1', + items: [{ type: 'plan', id: 'item-1', text: 'Plan' }], + }, + ], + nextCursor: null, + backwardsCursor: null, + }); + + await expect(service.listTurnItems('t1', 'turn-1')).resolves.toEqual([ + { + turnId: 'turn-1', + item: { type: 'plan', id: 'item-1', text: 'Plan' }, + }, + ]); + expect(mockCodex.request).toHaveBeenNthCalledWith( + 2, + 'thread/turns/list', + expect.objectContaining({ + threadId: 't1', + itemsView: 'full', + sortDirection: 'desc', + }), + ); + }); }); diff --git a/src/threads/thread-history.service.ts b/src/threads/thread-history.service.ts index eda2926..2b0ff9e 100644 --- a/src/threads/thread-history.service.ts +++ b/src/threads/thread-history.service.ts @@ -1,6 +1,7 @@ /** Experimental paged thread-history access isolated from stable Codex types. */ import { Injectable, Logger } from '@nestjs/common'; import type { v2 } from '../codex/codex-schema'; +import { isCodexRpcError } from '../codex/codex-errors'; import { CodexService } from '../codex/codex.service'; export type TurnItemsView = 'notLoaded' | 'summary' | 'full'; @@ -163,6 +164,27 @@ export class ThreadHistoryService { async listTurnItems( threadId: string, turnId: string, + ): Promise { + try { + return await this.listTurnItemsDirect(threadId, turnId); + } catch (err) { + if ( + !isCodexRpcError(err) || + err.code !== -32601 || + err.method !== 'thread/items/list' + ) { + throw err; + } + this.logger.debug( + `thread/items/list is unavailable; falling back to full turn pages for thread=${threadId} turn=${turnId}`, + ); + return this.listTurnItemsFromTurns(threadId, turnId); + } + } + + private async listTurnItemsDirect( + threadId: string, + turnId: string, ): Promise { const entries: ThreadItemEntry[] = []; let cursor: string | undefined; @@ -201,6 +223,30 @@ export class ThreadHistoryService { return entries.filter((entry) => !entry.turnId || entry.turnId === turnId); } + /** Compatibility path for app-server builds that expose turns paging first. */ + private async listTurnItemsFromTurns( + threadId: string, + turnId: string, + ): Promise { + let cursor: string | null | undefined; + for (let page = 0; page < TURN_COUNT_MAX_PAGES; page += 1) { + const turnsPage = await this.listTurns({ + threadId, + cursor, + limit: TURN_COUNT_PAGE_SIZE, + sortDirection: 'desc', + itemsView: 'full', + }); + const turn = turnsPage.data.find((candidate) => candidate.id === turnId); + if (turn) { + return turn.items.map((item) => ({ turnId, item })); + } + cursor = turnsPage.nextCursor; + if (!cursor) break; + } + throw new Error(`Turn ${turnId} was not found in thread ${threadId}`); + } + /** * Counts turns for graph nodes without resuming. *