From e44497df2dace5b3f5d74d2af8e1eb5a5a0c8987 Mon Sep 17 00:00:00 2001 From: "acp-release-bot[bot]" <246668977+acp-release-bot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:33:09 +0300 Subject: [PATCH 01/45] Update codex to 0.142.2 (#239) * Update codex to 0.142.2 * fix: verify types and tests after codex update --------- Co-authored-by: github-actions[bot] --- package-lock.json | 56 +++++++++---------- package.json | 2 +- src/app-server/LegacyAppPathString.ts | 8 +-- src/app-server/ResponseItem.ts | 2 +- ...ModelSafetyBufferingUpdatedNotification.ts | 2 +- src/app-server/v2/PluginInterface.ts | 8 +++ 6 files changed, 43 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index e036ee8a..0b291df7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^0.28.1", - "@openai/codex": "^0.142.0", + "@openai/codex": "^0.142.2", "diff": "^8.0.3", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", @@ -872,9 +872,9 @@ } }, "node_modules/@openai/codex": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0.tgz", - "integrity": "sha512-c7WftbRyE4zOLJV5p73mcGn4jVK3FmK84Q65hrZrXboZzLPDWRfbFedCbsIjU4PJS/kvgyMPhv7dH4/nhXzUyg==", + "version": "0.142.2", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2.tgz", + "integrity": "sha512-/89NO3WUTU3WmavBHuVyJzfGXasjT8DtrT6kU5VxAiR4pkZj4M0c4VHmSvzqnmpqsODsy2THHKbiiNBi/d2jaQ==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -883,19 +883,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.142.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.142.0-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.2-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.2-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.2-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.142.2-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.2-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.142.2-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.142.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-darwin-arm64.tgz", - "integrity": "sha512-jwbriCRTSNfqoGov5bNnnYAKarnNv4QfGkut8psKAajebL6VPI2ZjCxCJ1OFA5HhUQsN8GQgEjKlj6WhYcMmUA==", + "version": "0.142.2-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-darwin-arm64.tgz", + "integrity": "sha512-NtbEXS5nvfT6FNoQSzS0/rCRfG5cNWgVZLxArCFCCXm+Z5vOz3WSVX041ChxIl+dKwRpU6HMpSd9tR5oPBBWZA==", "cpu": [ "arm64" ], @@ -910,9 +910,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.142.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-darwin-x64.tgz", - "integrity": "sha512-qwdPfW8sOBEfWw1Rt7baveUsOjrudrM7qfKNnJvHPRrwtt4jemTd22VtZ6r02kUQdLwt6Ddvs0Pwzk6QJW6PqA==", + "version": "0.142.2-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-darwin-x64.tgz", + "integrity": "sha512-9IjBzTlvXbr+SDDWarM/d0C6C1addJV03gLzmqbU7ptLZBR2H/Hoxig924r04kEAKxo0nmdu9e+x3IMrIxoTtg==", "cpu": [ "x64" ], @@ -927,9 +927,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.142.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-linux-arm64.tgz", - "integrity": "sha512-gX9hCK59bE0Itnous1MCrKiXTYuoGVE6oJUE0kyRSzaBD267sh9TNR3DXKbY7TsP7iAs19n8YXDJNdHZJtakSQ==", + "version": "0.142.2-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-linux-arm64.tgz", + "integrity": "sha512-iOZDjULUawtPd7FFfDujhcj7f9DS8CyT4Be9cFAnGKvUFlAjeEa3/dgj4lQ5lwhzgQchoTduXS4NuAWDXNqp/Q==", "cpu": [ "arm64" ], @@ -944,9 +944,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.142.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-linux-x64.tgz", - "integrity": "sha512-nywS+ogFPRhZnQnKL+jzWKzFhmjTQbYPh6n8dtQ/E+sJ3FoZVcb9a6kvHL5yjgtlCiDkP0jPDYY5AK3tDShYyw==", + "version": "0.142.2-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-linux-x64.tgz", + "integrity": "sha512-96zStaXK8+NjKrytlxbGJ33MeMvU0SALFarYVgHsFwrTCeW8g3xsUaUDIv8YYRipyzQIWoHczLRYRJPSH4i6QA==", "cpu": [ "x64" ], @@ -961,9 +961,9 @@ }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.142.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-win32-arm64.tgz", - "integrity": "sha512-Lz8xEn7uNn0XTi8k0jApKM5P9BD7QvAH3ZbtlGi1zfSOgh1kmgpfXxaTlJF503mozdHaA2r6UA7hfTi+bI0CvQ==", + "version": "0.142.2-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-win32-arm64.tgz", + "integrity": "sha512-FZBlDmx71tvuadbhXMIte2qZiQ3coSYCi+j1CcZQ0oVkhgb/CTZX6KsIfJtvjGR4fty6mbDWdY7kEOd9iHUjzg==", "cpu": [ "arm64" ], @@ -978,9 +978,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.142.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.0-win32-x64.tgz", - "integrity": "sha512-LkARkXh3NM0XA8R8hFAgjx017fadfb9RgmqLzdhO1Qt/bDVSJnmDf4cwJ2h+FWZcm9/rRk3JC/IuqBvwg/TN+Q==", + "version": "0.142.2-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-win32-x64.tgz", + "integrity": "sha512-EZOCKvcBNCpipuffm82MHrPhnzs502mo5IoFvXbm1TOO87pH3x5KfRzUO5Wjf78FEelXzRe0j/kPzm+Xw1whLg==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 6a2109cb..dbf33517 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^0.28.1", - "@openai/codex": "^0.142.0", + "@openai/codex": "^0.142.2", "diff": "^8.0.3", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", diff --git a/src/app-server/LegacyAppPathString.ts b/src/app-server/LegacyAppPathString.ts index 04e465b8..e39784a8 100644 --- a/src/app-server/LegacyAppPathString.ts +++ b/src/app-server/LegacyAppPathString.ts @@ -11,10 +11,10 @@ * * When converting from [`PathUri`], "native" refers to the supplied * [`PathConvention`], which may be foreign to the operating system running - * this process. The inner string is private so path-producing code must convert - * from [`AbsolutePathBuf`] or use [`Self::from_path_uri`] instead of bypassing - * the intended conversion boundary. Non-UTF-8 paths are converted to UTF-8 - * lossily because this API value is serialized as a JSON string. + * this process. The inner string is private so path-producing code must use a + * path conversion method instead of bypassing the intended conversion + * boundary. Non-UTF-8 paths are converted to UTF-8 lossily because this API + * value is serialized as a JSON string. * * Deserialization accepts any UTF-8 string without interpreting or validating * it. That unrestricted construction path is intentionally available only to diff --git a/src/app-server/ResponseItem.ts b/src/app-server/ResponseItem.ts index 074e11a1..62547d3e 100644 --- a/src/app-server/ResponseItem.ts +++ b/src/app-server/ResponseItem.ts @@ -20,4 +20,4 @@ id?: string, /** * Set when using the Responses API. */ -call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" }; +call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" }; diff --git a/src/app-server/v2/ModelSafetyBufferingUpdatedNotification.ts b/src/app-server/v2/ModelSafetyBufferingUpdatedNotification.ts index 484f8c61..5abc9f3b 100644 --- a/src/app-server/v2/ModelSafetyBufferingUpdatedNotification.ts +++ b/src/app-server/v2/ModelSafetyBufferingUpdatedNotification.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ModelSafetyBufferingUpdatedNotification = { threadId: string, turnId: string, model: string, useCases: Array, reasons: Array, }; +export type ModelSafetyBufferingUpdatedNotification = { threadId: string, turnId: string, model: string, useCases: Array, reasons: Array, showBufferingUi: boolean, fasterModel: string | null, }; diff --git a/src/app-server/v2/PluginInterface.ts b/src/app-server/v2/PluginInterface.ts index 4e97ee66..1e57d497 100644 --- a/src/app-server/v2/PluginInterface.ts +++ b/src/app-server/v2/PluginInterface.ts @@ -21,10 +21,18 @@ composerIconUrl: string | null, * Local logo path, resolved from the installed plugin package. */ logo: AbsolutePathBuf | null, +/** + * Local dark-mode logo path, resolved from the installed plugin package. + */ +logoDark: AbsolutePathBuf | null, /** * Remote logo URL from the plugin catalog. */ logoUrl: string | null, +/** + * Remote dark-mode logo URL from the plugin catalog. + */ +logoUrlDark: string | null, /** * Local screenshot paths, resolved from the installed plugin package. */ From 30e61444c499019856b052244e6660057f1c7e29 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 25 Jun 2026 12:58:59 +0200 Subject: [PATCH 02/45] Pin GitHub Actions to commit SHAs (#218) Also sets up dependabot for just actions --- .github/dependabot.yml | 11 +++++++++++ .github/workflows/ci.yml | 4 ++-- .github/workflows/codex-update.yml | 17 ++++++++--------- 3 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..bb9dcf0a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96963afe..2372d7d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v6 with: - node-version: '24' - - uses: oven-sh/setup-bun@v2 + node-version: "24" + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 with: bun-version: 1.3.11 - name: Install dependencies diff --git a/.github/workflows/codex-update.yml b/.github/workflows/codex-update.yml index eba8d87f..d26db41b 100644 --- a/.github/workflows/codex-update.yml +++ b/.github/workflows/codex-update.yml @@ -2,7 +2,7 @@ name: Codex Update on: schedule: - - cron: '0 5 * * *' + - cron: "0 5 * * *" workflow_dispatch: permissions: @@ -10,9 +10,9 @@ permissions: packages: read env: - CODEX_PACKAGE: '@openai/codex' - CODEX_UPDATE_LABEL: 'codex-update' - GITHUB_BOT_USERNAME: 'github-actions[bot]' + CODEX_PACKAGE: "@openai/codex" + CODEX_UPDATE_LABEL: "codex-update" + GITHUB_BOT_USERNAME: "github-actions[bot]" jobs: check: @@ -83,7 +83,7 @@ jobs: - name: Finalize update with Codex id: codex-finalize - uses: openai/codex-action@v1 + uses: openai/codex-action@10cb888d2ed3b99867f7e7ccff174a861a75aeb6 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} codex-version: ${{ env.VERSION }} @@ -91,12 +91,12 @@ jobs: codex-args: >- -c sandbox_workspace_write.writable_roots=["${{ github.workspace }}/.git"] prompt: > - Finalize the update using codex-update-compat skill. - Commit the changes, the message should mention that types or/and tests after the update were fixed. + Finalize the update using codex-update-compat skill. + Commit the changes, the message should mention that types or/and tests after the update were fixed. When creating the final message do not mention: * Validation run details. * Commits. - * Hyperlinks. + * Hyperlinks. Mention only previously failed tests with failure reasons, also what you changed and why. - name: Push branch updates @@ -143,4 +143,3 @@ jobs: gh pr close "$pr_number" \ --comment "Closing obsolete update PR" done - From 94712cafa0d23bcc1ea4e2d1aadfb8d3f7128fbb Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 25 Jun 2026 12:59:15 +0200 Subject: [PATCH 03/45] Cleanups (#219) * Add missing TypeScript semicolons * Remove unused ACP namespace import * Remove unused stdin close argument * Clarify release archive and Codex update docs * Remove duplicate sed range script pattern --- readme-dev.md | 4 ++-- src/CodexAcpClient.ts | 4 ++-- src/CodexAcpServer.ts | 10 +++++----- src/CodexEventHandler.ts | 1 - src/CodexJsonRpcConnection.ts | 2 +- src/ResponseItemHistoryFallback.ts | 3 +-- src/__tests__/CodexACPAgent/CodexAcpClient.test.ts | 8 ++++---- src/index.ts | 2 +- 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/readme-dev.md b/readme-dev.md index 587a81c9..d263a0ac 100644 --- a/readme-dev.md +++ b/readme-dev.md @@ -31,7 +31,7 @@ Run from sources Run from binaries -1. Download a `codex-acp-.zip` archive from https://github.com/agentclientprotocol/codex-acp/releases +1. Download a `codex-acp-.zip` archive from https://github.com/agentclientprotocol/codex-acp/releases (`` is one of: `linux`, `darwin`, `win32`) 2. Unzip the archive: ```bash unzip codex-acp-.zip @@ -69,6 +69,6 @@ npm run package:all ### Update supported Codex version -1. Update Codex dependency: `package.json` +1. Update the `@openai/codex` version in `package.json` (under `dependencies`). 2. Regenerate Codex types in `src/app-server/`: `npm run generate-types` 3. Ensure there are no type errors or failed tests: `npm run typecheck` and `npm run test` diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 4fea0c46..b5c63835 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -107,7 +107,7 @@ export class CodexAcpClient { case "gateway": if (!authRequest._meta) throw RequestError.invalidRequest(); - const gatewaySettings = authRequest._meta["gateway"] + const gatewaySettings = authRequest._meta["gateway"]; if (!gatewaySettings) throw RequestError.invalidRequest(); const baseUrl = gatewaySettings.baseUrl; @@ -127,7 +127,7 @@ export class CodexAcpClient { http_headers: headers, wire_api: "responses" } - } + }; // Early return: model provider information will be sent to Codex later during the session creation return true; diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index d37eac17..52a4930d 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -323,7 +323,7 @@ export class CodexAcpServer { let sessionMetadata: SessionMetadata; let resumeSubscribed = false; if ("sessionId" in request) { - logger.log(`Resume existing session: ${request.sessionId}...`) + logger.log(`Resume existing session: ${request.sessionId}...`); try { sessionMetadata = await this.runWithProcessCheck(() => this.codexAcpClient.resumeSession(request, () => { @@ -337,7 +337,7 @@ export class CodexAcpServer { throw err; } } else { - logger.log(`Create new session...`) + logger.log(`Create new session...`); sessionMetadata = await this.runWithProcessCheck(() => this.codexAcpClient.newSession(request)); } @@ -378,7 +378,7 @@ export class CodexAcpServer { currentModelSupportsFast: currentModelSupportsFast, sessionMcpServers: sessionMcpServers, terminalOutputMode: this.terminalOutputMode, - } + }; this.sessions.set(sessionId, sessionState); resumeSubscribed = false; @@ -1318,7 +1318,7 @@ export class CodexAcpServer { _meta: this.buildQuotaMeta(sessionState), }; } - const error = eventHandler.getFailure() + const error = eventHandler.getFailure(); if (error) { // noinspection ExceptionCaughtLocallyJS throw error; @@ -1420,7 +1420,7 @@ export class CodexAcpServer { }; } - const error = eventHandler.getFailure() + const error = eventHandler.getFailure(); if (error) { // noinspection ExceptionCaughtLocallyJS throw error; diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 105625e3..ab2c1170 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -4,7 +4,6 @@ import type { ServerNotification } from "./app-server"; import type {SessionState} from "./CodexAcpServer"; -import * as acp from "@agentclientprotocol/sdk"; import {type PlanEntry, RequestError} from "@agentclientprotocol/sdk"; import {ACPSessionConnection, type AcpClientConnection, type UpdateSessionEvent} from "./ACPSessionConnection"; import type { diff --git a/src/CodexJsonRpcConnection.ts b/src/CodexJsonRpcConnection.ts index 343845ba..0f79117a 100644 --- a/src/CodexJsonRpcConnection.ts +++ b/src/CodexJsonRpcConnection.ts @@ -15,7 +15,7 @@ export interface CodexConnection { export function startCodexConnection(codexPath?: string, env?: NodeJS.ProcessEnv): CodexConnection { const spawnEnv = env ?? process.env; - let codex: ChildProcessWithoutNullStreams + let codex: ChildProcessWithoutNullStreams; if (codexPath) { codex = process.platform === 'win32' ? spawn(`"${codexPath}" app-server`, { shell: true, env: spawnEnv }) diff --git a/src/ResponseItemHistoryFallback.ts b/src/ResponseItemHistoryFallback.ts index eb6f7dad..1d2f8f6e 100644 --- a/src/ResponseItemHistoryFallback.ts +++ b/src/ResponseItemHistoryFallback.ts @@ -975,8 +975,7 @@ function sedFileArguments(args: string[]): string[] { } function looksLikeSedRangeScript(arg: string): boolean { - return /^(\d+|\$)?(,(\d+|\$))?[pd]$/.test(arg) - || /^(\d+|\$)?(,(\d+|\$))?p$/.test(arg); + return /^(\d+|\$)?(,(\d+|\$))?[pd]$/.test(arg); } function headTailFileArguments(args: string[]): string[] { diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 2edececc..19477329 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -56,10 +56,10 @@ describe('ACP server test', { timeout: 40_000 }, () => { keyFixture.clearCodexConnectionDump(); - const authRequest: CodexAuthRequest = { methodId: "api-key", _meta: { "api-key": { apiKey: "TOKEN" }}} + const authRequest: CodexAuthRequest = { methodId: "api-key", _meta: { "api-key": { apiKey: "TOKEN" }}}; await codexAcpAgent.authenticate(authRequest); const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []}); - expect(newSessionResponse.sessionId).toBeDefined() + expect(newSessionResponse.sessionId).toBeDefined(); const transportEvents = keyFixture.getCodexConnectionEvents([...ignoredFields, "upgrade"]); const transportMethods = transportEvents.flatMap(event => "method" in event ? [event.method] : []); @@ -149,8 +149,8 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(authenticatedResponse).toEqual({type: "gateway", name: "custom-gateway"}); const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []}); - expect(newSessionResponse.sessionId).toBeDefined() - }) + expect(newSessionResponse.sessionId).toBeDefined(); + }); it('should show account in /status for api key auth and hide it for gateway auth', async () => { const authFixture = createTestFixture(); diff --git a/src/index.ts b/src/index.ts index 6437a671..64a5caa1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,7 +76,7 @@ function startAcpServer() { stderr = (stderr + data.toString()).slice(-maxStderrTailChars); }); - process.stdin.on("close", (chunk: Buffer) => { + process.stdin.on("close", () => { codexConnection.process.stdin.end(); // Kill the codex process if it doesn't exit naturally setTimeout(() => { From 302e9959d2e87da27f942263efb1639ec3a4a414 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 25 Jun 2026 12:59:35 +0200 Subject: [PATCH 04/45] fix: keep exec command fallback tracking consistent (#220) * fix: keep exec command fallback tracking consistent * Test response item history fallback statuses --- src/ResponseItemHistoryFallback.ts | 11 +++++---- .../response-item-history-fallback.test.ts | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/ResponseItemHistoryFallback.ts b/src/ResponseItemHistoryFallback.ts index 1d2f8f6e..2591c6ad 100644 --- a/src/ResponseItemHistoryFallback.ts +++ b/src/ResponseItemHistoryFallback.ts @@ -367,15 +367,16 @@ function createFunctionCallUpdate(item: JsonRecord): LegacyFunctionCallUpdate | return null; } + const isExecCommand = name === "exec_command"; const args = parseFunctionArguments(item["arguments"]); - const command = name === "exec_command" ? commandFromFunctionArguments(args) : null; - const cwd = name === "exec_command" ? cwdFromFunctionArguments(args) : ""; + const command = isExecCommand ? commandFromFunctionArguments(args) : null; + const cwd = isExecCommand ? cwdFromFunctionArguments(args) : ""; const commandAction = command ? inferCommandAction(command, cwd) : null; if (commandAction) { return { update: createCommandActionEvent(toolCallId, "inProgress", cwd, commandAction), usesTerminal: false, - isExecCommand: true, + isExecCommand, }; } @@ -389,13 +390,13 @@ function createFunctionCallUpdate(item: JsonRecord): LegacyFunctionCallUpdate | }; if (!functionCallUsesTerminal(item)) { - return { update, usesTerminal: false, isExecCommand: false }; + return { update, usesTerminal: false, isExecCommand }; } return { update: withTerminalContent(update, toolCallId, cwd), usesTerminal: true, - isExecCommand: true, + isExecCommand, }; } diff --git a/src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts b/src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts index f481088e..72301794 100644 --- a/src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts +++ b/src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts @@ -19,6 +19,18 @@ describe("ResponseItemHistoryFallback", () => { ]); }); + it("does not recover function calls when all parsed tool call ids already exist", () => { + const updates = parseResponseItemHistoryFallback(jsonl([ + functionCall("call-existing-a", "rg \"ExistingA\" src"), + functionCallOutput("call-existing-a", "Chunk ID: existing-a\nProcess exited with code 0\nOutput:\nsrc/a.ts\n"), + functionCall("call-existing-b", "rg \"ExistingB\" src"), + functionCallOutput("call-existing-b", "Chunk ID: existing-b\nProcess exited with code 0\nOutput:\nsrc/b.ts\n"), + ]), "terminal_output", new Set(["call-existing-a", "call-existing-b"])); + + expect(toolCallIds(updates)).toEqual([]); + expect(toolCallUpdateStatuses(updates)).toEqual([]); + }); + it("does not duplicate adjacent reasoning from event and response item records", () => { const updates = parseResponseItemHistoryFallback(jsonl([ { @@ -53,6 +65,17 @@ describe("ResponseItemHistoryFallback", () => { { toolCallId: "call-read-failed", status: "failed" }, ]); }); + + it("marks exec command outputs without exit footers completed when they do not report errors", () => { + const updates = parseResponseItemHistoryFallback(jsonl([ + functionCall("call-read-ok", "cat existing.txt"), + functionCallOutput("call-read-ok", "existing file contents\n"), + ]), "terminal_output"); + + expect(toolCallUpdateStatuses(updates)).toEqual([ + { toolCallId: "call-read-ok", status: "completed" }, + ]); + }); }); function jsonl(records: unknown[]): string { From a1935481248805959e7739d86b7b682a5fa00ce7 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 25 Jun 2026 13:00:29 +0200 Subject: [PATCH 05/45] fix: Map collab agent tool call events (#223) Matches replay behavior. Closes #222 --- src/CodexAcpServer.ts | 34 +------- src/CodexEventHandler.ts | 6 +- src/CodexToolCallMapper.ts | 39 ++++++++- .../CodexACPAgent/collab-agent-events.test.ts | 87 +++++++++++++++++++ .../data/collab-agent-tool-call-flow.json | 57 ++++++++++++ 5 files changed, 189 insertions(+), 34 deletions(-) create mode 100644 src/__tests__/CodexACPAgent/collab-agent-events.test.ts create mode 100644 src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 52a4930d..11c33a7e 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -10,7 +10,6 @@ import {ACPSessionConnection, type AcpClientConnection, type UpdateSessionEvent} import type {InputModality, ReasoningEffort} from "./app-server"; import type { Account, - CollabAgentToolCallStatus, Model, ReasoningEffortOption, Thread, @@ -45,6 +44,7 @@ import { LEGACY_SET_SESSION_MODEL_METHOD, } from "./AcpExtensions"; import { + createCollabAgentToolCallUpdate, createCommandExecutionCompleteUpdate, createCommandExecutionUpdate, createDynamicToolCallUpdate, @@ -907,7 +907,7 @@ export class CodexAcpServer { case "dynamicToolCall": return [await createDynamicToolCallUpdate(item)]; case "collabAgentToolCall": - return [this.createCollabAgentToolCallUpdate(item)]; + return [createCollabAgentToolCallUpdate(item)]; case "webSearch": return [this.createWebSearchUpdate(item)]; case "imageView": @@ -947,25 +947,6 @@ export class CodexAcpServer { })); } - private createCollabAgentToolCallUpdate( - item: ThreadItem & { type: "collabAgentToolCall" } - ): UpdateSessionEvent { - return { - sessionUpdate: "tool_call", - toolCallId: item.id, - kind: "other", - title: `collab.${item.tool}`, - status: this.toAcpToolCallStatus(item.status), - rawInput: { - prompt: item.prompt, - senderThreadId: item.senderThreadId, - receiverThreadIds: item.receiverThreadIds, - agentsStates: item.agentsStates, - status: item.status, - }, - }; - } - private createWebSearchUpdate( item: ThreadItem & { type: "webSearch" } ): UpdateSessionEvent { @@ -1017,17 +998,6 @@ export class CodexAcpServer { }; } - private toAcpToolCallStatus(status: CollabAgentToolCallStatus): "in_progress" | "completed" | "failed" { - switch (status) { - case "inProgress": - return "in_progress"; - case "completed": - return "completed"; - case "failed": - return "failed"; - } - } - private userInputToContentBlocks(input: UserInput): acp.ContentBlock[] { switch (input.type) { case "text": diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index ab2c1170..eb22e74b 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -33,6 +33,8 @@ import type { McpStartupCompleteEvent } from "./app-server"; import {toTokenCount} from "./TokenCount"; import { commandExecutionUsesTerminalOutput, + createCollabAgentToolCallCompleteUpdate, + createCollabAgentToolCallUpdate, createCommandExecutionUpdate, createDynamicToolCallUpdate, createFileChangeUpdate, @@ -354,6 +356,7 @@ export class CodexEventHandler { this.activeImageGenerationItems.add(event.item.id); return createImageGenerationStartUpdate(event.item); case "collabAgentToolCall": + return createCollabAgentToolCallUpdate(event.item); case "subAgentActivity": case "sleep": case "userMessage": @@ -404,12 +407,13 @@ export class CodexEventHandler { return this.createCompletedReasoningEvent(event.item); case "webSearch": return createWebSearchCompleteUpdate(event.item); + case "collabAgentToolCall": + return createCollabAgentToolCallCompleteUpdate(event.item); case "exitedReviewMode": return this.createExitedReviewModeEvent(event.item); case "contextCompaction": return this.createContextCompactedEvent(); //ignored types - case "collabAgentToolCall": case "subAgentActivity": case "sleep": case "userMessage": diff --git a/src/CodexToolCallMapper.ts b/src/CodexToolCallMapper.ts index c098bee6..61060c7a 100644 --- a/src/CodexToolCallMapper.ts +++ b/src/CodexToolCallMapper.ts @@ -9,6 +9,7 @@ import type { FuzzyFileSearchSessionUpdatedNotification } from "./app-server"; import type { + CollabAgentToolCallStatus, CommandAction, CommandExecutionStatus, DynamicToolCallStatus, @@ -32,12 +33,13 @@ import { type TerminalOutputMode, } from "./TerminalOutputMode"; -type CodexItemStatus = CommandExecutionStatus | PatchApplyStatus | McpToolCallStatus | DynamicToolCallStatus; +type CodexItemStatus = CommandExecutionStatus | PatchApplyStatus | McpToolCallStatus | DynamicToolCallStatus | CollabAgentToolCallStatus; type AcpToolCallStatus = "pending" | "in_progress" | "completed" | "failed"; type GuardianApprovalReviewNotification = | ItemGuardianApprovalReviewStartedNotification | ItemGuardianApprovalReviewCompletedNotification; type WebSearchItem = ThreadItem & { type: "webSearch" }; +type CollabAgentToolCallItem = ThreadItem & { type: "collabAgentToolCall" }; type CommandExecutionItem = ThreadItem & { type: "commandExecution" }; type AcpToolCallEvent = Extract; @@ -356,6 +358,41 @@ export function createWebSearchCompleteUpdate( }; } +export function createCollabAgentToolCallUpdate( + item: CollabAgentToolCallItem +): UpdateSessionEvent { + return { + sessionUpdate: "tool_call", + toolCallId: item.id, + kind: "other", + title: item.tool, + status: toAcpStatus(item.status), + rawInput: createCollabAgentToolCallRawInput(item), + }; +} + +export function createCollabAgentToolCallCompleteUpdate( + item: CollabAgentToolCallItem +): UpdateSessionEvent { + return { + sessionUpdate: "tool_call_update", + toolCallId: item.id, + title: item.tool, + status: toAcpStatus(item.status), + rawInput: createCollabAgentToolCallRawInput(item), + }; +} + +function createCollabAgentToolCallRawInput(item: CollabAgentToolCallItem) { + return { + prompt: item.prompt, + senderThreadId: item.senderThreadId, + receiverThreadIds: item.receiverThreadIds, + agentsStates: item.agentsStates, + status: item.status, + }; +} + export function formatWebSearchTitle(item: WebSearchItem): string { const action = item.action; if (!action) { diff --git a/src/__tests__/CodexACPAgent/collab-agent-events.test.ts b/src/__tests__/CodexACPAgent/collab-agent-events.test.ts new file mode 100644 index 00000000..9964d2d1 --- /dev/null +++ b/src/__tests__/CodexACPAgent/collab-agent-events.test.ts @@ -0,0 +1,87 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ServerNotification } from "../../app-server"; +import type { SessionState } from "../../CodexAcpServer"; +import { AgentMode } from "../../AgentMode"; +import { + createCodexMockTestFixture, + createTestSessionState, + setupPromptAndSendNotifications, + type CodexMockTestFixture, +} from "../acp-test-utils"; + +describe("CodexEventHandler - collab agent tool call events", () => { + let mockFixture: CodexMockTestFixture; + const sessionId = "test-session-id"; + + beforeEach(() => { + mockFixture = createCodexMockTestFixture(); + vi.clearAllMocks(); + }); + + const sessionState: SessionState = createTestSessionState({ + sessionId, + currentModelId: "model-id[effort]", + agentMode: AgentMode.DEFAULT_AGENT_MODE, + }); + + it("maps live collab agent tool calls to ACP tool call updates", async () => { + const notifications: ServerNotification[] = [ + { + method: "item/started", + params: { + threadId: sessionId, + turnId: "turn-1", + startedAtMs: 0, + item: { + type: "collabAgentToolCall", + id: "call-spawn-weather", + tool: "spawnAgent", + status: "inProgress", + senderThreadId: "thread-main", + receiverThreadIds: ["thread-paris"], + prompt: "Find the current weather in Paris.", + model: null, + reasoningEffort: null, + agentsStates: { + "thread-paris": { + status: "running", + message: "Checking weather", + }, + }, + }, + }, + }, + { + method: "item/completed", + params: { + threadId: sessionId, + turnId: "turn-1", + completedAtMs: 0, + item: { + type: "collabAgentToolCall", + id: "call-spawn-weather", + tool: "spawnAgent", + status: "completed", + senderThreadId: "thread-main", + receiverThreadIds: ["thread-paris"], + prompt: "Find the current weather in Paris.", + model: null, + reasoningEffort: null, + agentsStates: { + "thread-paris": { + status: "completed", + message: null, + }, + }, + }, + }, + }, + ]; + + await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, notifications); + + await expect(`${mockFixture.getAcpConnectionDump([])}\n`).toMatchFileSnapshot( + "data/collab-agent-tool-call-flow.json" + ); + }); +}); diff --git a/src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json b/src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json new file mode 100644 index 00000000..8e7ed13c --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/collab-agent-tool-call-flow.json @@ -0,0 +1,57 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call", + "toolCallId": "call-spawn-weather", + "kind": "other", + "title": "spawnAgent", + "status": "in_progress", + "rawInput": { + "prompt": "Find the current weather in Paris.", + "senderThreadId": "thread-main", + "receiverThreadIds": [ + "thread-paris" + ], + "agentsStates": { + "thread-paris": { + "status": "running", + "message": "Checking weather" + } + }, + "status": "inProgress" + } + } + } + ] +} +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call_update", + "toolCallId": "call-spawn-weather", + "title": "spawnAgent", + "status": "completed", + "rawInput": { + "prompt": "Find the current weather in Paris.", + "senderThreadId": "thread-main", + "receiverThreadIds": [ + "thread-paris" + ], + "agentsStates": { + "thread-paris": { + "status": "completed", + "message": null + } + }, + "status": "completed" + } + } + } + ] +} From 374e684ec7c69663843f3932684a057b0270a113 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 25 Jun 2026 13:00:48 +0200 Subject: [PATCH 06/45] fix: Remove sub-agent sources from session listing (#235) Closes #234 --- src/CodexAcpClient.ts | 26 +++++++------------ .../CodexACPAgent/list-sessions.test.ts | 5 ---- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index b5c63835..c85a3927 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -580,11 +580,6 @@ export class CodexAcpClient { "vscode", "exec", "appServer", - "subAgent", - "subAgentReview", - "subAgentCompact", - "subAgentThreadSpawn", - "subAgentOther", "unknown", ]; const requestedCwd = request.cwd?.trim() ?? null; @@ -605,26 +600,23 @@ export class CodexAcpClient { sourceKinds: sourceKinds, }); + const mapThreadToSession = (thread: Thread) => ({ + sessionId: thread.id, + cwd: thread.cwd, + title: (thread.name ?? thread.preview) || null, + updatedAt: new Date(thread.updatedAt * 1000).toISOString(), + }); + if (listResponse.data.length === 0) { const diagnostics = await this.runSessionListDiagnostics(); logger.log("Session list diagnostics", diagnostics); } - let sessions = listResponse.data.map((thread) => ({ - sessionId: thread.id, - cwd: thread.cwd, - title: (thread.name ?? thread.preview) || null, - updatedAt: new Date(thread.updatedAt * 1000).toISOString(), - })); + let sessions = listResponse.data.map(mapThreadToSession); if (requestedCwd) { const filtered = listResponse.data .filter(filterByCwd) - .map((thread) => ({ - sessionId: thread.id, - cwd: thread.cwd, - title: (thread.name ?? thread.preview) || null, - updatedAt: new Date(thread.updatedAt * 1000).toISOString(), - })); + .map(mapThreadToSession); if (filtered.length > 0 || path.isAbsolute(requestedCwd)) { sessions = filtered; } else { diff --git a/src/__tests__/CodexACPAgent/list-sessions.test.ts b/src/__tests__/CodexACPAgent/list-sessions.test.ts index 2b2b4e7e..36529576 100644 --- a/src/__tests__/CodexACPAgent/list-sessions.test.ts +++ b/src/__tests__/CodexACPAgent/list-sessions.test.ts @@ -81,11 +81,6 @@ describe("CodexACPAgent - list sessions", () => { "vscode", "exec", "appServer", - "subAgent", - "subAgentReview", - "subAgentCompact", - "subAgentThreadSpawn", - "subAgentOther", "unknown", ], })); From 3a635987069b6cf068afafc9cb135c1591ec8aa5 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 25 Jun 2026 13:02:06 +0200 Subject: [PATCH 07/45] Support API key auth from environment variables (#236) Has the behavior from the old adapter of reading the env vars if one isn't supplied. Closes #234 --- README.md | 7 +- readme-dev.md | 12 +++ src/CodexAcpClient.ts | 37 ++++++--- src/CodexAuthMethod.ts | 28 ++++--- .../CodexACPAgent/CodexAcpClient.test.ts | 76 ++++++++++++++++++- .../CodexACPAgent/e2e/acp-e2e-test-utils.ts | 5 +- .../CodexACPAgent/initialize.test.ts | 26 +++++++ 7 files changed, 164 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 3145bfdf..fe401b19 100644 --- a/README.md +++ b/README.md @@ -40,17 +40,20 @@ CODEX_PATH=/path/to/codex npx -y @agentclientprotocol/codex-acp The adapter advertises ACP auth methods during initialization. Clients can authenticate with: -- ChatGPT login. -- OpenAI API key. +- ChatGPT login. Set `NO_BROWSER=1` to hide this method in remote or browserless environments. +- API key via `CODEX_API_KEY` or `OPENAI_API_KEY`. - A custom OpenAI-compatible gateway, when the client opts in to the gateway auth capability. ## Runtime options +- `CODEX_API_KEY` - API key used when the API-key auth method is selected. Takes precedence over `OPENAI_API_KEY`. +- `OPENAI_API_KEY` - fallback API key used when the API-key auth method is selected. - `CODEX_PATH` - run a specific Codex executable instead of the bundled package dependency. - `CODEX_CONFIG` - JSON object merged into the Codex session config. - `MODEL_PROVIDER` - model provider to pass to Codex for new sessions. - `DEFAULT_AUTH_REQUEST` - ACP auth request JSON used when Codex requires authentication. - `INITIAL_AGENT_MODE` - initial mode id: `read-only`, `agent`, or `agent-full-access`. +- `NO_BROWSER` - hide browser-based ChatGPT auth when set. - `APP_SERVER_LOGS` - directory for adapter logs. ## Development diff --git a/readme-dev.md b/readme-dev.md index d263a0ac..bc147807 100644 --- a/readme-dev.md +++ b/readme-dev.md @@ -1,6 +1,18 @@ This package uses the bundled `@openai/codex` dependency by default. Set `CODEX_PATH` to run a different Codex binary; versions other than the one specified in `package.json` may not be compatible. +### Runtime environment + +- `CODEX_API_KEY` - API key used when the API-key auth method is selected. Takes precedence over `OPENAI_API_KEY`. +- `OPENAI_API_KEY` - fallback API key used when the API-key auth method is selected. +- `CODEX_PATH` - run a specific Codex executable instead of the bundled package dependency. +- `CODEX_CONFIG` - JSON object merged into the Codex session config. +- `MODEL_PROVIDER` - model provider to pass to Codex for new sessions. +- `DEFAULT_AUTH_REQUEST` - ACP auth request JSON used when Codex requires authentication. +- `INITIAL_AGENT_MODE` - initial mode id: `read-only`, `agent`, or `agent-full-access`. +- `NO_BROWSER` - hide browser-based ChatGPT auth when set. +- `APP_SERVER_LOGS` - directory for adapter logs. + ### Quick start #### Develop on Windows? diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index c85a3927..f8be1d59 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -1,4 +1,4 @@ -import {isCodexAuthRequest} from "./CodexAuthMethod"; +import {CODEX_API_KEY_ENV_VAR, isCodexAuthRequest, OPENAI_API_KEY_ENV_VAR} from "./CodexAuthMethod"; import type {EmbeddedResourceResource} from "@agentclientprotocol/sdk"; import * as acp from "@agentclientprotocol/sdk"; import {type McpServer, RequestError} from "@agentclientprotocol/sdk"; @@ -84,15 +84,8 @@ export class CodexAcpClient { switch (authRequest.methodId) { case "api-key": { - if (!authRequest._meta || !authRequest._meta["api-key"]) throw RequestError.invalidRequest(); - const loginCompletedPromise = this.awaitNextLoginCompleted(); - await this.codexClient.accountLogin({ - type: "apiKey", - apiKey: authRequest._meta["api-key"].apiKey - }); - this.gatewayConfig = null; - const result = await loginCompletedPromise; - return result.success; + const apiKey = authRequest._meta?.["api-key"]?.apiKey ?? this.readApiKeyFromEnv(); + return await this.authenticateWithApiKey(apiKey); } case "chat-gpt": { const loginCompletedPromise = this.awaitNextLoginCompleted(); @@ -139,6 +132,30 @@ export class CodexAcpClient { return false; } + private async authenticateWithApiKey(apiKey: string): Promise { + const loginCompletedPromise = this.awaitNextLoginCompleted(); + await this.codexClient.accountLogin({ + type: "apiKey", + apiKey, + }); + this.gatewayConfig = null; + const result = await loginCompletedPromise; + return result.success; + } + + private readApiKeyFromEnv(): string { + for (const envVar of [CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR]) { + const value = process.env[envVar]?.trim(); + if (value) { + return value; + } + } + throw RequestError.internalError( + {envVars: [CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR]}, + `${CODEX_API_KEY_ENV_VAR} or ${OPENAI_API_KEY_ENV_VAR} is not set` + ); + } + async getAuthenticationStatus(): Promise { const modelProvider = await this.getCurrentModelProvider(); diff --git a/src/CodexAuthMethod.ts b/src/CodexAuthMethod.ts index d0fa327b..94c1833a 100644 --- a/src/CodexAuthMethod.ts +++ b/src/CodexAuthMethod.ts @@ -1,6 +1,18 @@ import type {AuthenticateRequest, AuthMethod, ClientCapabilities} from "@agentclientprotocol/sdk"; +export const CODEX_API_KEY_ENV_VAR = "CODEX_API_KEY"; +export const OPENAI_API_KEY_ENV_VAR = "OPENAI_API_KEY"; + +interface ApiKeyAuthRequest extends AuthenticateRequest { + methodId: "api-key"; + _meta?: { + "api-key"?: { + apiKey: string; + } + } | null; +} + const ApiKeyAuthMethod: AuthMethod = { id: "api-key", name: "API Key", @@ -12,15 +24,6 @@ const ApiKeyAuthMethod: AuthMethod = { } } -interface ApiKeyAuthRequest extends AuthenticateRequest { - methodId: "api-key"; - _meta: { - "api-key": { - apiKey: string; - } - }; -} - const ChatGptAuthMethod: AuthMethod = { id: "chat-gpt", name: "ChatGPT", @@ -54,8 +57,11 @@ export interface GatewayAuthRequest extends AuthenticateRequest { }; } -export function getCodexAuthMethods(clientCapabilities?: ClientCapabilities | null): AuthMethod[] { - const authMethods: AuthMethod[] = [ApiKeyAuthMethod, ChatGptAuthMethod]; +export function getCodexAuthMethods(clientCapabilities?: ClientCapabilities | null, env: NodeJS.ProcessEnv = process.env): AuthMethod[] { + const authMethods: AuthMethod[] = [ApiKeyAuthMethod]; + if (!env["NO_BROWSER"]) { + authMethods.push(ChatGptAuthMethod); + } const supportsGatewayAuth = clientCapabilities?.auth?._meta?.["gateway"] === true; if (supportsGatewayAuth) { authMethods.push(GatewayAuthMethod); diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 19477329..70582711 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1,7 +1,7 @@ // noinspection ES6RedundantAwait -import {beforeEach, describe, expect, it, vi} from 'vitest'; -import type {CodexAuthRequest} from "../../CodexAuthMethod"; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR, type CodexAuthRequest} from "../../CodexAuthMethod"; import type * as acp from "@agentclientprotocol/sdk"; import { createCodexMockTestFixture, @@ -25,6 +25,10 @@ describe('ACP server test', { timeout: 40_000 }, () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + const ignoredFields = ["thread", "cwd", "id", "createdAt", "path", "threadId", "userAgent", "sandbox", "conversationId", "origins", "supportedReasoningEfforts", "reasoningEffort", "model", "readOnlyAccess", "approvalsReviewer"]; it('should throw error without authentication', async () => { @@ -114,6 +118,74 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(logoutResponse).toEqual({type: "unauthenticated"}); }); + it('should authenticate with CODEX_API_KEY from the environment', async () => { + const envFixture = createTestFixture(); + const codexAcpAgent = envFixture.getCodexAcpAgent(); + vi.stubEnv(CODEX_API_KEY_ENV_VAR, "CODEX_ENV_TOKEN"); + vi.stubEnv(OPENAI_API_KEY_ENV_VAR, "OPENAI_ENV_TOKEN"); + + await codexAcpAgent.initialize({protocolVersion: 1}); + await envFixture.getCodexAcpClient().logout(); + envFixture.clearCodexConnectionDump(); + + await codexAcpAgent.authenticate({methodId: "api-key"}); + + const transportEvents = envFixture.getCodexConnectionEvents([]); + const loginRequest = transportEvents.find(event => + event.eventType === "request" && + "method" in event && + event.method === "account/login/start" + ); + expect(loginRequest).toEqual({ + eventType: "request", + method: "account/login/start", + params: { + type: "apiKey", + apiKey: "CODEX_ENV_TOKEN", + } + }); + await expect(codexAcpAgent.extMethod("authentication/status", {})).resolves.toEqual({type: "api-key"}); + }); + + it('should fall back to OPENAI_API_KEY from the environment', async () => { + const envFixture = createTestFixture(); + const codexAcpAgent = envFixture.getCodexAcpAgent(); + vi.stubEnv(CODEX_API_KEY_ENV_VAR, ""); + vi.stubEnv(OPENAI_API_KEY_ENV_VAR, "OPENAI_ENV_TOKEN"); + + await codexAcpAgent.initialize({protocolVersion: 1}); + await envFixture.getCodexAcpClient().logout(); + envFixture.clearCodexConnectionDump(); + + await codexAcpAgent.authenticate({methodId: "api-key"}); + + const transportEvents = envFixture.getCodexConnectionEvents([]); + const loginRequest = transportEvents.find(event => + event.eventType === "request" && + "method" in event && + event.method === "account/login/start" + ); + expect(loginRequest).toEqual({ + eventType: "request", + method: "account/login/start", + params: { + type: "apiKey", + apiKey: "OPENAI_ENV_TOKEN", + } + }); + await expect(codexAcpAgent.extMethod("authentication/status", {})).resolves.toEqual({type: "api-key"}); + }); + + it('should report a clear error when the selected API key env var is missing', async () => { + const envFixture = createTestFixture(); + const codexAcpAgent = envFixture.getCodexAcpAgent(); + vi.stubEnv(CODEX_API_KEY_ENV_VAR, ""); + vi.stubEnv(OPENAI_API_KEY_ENV_VAR, ""); + + await expect(codexAcpAgent.authenticate({methodId: "api-key"})) + .rejects.toThrow(`${CODEX_API_KEY_ENV_VAR} or ${OPENAI_API_KEY_ENV_VAR} is not set`); + }); + it('should authenticate with a gateway', async () => { const gatewayFixture = createTestFixture(); const codexAcpAgent = gatewayFixture.getCodexAcpAgent(); diff --git a/src/__tests__/CodexACPAgent/e2e/acp-e2e-test-utils.ts b/src/__tests__/CodexACPAgent/e2e/acp-e2e-test-utils.ts index 5b44be07..35bb5f3a 100644 --- a/src/__tests__/CodexACPAgent/e2e/acp-e2e-test-utils.ts +++ b/src/__tests__/CodexACPAgent/e2e/acp-e2e-test-utils.ts @@ -1,6 +1,7 @@ import * as acp from "@agentclientprotocol/sdk"; import {describe, expect} from "vitest"; import {AgentMode} from "../../../AgentMode"; +import {CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR} from "../../../CodexAuthMethod"; import {createSpawnedAgentFixture, type SpawnedAgentFixture,} from "./spawned-agent-fixture"; export { @@ -140,9 +141,9 @@ async function createSpawnedFixture( } export function requireLiveApiKey(): string { - const apiKey = process.env["CODEX_API_KEY"] ?? process.env["OPENAI_API_KEY"]; + const apiKey = process.env[CODEX_API_KEY_ENV_VAR] ?? process.env[OPENAI_API_KEY_ENV_VAR]; if (!apiKey) { - throw new Error("Live integration test requires CODEX_API_KEY or OPENAI_API_KEY."); + throw new Error(`Live integration test requires ${CODEX_API_KEY_ENV_VAR} or ${OPENAI_API_KEY_ENV_VAR}.`); } return apiKey; } diff --git a/src/__tests__/CodexACPAgent/initialize.test.ts b/src/__tests__/CodexACPAgent/initialize.test.ts index da4cc231..e7050101 100644 --- a/src/__tests__/CodexACPAgent/initialize.test.ts +++ b/src/__tests__/CodexACPAgent/initialize.test.ts @@ -83,4 +83,30 @@ describe('CodexACPAgent - initialize', () => { }) ])); }); + + it('should advertise API key auth with the legacy metadata method', () => { + expect(getCodexAuthMethods()).toEqual(expect.arrayContaining([ + expect.objectContaining({ + id: "api-key", + _meta: { + "api-key": { + provider: "openai", + }, + }, + }), + ])); + expect(getCodexAuthMethods()).not.toEqual(expect.arrayContaining([ + expect.objectContaining({type: "env_var"}), + expect.objectContaining({id: "codex-api-key"}), + expect.objectContaining({id: "openai-api-key"}), + ])); + }); + + it('should not advertise ChatGPT auth when browser auth is disabled', () => { + const methodIds = getCodexAuthMethods(undefined, {NO_BROWSER: "1"} as NodeJS.ProcessEnv) + .map((method) => method.id); + + expect(methodIds).not.toContain("chat-gpt"); + expect(methodIds).toEqual(expect.arrayContaining(["api-key"])); + }); }); From 72c9a106d07b0512986655998ae5784b9d587148 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:02:44 +0200 Subject: [PATCH 08/45] build(deps): bump the github-actions group with 3 updates (#241) Bumps the github-actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/create-github-app-token](https://github.com/actions/create-github-app-token) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `actions/checkout` from 4 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v7) Updates `actions/create-github-app-token` from 2 to 3 - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/create-github-app-token/compare/v2...v3) Updates `softprops/action-gh-release` from 2.6.2 to 3.0.1 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/3bb12739c298aeb8a4eeaf626c5b8d85266b0e65...718ea10b132b3b2eba29c1007bb80653f286566b) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/create-github-app-token dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: softprops/action-gh-release dependency-version: 3.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/codex-update.yml | 6 +++--- .github/workflows/e2e.yml | 2 +- .github/workflows/publish.yml | 8 ++++---- .github/workflows/version-bump.yml | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2372d7d5..6d54e2c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: "24" diff --git a/.github/workflows/codex-update.yml b/.github/workflows/codex-update.yml index d26db41b..30ff1218 100644 --- a/.github/workflows/codex-update.yml +++ b/.github/workflows/codex-update.yml @@ -22,7 +22,7 @@ jobs: version: ${{ steps.check.outputs.version }} branch: ${{ steps.check.outputs.branch }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -58,13 +58,13 @@ jobs: VERSION: ${{ needs.check.outputs.version }} steps: - name: Generate GitHub token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 id: generate-token with: app-id: ${{ secrets.RELEASE_PLZ_APP_ID }} private-key: ${{ secrets.RELEASE_PLZ_APP_PRIVATE_KEY }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c70840e5..3a99edcf 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -13,7 +13,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: '24' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 53f92b57..b3595b5d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: '24' @@ -42,7 +42,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: '24' @@ -56,14 +56,14 @@ jobs: permissions: {} steps: - name: Generate GitHub token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 id: generate-token with: app-id: ${{ secrets.RELEASE_PLZ_APP_ID }} private-key: ${{ secrets.RELEASE_PLZ_APP_PRIVATE_KEY }} - name: Create Release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: token: ${{ steps.generate-token.outputs.token }} generate_release_notes: true diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 80d56b32..2aeeeb9c 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -19,13 +19,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Generate GitHub token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 id: generate-token with: app-id: ${{ secrets.RELEASE_PLZ_APP_ID }} private-key: ${{ secrets.RELEASE_PLZ_APP_PRIVATE_KEY }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: token: ${{ steps.generate-token.outputs.token }} From 1f58237b08fe4877988352f32d448a380603fe70 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 25 Jun 2026 15:03:43 +0200 Subject: [PATCH 09/45] Handle ACP request cancellation (#221) * Handle ACP request cancellation ACP SDK v0.29.0 added support for the draft of request cancellation. * Resolve turn interruption only on close cancellation --- package-lock.json | 8 +- package.json | 2 +- src/CodexAcpClient.ts | 2 +- src/CodexAcpServer.ts | 233 +++++++++++++----- src/CodexAppServerClient.ts | 7 +- src/CodexApprovalHandler.ts | 27 +- src/CodexCommands.ts | 30 ++- src/CodexElicitationHandler.ts | 14 +- .../CodexACPAgent/CodexAcpClient.test.ts | 228 ++++++++++++++++- src/index.ts | 2 +- 10 files changed, 460 insertions(+), 93 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b291df7..2fffe2d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "^0.28.1", + "@agentclientprotocol/sdk": "^0.29.0", "@openai/codex": "^0.142.2", "diff": "^8.0.3", "open": "^11.0.0", @@ -29,9 +29,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.28.1.tgz", - "integrity": "sha512-Z2Frs6YtPhnZZ+XwFXyQkRDXY0fn8FjCalEs0W4yUhQnY4TztmNq0/RnfzWdFN3vqT3h0jTz5klzYbZHGxCDyQ==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.29.0.tgz", + "integrity": "sha512-Sb8xJ/cIhqlvxeJiB0+1scBk0LAkVAwJ9WAFG9XCe3LEI5JWAyOxbqaCa8O1HmNnrk4+qHN11eaKH6zL2iHIYA==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" diff --git a/package.json b/package.json index dbf33517..fd3db924 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "vitest": "^4.0.10" }, "dependencies": { - "@agentclientprotocol/sdk": "^0.28.1", + "@agentclientprotocol/sdk": "^0.29.0", "@openai/codex": "^0.142.2", "diff": "^8.0.3", "open": "^11.0.0", diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index f8be1d59..042c24f1 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -309,7 +309,7 @@ export class CodexAcpClient { async runReview( sessionId: string, target: ReviewTarget, - onTurnStarted?: (turnId: string) => void, + onTurnStarted?: (turnId: string, threadId: string) => void, ): Promise { return await this.codexClient.runReview({ threadId: sessionId, diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 11c33a7e..76b80af5 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -100,6 +100,10 @@ interface PendingTurnStart { interface ActivePrompt { completion: Promise; closeSignal: Promise; + cancelSignal: Promise; + signal: AbortSignal; + currentTurn: { threadId: string, turnId: string } | null; + requestCancel: () => void; requestClose: () => void; complete: () => void; } @@ -1116,17 +1120,33 @@ export class CodexAcpServer { const closeSignal = new Promise((resolve) => { resolveCloseSignal = resolve; }); + let resolveCancelSignal: (value: null) => void = () => {}; + const cancelSignal = new Promise((resolve) => { + resolveCancelSignal = resolve; + }); + const abortController = new AbortController(); let completed = false; let closeRequested = false; const activePrompt: ActivePrompt = { completion, closeSignal, + cancelSignal, + signal: abortController.signal, + currentTurn: null, + requestCancel: () => { + if (abortController.signal.aborted) { + return; + } + abortController.abort(); + resolveCancelSignal(null); + }, requestClose: () => { if (closeRequested) { return; } closeRequested = true; + activePrompt.requestCancel(); resolveCloseSignal(null); }, complete: () => { @@ -1145,6 +1165,46 @@ export class CodexAcpServer { return activePrompt; } + private cancelBeforeTurnStarted(activePrompt: ActivePrompt): Promise { + return activePrompt.cancelSignal.then(() => { + if (activePrompt.currentTurn === null) { + return null; + } + return new Promise(() => {}); + }); + } + + private observePromptRequestCancellation( + signal: AbortSignal | undefined, + sessionState: SessionState, + activePrompt: ActivePrompt, + ): () => void { + if (!signal) { + return () => {}; + } + + const onAbort = () => { + if (this.activePrompts.get(sessionState.sessionId) !== activePrompt) { + return; + } + logger.log("Prompt request cancelled", {sessionId: sessionState.sessionId}); + activePrompt.requestCancel(); + const turn = activePrompt.currentTurn; + if (!turn) { + return; + } + void this.requestTurnInterrupt(turn, "Cancel"); + }; + + if (signal.aborted) { + onAbort(); + return () => {}; + } + + signal.addEventListener("abort", onAbort, {once: true}); + return () => signal.removeEventListener("abort", onAbort); + } + private createPendingTurnStart(): PendingTurnStart { let resolve: (turnId: string | null) => void = () => {}; const promise = new Promise((innerResolve) => { @@ -1153,26 +1213,48 @@ export class CodexAcpServer { return {promise, resolve}; } - private interruptLateStartedTurn(sessionId: string, turnId: string): void { + private async interruptPromptTurn( + turn: { threadId: string, turnId: string }, + requestName: "Cancel" | "Close", + ): Promise { this.codexAcpClient.markTurnStale({ - threadId: sessionId, - turnId, + threadId: turn.threadId, + turnId: turn.turnId, }); - void this.runWithProcessCheck(() => this.codexAcpClient.turnInterrupt({ - threadId: sessionId, - turnId, - })).catch((err) => { - logger.error(`Close - late turnInterrupt failed`, err); - }).finally(() => { + try { + await this.requestTurnInterrupt(turn, requestName); + } finally { this.codexAcpClient.resolveTurnInterrupted({ - threadId: sessionId, - turnId, + threadId: turn.threadId, + turnId: turn.turnId, }); - }); + } + } + + private async requestTurnInterrupt( + turn: { threadId: string, turnId: string }, + requestName: "Cancel" | "Close", + ): Promise { + try { + await this.runWithProcessCheck(() => this.codexAcpClient.turnInterrupt({ + threadId: turn.threadId, + turnId: turn.turnId, + })); + logger.log(`${requestName} - turnInterrupt succeeded`, { + sessionId: turn.threadId, + currentTurnId: turn.turnId, + }); + } catch (err) { + logger.error(`${requestName} - turnInterrupt failed`, err); + } } - private promptIsClosedOrStale(sessionId: string, activePrompt: ActivePrompt): boolean { - return this.activePrompts.get(sessionId) !== activePrompt || this.sessionIsClosing(sessionId); + private interruptLateStartedTurn(turn: { threadId: string, turnId: string }): void { + void this.interruptPromptTurn(turn, "Close"); + } + + private promptShouldStop(sessionId: string, activePrompt: ActivePrompt): boolean { + return activePrompt.signal.aborted || this.activePrompts.get(sessionId) !== activePrompt || this.sessionIsClosing(sessionId); } private async interruptSessionTurn( @@ -1242,7 +1324,7 @@ export class CodexAcpServer { return turnId; } - async prompt(params: acp.PromptRequest): Promise { + async prompt(params: acp.PromptRequest, signal?: AbortSignal): Promise { logger.log("Prompt received", { sessionId: params.sessionId, prompt: params.prompt, @@ -1252,11 +1334,12 @@ export class CodexAcpServer { sessionState.lastTokenUsage = null; const activePrompt = this.trackActivePrompt(params.sessionId); let pendingTurnStart: PendingTurnStart | null = null; + const disposePromptRequestCancellation = this.observePromptRequestCancellation(signal, sessionState, activePrompt); try { const eventHandler = new CodexEventHandler(this.connection, sessionState); - const approvalHandler = new CodexApprovalHandler(this.connection, sessionState); - const elicitationHandler = new CodexElicitationHandler(this.connection, sessionState); + const approvalHandler = new CodexApprovalHandler(this.connection, sessionState, activePrompt.signal); + const elicitationHandler = new CodexElicitationHandler(this.connection, sessionState, activePrompt.signal); await this.codexAcpClient.subscribeToSessionEvents(params.sessionId, (event) => { elicitationHandler.handleNotification(event); @@ -1265,28 +1348,40 @@ export class CodexAcpServer { approvalHandler, elicitationHandler); - const commandResult = await this.availableCommands.tryHandleCommand(params.prompt, sessionState); + if (activePrompt.signal.aborted) { + return this.cancelledPromptResponse(sessionState); + } + + const commandPromise = this.availableCommands.tryHandleCommand(params.prompt, sessionState, { + onTurnStarted: (turnId, threadId) => { + const turn = {threadId, turnId}; + activePrompt.currentTurn = turn; + if (this.promptShouldStop(params.sessionId, activePrompt)) { + this.interruptLateStartedTurn(turn); + return; + } + sessionState.currentTurnId = turnId; + }, + }); + void commandPromise.catch((err) => { + if (this.activePrompts.get(params.sessionId) !== activePrompt) { + logger.error(`Command for cancelled prompt ${params.sessionId} failed after prompt returned`, err); + } + }); + const commandResult = await Promise.race([ + commandPromise, + activePrompt.closeSignal, + this.cancelBeforeTurnStarted(activePrompt), + ]); + if (commandResult === null) { + return this.cancelledPromptResponse(sessionState); + } if (commandResult.handled) { logger.log("Prompt handled by a command"); await this.codexAcpClient.waitForSessionNotifications(params.sessionId); if (commandResult.turnCompleted?.turn.status === "interrupted") { - if (!this.sessionIsClosing(params.sessionId) && this.sessions.has(params.sessionId)) { - await this.connection.notify(acp.methods.client.session.update, { - sessionId: params.sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: "*Conversation interrupted*" - } - } - }); - } - return { - stopReason: "cancelled", - usage: this.buildPromptUsage(sessionState.lastTokenUsage), - _meta: this.buildQuotaMeta(sessionState), - }; + await this.notifyConversationInterrupted(params.sessionId); + return this.cancelledPromptResponse(sessionState); } const error = eventHandler.getFailure(); if (error) { @@ -1301,11 +1396,7 @@ export class CodexAcpServer { } if (this.sessionIsClosing(params.sessionId)) { - return { - stopReason: "cancelled", - usage: this.buildPromptUsage(sessionState.lastTokenUsage), - _meta: this.buildQuotaMeta(sessionState), - }; + return this.cancelledPromptResponse(sessionState); } const modelId = ModelId.fromString(sessionState.currentModelId); @@ -1340,54 +1431,37 @@ export class CodexAcpServer { sessionState.cwd, sessionState.additionalDirectories, (turnId) => { - if (this.promptIsClosedOrStale(params.sessionId, activePrompt)) { - this.interruptLateStartedTurn(params.sessionId, turnId); + const turn = {threadId: params.sessionId, turnId}; + activePrompt.currentTurn = turn; + if (this.promptShouldStop(params.sessionId, activePrompt)) { + this.interruptLateStartedTurn(turn); return; } sessionState.currentTurnId = turnId; pendingTurnStart?.resolve(turnId); }, - () => this.promptIsClosedOrStale(params.sessionId, activePrompt), + () => this.promptShouldStop(params.sessionId, activePrompt), )); void sendPromptPromise.catch((err) => { if (this.activePrompts.get(params.sessionId) !== activePrompt) { - logger.error(`Prompt for closed session ${params.sessionId} failed after close`, err); + logger.error(`Prompt for cancelled session ${params.sessionId} failed after prompt returned`, err); } }); const turnCompleted = await Promise.race([ sendPromptPromise, activePrompt.closeSignal, + this.cancelBeforeTurnStarted(activePrompt), ]); if (turnCompleted === null) { - return { - stopReason: "cancelled", - usage: this.buildPromptUsage(sessionState.lastTokenUsage), - _meta: this.buildQuotaMeta(sessionState), - }; + return this.cancelledPromptResponse(sessionState); } await this.codexAcpClient.waitForSessionNotifications(params.sessionId); - // Check if turn was interrupted (cancelled) if (turnCompleted.turn.status === "interrupted") { - if (!this.sessionIsClosing(params.sessionId) && this.sessions.has(params.sessionId)) { - await this.connection.notify(acp.methods.client.session.update, { - sessionId: params.sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: "*Conversation interrupted*" - } - } - }); - } - return { - stopReason: "cancelled", - usage: this.buildPromptUsage(sessionState.lastTokenUsage), - _meta: this.buildQuotaMeta(sessionState), - }; + await this.notifyConversationInterrupted(params.sessionId); + return this.cancelledPromptResponse(sessionState); } const error = eventHandler.getFailure(); @@ -1406,6 +1480,7 @@ export class CodexAcpServer { throw err; } finally { logger.log("Prompt completed", {sessionId: params.sessionId}); + disposePromptRequestCancellation(); sessionState.currentTurnId = null; if (pendingTurnStart !== null && this.pendingTurnStarts.get(params.sessionId) === pendingTurnStart) { this.pendingTurnStarts.delete(params.sessionId); @@ -1415,6 +1490,30 @@ export class CodexAcpServer { } } + private cancelledPromptResponse(sessionState: SessionState): acp.PromptResponse { + return { + stopReason: "cancelled", + usage: this.buildPromptUsage(sessionState.lastTokenUsage), + _meta: this.buildQuotaMeta(sessionState), + }; + } + + private async notifyConversationInterrupted(sessionId: string): Promise { + if (this.sessionIsClosing(sessionId) || !this.sessions.has(sessionId)) { + return; + } + await this.connection.notify(acp.methods.client.session.update, { + sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: "*Conversation interrupted*" + } + } + }); + } + private buildQuotaMeta(sessionState: SessionState): { quota: QuotaMeta } { const lastTokenUsage = sessionState.lastTokenUsage; diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index 969e161d..6a5a0b51 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -242,7 +242,10 @@ export class CodexAppServerClient { } } - async runReview(params: ReviewStartParams, onTurnStarted?: (turnId: string) => void): Promise { + async runReview( + params: ReviewStartParams, + onTurnStarted?: (turnId: string, threadId: string) => void, + ): Promise { const capturedCompletions: Array = []; const releaseCapture = this.captureTurnCompletions(params.threadId, (event) => { capturedCompletions.push(event); @@ -250,7 +253,7 @@ export class CodexAppServerClient { try { const reviewStarted = await this.reviewStart(params); - onTurnStarted?.(reviewStarted.turn.id); + onTurnStarted?.(reviewStarted.turn.id, reviewStarted.reviewThreadId); const earlyCompletion = capturedCompletions.find(event => event.turn.id === reviewStarted.turn.id); releaseCapture(); if (earlyCompletion) { diff --git a/src/CodexApprovalHandler.ts b/src/CodexApprovalHandler.ts index 868bbe73..dd73b8ae 100644 --- a/src/CodexApprovalHandler.ts +++ b/src/CodexApprovalHandler.ts @@ -46,13 +46,16 @@ function permissionOption( export class CodexApprovalHandler implements ApprovalHandler { private readonly connection: AcpClientConnection; private readonly sessionState: SessionState; + private readonly cancellationSignal: AbortSignal | undefined; constructor( connection: AcpClientConnection, - sessionState: SessionState + sessionState: SessionState, + cancellationSignal?: AbortSignal, ) { this.connection = connection; this.sessionState = sessionState; + this.cancellationSignal = cancellationSignal; } async handleCommandExecution( @@ -61,7 +64,11 @@ export class CodexApprovalHandler implements ApprovalHandler { try { const sessionId = this.sessionState.sessionId; const acpRequest = this.buildCommandPermissionRequest(sessionId, params); - const response = await this.connection.request(acp.methods.client.session.requestPermission, acpRequest); + const response = await this.connection.request( + acp.methods.client.session.requestPermission, + acpRequest, + this.requestOptions(), + ); return this.convertCommandResponse(params, response); } catch (error) { logger.error("Error requesting command execution permission", error); @@ -75,7 +82,11 @@ export class CodexApprovalHandler implements ApprovalHandler { try { const sessionId = this.sessionState.sessionId; const acpRequest = this.buildFileChangePermissionRequest(sessionId, params); - const response = await this.connection.request(acp.methods.client.session.requestPermission, acpRequest); + const response = await this.connection.request( + acp.methods.client.session.requestPermission, + acpRequest, + this.requestOptions(), + ); return this.convertFileChangeResponse(params, response); } catch (error) { logger.error("Error requesting file change permission", error); @@ -89,7 +100,11 @@ export class CodexApprovalHandler implements ApprovalHandler { try { const sessionId = this.sessionState.sessionId; const acpRequest = this.buildPermissionsRequest(sessionId, params); - const response = await this.connection.request(acp.methods.client.session.requestPermission, acpRequest); + const response = await this.connection.request( + acp.methods.client.session.requestPermission, + acpRequest, + this.requestOptions(), + ); return this.convertPermissionsResponse(params, response); } catch (error) { logger.error("Error requesting permissions", error); @@ -97,6 +112,10 @@ export class CodexApprovalHandler implements ApprovalHandler { } } + private requestOptions(): acp.SendRequestOptions | undefined { + return this.cancellationSignal ? {cancellationSignal: this.cancellationSignal} : undefined; + } + private buildCommandPermissionRequest( sessionId: string, params: CommandExecutionRequestApprovalParams diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index e4c6f8c8..e483762b 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -17,6 +17,10 @@ export type CommandHandleResult = | { handled: false } | { handled: true, turnCompleted?: TurnCompletedNotification }; +export type CommandHandleOptions = { + onTurnStarted?: (turnId: string, threadId: string) => void; +}; + export class CodexCommands { private readonly connection: AcpClientConnection; private readonly codexAcpClient: CodexAcpClient; @@ -139,7 +143,11 @@ export class CodexCommands { }; } - async tryHandleCommand(prompt: acp.ContentBlock[], sessionState: SessionState): Promise { + async tryHandleCommand( + prompt: acp.ContentBlock[], + sessionState: SessionState, + options: CommandHandleOptions = {}, + ): Promise { const command = this.parseCommand(prompt); if (command === null) return { handled: false }; const commandName = command.name; @@ -153,7 +161,7 @@ export class CodexCommands { } case "review": { const target = this.buildReviewTarget(command.rest); - const turnCompleted = await this.runReviewCommand(sessionState, target); + const turnCompleted = await this.runReviewCommand(sessionState, target, options); return { handled: true, turnCompleted }; } case "review-branch": { @@ -164,7 +172,7 @@ export class CodexCommands { const turnCompleted = await this.runReviewCommand(sessionState, { type: "baseBranch", branch: command.rest, - }); + }, options); return { handled: true, turnCompleted }; } case "review-commit": { @@ -176,7 +184,7 @@ export class CodexCommands { type: "commit", sha: command.rest, title: null, - }); + }, options); return { handled: true, turnCompleted }; } case "status": { @@ -241,12 +249,20 @@ export class CodexCommands { } } - private async runReviewCommand(sessionState: SessionState, target: ReviewTarget): Promise { + private async runReviewCommand( + sessionState: SessionState, + target: ReviewTarget, + options: CommandHandleOptions, + ): Promise { return await this.runWithProcessCheck(() => this.codexAcpClient.runReview( sessionState.sessionId, target, - (turnId) => { - sessionState.currentTurnId = turnId; + (turnId, threadId) => { + if (options.onTurnStarted) { + options.onTurnStarted(turnId, threadId); + } else { + sessionState.currentTurnId = turnId; + } }, )); } diff --git a/src/CodexElicitationHandler.ts b/src/CodexElicitationHandler.ts index 21515e56..2a15df04 100644 --- a/src/CodexElicitationHandler.ts +++ b/src/CodexElicitationHandler.ts @@ -69,6 +69,7 @@ function buildToolApprovalOptions(persistOptions: Set): acp.Permis export class CodexElicitationHandler implements ElicitationHandler { private readonly connection: AcpClientConnection; private readonly sessionState: SessionState; + private readonly cancellationSignal: AbortSignal | undefined; // In Rust, the MCP elicitation handler receives ElicitationRequestEvent directly from the MCP // protocol layer, where id is set to "mcp_tool_call_approval_" — the call ID is extracted // by stripping that prefix. @@ -86,9 +87,10 @@ export class CodexElicitationHandler implements ElicitationHandler { // (threadId, serverName). private readonly pendingMcpApprovals = new Map(); - constructor(connection: AcpClientConnection, sessionState: SessionState) { + constructor(connection: AcpClientConnection, sessionState: SessionState, cancellationSignal?: AbortSignal) { this.connection = connection; this.sessionState = sessionState; + this.cancellationSignal = cancellationSignal; } handleNotification(notification: ServerNotification): void { @@ -112,7 +114,11 @@ export class CodexElicitationHandler implements ElicitationHandler { ): Promise { try { const { request, correlatedCallId } = this.buildPermissionRequest(params); - const response = await this.connection.request(acp.methods.client.session.requestPermission, request); + const response = await this.connection.request( + acp.methods.client.session.requestPermission, + request, + this.requestOptions(), + ); if (correlatedCallId !== undefined && response.outcome.outcome !== "cancelled") { const optionId = response.outcome.optionId; if (optionId !== McpApprovalOptionId.Decline) { @@ -129,6 +135,10 @@ export class CodexElicitationHandler implements ElicitationHandler { } } + private requestOptions(): acp.SendRequestOptions | undefined { + return this.cancellationSignal ? {cancellationSignal: this.cancellationSignal} : undefined; + } + private buildPermissionRequest( params: McpServerElicitationRequestParams ): { request: acp.RequestPermissionRequest; correlatedCallId: string | undefined } { diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 70582711..9f822c97 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -693,7 +693,7 @@ describe('ACP server test', { timeout: 40_000 }, () => { return onServerNotification; } - function createTurn(id: string, status: "inProgress" | "completed") { + function createTurn(id: string, status: "inProgress" | "completed" | "interrupted") { return { id, items: [], @@ -720,6 +720,14 @@ describe('ACP server test', { timeout: 40_000 }, () => { await new Promise((resolve) => setTimeout(resolve, 0)); } + function deferred(): {promise: Promise, resolve: (value: T) => void} { + let resolve: (value: T) => void = () => {}; + const promise = new Promise((innerResolve) => { + resolve = innerResolve; + }); + return {promise, resolve}; + } + it('should map events from dump', async () => { fixture.getCodexAppServerClient().onServerNotification = loadNotifications(); @@ -924,6 +932,150 @@ describe('ACP server test', { timeout: 40_000 }, () => { })).resolves.toMatchObject({stopReason: "end_turn"}); }); + it('cancels an active prompt when the ACP prompt request is cancelled', async () => { + const { mockFixture, sessionState } = setupPromptFixture(); + const turnCompleted = deferred(); + vi.spyOn(mockFixture.getCodexAppServerClient(), "awaitTurnCompleted") + .mockReturnValue(turnCompleted.promise); + const turnInterruptSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "turnInterrupt") + .mockImplementation(async ({threadId, turnId}) => { + turnCompleted.resolve({ + threadId, + turn: createTurn(turnId, "interrupted"), + }); + }); + const controller = new AbortController(); + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "long running prompt" }], + }, controller.signal); + + await vi.waitFor(() => { + expect(sessionState.currentTurnId).toBe("turn-id"); + }); + + controller.abort(); + + await vi.waitFor(() => { + expect(turnInterruptSpy).toHaveBeenCalledWith({ + threadId: "session-id", + turnId: "turn-id", + }); + }); + await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"}); + }); + + it('returns success when a cancelled ACP prompt request completes before interruption wins', async () => { + const { mockFixture, sessionState } = setupPromptFixture(); + const turnCompleted = deferred(); + vi.spyOn(mockFixture.getCodexAppServerClient(), "awaitTurnCompleted") + .mockReturnValue(turnCompleted.promise); + const turnInterrupt = deferred(); + const turnInterruptSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "turnInterrupt") + .mockReturnValue(turnInterrupt.promise); + const controller = new AbortController(); + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "long running prompt" }], + }, controller.signal); + + await vi.waitFor(() => { + expect(sessionState.currentTurnId).toBe("turn-id"); + }); + + controller.abort(); + await vi.waitFor(() => { + expect(turnInterruptSpy).toHaveBeenCalledWith({ + threadId: "session-id", + turnId: "turn-id", + }); + }); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "turn-id", + itemId: "tail-item", + delta: "tail output", + }, + }); + turnCompleted.resolve({ + threadId: "session-id", + turn: createTurn("turn-id", "completed"), + }); + await expect(promptPromise).resolves.toMatchObject({stopReason: "end_turn"}); + expect(mockFixture.getAcpConnectionDump([])).toContain("tail output"); + turnInterrupt.resolve(undefined); + }); + + it('interrupts a late-started turn after the ACP prompt request is cancelled', async () => { + const { mockFixture } = setupPromptFixture(); + const turnStart = deferred<{turn: ReturnType}>(); + const turnStartCalled = deferred(); + vi.spyOn(mockFixture.getCodexAppServerClient(), "turnStart") + .mockImplementation(async () => { + turnStartCalled.resolve(); + return await turnStart.promise; + }); + const turnCompleted = deferred(); + vi.spyOn(mockFixture.getCodexAppServerClient(), "awaitTurnCompleted") + .mockReturnValue(turnCompleted.promise); + const turnInterruptSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "turnInterrupt") + .mockImplementation(async ({threadId, turnId}) => { + turnCompleted.resolve({ + threadId, + turn: createTurn(turnId, "interrupted"), + }); + }); + const controller = new AbortController(); + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "long running prompt" }], + }, controller.signal); + + await turnStartCalled.promise; + controller.abort(); + + await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"}); + expect(turnInterruptSpy).not.toHaveBeenCalled(); + + turnStart.resolve({turn: createTurn("late-turn-id", "inProgress")}); + await vi.waitFor(() => { + expect(turnInterruptSpy).toHaveBeenCalledWith({ + threadId: "session-id", + turnId: "late-turn-id", + }); + }); + }); + + it('returns cancelled when the ACP prompt request is cancelled during startup work', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture(); + const skillsRefresh = deferred<{data: []}>(); + const listSkillsSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "listSkills") + .mockReturnValue(skillsRefresh.promise); + const controller = new AbortController(); + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "long running prompt" }], + }, controller.signal); + + await vi.waitFor(() => { + expect(listSkillsSpy).toHaveBeenCalled(); + }); + + controller.abort(); + await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"}); + + skillsRefresh.resolve({data: []}); + await flushAsyncWork(); + expect(turnStartSpy).not.toHaveBeenCalled(); + }); + it('should send attachments as prompt items', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpAgent = mockFixture.getCodexAcpAgent(); @@ -1124,6 +1276,46 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(promptResolved).toBe(true); }); + it('interrupts a late-started review slash command after the ACP prompt request is cancelled', async () => { + const { mockFixture } = setupPromptFixture(); + const reviewStart = deferred(); + const reviewStartSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "reviewStart") + .mockReturnValue(reviewStart.promise); + const reviewCompleted = deferred(); + const awaitTurnCompletedSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "awaitTurnCompleted") + .mockReturnValue(reviewCompleted.promise); + const turnInterruptSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "turnInterrupt") + .mockImplementation(async ({threadId, turnId}) => { + reviewCompleted.resolve({ + threadId, + turn: createTurn(turnId, "interrupted"), + }); + }); + const controller = new AbortController(); + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/review" }], + }, controller.signal); + + await vi.waitFor(() => { + expect(reviewStartSpy).toHaveBeenCalled(); + }); + + controller.abort(); + await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"}); + + reviewStart.resolve(createReviewStartResponse("review-thread-id", "review-turn-id")); + + await vi.waitFor(() => { + expect(turnInterruptSpy).toHaveBeenCalledWith({ + threadId: "review-thread-id", + turnId: "review-turn-id", + }); + }); + expect(awaitTurnCompletedSpy).toHaveBeenCalledWith("review-thread-id", "review-turn-id"); + }); + it('returns cancelled when review slash command is interrupted', async () => { const { mockFixture } = setupPromptFixture(); vi.spyOn(mockFixture.getCodexAppServerClient(), "reviewStart") @@ -1172,6 +1364,31 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(mockFixture.getAcpConnectionDump([])).toContain("Context compacted"); }); + it('returns cancelled promptly when non-interruptible slash command startup is cancelled', async () => { + const { mockFixture } = setupPromptFixture(); + const compactStartSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadCompactStart") + .mockResolvedValue({}); + const controller = new AbortController(); + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/compact" }], + }, controller.signal); + + await vi.waitFor(() => { + expect(compactStartSpy).toHaveBeenCalledWith({ threadId: "session-id" }); + }); + + controller.abort(); + await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"}); + + mockFixture.sendServerNotification({ + method: "thread/compacted", + params: { threadId: "session-id", turnId: "compact-turn-id" }, + }); + await flushAsyncWork(); + }); + it('reports missing review slash command input', async () => { const { mockFixture } = setupPromptFixture(); const reviewStartSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "reviewStart") @@ -1375,11 +1592,14 @@ describe('ACP server test', { timeout: 40_000 }, () => { return { mockFixture, sessionState, turnStartSpy }; } - function createReviewStartResponse(): ReviewStartResponse { + function createReviewStartResponse( + reviewThreadId: string = "session-id", + turnId: string = "review-turn-id", + ): ReviewStartResponse { return { - reviewThreadId: "session-id", + reviewThreadId, turn: { - id: "review-turn-id", + id: turnId, items: [], itemsView: "notLoaded", status: "inProgress", diff --git a/src/index.ts b/src/index.ts index 64a5caa1..5f8a83dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -124,7 +124,7 @@ function startAcpServer() { .onRequest(acp.methods.agent.session.setConfigOption, (ctx) => getAgent().setSessionConfigOption(ctx.params)) .onRequest(acp.methods.agent.authenticate, (ctx) => getAgent().authenticate(ctx.params)) .onRequest(acp.methods.agent.logout, (ctx) => getAgent().logout(ctx.params)) - .onRequest(acp.methods.agent.session.prompt, (ctx) => getAgent().prompt(ctx.params)) + .onRequest(acp.methods.agent.session.prompt, (ctx) => getAgent().prompt(ctx.params, ctx.signal)) .onNotification(acp.methods.agent.session.cancel, (ctx) => getAgent().cancel(ctx.params)) .onRequest("authentication/status", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/status", ctx.params)) .onRequest("authentication/logout", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/logout", ctx.params)) From 54dcc0485e3edd64c8b326854fa77ebb27c779bf Mon Sep 17 00:00:00 2001 From: Dave Melville <73310150+david-melville@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:06:09 -0400 Subject: [PATCH 10/45] fix: use configured model provider on session resume (#224) When resuming or loading a session, getResumeModelProvider() now reads the model_provider value from Codex config (config/read) before falling back to "openai". This fixes a v1.0.0 regression where sessions resumed via ACP always used OpenAI even when ~/.codex/config.toml specified model_provider = "azure". --- src/CodexAcpClient.ts | 14 +++---- .../CodexACPAgent/CodexAcpClient.test.ts | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 042c24f1..4e906e33 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -195,7 +195,7 @@ export class CodexAcpClient { return sessionModelProvider; } const settingsModelProvider = await this.codexClient.configRead({includeLayers: false}); - return settingsModelProvider.config.model_provider ?? null; + return settingsModelProvider?.config?.model_provider ?? null; } async logout(): Promise { @@ -228,7 +228,7 @@ export class CodexAcpClient { const response = await this.codexClient.threadResume({ config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []), cwd: request.cwd, - modelProvider: this.getResumeModelProvider(), + modelProvider: await this.getResumeModelProvider(), threadId: request.sessionId, }); onSubscribed?.(); @@ -250,7 +250,7 @@ export class CodexAcpClient { const response = await this.codexClient.threadResume({ config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []), cwd: request.cwd, - modelProvider: this.getResumeModelProvider(), + modelProvider: await this.getResumeModelProvider(), threadId: request.sessionId, }); onSubscribed?.(); @@ -380,10 +380,10 @@ export class CodexAcpClient { return this.gatewayConfig?.modelProvider ?? this.modelProvider; } - private getResumeModelProvider(): string { - // Passing `null` forces codex to use the persisted provider for resumed session instead of default one - // Explicit fallback to "openai" fixes error `Model provider not found` at least for ChatGPT authentication - return this.getModelProvider() ?? "openai"; + private async getResumeModelProvider(): Promise { + // Prefer an explicit/gateway provider, then the provider persisted in Codex config. + // Keep OpenAI as the final fallback for ChatGPT-authenticated sessions without a configured provider. + return (await this.getCurrentModelProvider()) ?? "openai"; } private async refreshSkills( diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 9f822c97..e80aa94e 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -437,6 +437,46 @@ describe('ACP server test', { timeout: 40_000 }, () => { }); }); + it('uses configured model provider when resuming sessions without an explicit provider', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpClient = mockFixture.getCodexAcpClient(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + + vi.spyOn(codexAppServerClient, "skillsExtraRootsSet").mockResolvedValue(undefined); + vi.spyOn(codexAppServerClient, "listSkills").mockResolvedValue({data: []}); + vi.spyOn(codexAppServerClient, "configRead").mockResolvedValue({ + config: { + model_provider: "azure", + }, + } as any); + const threadResumeSpy = vi.spyOn(codexAppServerClient, "threadResume").mockResolvedValue({ + thread: {id: "thread-id"} as any, + model: "gpt-5", + reasoningEffort: "medium", + serviceTier: null, + } as any); + vi.spyOn(codexAppServerClient, "threadRead").mockResolvedValue({ + thread: {id: "thread-id"} as any, + }); + vi.spyOn(codexAppServerClient, "listModels").mockResolvedValue({ + data: [createTestModel({id: "gpt-5"})], + nextCursor: null, + }); + + await codexAcpClient.resumeSession({ + sessionId: "resume-id", + cwd: "/workspace", + }); + await codexAcpClient.loadSession({ + sessionId: "load-id", + cwd: "/workspace", + mcpServers: [], + }); + + expect(threadResumeSpy.mock.calls[0]![0].modelProvider).toBe("azure"); + expect(threadResumeSpy.mock.calls[1]![0].modelProvider).toBe("azure"); + }); + it('rejects malformed ACP additional directories', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpClient = mockFixture.getCodexAcpClient(); From b7c78dd561cbee1c36dd1134f7c8550c65410e13 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 26 Jun 2026 14:14:35 +0200 Subject: [PATCH 11/45] fix: Track configured auth for turn errors (#245) * fix: Track configured auth for turn errors Closes #244 * Clear session auth state on logout * Refresh session auth state after logout * Refresh session auth state after login * Treat custom providers as auth configured * Track auth state by model provider * Normalize configured auth error responses * Test provider auth state for restored sessions --- src/CodexAcpClient.ts | 8 + src/CodexAcpServer.ts | 79 +++++- src/CodexCommands.ts | 8 +- src/CodexEventHandler.ts | 37 ++- .../CodexACPAgent/CodexAcpClient.test.ts | 228 ++++++++++++++++++ .../CodexACPAgent/auth-error-events.test.ts | 176 ++++++++++++++ .../CodexACPAgent/new-session-logout.test.ts | 60 ++++- src/__tests__/acp-test-utils.ts | 2 + 8 files changed, 581 insertions(+), 17 deletions(-) create mode 100644 src/__tests__/CodexACPAgent/auth-error-events.test.ts diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 4e906e33..ed8bdb18 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -217,6 +217,10 @@ export class CodexAcpClient { return response.requiresOpenaiAuth && !response.account; } + hasGatewayAuth(): boolean { + return this.gatewayConfig !== null; + } + async getAccount(): Promise { return this.codexClient.accountRead({refreshToken: false}); } @@ -238,6 +242,7 @@ export class CodexAcpClient { sessionId: request.sessionId, currentModelId: currentModelId, models: codexModels, + modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, additionalDirectories, } @@ -264,6 +269,7 @@ export class CodexAcpClient { sessionId: request.sessionId, currentModelId: currentModelId, models: codexModels, + modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, thread: historyResponse.thread, additionalDirectories, @@ -289,6 +295,7 @@ export class CodexAcpClient { sessionId: response.thread.id, currentModelId: currentModelId, models: codexModels, + modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, additionalDirectories, }; @@ -698,6 +705,7 @@ export type SessionMetadata = { sessionId: string, currentModelId: string, models: Model[], + modelProvider?: string | null, currentServiceTier?: ServiceTier | null, additionalDirectories: string[], } diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 76b80af5..720ff95f 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -3,7 +3,7 @@ import {RequestError, type SessionId, type SessionModeState} from "@agentclientp import {CodexEventHandler} from "./CodexEventHandler"; import {CodexApprovalHandler} from "./CodexApprovalHandler"; import {CodexElicitationHandler} from "./CodexElicitationHandler"; -import {type CodexAuthRequest, getCodexAuthMethods} from "./CodexAuthMethod"; +import {type CodexAuthRequest, getCodexAuthMethods, isCodexAuthRequest} from "./CodexAuthMethod"; import {CodexAcpClient, type SessionMetadata, type SessionMetadataWithThread} from "./CodexAcpClient"; import type {McpStartupResult} from "./CodexAppServerClient"; import {ACPSessionConnection, type AcpClientConnection, type UpdateSessionEvent} from "./ACPSessionConnection"; @@ -79,6 +79,8 @@ export interface SessionState { modelContextWindow: number | null; rateLimits: RateLimitsMap | null; account: Account | null; + authConfigured: boolean; + authProvider: string | null; cwd: string; additionalDirectories: string[]; fastModeEnabled: boolean; @@ -87,6 +89,11 @@ export interface SessionState { terminalOutputMode: TerminalOutputMode; } +interface ActiveAuthState { + account: Account | null; + authConfigured: boolean; +} + interface PendingMcpStartupSession { requestedServers: Set; afterVersion: number; @@ -156,7 +163,8 @@ export class CodexAcpServer { this.availableCommands = new CodexCommands( connection, codexAcpClient, - (operation) => this.runWithProcessCheck(operation) + (operation) => this.runWithProcessCheck(operation), + () => this.refreshSessionsAuthState(null) ); } @@ -247,6 +255,7 @@ export class CodexAcpServer { async handleError(e: Error){ if (e.message.includes("log out") || e.message.includes("cloud requirements")) { await this.runWithProcessCheck(() => this.codexAcpClient.logout()); + await this.refreshSessionsAuthState(null); throw RequestError.internalError(`${(e.message)}\n\nYou have been logged out. Please try again.`); } } @@ -346,9 +355,10 @@ export class CodexAcpServer { } const {sessionId, currentModelId, models} = sessionMetadata; - let account: Account | null; + const authProvider = sessionMetadata.modelProvider ?? this.codexAcpClient.getModelProvider(); + let authState: ActiveAuthState; try { - account = await this.getActiveAccount(); + authState = await this.getAuthStateForProvider(authProvider); } catch (err) { if (resumeSubscribed && requestedSessionGeneration !== null) { await this.cleanupStaleSessionOpen(sessionId, requestedSessionGeneration); @@ -375,7 +385,9 @@ export class CodexAcpServer { totalTokenUsage: null, modelContextWindow: null, rateLimits: null, - account: account, + account: authState.account, + authConfigured: authState.authConfigured, + authProvider: authProvider, cwd: request.cwd, additionalDirectories: sessionMetadata.additionalDirectories, fastModeEnabled: sessionMetadata.currentServiceTier === "fast", @@ -401,12 +413,36 @@ export class CodexAcpServer { return [sessionId, sessionModelState, sessionModeState]; } - private async getActiveAccount(){ - if (this.codexAcpClient.getModelProvider()) { - return null + private async getAuthStateForProvider(authProvider: string | null): Promise { + if (!this.authProviderUsesOpenAiAccount(authProvider)) { + return { + account: null, + authConfigured: true, + }; } const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount()); - return accountResponse.account; + return { + account: accountResponse.account, + authConfigured: accountResponse.account !== null || !accountResponse.requiresOpenaiAuth, + }; + } + + private authProviderUsesOpenAiAccount(authProvider: string | null): boolean { + return authProvider === null || authProvider === "openai"; + } + + private authProvidersMatch(a: string | null, b: string | null): boolean { + if (this.authProviderUsesOpenAiAccount(a) && this.authProviderUsesOpenAiAccount(b)) { + return true; + } + return a === b; + } + + private getAuthProviderForAuthenticateRequest(request: acp.AuthenticateRequest): string | null { + if (isCodexAuthRequest(request) && request.methodId === "gateway") { + return "custom-gateway"; + } + return null; } async loadSession(params: acp.LoadSessionRequest): Promise { @@ -565,6 +601,7 @@ export class CodexAcpServer { logger.log("Authenticate request failed"); throw RequestError.invalidParams(); } + await this.refreshSessionsAuthState(this.getAuthProviderForAuthenticateRequest(_params)); logger.log("Authenticate request completed"); return { }; } @@ -572,9 +609,24 @@ export class CodexAcpServer { async logout(_params: acp.LogoutRequest): Promise { logger.log("Logout request received"); await this.runWithProcessCheck(() => this.codexAcpClient.logout()); + await this.refreshSessionsAuthState(null); logger.log("Logout request completed"); } + private async refreshSessionsAuthState(authProvider: string | null): Promise { + if (this.sessions.size === 0) return; + + const sessionsToRefresh = [...this.sessions.values()] + .filter(sessionState => this.authProvidersMatch(sessionState.authProvider, authProvider)); + if (sessionsToRefresh.length === 0) return; + + const authState = await this.getAuthStateForProvider(authProvider); + for (const sessionState of sessionsToRefresh) { + sessionState.account = authState.account; + sessionState.authConfigured = authState.authConfigured; + } + } + async setSessionMode( _params: acp.SetSessionModeRequest, ): Promise { @@ -798,9 +850,10 @@ export class CodexAcpServer { } const {sessionId, currentModelId, models, thread} = sessionMetadata; - let account: Account | null; + const authProvider = sessionMetadata.modelProvider ?? this.codexAcpClient.getModelProvider(); + let authState: ActiveAuthState; try { - account = await this.getActiveAccount(); + authState = await this.getAuthStateForProvider(authProvider); } catch (err) { if (subscribed) { await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration); @@ -826,7 +879,9 @@ export class CodexAcpServer { totalTokenUsage: null, modelContextWindow: null, rateLimits: null, - account: account, + account: authState.account, + authConfigured: authState.authConfigured, + authProvider: authProvider, cwd: request.cwd, additionalDirectories: sessionMetadata.additionalDirectories, fastModeEnabled: sessionMetadata.currentServiceTier === "fast", diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index e483762b..457d911e 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -21,19 +21,24 @@ export type CommandHandleOptions = { onTurnStarted?: (turnId: string, threadId: string) => void; }; +export type LogoutHandler = () => void | Promise; + export class CodexCommands { private readonly connection: AcpClientConnection; private readonly codexAcpClient: CodexAcpClient; private readonly runWithProcessCheck: (operation: () => Promise) => Promise; + private readonly onLogout: LogoutHandler; constructor( connection: AcpClientConnection, codexAcpClient: CodexAcpClient, - runWithProcessCheck: (operation: () => Promise) => Promise + runWithProcessCheck: (operation: () => Promise) => Promise, + onLogout: LogoutHandler = () => {} ) { this.connection = connection; this.codexAcpClient = codexAcpClient; this.runWithProcessCheck = runWithProcessCheck; + this.onLogout = onLogout; } async publish(sessionId: string): Promise { @@ -198,6 +203,7 @@ export class CodexCommands { } case "logout": { await this.runWithProcessCheck(() => this.codexAcpClient.logout()); + await this.onLogout(); const session = new ACPSessionConnection(this.connection, sessionId); await session.update({ sessionUpdate: "agent_message_chunk", diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index eb22e74b..513d5f51 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -589,9 +589,15 @@ export class CodexEventHandler { } private async createErrorEvent(params: ErrorNotification): Promise { - const error = params.error.codexErrorInfo - if (error == "unauthorized" || error == "usageLimitExceeded" || this.getHttpStatusCode(error) == 401) { - this.failure = RequestError.authRequired(); + const error = params.error.codexErrorInfo; + if (error === "usageLimitExceeded") { + this.failure = RequestError.internalError( + this.createTurnErrorData(params.error), + ); + } else if (this.isAuthenticationRequiredError(error)) { + this.failure = this.sessionState.authConfigured + ? RequestError.internalError(this.createTurnErrorData(params.error)) + : RequestError.authRequired(this.createTurnErrorData(params.error), params.error.message); } return { sessionUpdate: "agent_message_chunk", @@ -602,6 +608,10 @@ export class CodexEventHandler { } } + private isAuthenticationRequiredError(error: CodexErrorInfo | null): boolean { + return error === "unauthorized" || this.getHttpStatusCode(error) === 401; + } + private getHttpStatusCode(error: CodexErrorInfo | null): number | null { if (error !== null && typeof error === "object") { if ("httpConnectionFailed" in error) { @@ -617,6 +627,27 @@ export class CodexEventHandler { return null; } + private createTurnErrorData(error: ErrorNotification["error"]): { + message: string; + codexErrorInfo?: CodexErrorInfo; + additionalDetails?: string; + } { + const data: { + message: string; + codexErrorInfo?: CodexErrorInfo; + additionalDetails?: string; + } = { + message: error.additionalDetails ?? error.message, + }; + if (error.codexErrorInfo !== null) { + data.codexErrorInfo = error.codexErrorInfo; + } + if (error.additionalDetails !== null) { + data.additionalDetails = error.additionalDetails; + } + return data; + } + private handleTokenUsageUpdated(params: ThreadTokenUsageUpdatedNotification): void { this.sessionState.lastTokenUsage = toTokenCount(params.tokenUsage.last); this.sessionState.totalTokenUsage = toTokenCount(params.tokenUsage.total); diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index e80aa94e..54436c6a 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -477,6 +477,64 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(threadResumeSpy.mock.calls[1]![0].modelProvider).toBe("azure"); }); + it('tracks configured model provider auth state for resumed and loaded sessions', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const codexAcpClient = mockFixture.getCodexAcpClient(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + + vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false); + const getAccountSpy = vi.spyOn(codexAcpClient, "getAccount").mockResolvedValue({ + account: null, + requiresOpenaiAuth: true, + }); + vi.spyOn(codexAppServerClient, "skillsExtraRootsSet").mockResolvedValue(undefined); + vi.spyOn(codexAppServerClient, "listSkills").mockResolvedValue({data: []}); + vi.spyOn(codexAppServerClient, "configRead").mockResolvedValue({ + config: { + model_provider: "azure", + }, + } as any); + const threadResumeSpy = vi.spyOn(codexAppServerClient, "threadResume").mockResolvedValue({ + thread: {id: "thread-id"} as any, + model: "gpt-5", + modelProvider: "azure", + reasoningEffort: "medium", + serviceTier: null, + } as any); + vi.spyOn(codexAppServerClient, "threadRead").mockResolvedValue({ + thread: {id: "thread-id", turns: []} as any, + }); + vi.spyOn(codexAppServerClient, "listModels").mockResolvedValue({ + data: [createTestModel({id: "gpt-5"})], + nextCursor: null, + }); + + await codexAcpAgent.resumeSession({ + sessionId: "resume-id", + cwd: "/workspace", + }); + await codexAcpAgent.loadSession({ + sessionId: "load-id", + cwd: "/workspace", + mcpServers: [], + }); + + expect(threadResumeSpy.mock.calls[0]![0].modelProvider).toBe("azure"); + expect(threadResumeSpy.mock.calls[1]![0].modelProvider).toBe("azure"); + expect(getAccountSpy).not.toHaveBeenCalled(); + expect(codexAcpAgent.getSessionState("resume-id")).toMatchObject({ + account: null, + authConfigured: true, + authProvider: "azure", + }); + expect(codexAcpAgent.getSessionState("load-id")).toMatchObject({ + account: null, + authConfigured: true, + authProvider: "azure", + }); + }); + it('rejects malformed ACP additional directories', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpClient = mockFixture.getCodexAcpClient(); @@ -1459,6 +1517,176 @@ describe('ACP server test', { timeout: 40_000 }, () => { await expect(fixture.getAcpConnectionDump(["sessionId"])).toMatchFileSnapshot("data/command-logout.json"); }); + it('clears active session auth state when logout command signs out', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const codexAcpClient = mockFixture.getCodexAcpClient(); + const model = createTestModel(); + const currentModelId = ModelId.create(model.id, model.defaultReasoningEffort).toString(); + + vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false); + vi.spyOn(codexAcpClient, "getModelProvider").mockReturnValue("openai"); + const getAccountSpy = vi.spyOn(codexAcpClient, "getAccount") + .mockResolvedValueOnce({ + account: { type: "apiKey" }, + requiresOpenaiAuth: false, + }) + .mockResolvedValueOnce({ + account: { type: "apiKey" }, + requiresOpenaiAuth: false, + }) + .mockResolvedValueOnce({ + account: null, + requiresOpenaiAuth: true, + }) + .mockResolvedValueOnce({ + account: { type: "apiKey" }, + requiresOpenaiAuth: false, + }); + vi.spyOn(codexAcpClient, "newSession") + .mockResolvedValueOnce({ + sessionId: "session-1", + currentModelId, + models: [model], + additionalDirectories: [], + }) + .mockResolvedValueOnce({ + sessionId: "session-2", + currentModelId, + models: [model], + additionalDirectories: [], + }); + const logoutSpy = vi.spyOn(codexAcpClient, "logout").mockResolvedValue(); + const authenticateSpy = vi.spyOn(codexAcpClient, "authenticate").mockResolvedValue(true); + + const session1 = await codexAcpAgent.newSession({cwd: "/workspace", mcpServers: []}); + const session2 = await codexAcpAgent.newSession({cwd: "/workspace", mcpServers: []}); + expect(codexAcpAgent.getSessionState(session1.sessionId).authConfigured).toBe(true); + expect(codexAcpAgent.getSessionState(session2.sessionId).authConfigured).toBe(true); + + await codexAcpAgent.prompt({ + sessionId: session1.sessionId, + prompt: [{ type: "text", text: "/logout" }], + }); + + expect(logoutSpy).toHaveBeenCalledOnce(); + expect(getAccountSpy).toHaveBeenCalledTimes(3); + expect(codexAcpAgent.getSessionState(session1.sessionId)).toMatchObject({ + account: null, + authConfigured: false, + }); + expect(codexAcpAgent.getSessionState(session2.sessionId)).toMatchObject({ + account: null, + authConfigured: false, + }); + + await codexAcpAgent.authenticate({methodId: "api-key"}); + + expect(authenticateSpy).toHaveBeenCalledWith({methodId: "api-key"}); + expect(getAccountSpy).toHaveBeenCalledTimes(4); + expect(codexAcpAgent.getSessionState(session1.sessionId)).toMatchObject({ + account: { type: "apiKey" }, + authConfigured: true, + }); + expect(codexAcpAgent.getSessionState(session2.sessionId)).toMatchObject({ + account: { type: "apiKey" }, + authConfigured: true, + }); + }); + + it('does not overwrite OpenAI session auth state when gateway auth succeeds', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const codexAcpClient = mockFixture.getCodexAcpClient(); + const model = createTestModel(); + const currentModelId = ModelId.create(model.id, model.defaultReasoningEffort).toString(); + + vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false); + vi.spyOn(codexAcpClient, "getModelProvider").mockReturnValue(null); + const getAccountSpy = vi.spyOn(codexAcpClient, "getAccount") + .mockResolvedValue({ + account: { type: "apiKey" }, + requiresOpenaiAuth: false, + }); + vi.spyOn(codexAcpClient, "newSession").mockResolvedValue({ + sessionId: "openai-session", + currentModelId, + models: [model], + modelProvider: "openai", + additionalDirectories: [], + }); + const authenticateSpy = vi.spyOn(codexAcpClient, "authenticate").mockResolvedValue(true); + + const session = await codexAcpAgent.newSession({cwd: "/workspace", mcpServers: []}); + expect(codexAcpAgent.getSessionState(session.sessionId)).toMatchObject({ + account: { type: "apiKey" }, + authConfigured: true, + authProvider: "openai", + }); + + const gatewayAuthRequest: CodexAuthRequest = { + methodId: "gateway", + _meta: { + "gateway": { + baseUrl: "https://www.example.com", + headers: { + "Custom-Auth-Header": "TOKEN", + }, + }, + }, + }; + await codexAcpAgent.authenticate(gatewayAuthRequest); + + expect(authenticateSpy).toHaveBeenCalledWith(gatewayAuthRequest); + expect(getAccountSpy).toHaveBeenCalledTimes(1); + expect(codexAcpAgent.getSessionState(session.sessionId)).toMatchObject({ + account: { type: "apiKey" }, + authConfigured: true, + authProvider: "openai", + }); + }); + + it('keeps custom provider sessions auth configured without account state', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const codexAcpClient = mockFixture.getCodexAcpClient(); + const model = createTestModel(); + const currentModelId = ModelId.create(model.id, model.defaultReasoningEffort).toString(); + + vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false); + vi.spyOn(codexAcpClient, "getModelProvider").mockReturnValue("custom-provider"); + const getAccountSpy = vi.spyOn(codexAcpClient, "getAccount") + .mockResolvedValue({ + account: null, + requiresOpenaiAuth: true, + }); + vi.spyOn(codexAcpClient, "newSession").mockResolvedValue({ + sessionId: "custom-provider-session", + currentModelId, + models: [model], + additionalDirectories: [], + }); + const logoutSpy = vi.spyOn(codexAcpClient, "logout").mockResolvedValue(); + + const session = await codexAcpAgent.newSession({cwd: "/workspace", mcpServers: []}); + expect(codexAcpAgent.getSessionState(session.sessionId)).toMatchObject({ + account: null, + authConfigured: true, + }); + + await codexAcpAgent.prompt({ + sessionId: session.sessionId, + prompt: [{ type: "text", text: "/logout" }], + }); + + expect(logoutSpy).toHaveBeenCalledOnce(); + expect(getAccountSpy).not.toHaveBeenCalled(); + expect(codexAcpAgent.getSessionState(session.sessionId)).toMatchObject({ + account: null, + authConfigured: true, + }); + }); + it('handles skills command', async () => { const codexAcpAgent = fixture.getCodexAcpAgent(); await codexAcpAgent.initialize({protocolVersion: 1}); diff --git a/src/__tests__/CodexACPAgent/auth-error-events.test.ts b/src/__tests__/CodexACPAgent/auth-error-events.test.ts new file mode 100644 index 00000000..f4917991 --- /dev/null +++ b/src/__tests__/CodexACPAgent/auth-error-events.test.ts @@ -0,0 +1,176 @@ +import { describe, expect, it, vi } from "vitest"; +import type { ErrorNotification, TurnCompletedNotification } from "../../app-server/v2"; +import type { SessionState } from "../../CodexAcpServer"; +import { + createCodexMockTestFixture, + createTestSessionState, +} from "../acp-test-utils"; + +const configuredAuthFailureCases: Array<{ + name: string; + turnError: ErrorNotification["error"]; + sessionOverrides?: Partial; + expectedData: unknown; +}> = [ + { + name: "rejected API key", + sessionOverrides: { + account: null, + authConfigured: true, + }, + turnError: { + message: "API key was rejected", + codexErrorInfo: "unauthorized", + additionalDetails: null, + }, + expectedData: { + message: "API key was rejected", + codexErrorInfo: "unauthorized", + }, + }, + { + name: "usage limit exceeded", + turnError: { + message: "Usage limits were exceeded", + codexErrorInfo: "usageLimitExceeded", + additionalDetails: null, + }, + expectedData: { + message: "Usage limits were exceeded", + codexErrorInfo: "usageLimitExceeded", + }, + }, + { + name: "HTTP 401", + turnError: { + message: "Provider returned 401", + codexErrorInfo: { + responseStreamDisconnected: { + httpStatusCode: 401, + }, + }, + additionalDetails: "HTTP status 401", + }, + expectedData: { + message: "HTTP status 401", + codexErrorInfo: { + responseStreamDisconnected: { + httpStatusCode: 401, + }, + }, + additionalDetails: "HTTP status 401", + }, + }, +]; + +describe("CodexEventHandler - auth error events", () => { + it("returns AuthRequired for auth errors when no auth is configured", async () => { + const error = await runPromptWithError(createTestSessionState({ + sessionId: "unauthenticated-session", + account: null, + authConfigured: false, + }), { + message: "Authentication is required", + codexErrorInfo: "unauthorized", + additionalDetails: null, + }); + + expect(error).toMatchObject({ + code: -32000, + message: "Authentication required: Authentication is required", + data: { + message: "Authentication is required", + codexErrorInfo: "unauthorized", + }, + }); + }); + + it.each(configuredAuthFailureCases)( + "returns InternalError with details for $name when auth is configured", + async ({turnError, sessionOverrides, expectedData}) => { + const error = await runPromptWithError(createTestSessionState({ + sessionId: "authenticated-session", + account: { type: "apiKey" }, + ...sessionOverrides, + }), turnError); + + expect(error).toMatchObject({ + code: -32603, + message: "Internal error", + data: expectedData, + }); + expect(error).not.toMatchObject({ + code: -32000, + }); + }, + ); +}); + +async function runPromptWithError( + sessionState: SessionState, + turnError: ErrorNotification["error"], +): Promise { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const turnCompleted = deferred(); + const turnStartSpy = vi.spyOn(codexAppServerClient, "turnStart").mockResolvedValue({ + turn: createTurn("inProgress"), + }); + vi.spyOn(codexAppServerClient, "awaitTurnCompleted").mockReturnValue(turnCompleted.promise); + vi.spyOn(codexAcpAgent, "getSessionState").mockReturnValue(sessionState); + + const promptPromise = codexAcpAgent.prompt({ + sessionId: sessionState.sessionId, + prompt: [{ type: "text", text: "test" }], + }); + + await vi.waitFor(() => { + expect(turnStartSpy).toHaveBeenCalled(); + }); + + mockFixture.sendServerNotification({ + method: "error", + params: { + threadId: sessionState.sessionId, + turnId: "turn-id", + willRetry: false, + error: turnError, + }, + }); + + turnCompleted.resolve({ + threadId: sessionState.sessionId, + turn: createTurn("completed"), + }); + + let caughtError: unknown; + try { + await promptPromise; + } catch (error) { + caughtError = error; + } + expect(caughtError).toBeDefined(); + return caughtError; +} + +function createTurn(status: "inProgress" | "completed") { + return { + id: "turn-id", + items: [], + itemsView: "notLoaded" as const, + status, + error: null, + startedAt: null, + completedAt: null, + durationMs: null, + }; +} + +function deferred(): {promise: Promise, resolve: (value: T) => void} { + let resolve: (value: T) => void = () => {}; + const promise = new Promise((innerResolve) => { + resolve = innerResolve; + }); + return {promise, resolve}; +} diff --git a/src/__tests__/CodexACPAgent/new-session-logout.test.ts b/src/__tests__/CodexACPAgent/new-session-logout.test.ts index 36a6f492..c8603f13 100644 --- a/src/__tests__/CodexACPAgent/new-session-logout.test.ts +++ b/src/__tests__/CodexACPAgent/new-session-logout.test.ts @@ -1,5 +1,6 @@ import {describe, expect, it, vi} from "vitest"; -import {createCodexMockTestFixture} from "../acp-test-utils"; +import {createCodexMockTestFixture, createTestModel} from "../acp-test-utils"; +import {ModelId} from "../../ModelId"; describe("New session logout handling", () => { it("logs out when newSession fails with an error containing log out", async () => { @@ -39,4 +40,61 @@ describe("New session logout handling", () => { }); expect(logoutSpy).toHaveBeenCalledOnce(); }); + + it("refreshes OpenAI sessions when newSession error forces logout", async () => { + const fixture = createCodexMockTestFixture(); + const codexAcpAgent = fixture.getCodexAcpAgent(); + const codexAcpClient = fixture.getCodexAcpClient(); + const model = createTestModel(); + const currentModelId = ModelId.create(model.id, model.defaultReasoningEffort).toString(); + vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false); + const getAccountSpy = vi.spyOn(codexAcpClient, "getAccount") + .mockResolvedValueOnce({ + account: { type: "apiKey" }, + requiresOpenaiAuth: false, + }) + .mockResolvedValueOnce({ + account: null, + requiresOpenaiAuth: true, + }); + + const errorMessage = `Internal error: "failed to reload config: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again."`; + vi.spyOn(codexAcpClient, "newSession") + .mockResolvedValueOnce({ + sessionId: "openai-session", + currentModelId, + models: [model], + modelProvider: "openai", + additionalDirectories: [], + }) + .mockResolvedValueOnce({ + sessionId: "custom-provider-session", + currentModelId, + models: [model], + modelProvider: "custom-provider", + additionalDirectories: [], + }) + .mockRejectedValueOnce(new Error(errorMessage)); + const logoutSpy = vi.spyOn(codexAcpClient, "logout").mockResolvedValue(); + + const openAiSession = await codexAcpAgent.newSession({cwd: "/workspace", mcpServers: []}); + const customProviderSession = await codexAcpAgent.newSession({cwd: "/workspace", mcpServers: []}); + + await expect(codexAcpAgent.newSession({cwd: "/workspace", mcpServers: []})) + .rejects.toMatchObject({ + data: expect.stringContaining("You have been logged out. Please try again."), + }); + + expect(logoutSpy).toHaveBeenCalledOnce(); + expect(getAccountSpy).toHaveBeenCalledTimes(2); + expect(codexAcpAgent.getSessionState(openAiSession.sessionId)).toMatchObject({ + account: null, + authConfigured: false, + }); + expect(codexAcpAgent.getSessionState(customProviderSession.sessionId)).toMatchObject({ + account: null, + authConfigured: true, + authProvider: "custom-provider", + }); + }); }); diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index 8e74751f..2f50d49d 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -357,6 +357,8 @@ export function createTestSessionState(overrides?: Partial): Sessi modelContextWindow: null, rateLimits: null, account: null, + authConfigured: overrides?.account !== undefined ? overrides.account !== null : false, + authProvider: null, cwd: "/test/cwd", additionalDirectories: [], sessionId: "session-id", From b719332ceb1306d9262edb76ea69bdb13ef017a0 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 26 Jun 2026 14:14:52 +0200 Subject: [PATCH 12/45] Update ACP SDK to 1.0.0 (#246) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fffe2d7..c00f2766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "^0.29.0", + "@agentclientprotocol/sdk": "^1.0.0", "@openai/codex": "^0.142.2", "diff": "^8.0.3", "open": "^11.0.0", @@ -29,9 +29,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.29.0.tgz", - "integrity": "sha512-Sb8xJ/cIhqlvxeJiB0+1scBk0LAkVAwJ9WAFG9XCe3LEI5JWAyOxbqaCa8O1HmNnrk4+qHN11eaKH6zL2iHIYA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.0.0.tgz", + "integrity": "sha512-5FHgLbLZhSAq9F+9SU7AOCXTDzaw/IYx/AvMjrZDs/YFZ9ZPxkdn9ti3fSkny0huHfj2hrqF6pqdns9gN5/Fig==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" diff --git a/package.json b/package.json index fd3db924..7f648292 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "vitest": "^4.0.10" }, "dependencies": { - "@agentclientprotocol/sdk": "^0.29.0", + "@agentclientprotocol/sdk": "^1.0.0", "@openai/codex": "^0.142.2", "diff": "^8.0.3", "open": "^11.0.0", From 373c42d548316ff76be68061f9c0a9e121e01bcc Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 26 Jun 2026 14:21:08 +0200 Subject: [PATCH 13/45] fix: Skip ChatGPT login when already authenticated (#247) Closes #243 --- src/CodexAcpClient.ts | 5 +++++ .../CodexACPAgent/CodexAcpClient.test.ts | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index ed8bdb18..16386b69 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -88,6 +88,11 @@ export class CodexAcpClient { return await this.authenticateWithApiKey(apiKey); } case "chat-gpt": { + const accountResponse = await this.codexClient.accountRead({refreshToken: true}); + if (accountResponse.account?.type === "chatgpt") { + this.gatewayConfig = null; + return true; + } const loginCompletedPromise = this.awaitNextLoginCompleted(); const loginResponse = await this.codexClient.accountLogin({type: "chatgpt"}); if (loginResponse.type == "chatgpt") { diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 54436c6a..4ec12468 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -186,6 +186,22 @@ describe('ACP server test', { timeout: 40_000 }, () => { .rejects.toThrow(`${CODEX_API_KEY_ENV_VAR} or ${OPENAI_API_KEY_ENV_VAR} is not set`); }); + it('should not start ChatGPT login when already authenticated', async () => { + const chatGptFixture = createCodexMockTestFixture(); + const codexAppServerClient = chatGptFixture.getCodexAppServerClient(); + const accountReadSpy = vi.spyOn(codexAppServerClient, "accountRead").mockResolvedValue({ + account: { type: "chatgpt", email: "test@example.com", planType: "pro" }, + requiresOpenaiAuth: false, + }); + const accountLoginSpy = vi.spyOn(codexAppServerClient, "accountLogin"); + + await expect(chatGptFixture.getCodexAcpAgent().authenticate({methodId: "chat-gpt"})) + .resolves.toEqual({}); + + expect(accountReadSpy).toHaveBeenCalledWith({refreshToken: true}); + expect(accountLoginSpy).not.toHaveBeenCalled(); + }); + it('should authenticate with a gateway', async () => { const gatewayFixture = createTestFixture(); const codexAcpAgent = gatewayFixture.getCodexAcpAgent(); From ebc65b5bfc0c8e189ed09f6fb51ffcebacd9c154 Mon Sep 17 00:00:00 2001 From: scottzx <59874469+scottzx@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:40:38 +0800 Subject: [PATCH 14/45] Keep configured model id for custom providers instead of forcing the default (#199) * fix: keep configured model id for custom providers instead of forcing the default createModelId() looked up the configured/returned model id in Codex's listModels() catalog and, when it was absent, substituted availableModels.find(m => m.isDefault). Custom providers (self-hosted or third-party endpoints) expose model ids that the catalog does not enumerate, so this silently replaced the user's configured model with the built-in default. That default id was then pinned onto every runTurn, making requests to the custom endpoint fail with "unknown model ''". The Codex CLI handles the same case by keeping the configured model id and warning "Model metadata not found. Defaulting to fallback metadata." This change mirrors that behavior: keep the requested model id when it is not in the catalog, and only fall back to isDefault when no model id was provided at all. Downstream session state already degrades gracefully for unknown models (supportedReasoningEfforts ?? [], inputModalities ?? ["text","image"]). Adds unit tests covering an uncatalogued model id with and without an explicit reasoning effort. * Handle uncataloged current models in config --------- Co-authored-by: Ben Brandt --- src/CodexAcpClient.ts | 27 +++++++++++++---- src/CodexAcpServer.ts | 15 ++++++++-- src/ModelConfigOption.ts | 19 ++++++++---- .../CodexACPAgent/CodexAcpClient.test.ts | 10 +++++++ .../session-config-options.test.ts | 29 +++++++++++++++++++ 5 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 16386b69..6f43c0c8 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -446,15 +446,30 @@ export class CodexAcpClient { * Falls back to model defaults if parameters are missing or unsupported. */ createModelId(availableModels: Model[], modelId: string | null, reasoningEffort: ReasoningEffort | null): ModelId { - const selectedModel = - availableModels.find(m => m.id === modelId) ?? - availableModels.find(m => m.isDefault); - - if (!selectedModel) { + const selectedModel = availableModels.find(m => m.id === modelId); + if (selectedModel) { + return ModelId.create(selectedModel.id, reasoningEffort ?? selectedModel.defaultReasoningEffort); + } + + // The configured model is not in Codex's advertised catalog. This is + // expected for custom providers (e.g. a self-hosted or third-party + // model), whose model ids the catalog does not enumerate. Keep the + // requested model id instead of silently substituting the built-in + // default. This mirrors the Codex CLI, which keeps the configured model + // and merely warns "Model metadata not found. Defaulting to fallback + // metadata." Substituting the default here pins a wrong model id onto + // every turn and makes requests to custom-provider endpoints fail with + // "unknown model". + if (modelId) { + return ModelId.create(modelId, reasoningEffort ?? "medium"); + } + + const defaultModel = availableModels.find(m => m.isDefault); + if (!defaultModel) { throw new Error(`Model selection failed: No model found for ID "${modelId}" and no default model is defined.`); } - return ModelId.create(selectedModel.id, reasoningEffort ?? selectedModel.defaultReasoningEffort); + return ModelId.create(defaultModel.id, reasoningEffort ?? defaultModel.defaultReasoningEffort); } async subscribeToSessionEvents( diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 720ff95f..f5d628a0 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -694,6 +694,10 @@ export class CodexAcpServer { private applyModelChange(sessionState: SessionState, value: string): void { const model = sessionState.availableModels.find(m => m.id === value); if (!model) { + const currentModel = ModelId.fromString(sessionState.currentModelId).model; + if (value === currentModel) { + return; + } throw RequestError.invalidParams(); } const currentEffort = ModelId.fromString(sessionState.currentModelId).effort; @@ -763,12 +767,17 @@ export class CodexAcpServer { private createSessionConfigOptions(sessionState: SessionState): Array { const currentModelId = ModelId.fromString(sessionState.currentModelId); - return [ + const configOptions = [ sessionState.agentMode.toConfigOption(), createModelConfigOption(sessionState.availableModels, currentModelId.model), - createReasoningEffortConfigOption(sessionState.supportedReasoningEfforts, currentModelId.effort), - createFastModeConfigOption(sessionState.fastModeEnabled), ]; + if (sessionState.supportedReasoningEfforts.length > 0) { + configOptions.push( + createReasoningEffortConfigOption(sessionState.supportedReasoningEfforts, currentModelId.effort), + ); + } + configOptions.push(createFastModeConfigOption(sessionState.fastModeEnabled)); + return configOptions; } private createSessionConfigOptionsResponse(sessionState: SessionState): { diff --git a/src/ModelConfigOption.ts b/src/ModelConfigOption.ts index 00bb5153..035b59fb 100644 --- a/src/ModelConfigOption.ts +++ b/src/ModelConfigOption.ts @@ -14,6 +14,19 @@ export function findSupportedEffort( } export function createModelConfigOption(availableModels: Array, currentBaseModelId: string): SessionConfigOption { + const options: Array<{ value: string; name: string; description: string | null }> = availableModels.map(model => ({ + value: model.id, + name: model.displayName, + description: model.description, + })); + if (!availableModels.some(model => model.id === currentBaseModelId)) { + options.unshift({ + value: currentBaseModelId, + name: currentBaseModelId, + description: null, + }); + } + return { id: MODEL_CONFIG_ID, name: "Model", @@ -21,11 +34,7 @@ export function createModelConfigOption(availableModels: Array, currentBa category: "model", type: "select", currentValue: currentBaseModelId, - options: availableModels.map(model => ({ - value: model.id, - name: model.displayName, - description: model.description, - })), + options, }; } diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 4ec12468..dd68685c 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1840,6 +1840,16 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(result).toEqual(ModelId.create('5.2-codex', 'medium')); }); + it('should keep a model id that is not in the advertised catalog (custom provider)', () => { + const result = fixture.getCodexAcpClient().createModelId(mockModels, 'MiniMax-M3', 'high'); + expect(result).toEqual(ModelId.create('MiniMax-M3', 'high')); + }); + + it('should default the effort for an uncatalogued model when reasoningEffort is null', () => { + const result = fixture.getCodexAcpClient().createModelId(mockModels, 'MiniMax-M3', null); + expect(result).toEqual(ModelId.create('MiniMax-M3', 'medium')); + }); + /** * Sets up a mock fixture with turnStart/awaitTurnCompleted spied on, * and a given session state. Returns the fixture and turnStart spy. diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index bf8dc5cd..391cc043 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -90,6 +90,35 @@ describe("Session config options", () => { ); }); + it("shows the current uncataloged model as its own selectable option", async () => { + const {fast, slow} = buildModels(); + const {codexAcpAgent, response} = await createSession("custom-model[high]", [fast, slow]); + + const ids = response.configOptions?.map(o => o.id); + expect(ids).toEqual([MODE_CONFIG_ID, MODEL_CONFIG_ID, "fast-mode"]); + + const modelOption = response.configOptions?.find(o => o.id === MODEL_CONFIG_ID); + expect(modelOption).toMatchObject({ + category: "model", + currentValue: "custom-model", + type: "select", + options: [ + {value: "custom-model", name: "custom-model", description: null}, + {value: "fast-model", name: "Fast model", description: "Frontier"}, + {value: "slow-model", name: "Slow model", description: "Strong"}, + ], + }); + expect(response.configOptions?.some(o => o.id === REASONING_EFFORT_CONFIG_ID)).toBe(false); + + await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: MODEL_CONFIG_ID, + value: "custom-model", + }); + + expect(codexAcpAgent.getSessionState("session-id").currentModelId).toBe("custom-model[high]"); + }); + it("keeps the legacy models list as combined model/effort entries", async () => { const {fast, slow} = buildModels(); const {response} = await createSession("fast-model[medium]", [fast, slow]); From 956f71a9bf7f8b9080f8267ddf856549dab2baa3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Jun 2026 12:49:39 +0000 Subject: [PATCH 15/45] Release v1.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c00f2766..30d55944 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentclientprotocol/codex-acp", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentclientprotocol/codex-acp", - "version": "1.0.0", + "version": "1.0.1", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.0.0", diff --git a/package.json b/package.json index 7f648292..bf9bb733 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.0.0", + "version": "1.0.1", "description": "", "main": "dist/index.js", "bin": { From b861bcaed22b232006be9723f9a4b60f2a14da08 Mon Sep 17 00:00:00 2001 From: "acp-release-bot[bot]" <246668977+acp-release-bot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:14:30 +0300 Subject: [PATCH 16/45] Update codex to 0.142.3 (#249) * Update codex to 0.142.3 * Fix types and tests after Codex update --------- Co-authored-by: github-actions[bot] --- package-lock.json | 56 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30d55944..b61252c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.0.0", - "@openai/codex": "^0.142.2", + "@openai/codex": "^0.142.3", "diff": "^8.0.3", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", @@ -872,9 +872,9 @@ } }, "node_modules/@openai/codex": { - "version": "0.142.2", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2.tgz", - "integrity": "sha512-/89NO3WUTU3WmavBHuVyJzfGXasjT8DtrT6kU5VxAiR4pkZj4M0c4VHmSvzqnmpqsODsy2THHKbiiNBi/d2jaQ==", + "version": "0.142.3", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3.tgz", + "integrity": "sha512-uOS7gTFHtN6cQGILd18hc+mMSqf9tXEZpKV4n4SqyEmPlomNtUpKFpTvYtzIqnRLG1hrZROP2Z8sL+o+PNJrxg==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -883,19 +883,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.2-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.2-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.2-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.142.2-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.2-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.142.2-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.3-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.3-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.3-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.142.3-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.3-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.142.3-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.142.2-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-darwin-arm64.tgz", - "integrity": "sha512-NtbEXS5nvfT6FNoQSzS0/rCRfG5cNWgVZLxArCFCCXm+Z5vOz3WSVX041ChxIl+dKwRpU6HMpSd9tR5oPBBWZA==", + "version": "0.142.3-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-darwin-arm64.tgz", + "integrity": "sha512-/ctHm6s2vpUuFY0D212BbzretO0hi1FqUioD1eUoK/qcfG4nhVl3710m5GdtadYQLJbNmdXFfAWrBIQbG/aB0A==", "cpu": [ "arm64" ], @@ -910,9 +910,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.142.2-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-darwin-x64.tgz", - "integrity": "sha512-9IjBzTlvXbr+SDDWarM/d0C6C1addJV03gLzmqbU7ptLZBR2H/Hoxig924r04kEAKxo0nmdu9e+x3IMrIxoTtg==", + "version": "0.142.3-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-darwin-x64.tgz", + "integrity": "sha512-TVBDgkcAmQtTM2atJYeJpGjOOCerczZcwFOUF9aOQVTmMxgz4E0tuR2jqcR648FAtE81r5iNh2EGFN+Fte8ayA==", "cpu": [ "x64" ], @@ -927,9 +927,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.142.2-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-linux-arm64.tgz", - "integrity": "sha512-iOZDjULUawtPd7FFfDujhcj7f9DS8CyT4Be9cFAnGKvUFlAjeEa3/dgj4lQ5lwhzgQchoTduXS4NuAWDXNqp/Q==", + "version": "0.142.3-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-linux-arm64.tgz", + "integrity": "sha512-eB4gtmnwMJSt07YP1Xl19U0zqH4aDcZNC3w9dkzMxbn0NJrkAmH8uB8KLSmVez7C/28QP8E9aaNy+EeZQfaxVA==", "cpu": [ "arm64" ], @@ -944,9 +944,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.142.2-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-linux-x64.tgz", - "integrity": "sha512-96zStaXK8+NjKrytlxbGJ33MeMvU0SALFarYVgHsFwrTCeW8g3xsUaUDIv8YYRipyzQIWoHczLRYRJPSH4i6QA==", + "version": "0.142.3-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-linux-x64.tgz", + "integrity": "sha512-EQ5svml7q8lVm9vyCGYyR83OgkF995xSrlLq5IwgIT1t+TGhmLajo0mHjmQTahIW8gSmGMsRZTTI+p5FjRPrVA==", "cpu": [ "x64" ], @@ -961,9 +961,9 @@ }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.142.2-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-win32-arm64.tgz", - "integrity": "sha512-FZBlDmx71tvuadbhXMIte2qZiQ3coSYCi+j1CcZQ0oVkhgb/CTZX6KsIfJtvjGR4fty6mbDWdY7kEOd9iHUjzg==", + "version": "0.142.3-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-win32-arm64.tgz", + "integrity": "sha512-An0/fDUanbMFXRWD27gKgLM0aYIztrlHdNrLX6SDWXOkoj8dxOCfW1Y/6ToZNI7TxxLQMN2CYXLKW3y+hI9MIQ==", "cpu": [ "arm64" ], @@ -978,9 +978,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.142.2-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.2-win32-x64.tgz", - "integrity": "sha512-EZOCKvcBNCpipuffm82MHrPhnzs502mo5IoFvXbm1TOO87pH3x5KfRzUO5Wjf78FEelXzRe0j/kPzm+Xw1whLg==", + "version": "0.142.3-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-win32-x64.tgz", + "integrity": "sha512-Oj9QroSPpvHG5sD84alPBd3J/MLtnumm/01n+xbglXtph24pLLyONLp2SAB4tARJzROlZ1Vf3WsrNpcU1iGXWA==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index bf9bb733..8d72b30d 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^1.0.0", - "@openai/codex": "^0.142.2", + "@openai/codex": "^0.142.3", "diff": "^8.0.3", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", From 50c6adfd79a482a942f190dc93fc2b53c99cbf2d Mon Sep 17 00:00:00 2001 From: "acp-release-bot[bot]" <246668977+acp-release-bot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:25:34 +0300 Subject: [PATCH 17/45] Update codex to 0.142.4 (#253) * Update codex to 0.142.4 * Fix types and tests after Codex update --------- Co-authored-by: github-actions[bot] --- package-lock.json | 56 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index b61252c4..329fe14d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.0.0", - "@openai/codex": "^0.142.3", + "@openai/codex": "^0.142.4", "diff": "^8.0.3", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", @@ -872,9 +872,9 @@ } }, "node_modules/@openai/codex": { - "version": "0.142.3", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3.tgz", - "integrity": "sha512-uOS7gTFHtN6cQGILd18hc+mMSqf9tXEZpKV4n4SqyEmPlomNtUpKFpTvYtzIqnRLG1hrZROP2Z8sL+o+PNJrxg==", + "version": "0.142.4", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4.tgz", + "integrity": "sha512-BW9+3Cs7tChgkc0QhUsZIDm6XgpSDSeJkljRSHhJZqDwlaxDnNVhRP/sZwhgrTn3f2jPn3jc4WTKfLOTk+g5pA==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -883,19 +883,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.3-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.3-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.3-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.142.3-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.3-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.142.3-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.4-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.4-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.4-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.142.4-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.4-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.142.4-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.142.3-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-darwin-arm64.tgz", - "integrity": "sha512-/ctHm6s2vpUuFY0D212BbzretO0hi1FqUioD1eUoK/qcfG4nhVl3710m5GdtadYQLJbNmdXFfAWrBIQbG/aB0A==", + "version": "0.142.4-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-darwin-arm64.tgz", + "integrity": "sha512-CfXp5jilBOxD8zLuG6Nk7w5ZPP3roE3s1rsr60za9PQKI7kaJ/5G+oP3Z0N1CoB4mpy+5JHkMLTZAZZPGFlSgQ==", "cpu": [ "arm64" ], @@ -910,9 +910,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.142.3-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-darwin-x64.tgz", - "integrity": "sha512-TVBDgkcAmQtTM2atJYeJpGjOOCerczZcwFOUF9aOQVTmMxgz4E0tuR2jqcR648FAtE81r5iNh2EGFN+Fte8ayA==", + "version": "0.142.4-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-darwin-x64.tgz", + "integrity": "sha512-H9ia31pkJFtBk+UrREAfJiTUfcvgactKbG4W4SF2LFfJOTRsrsJZGtscHQ8/lLGeKEfOk/psrbSOPDyrZnxvnQ==", "cpu": [ "x64" ], @@ -927,9 +927,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.142.3-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-linux-arm64.tgz", - "integrity": "sha512-eB4gtmnwMJSt07YP1Xl19U0zqH4aDcZNC3w9dkzMxbn0NJrkAmH8uB8KLSmVez7C/28QP8E9aaNy+EeZQfaxVA==", + "version": "0.142.4-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-linux-arm64.tgz", + "integrity": "sha512-/bBcEm+OKmlGHH0c88jrVgYMRyaNC/hGOW/znP/PiHQEeIf4UdsUGQ7x4FAZm648140cgaeKYIBoRBbHizS+5w==", "cpu": [ "arm64" ], @@ -944,9 +944,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.142.3-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-linux-x64.tgz", - "integrity": "sha512-EQ5svml7q8lVm9vyCGYyR83OgkF995xSrlLq5IwgIT1t+TGhmLajo0mHjmQTahIW8gSmGMsRZTTI+p5FjRPrVA==", + "version": "0.142.4-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-linux-x64.tgz", + "integrity": "sha512-t3tNOkDdiO5/gRqkTmuMcAj5JjCbeWotOJzxV7pPkD2u2KvzVRrk7jPAjuy+85wPc12lsIupYgxHlK7CkVLOjA==", "cpu": [ "x64" ], @@ -961,9 +961,9 @@ }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.142.3-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-win32-arm64.tgz", - "integrity": "sha512-An0/fDUanbMFXRWD27gKgLM0aYIztrlHdNrLX6SDWXOkoj8dxOCfW1Y/6ToZNI7TxxLQMN2CYXLKW3y+hI9MIQ==", + "version": "0.142.4-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-win32-arm64.tgz", + "integrity": "sha512-QBGeoYk3pmVscVIxeW21cvPJIjsnmD5dOTqMAzypXGik5soNIKHbps2Kj1LBKOjVbbx5qoDEbTwDfYmnjfqgdQ==", "cpu": [ "arm64" ], @@ -978,9 +978,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.142.3-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.3-win32-x64.tgz", - "integrity": "sha512-Oj9QroSPpvHG5sD84alPBd3J/MLtnumm/01n+xbglXtph24pLLyONLp2SAB4tARJzROlZ1Vf3WsrNpcU1iGXWA==", + "version": "0.142.4-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-win32-x64.tgz", + "integrity": "sha512-W0rSPT0t7FnKlJYNu9/HpHzaSOutuSSYjIVunKorYmrScXk9pQxBEhEuyn16JOtywc/2nX8nHEmWsuj2sd8suw==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 8d72b30d..f2ad46e0 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^1.0.0", - "@openai/codex": "^0.142.3", + "@openai/codex": "^0.142.4", "diff": "^8.0.3", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", From 211b7d75e2185c823ca4e8a6069953bcc635d6f2 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 29 Jun 2026 16:00:04 +0200 Subject: [PATCH 18/45] feat: Add /goal support (#240) * feat: Add /goal support * Suppress multiple identical notifications * Wait on turns * Wait for goal turn * Add newlines after notifications * Remove unsupported edit subcommand * Handle goal commands without continuation turns * Separate goal status updates from prior text * Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Interrupt late-started goal commands on cancel * Suppress stale turn notifications after routing them * Wait for thread idle when no goal turn starts * Handle cancellation before goal turn routing * Wait for goal updates before finishing goal set * Add grace periods for goal turn routing * Wait for goal notifications in ACP client * Wait for goal turn completion before resolving * Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- src/CodexAcpClient.ts | 34 + src/CodexAcpServer.ts | 27 +- src/CodexAppServerClient.ts | 380 +++++- src/CodexCommands.ts | 90 +- src/CodexEventHandler.ts | 42 +- .../CodexACPAgent/CodexAcpClient.test.ts | 1100 ++++++++++++++++- .../data/available-commands-build-in.json | 7 + .../data/available-commands-skills.json | 7 + .../data/load-session-history.json | 7 + ...ession-response-item-history-fallback.json | 7 + .../data/thread-goal-cleared.json | 2 +- .../data/thread-goal-updated-multiline.json | 2 +- .../data/thread-goal-updated.json | 2 +- .../CodexACPAgent/thread-goal-events.test.ts | 116 +- 14 files changed, 1787 insertions(+), 36 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 6f43c0c8..2d5dbd64 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -33,6 +33,7 @@ import type { SkillsListResponse, SandboxPolicy, Thread, + ThreadGoalStatus, ThreadSourceKind, TurnCompletedNotification, UserInput, @@ -334,6 +335,39 @@ export class CodexAcpClient { await this.codexClient.runCompact({threadId: sessionId}); } + async setGoal( + sessionId: string, + objective: string, + onTurnStarted?: (turnId: string) => void, + ): Promise { + return await this.codexClient.runGoalSet({ + threadId: sessionId, + objective, + status: "active", + }, onTurnStarted); + } + + async setGoalStatus(sessionId: string, status: ThreadGoalStatus): Promise { + await this.codexClient.runGoalSet({ + threadId: sessionId, + status, + }); + } + + async resumeGoal( + sessionId: string, + onTurnStarted?: (turnId: string) => void, + ): Promise { + return await this.codexClient.runGoalSet({ + threadId: sessionId, + status: "active", + }, onTurnStarted); + } + + async clearGoal(sessionId: string): Promise { + await this.codexClient.runGoalClear({threadId: sessionId}); + } + async awaitMcpServerStartup(serverNames: Array, afterVersion: number): Promise { return await this.codexClient.awaitMcpServerStartup(serverNames, afterVersion); } diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index f5d628a0..e80280e2 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -14,6 +14,7 @@ import type { ReasoningEffortOption, Thread, ThreadItem, + TurnCompletedNotification, UserInput } from "./app-server/v2"; import type {RateLimitsMap} from "./RateLimitsMap"; @@ -66,6 +67,12 @@ import packageJson from "../package.json"; import {isJetBrains2026_1Client} from "./JBUtils"; import {resolveTerminalOutputMode, type TerminalOutputMode} from "./TerminalOutputMode"; +export interface ThreadGoalSnapshot { + objective: string; + status: string; + tokenBudget: number | null; +} + export interface SessionState { sessionId: string, currentModelId: string, @@ -87,6 +94,7 @@ export interface SessionState { currentModelSupportsFast: boolean; sessionMcpServers?: Array; terminalOutputMode: TerminalOutputMode; + currentGoal?: ThreadGoalSnapshot | null; } interface ActiveAuthState { @@ -1398,6 +1406,13 @@ export class CodexAcpServer { sessionState.lastTokenUsage = null; const activePrompt = this.trackActivePrompt(params.sessionId); let pendingTurnStart: PendingTurnStart | null = null; + const ensurePendingTurnStart = (): PendingTurnStart => { + if (pendingTurnStart === null) { + pendingTurnStart = this.createPendingTurnStart(); + this.pendingTurnStarts.set(params.sessionId, pendingTurnStart); + } + return pendingTurnStart; + }; const disposePromptRequestCancellation = this.observePromptRequestCancellation(signal, sessionState, activePrompt); try { @@ -1417,6 +1432,9 @@ export class CodexAcpServer { } const commandPromise = this.availableCommands.tryHandleCommand(params.prompt, sessionState, { + onTurnStartPending: () => { + ensurePendingTurnStart(); + }, onTurnStarted: (turnId, threadId) => { const turn = {threadId, turnId}; activePrompt.currentTurn = turn; @@ -1425,6 +1443,7 @@ export class CodexAcpServer { return; } sessionState.currentTurnId = turnId; + pendingTurnStart?.resolve(turnId); }, }); void commandPromise.catch((err) => { @@ -1483,8 +1502,7 @@ export class CodexAcpServer { sessionState.fastModeEnabled, sessionState.currentModelSupportsFast, ); - pendingTurnStart = this.createPendingTurnStart(); - this.pendingTurnStarts.set(params.sessionId, pendingTurnStart); + ensurePendingTurnStart(); const sendPromptPromise = this.runWithProcessCheck( () => this.codexAcpClient.sendPrompt( params, @@ -1546,10 +1564,11 @@ export class CodexAcpServer { logger.log("Prompt completed", {sessionId: params.sessionId}); disposePromptRequestCancellation(); sessionState.currentTurnId = null; - if (pendingTurnStart !== null && this.pendingTurnStarts.get(params.sessionId) === pendingTurnStart) { + const registeredPendingTurnStart = this.pendingTurnStarts.get(params.sessionId); + if (registeredPendingTurnStart !== undefined) { this.pendingTurnStarts.delete(params.sessionId); + registeredPendingTurnStart.resolve(null); } - pendingTurnStart?.resolve(null); activePrompt.complete(); } } diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index 6a5a0b51..d1bc210c 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -26,10 +26,19 @@ import type { SkillsExtraRootsSetParams, SkillsListParams, SkillsListResponse, + ThreadGoal, + ThreadGoalUpdatedNotification, + ThreadStatus, + ThreadStatusChangedNotification, ThreadArchiveParams, ThreadArchiveResponse, ThreadCompactStartParams, ThreadCompactStartResponse, + ThreadGoalClearedNotification, + ThreadGoalClearParams, + ThreadGoalClearResponse, + ThreadGoalSetParams, + ThreadGoalSetResponse, ThreadLoadedListParams, ThreadLoadedListResponse, ThreadListParams, @@ -101,6 +110,8 @@ const McpServerElicitationRequest = new RequestType< void >('mcpServer/elicitation/request'); +const GOAL_RUNTIME_EFFECTS_GRACE_MS = 1_000; + /** * A type-safe client over the Codex App Server's JSON-RPC API. * Maps each request to its expected response and exposes clear, typed methods for supported JSON-RPC operations. @@ -115,6 +126,10 @@ export class CodexAppServerClient { private readonly pendingTurnCompletionResolvers = new Map void>>(); private readonly pendingCompactionCompletionResolvers = new Map void>>(); private readonly turnCompletionCaptures = new Map void>>(); + private readonly turnRoutingCaptures = new Map void>>(); + private readonly threadStatusCaptures = new Map void>>(); + private readonly threadGoalUpdateCaptures = new Map void>>(); + private readonly threadGoalClearedCaptures = new Map void>>(); private readonly staleTurnIds = new Map>(); constructor(connection: MessageConnection) { @@ -136,15 +151,21 @@ export class CodexAppServerClient { if (isCompactionCompletedNotification(serverNotification)) { this.recordCompactionCompleted(serverNotification); } + if (isThreadStatusChangedNotification(serverNotification)) { + this.recordThreadStatusChanged(serverNotification.params); + } + if (isThreadGoalUpdatedNotification(serverNotification)) { + this.recordThreadGoalUpdated(serverNotification.params); + } + if (isThreadGoalClearedNotification(serverNotification)) { + this.recordThreadGoalCleared(serverNotification.params); + } const routing = extractTurnRouting(serverNotification); - const staleTurnNotification = this.isStaleTurn(routing.threadId, routing.turnId); - if (staleTurnNotification) { - if (isTurnCompletedNotification(serverNotification) && routing.threadId !== null && routing.turnId !== null) { - this.clearStaleTurn(routing.threadId, routing.turnId); - } - for (const callback of this.codexEventHandlers) { - callback({ eventType: "notification", ...serverNotification }); - } + if (this.handleStaleTurnNotification(serverNotification, routing)) { + return; + } + this.recordTurnRouting(routing); + if (this.handleStaleTurnNotification(serverNotification, routing)) { return; } this.notify(serverNotification); @@ -265,6 +286,185 @@ export class CodexAppServerClient { } } + async runGoalSet( + params: ThreadGoalSetParams, + onTurnStarted?: (turnId: string) => void, + runtimeEffectsGraceMs = GOAL_RUNTIME_EFFECTS_GRACE_MS, + ): Promise { + let goalTurnId: string | null = null; + const capturedCompletions: Array = []; + let resolveGoalTurnCompleted: (event: TurnCompletedNotification) => void = () => {}; + const goalTurnCompleted = new Promise((resolve) => { + resolveGoalTurnCompleted = resolve; + }); + const releaseCompletionCapture = this.captureTurnCompletions(params.threadId, (event) => { + capturedCompletions.push(event); + if (goalTurnId === event.turn.id) { + resolveGoalTurnCompleted(event); + } + }); + let resolveGoalTurnStarted: (turnId: string) => void = () => {}; + const goalTurnStarted = new Promise((resolve) => { + resolveGoalTurnStarted = resolve; + }); + let resolveGoalUpdateHandled: () => void = () => {}; + const matchingGoalUpdateHandled = new Promise((resolve) => { + resolveGoalUpdateHandled = () => resolve(null); + }); + let goalUpdateHandled = false; + let expectedGoal: ThreadGoal | null = null; + const noGoalTurnStarted = this.createNoGoalTurnStartedPromise(runtimeEffectsGraceMs); + const capturedGoalUpdates: Array = []; + const releaseRoutingCapture = this.captureTurnRoutings(params.threadId, (turnId) => { + if (!goalUpdateHandled || goalTurnId !== null) { + return; + } + goalTurnId = turnId; + onTurnStarted?.(turnId); + resolveGoalTurnStarted(turnId); + }); + const releaseGoalUpdateCapture = this.captureThreadGoalUpdates(params.threadId, (event) => { + capturedGoalUpdates.push(event); + if (expectedGoal !== null && goalsMatch(event.goal, expectedGoal)) { + goalUpdateHandled = true; + resolveGoalUpdateHandled(); + noGoalTurnStarted.goalUpdated(); + } + }); + const releaseStatusCapture = this.captureThreadStatuses(params.threadId, (status) => { + if (!goalUpdateHandled || goalTurnId !== null) { + return; + } + noGoalTurnStarted.threadStatusChanged(status); + }); + + try { + const goalSetResponse = await this.threadGoalSet(params); + expectedGoal = goalSetResponse.goal; + if (capturedGoalUpdates.some(event => goalsMatch(event.goal, expectedGoal!))) { + goalUpdateHandled = true; + resolveGoalUpdateHandled(); + noGoalTurnStarted.goalUpdated(); + } + if (expectedGoal.status !== "active") { + await matchingGoalUpdateHandled; + return null; + } + const turnId = goalTurnId ?? await Promise.race([goalTurnStarted, noGoalTurnStarted.promise]); + noGoalTurnStarted.release(); + releaseRoutingCapture(); + releaseStatusCapture(); + releaseGoalUpdateCapture(); + if (turnId === null) { + return null; + } + const earlyCompletion = capturedCompletions.find(event => event.turn.id === turnId); + if (earlyCompletion) { + return earlyCompletion; + } + return await goalTurnCompleted; + } finally { + noGoalTurnStarted.release(); + releaseCompletionCapture(); + releaseRoutingCapture(); + releaseStatusCapture(); + releaseGoalUpdateCapture(); + } + } + + async runGoalClear(params: ThreadGoalClearParams): Promise { + let goalClearedHandled = false; + let resolveGoalClearedHandled: () => void = () => {}; + const matchingGoalClearedHandled = new Promise((resolve) => { + resolveGoalClearedHandled = () => resolve(); + }); + const releaseGoalClearedCapture = this.captureThreadGoalClears(params.threadId, () => { + goalClearedHandled = true; + resolveGoalClearedHandled(); + }); + + try { + const response = await this.threadGoalClear(params); + if (!response.cleared || goalClearedHandled) { + return; + } + await matchingGoalClearedHandled; + } finally { + releaseGoalClearedCapture(); + } + } + + private createNoGoalTurnStartedPromise( + runtimeEffectsGraceMs: number, + ): { + promise: Promise, + release: () => void, + goalUpdated: () => void, + threadStatusChanged: (status: ThreadStatus) => void, + } { + let released = false; + let resolved = false; + let goalUpdated = false; + let activeAfterGoalUpdate = false; + let timeout: ReturnType | null = null; + let resolveNoGoalTurnStarted: () => void = () => {}; + const clearTimer = () => { + if (timeout !== null) { + clearTimeout(timeout); + timeout = null; + } + }; + const resolveNoTurn = () => { + if (released || resolved) { + return; + } + resolved = true; + clearTimer(); + resolveNoGoalTurnStarted(); + }; + const scheduleNoTurnTimer = () => { + if (released || resolved || !goalUpdated || activeAfterGoalUpdate || timeout !== null) { + return; + } + timeout = setTimeout(resolveNoTurn, runtimeEffectsGraceMs); + }; + const release = () => { + if (released) { + return; + } + released = true; + clearTimer(); + }; + const promise = new Promise((resolve) => { + resolveNoGoalTurnStarted = () => { + resolve(null); + }; + }); + const handleGoalUpdated = () => { + goalUpdated = true; + scheduleNoTurnTimer(); + }; + const handleThreadStatusChanged = (status: ThreadStatus) => { + if (!goalUpdated || released || resolved) { + return; + } + if (status.type === "active") { + activeAfterGoalUpdate = true; + clearTimer(); + return; + } + if (activeAfterGoalUpdate) { + resolveNoTurn(); + } + }; + return { + promise, + release, + goalUpdated: handleGoalUpdated, + threadStatusChanged: handleThreadStatusChanged, + }; + } + async runCompact(params: ThreadCompactStartParams): Promise { const compactionCompleted = this.awaitCompactionCompleted(params.threadId); await this.threadCompactStart(params); @@ -317,6 +517,14 @@ export class CodexAppServerClient { return await this.sendRequest({ method: "thread/compact/start", params: params }); } + async threadGoalSet(params: ThreadGoalSetParams): Promise { + return await this.sendRequest({ method: "thread/goal/set", params: params }); + } + + async threadGoalClear(params: ThreadGoalClearParams): Promise { + return await this.sendRequest({ method: "thread/goal/clear", params: params }); + } + async listMcpServerStatus(params: ListMcpServerStatusParams): Promise { return await this.sendRequest({ method: "mcpServerStatus/list", params }); } @@ -469,6 +677,65 @@ export class CodexAppServerClient { } } + private recordThreadStatusChanged(event: ThreadStatusChangedNotification): void { + const captures = this.threadStatusCaptures.get(event.threadId); + if (!captures) { + return; + } + for (const capture of captures) { + capture(event.status); + } + } + + private recordThreadGoalUpdated(event: ThreadGoalUpdatedNotification): void { + const captures = this.threadGoalUpdateCaptures.get(event.threadId); + if (!captures) { + return; + } + for (const capture of captures) { + capture(event); + } + } + + private recordThreadGoalCleared(event: ThreadGoalClearedNotification): void { + const captures = this.threadGoalClearedCaptures.get(event.threadId); + if (!captures) { + return; + } + for (const capture of captures) { + capture(); + } + } + + private recordTurnRouting(routing: { threadId: string | null, turnId: string | null }): void { + if (routing.threadId === null || routing.turnId === null) { + return; + } + const captures = this.turnRoutingCaptures.get(routing.threadId); + if (!captures) { + return; + } + for (const capture of captures) { + capture(routing.turnId); + } + } + + private handleStaleTurnNotification( + notification: ServerNotification, + routing: { threadId: string | null, turnId: string | null }, + ): boolean { + if (!this.isStaleTurn(routing.threadId, routing.turnId)) { + return false; + } + if (isTurnCompletedNotification(notification) && routing.threadId !== null && routing.turnId !== null) { + this.clearStaleTurn(routing.threadId, routing.turnId); + } + for (const callback of this.codexEventHandlers) { + callback({ eventType: "notification", ...notification }); + } + return true; + } + private isStaleTurn(threadId: string | null, turnId: string | null): boolean { if (threadId === null || turnId === null) { return false; @@ -514,6 +781,74 @@ export class CodexAppServerClient { }; } + private captureTurnRoutings(threadId: string, capture: (turnId: string) => void): () => void { + const captures = this.turnRoutingCaptures.get(threadId) ?? new Set<(turnId: string) => void>(); + captures.add(capture); + this.turnRoutingCaptures.set(threadId, captures); + let released = false; + return () => { + if (released) { + return; + } + released = true; + captures.delete(capture); + if (captures.size === 0) { + this.turnRoutingCaptures.delete(threadId); + } + }; + } + + private captureThreadStatuses(threadId: string, capture: (status: ThreadStatus) => void): () => void { + const captures = this.threadStatusCaptures.get(threadId) ?? new Set<(status: ThreadStatus) => void>(); + captures.add(capture); + this.threadStatusCaptures.set(threadId, captures); + let released = false; + return () => { + if (released) { + return; + } + released = true; + captures.delete(capture); + if (captures.size === 0) { + this.threadStatusCaptures.delete(threadId); + } + }; + } + + private captureThreadGoalUpdates(threadId: string, capture: (event: ThreadGoalUpdatedNotification) => void): () => void { + const captures = this.threadGoalUpdateCaptures.get(threadId) ?? new Set<(event: ThreadGoalUpdatedNotification) => void>(); + captures.add(capture); + this.threadGoalUpdateCaptures.set(threadId, captures); + let released = false; + return () => { + if (released) { + return; + } + released = true; + captures.delete(capture); + if (captures.size === 0) { + this.threadGoalUpdateCaptures.delete(threadId); + } + }; + } + + private captureThreadGoalClears(threadId: string, capture: () => void): () => void { + const captures = this.threadGoalClearedCaptures.get(threadId) ?? new Set<() => void>(); + captures.add(capture); + this.threadGoalClearedCaptures.set(threadId, captures); + let released = false; + return () => { + if (released) { + return; + } + released = true; + captures.delete(capture); + if (captures.size === 0) { + this.threadGoalClearedCaptures.delete(threadId); + } + }; + } + private resolveMcpServerStartupResolvers(): void { const pendingResolvers: Array = []; for (const resolver of this.mcpServerStartupResolvers) { @@ -618,6 +953,27 @@ function isTurnCompletedNotification(notification: ServerNotification): notifica return notification.method === "turn/completed"; } +function isThreadStatusChangedNotification(notification: ServerNotification): notification is { + method: "thread/status/changed"; + params: ThreadStatusChangedNotification; +} { + return notification.method === "thread/status/changed"; +} + +function isThreadGoalUpdatedNotification(notification: ServerNotification): notification is { + method: "thread/goal/updated"; + params: ThreadGoalUpdatedNotification; +} { + return notification.method === "thread/goal/updated"; +} + +function isThreadGoalClearedNotification(notification: ServerNotification): notification is { + method: "thread/goal/cleared"; + params: ThreadGoalClearedNotification; +} { + return notification.method === "thread/goal/cleared"; +} + function isCompactionCompletedNotification(notification: ServerNotification): notification is CompactionCompletedNotification { if (notification.method === "thread/compacted") { return true; @@ -625,6 +981,14 @@ function isCompactionCompletedNotification(notification: ServerNotification): no return notification.method === "item/completed" && notification.params.item.type === "contextCompaction"; } +function goalsMatch(left: ThreadGoal, right: ThreadGoal): boolean { + return left.threadId === right.threadId + && left.objective === right.objective + && left.status === right.status + && left.tokenBudget === right.tokenBudget + && left.updatedAt === right.updatedAt; +} + function extractThreadId(notification: ServerNotification): string | null { const params = notification.params as { threadId?: unknown } | undefined; if (params && typeof params.threadId === "string") { diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index 457d911e..0698002c 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -18,6 +18,7 @@ export type CommandHandleResult = | { handled: true, turnCompleted?: TurnCompletedNotification }; export type CommandHandleOptions = { + onTurnStartPending?: () => void; onTurnStarted?: (turnId: string, threadId: string) => void; }; @@ -121,6 +122,11 @@ export class CodexCommands { description: "Summarize conversation to avoid hitting the context limit.", input: null }, + { + name: "goal", + description: "Set, pause, resume, or clear a task goal.", + input: { hint: "[|clear|pause|resume]" } + }, { name: "logout", description: "Sign out of Codex. This option is available when you are logged in via ChatGPT.", @@ -164,6 +170,9 @@ export class CodexCommands { await this.runWithProcessCheck(() => this.codexAcpClient.runCompact(sessionId)); return { handled: true }; } + case "goal": { + return await this.runGoalCommand(sessionState, command.rest, options); + } case "review": { const target = this.buildReviewTarget(command.rest); const turnCompleted = await this.runReviewCommand(sessionState, target, options); @@ -260,19 +269,90 @@ export class CodexCommands { target: ReviewTarget, options: CommandHandleOptions, ): Promise { + options.onTurnStartPending?.(); return await this.runWithProcessCheck(() => this.codexAcpClient.runReview( sessionState.sessionId, target, (turnId, threadId) => { - if (options.onTurnStarted) { - options.onTurnStarted(turnId, threadId); - } else { - sessionState.currentTurnId = turnId; - } + this.handleCommandTurnStarted(sessionState, options, turnId, threadId); }, )); } + private async runGoalCommand( + sessionState: SessionState, + rest: string, + options: CommandHandleOptions, + ): Promise { + const sessionId = sessionState.sessionId; + const argument = rest.trim(); + if (argument.length === 0) { + await this.sendCommandUsageMessage("goal", "[|clear|pause|resume]", sessionId); + return { handled: true }; + } + + switch (argument.toLowerCase()) { + case "pause": + await this.runWithProcessCheck(() => this.codexAcpClient.setGoalStatus(sessionId, "paused")); + return { handled: true }; + case "resume": + options.onTurnStartPending?.(); + return this.createGoalCommandResult(await this.runWithProcessCheck(() => this.codexAcpClient.resumeGoal( + sessionId, + (turnId) => { + this.handleCommandTurnStarted(sessionState, options, turnId, sessionId); + }, + ))); + case "clear": + await this.runWithProcessCheck(() => this.codexAcpClient.clearGoal(sessionId)); + return { handled: true }; + } + + if (argument.length > 4000) { + const session = new ACPSessionConnection(this.connection, sessionId); + await session.update({ + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: 'Command "/goal" requires goal text of at most 4000 characters.' + } + }); + return { handled: true }; + } + + options.onTurnStartPending?.(); + return this.createGoalCommandResult(await this.runWithProcessCheck(() => this.codexAcpClient.setGoal( + sessionId, + argument, + (turnId) => { + this.handleCommandTurnStarted(sessionState, options, turnId, sessionId); + }, + ))); + } + + private handleCommandTurnStarted( + sessionState: SessionState, + options: CommandHandleOptions, + turnId: string, + threadId: string, + ): void { + if (options.onTurnStarted) { + options.onTurnStarted(turnId, threadId); + } else { + sessionState.currentTurnId = turnId; + } + } + + private createGoalCommandResult(turnCompleted: TurnCompletedNotification | null): CommandHandleResult { + if (turnCompleted === null) { + return { handled: true }; + } + return { + handled: true, + turnCompleted, + }; + } + private buildReviewTarget(instructions: string): ReviewTarget { if (instructions.length === 0) { return { type: "uncommittedChanges" }; diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 513d5f51..4ed0cdbe 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -3,7 +3,7 @@ import type { FuzzyFileSearchSessionUpdatedNotification, ServerNotification } from "./app-server"; -import type {SessionState} from "./CodexAcpServer"; +import type {SessionState, ThreadGoalSnapshot} from "./CodexAcpServer"; import {type PlanEntry, RequestError} from "@agentclientprotocol/sdk"; import {ACPSessionConnection, type AcpClientConnection, type UpdateSessionEvent} from "./ACPSessionConnection"; import type { @@ -266,9 +266,15 @@ export class CodexEventHandler { }; } - private createThreadGoalUpdatedEvent(event: ThreadGoalUpdatedNotification): UpdateSessionEvent { + private createThreadGoalUpdatedEvent(event: ThreadGoalUpdatedNotification): UpdateSessionEvent | null { + const goalSnapshot = this.createThreadGoalSnapshot(event); + if (this.sameThreadGoalSnapshot(this.sessionState.currentGoal, goalSnapshot)) { + return null; + } + this.sessionState.currentGoal = goalSnapshot; + const status = this.formatThreadGoalStatus(event.goal.status); - const objective = event.goal.objective.trim(); + const objective = goalSnapshot.objective; const text = objective.includes("\n") ? `Goal updated (${status}):\n${objective}` : `Goal updated (${status}): ${objective}`; @@ -276,7 +282,7 @@ export class CodexEventHandler { sessionUpdate: "agent_message_chunk", content: { type: "text", - text, + text: `\n\n${text}\n\n`, }, }; } @@ -298,16 +304,40 @@ export class CodexEventHandler { } } - private createThreadGoalClearedEvent(_event: ThreadGoalClearedNotification): UpdateSessionEvent { + private createThreadGoalClearedEvent(_event: ThreadGoalClearedNotification): UpdateSessionEvent | null { + if (this.sessionState.currentGoal === null) { + return null; + } + this.sessionState.currentGoal = null; + return { sessionUpdate: "agent_message_chunk", content: { type: "text", - text: "Goal cleared.", + text: "\n\nGoal cleared.\n\n", }, }; } + private createThreadGoalSnapshot(event: ThreadGoalUpdatedNotification): ThreadGoalSnapshot { + return { + objective: event.goal.objective.trim(), + status: event.goal.status, + tokenBudget: event.goal.tokenBudget, + }; + } + + private sameThreadGoalSnapshot( + left: ThreadGoalSnapshot | null | undefined, + right: ThreadGoalSnapshot + ): boolean { + return left !== null + && left !== undefined + && left.objective === right.objective + && left.status === right.status + && left.tokenBudget === right.tokenBudget; + } + private createReasoningDeltaEvent( event: ReasoningSummaryTextDeltaNotification | ReasoningTextDeltaNotification ): UpdateSessionEvent { diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index dd68685c..87af33ac 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -13,7 +13,7 @@ import { import type {ServerNotification} from "../../app-server"; import type {SessionState} from "../../CodexAcpServer"; import {AgentMode} from "../../AgentMode"; -import type {Model, ReviewStartResponse, TurnCompletedNotification, TurnStartParams} from "../../app-server/v2"; +import type {Model, ReviewStartResponse, ThreadGoal, TurnCompletedNotification, TurnStartParams} from "../../app-server/v2"; import type {RateLimitsMap} from "../../RateLimitsMap"; import {ModelId} from "../../ModelId"; @@ -1478,6 +1478,1090 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(mockFixture.getAcpConnectionDump([])).toContain("Context compacted"); }); + it('handles goal slash commands through Codex app server', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture(); + const goalRunSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "runGoalSet") + .mockResolvedValue({ + threadId: "session-id", + turn: createTurn("goal-turn-id", "completed"), + }); + const goalClearSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "runGoalClear") + .mockResolvedValue(undefined); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }], + }); + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal pause" }], + }); + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal resume" }], + }); + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal clear" }], + }); + + expect(goalRunSpy).toHaveBeenNthCalledWith(1, { + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, expect.any(Function)); + expect(goalRunSpy).toHaveBeenNthCalledWith(2, { + threadId: "session-id", + status: "paused", + }); + expect(goalRunSpy).toHaveBeenNthCalledWith(3, { + threadId: "session-id", + status: "active", + }, expect.any(Function)); + expect(goalClearSpy).toHaveBeenCalledWith({ threadId: "session-id" }); + expect(turnStartSpy).not.toHaveBeenCalled(); + }); + + it('waits for goal slash command turn routing', async () => { + const { mockFixture } = setupPromptFixture(); + const goal = createThreadGoal(); + vi.spyOn(mockFixture.getCodexAppServerClient(), "threadGoalSet") + .mockResolvedValue({ goal }); + const awaitTurnCompletedSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "awaitTurnCompleted"); + + let promptResolved = false; + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }], + }).then((response) => { + promptResolved = true; + return response; + }); + + await vi.waitFor(() => { + expect(mockFixture.getCodexAppServerClient().threadGoalSet).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + await Promise.resolve(); + expect(promptResolved).toBe(false); + expect(mockFixture.getCodexAppServerClient().awaitTurnCompleted).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + mockFixture.sendServerNotification({ + method: "thread/status/changed", + params: { + threadId: "session-id", + status: { + type: "active", + activeFlags: [], + }, + }, + }); + await Promise.resolve(); + expect(promptResolved).toBe(false); + expect(mockFixture.getCodexAppServerClient().awaitTurnCompleted).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "turn/started", + params: { + threadId: "session-id", + turn: createTurn("goal-turn-id", "inProgress"), + }, + }); + await Promise.resolve(); + expect(promptResolved).toBe(false); + expect(mockFixture.getCodexAppServerClient().awaitTurnCompleted).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "I", + }, + }); + await Promise.resolve(); + expect(promptResolved).toBe(false); + + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "goal-turn-id")); + + await vi.waitFor(() => { + expect(promptResolved).toBe(true); + }); + await expect(promptPromise).resolves.toEqual(expect.objectContaining({ + stopReason: "end_turn", + })); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + }); + + it('does not complete no-turn goal slash command before the goal update and runtime grace are handled', async () => { + const { mockFixture } = setupPromptFixture(); + const goal = createThreadGoal({updatedAt: 1710000100}); + const threadGoalSetSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadGoalSet") + .mockResolvedValue({ goal }); + let promptResolved = false; + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }], + }).then((response) => { + promptResolved = true; + return response; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + await flushAsyncWork(); + expect(promptResolved).toBe(false); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + + await flushAsyncWork(); + expect(promptResolved).toBe(false); + + await expect(promptPromise).resolves.toEqual(expect.objectContaining({ + stopReason: "end_turn", + })); + expect(mockFixture.getAcpConnectionDump([])).toContain("Goal updated (active): Ship the migration and keep tests green"); + }); + + it('completes goal slash command when a turn routes after the goal update', async () => { + const { mockFixture } = setupPromptFixture(); + const goal = createThreadGoal({updatedAt: 1710000150}); + const threadGoalSetSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadGoalSet") + .mockResolvedValue({ goal }); + const awaitTurnCompletedSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "awaitTurnCompleted") + .mockResolvedValue({ + threadId: "session-id", + turn: createTurn("goal-turn-id", "completed"), + }); + let promptResolved = false; + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }], + }).then((response) => { + promptResolved = true; + return response; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + + await flushAsyncWork(); + expect(promptResolved).toBe(false); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "late goal output", + }, + }); + await flushAsyncWork(); + expect(promptResolved).toBe(false); + + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "goal-turn-id")); + + await expect(promptPromise).resolves.toEqual(expect.objectContaining({ + stopReason: "end_turn", + })); + expect(promptResolved).toBe(true); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + }); + + it('waits for goal turn completion after the goal completes before streamed output finishes', async () => { + const { mockFixture } = setupPromptFixture(); + const goal = createThreadGoal({updatedAt: 1710000160}); + const completedGoal = createThreadGoal({ + status: "complete", + updatedAt: 1710000170, + tokensUsed: 42, + timeUsedSeconds: 8, + }); + vi.spyOn(mockFixture.getCodexAppServerClient(), "threadGoalSet") + .mockResolvedValue({ goal }); + let promptResolved = false; + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal tell me a joke" }], + }).then((response) => { + promptResolved = true; + return response; + }); + + await vi.waitFor(() => { + expect(mockFixture.getCodexAppServerClient().threadGoalSet).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "tell me a joke", + status: "active", + }); + }); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + mockFixture.sendServerNotification({ + method: "thread/status/changed", + params: { + threadId: "session-id", + status: { + type: "active", + activeFlags: [], + }, + }, + }); + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + goal: completedGoal, + }, + }); + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "Why", + }, + }); + await flushAsyncWork(); + expect(promptResolved).toBe(false); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: " did the test wait?", + }, + }); + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "goal-turn-id")); + + await expect(promptPromise).resolves.toEqual(expect.objectContaining({ + stopReason: "end_turn", + })); + expect(promptResolved).toBe(true); + }); + + it('does not start the no-turn grace period before the goal update is handled', async () => { + vi.useFakeTimers(); + try { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal({updatedAt: 1710000200}); + const threadGoalSet = deferred<{goal: ThreadGoal}>(); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockReturnValue(threadGoalSet.promise); + const awaitTurnCompletedSpy = vi.spyOn(codexAppServerClient, "awaitTurnCompleted") + .mockResolvedValue({ + threadId: "session-id", + turn: createTurn("goal-turn-id", "completed"), + }); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalSet({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, undefined, undefined).finally(() => { + resultSettled = true; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + + threadGoalSet.resolve({goal}); + await vi.advanceTimersByTimeAsync(10_000); + await Promise.resolve(); + expect(resultSettled).toBe(false); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + await Promise.resolve(); + expect(resultSettled).toBe(false); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "late goal output", + }, + }); + + await vi.advanceTimersByTimeAsync(0); + await Promise.resolve(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "goal-turn-id")); + await vi.advanceTimersByTimeAsync(0); + await expect(resultPromise).resolves.toMatchObject({ + threadId: "session-id", + turn: { + id: "goal-turn-id", + status: "completed", + }, + }); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + + it('completes goal set when a turn routes after the goal update', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal({updatedAt: 1710000200}); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockResolvedValue({goal}); + const awaitTurnCompletedSpy = vi.spyOn(codexAppServerClient, "awaitTurnCompleted") + .mockResolvedValue({ + threadId: "session-id", + turn: createTurn("goal-turn-id", "completed"), + }); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalSet({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, undefined, undefined).finally(() => { + resultSettled = true; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + await flushAsyncWork(); + expect(resultSettled).toBe(false); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "late goal output", + }, + }); + + await flushAsyncWork(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "goal-turn-id")); + await expect(resultPromise).resolves.toMatchObject({ + threadId: "session-id", + turn: { + id: "goal-turn-id", + status: "completed", + }, + }); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + }); + + it('keeps goal set pending while the thread is active before a turn routes', async () => { + vi.useFakeTimers(); + try { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal({updatedAt: 1710000225}); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockResolvedValue({goal}); + const awaitTurnCompletedSpy = vi.spyOn(codexAppServerClient, "awaitTurnCompleted") + .mockResolvedValue({ + threadId: "session-id", + turn: createTurn("goal-turn-id", "completed"), + }); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalSet({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, undefined, undefined).finally(() => { + resultSettled = true; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + mockFixture.sendServerNotification({ + method: "thread/status/changed", + params: { + threadId: "session-id", + status: { + type: "active", + activeFlags: [], + }, + }, + }); + + await vi.advanceTimersByTimeAsync(10_000); + await Promise.resolve(); + expect(resultSettled).toBe(false); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "late goal output", + }, + }); + + await vi.advanceTimersByTimeAsync(0); + await Promise.resolve(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "goal-turn-id")); + await vi.advanceTimersByTimeAsync(0); + await expect(resultPromise).resolves.toMatchObject({ + threadId: "session-id", + turn: { + id: "goal-turn-id", + status: "completed", + }, + }); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + + it('keeps goal set pending after turn start until turn completion routes', async () => { + vi.useFakeTimers(); + try { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal({updatedAt: 1710000235}); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockResolvedValue({goal}); + const awaitTurnCompletedSpy = vi.spyOn(codexAppServerClient, "awaitTurnCompleted") + .mockResolvedValue({ + threadId: "session-id", + turn: createTurn("goal-turn-id", "completed"), + }); + const onTurnStarted = vi.fn(); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalSet({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, onTurnStarted).finally(() => { + resultSettled = true; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + mockFixture.sendServerNotification({ + method: "turn/started", + params: { + threadId: "session-id", + turn: createTurn("goal-turn-id", "inProgress"), + }, + }); + + await vi.advanceTimersByTimeAsync(10_000); + await Promise.resolve(); + expect(onTurnStarted).toHaveBeenCalledWith("goal-turn-id"); + expect(resultSettled).toBe(false); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "late goal output", + }, + }); + + await vi.advanceTimersByTimeAsync(0); + await Promise.resolve(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "goal-turn-id")); + await vi.advanceTimersByTimeAsync(0); + await expect(resultPromise).resolves.toMatchObject({ + threadId: "session-id", + turn: { + id: "goal-turn-id", + status: "completed", + }, + }); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + + it('completes goal set when active thread returns idle without routing a turn', async () => { + vi.useFakeTimers(); + try { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal({updatedAt: 1710000250}); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockResolvedValue({goal}); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalSet({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, undefined, undefined).finally(() => { + resultSettled = true; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + mockFixture.sendServerNotification({ + method: "thread/status/changed", + params: { + threadId: "session-id", + status: { + type: "active", + activeFlags: [], + }, + }, + }); + + await vi.advanceTimersByTimeAsync(10_000); + await Promise.resolve(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification({ + method: "thread/status/changed", + params: { + threadId: "session-id", + status: { type: "idle" }, + }, + }); + + await expect(resultPromise).resolves.toBeNull(); + } finally { + vi.useRealTimers(); + } + }); + + it('waits for paused goal update before completing goal status set', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal({status: "paused", updatedAt: 1710000260}); + const threadGoalSet = deferred<{goal: ThreadGoal}>(); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockReturnValue(threadGoalSet.promise); + const awaitTurnCompletedSpy = vi.spyOn(codexAppServerClient, "awaitTurnCompleted"); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalSet({ + threadId: "session-id", + status: "paused", + }).finally(() => { + resultSettled = true; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + status: "paused", + }); + }); + + threadGoalSet.resolve({goal}); + await flushAsyncWork(); + expect(resultSettled).toBe(false); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + + await expect(resultPromise).resolves.toBeNull(); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + }); + + it('waits for goal cleared notification before completing goal clear', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const threadGoalClear = deferred<{cleared: boolean}>(); + const threadGoalClearSpy = vi.spyOn(codexAppServerClient, "threadGoalClear") + .mockReturnValue(threadGoalClear.promise); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalClear({ + threadId: "session-id", + }).finally(() => { + resultSettled = true; + }); + + await vi.waitFor(() => { + expect(threadGoalClearSpy).toHaveBeenCalledWith({ + threadId: "session-id", + }); + }); + + threadGoalClear.resolve({cleared: true}); + await flushAsyncWork(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification({ + method: "thread/goal/cleared", + params: { + threadId: "session-id", + }, + }); + + await expect(resultPromise).resolves.toBeUndefined(); + }); + + it('interrupts a late-started goal slash command after the ACP prompt request is cancelled', async () => { + const { mockFixture } = setupPromptFixture(); + const goalCompleted = deferred(); + let startGoalTurn: () => void = () => {}; + const goalRunSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "runGoalSet") + .mockImplementation((_params, onTurnStarted) => { + startGoalTurn = () => onTurnStarted?.("goal-turn-id"); + return goalCompleted.promise; + }); + const turnInterruptSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "turnInterrupt") + .mockImplementation(async ({threadId, turnId}) => { + goalCompleted.resolve({ + threadId, + turn: createTurn(turnId, "interrupted"), + }); + }); + const controller = new AbortController(); + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }], + }, controller.signal); + + await vi.waitFor(() => { + expect(goalRunSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, expect.any(Function)); + }); + + controller.abort(); + await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"}); + expect(turnInterruptSpy).not.toHaveBeenCalled(); + + startGoalTurn(); + + await vi.waitFor(() => { + expect(turnInterruptSpy).toHaveBeenCalledWith({ + threadId: "session-id", + turnId: "goal-turn-id", + }); + }); + }); + + it('interrupts a goal slash command when ACP cancel arrives before the first routed turn', async () => { + const { mockFixture, sessionState } = setupPromptFixture(); + // @ts-expect-error - registering local session state for the ACP cancel path + mockFixture.getCodexAcpAgent().sessions.set("session-id", sessionState); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal(); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockResolvedValue({ goal }); + const turnInterruptSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "turnInterrupt") + .mockImplementation(async ({threadId, turnId}) => { + mockFixture.sendServerNotification({ + method: "turn/completed", + params: { + threadId, + turn: createTurn(turnId, "interrupted"), + }, + }); + }); + let cancelResolved = false; + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }], + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + + const cancelPromise = mockFixture.getCodexAcpAgent().cancel({sessionId: "session-id"}) + .then(() => { + cancelResolved = true; + }); + await flushAsyncWork(); + expect(cancelResolved).toBe(false); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "goal output", + }, + }); + + await vi.waitFor(() => { + expect(turnInterruptSpy).toHaveBeenCalledWith({ + threadId: "session-id", + turnId: "goal-turn-id", + }); + }); + await expect(cancelPromise).resolves.toBeUndefined(); + await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"}); + }); + + it('suppresses the first routed goal notification after cancellation marks the turn stale', async () => { + const { mockFixture } = setupPromptFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal(); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockResolvedValue({ goal }); + const turnInterruptSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "turnInterrupt") + .mockResolvedValue(undefined); + const controller = new AbortController(); + + const promptPromise = mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }], + }, controller.signal); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + + controller.abort(); + await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"}); + mockFixture.clearAcpConnectionDump(); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "leaked goal output", + }, + }); + + await vi.waitFor(() => { + expect(turnInterruptSpy).toHaveBeenCalledWith({ + threadId: "session-id", + turnId: "goal-turn-id", + }); + }); + await flushAsyncWork(); + expect(mockFixture.getAcpConnectionDump([])).not.toContain("leaked goal output"); + }); + + it('does not hang when goal set starts no continuation turn', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal({updatedAt: 1710000300}); + const threadGoalSetSpy = vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockResolvedValue({ goal }); + const awaitTurnCompletedSpy = vi.spyOn(codexAppServerClient, "awaitTurnCompleted"); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalSet({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, undefined, 0).finally(() => { + resultSettled = true; + }); + + await vi.waitFor(() => { + expect(threadGoalSetSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }); + }); + await flushAsyncWork(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + + await expect(resultPromise).resolves.toBeNull(); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + }); + + it('keeps goal set pending after elapsed startup time until a turn is routed', async () => { + vi.useFakeTimers(); + try { + const mockFixture = createCodexMockTestFixture(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + const goal = createThreadGoal({updatedAt: 1710000400}); + vi.spyOn(codexAppServerClient, "threadGoalSet") + .mockResolvedValue({ goal }); + const awaitTurnCompletedSpy = vi.spyOn(codexAppServerClient, "awaitTurnCompleted") + .mockResolvedValue({ + threadId: "session-id", + turn: createTurn("goal-turn-id", "completed"), + }); + let resultSettled = false; + + const resultPromise = codexAppServerClient.runGoalSet({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, undefined, undefined).finally(() => { + resultSettled = true; + }); + + await Promise.resolve(); + await vi.advanceTimersByTimeAsync(10_000); + await Promise.resolve(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification({ + method: "thread/goal/updated", + params: { + threadId: "session-id", + turnId: null, + goal, + }, + }); + await Promise.resolve(); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification({ + method: "item/agentMessage/delta", + params: { + threadId: "session-id", + turnId: "goal-turn-id", + itemId: "goal-message-id", + delta: "late goal output", + }, + }); + + await Promise.resolve(); + await Promise.resolve(); + await vi.advanceTimersByTimeAsync(0); + expect(resultSettled).toBe(false); + + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "goal-turn-id")); + await vi.advanceTimersByTimeAsync(0); + await expect(resultPromise).resolves.toMatchObject({ + threadId: "session-id", + turn: { + id: "goal-turn-id", + status: "completed", + }, + }); + expect(awaitTurnCompletedSpy).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + + it('completes goal slash command when app server starts no continuation turn', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture(); + const goalRunSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "runGoalSet") + .mockResolvedValue(null); + + const response = await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }], + }); + + expect(response.stopReason).toBe("end_turn"); + expect(goalRunSpy).toHaveBeenCalledWith({ + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + }, expect.any(Function)); + expect(turnStartSpy).not.toHaveBeenCalled(); + }); + + it('reports missing goal slash command input', async () => { + const { mockFixture } = setupPromptFixture(); + const goalSetSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadGoalSet") + .mockResolvedValue({ goal: createThreadGoal() }); + const goalClearSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadGoalClear") + .mockResolvedValue({ cleared: true }); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/goal" }], + }); + + expect(goalSetSpy).not.toHaveBeenCalled(); + expect(goalClearSpy).not.toHaveBeenCalled(); + const [event] = mockFixture.getAcpConnectionEvents([]); + expect(event).toBeDefined(); + expect(event!.args[0].update.content.text).toBe( + 'Command "/goal" requires [|clear|pause|resume].' + ); + }); it('returns cancelled promptly when non-interruptible slash command startup is cancelled', async () => { const { mockFixture } = setupPromptFixture(); const compactStartSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadCompactStart") @@ -1921,6 +3005,20 @@ describe('ACP server test', { timeout: 40_000 }, () => { }; } + function createThreadGoal(overrides?: Partial): ThreadGoal { + return { + threadId: "session-id", + objective: "Ship the migration and keep tests green", + status: "active", + tokenBudget: null, + tokensUsed: 0, + timeUsedSeconds: 0, + createdAt: 1710000000, + updatedAt: 1710000000, + ...overrides, + }; + } + it ('should disable reasoning.summary if key authorization is used', async () => { const { mockFixture, turnStartSpy } = setupPromptFixture({ account: { type: "apiKey" } }); diff --git a/src/__tests__/CodexACPAgent/data/available-commands-build-in.json b/src/__tests__/CodexACPAgent/data/available-commands-build-in.json index d805f6b0..26e78cbb 100644 --- a/src/__tests__/CodexACPAgent/data/available-commands-build-in.json +++ b/src/__tests__/CodexACPAgent/data/available-commands-build-in.json @@ -47,6 +47,13 @@ "description": "Summarize conversation to avoid hitting the context limit.", "input": null }, + { + "name": "goal", + "description": "Set, pause, resume, or clear a task goal.", + "input": { + "hint": "[|clear|pause|resume]" + } + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/available-commands-skills.json b/src/__tests__/CodexACPAgent/data/available-commands-skills.json index dc1b0101..59987072 100644 --- a/src/__tests__/CodexACPAgent/data/available-commands-skills.json +++ b/src/__tests__/CodexACPAgent/data/available-commands-skills.json @@ -47,6 +47,13 @@ "description": "Summarize conversation to avoid hitting the context limit.", "input": null }, + { + "name": "goal", + "description": "Set, pause, resume, or clear a task goal.", + "input": { + "hint": "[|clear|pause|resume]" + } + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/load-session-history.json b/src/__tests__/CodexACPAgent/data/load-session-history.json index c8d0b1eb..bb272cf7 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-history.json +++ b/src/__tests__/CodexACPAgent/data/load-session-history.json @@ -47,6 +47,13 @@ "description": "Summarize conversation to avoid hitting the context limit.", "input": null }, + { + "name": "goal", + "description": "Set, pause, resume, or clear a task goal.", + "input": { + "hint": "[|clear|pause|resume]" + } + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json b/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json index 94a65ad8..c5d03f99 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json +++ b/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json @@ -47,6 +47,13 @@ "description": "Summarize conversation to avoid hitting the context limit.", "input": null }, + { + "name": "goal", + "description": "Set, pause, resume, or clear a task goal.", + "input": { + "hint": "[|clear|pause|resume]" + } + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json b/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json index bfd4ac7a..c6fc87f1 100644 --- a/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json +++ b/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json @@ -7,7 +7,7 @@ "sessionUpdate": "agent_message_chunk", "content": { "type": "text", - "text": "Goal cleared." + "text": "\n\nGoal cleared.\n\n" } } } diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json b/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json index 79c6d2c0..b9336936 100644 --- a/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json +++ b/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json @@ -7,7 +7,7 @@ "sessionUpdate": "agent_message_chunk", "content": { "type": "text", - "text": "Goal updated (budget limited):\nFirst task\nSecond task" + "text": "\n\nGoal updated (budget limited):\nFirst task\nSecond task\n\n" } } } diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-updated.json b/src/__tests__/CodexACPAgent/data/thread-goal-updated.json index 19e32e97..264c1e34 100644 --- a/src/__tests__/CodexACPAgent/data/thread-goal-updated.json +++ b/src/__tests__/CodexACPAgent/data/thread-goal-updated.json @@ -7,7 +7,7 @@ "sessionUpdate": "agent_message_chunk", "content": { "type": "text", - "text": "Goal updated (active): Ship the goal update" + "text": "\n\nGoal updated (active): Ship the goal update\n\n" } } } diff --git a/src/__tests__/CodexACPAgent/thread-goal-events.test.ts b/src/__tests__/CodexACPAgent/thread-goal-events.test.ts index d121cf85..2db982de 100644 --- a/src/__tests__/CodexACPAgent/thread-goal-events.test.ts +++ b/src/__tests__/CodexACPAgent/thread-goal-events.test.ts @@ -18,12 +18,6 @@ describe("CodexEventHandler - thread goal events", () => { vi.clearAllMocks(); }); - const sessionState: SessionState = createTestSessionState({ - sessionId, - currentModelId: "model-id[effort]", - agentMode: AgentMode.DEFAULT_AGENT_MODE, - }); - it("should send thread goal updates as agent messages", async () => { const goalUpdatedNotification: ServerNotification = { method: "thread/goal/updated", @@ -43,7 +37,7 @@ describe("CodexEventHandler - thread goal events", () => { }, }; - await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [goalUpdatedNotification]); + await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [goalUpdatedNotification]); await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( "data/thread-goal-updated.json" @@ -69,7 +63,7 @@ describe("CodexEventHandler - thread goal events", () => { }, }; - await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [goalUpdatedNotification]); + await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [goalUpdatedNotification]); await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( "data/thread-goal-updated-multiline.json" @@ -84,10 +78,114 @@ describe("CodexEventHandler - thread goal events", () => { }, }; - await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [goalClearedNotification]); + await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [goalClearedNotification]); await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( "data/thread-goal-cleared.json" ); }); + + it("should suppress duplicate thread goal updates", async () => { + const goalUpdatedNotification: ServerNotification = { + method: "thread/goal/updated", + params: { + threadId: sessionId, + turnId: "turn-1", + goal: { + threadId: sessionId, + objective: "Ship the goal update", + status: "active", + tokenBudget: null, + tokensUsed: 42, + timeUsedSeconds: 12, + createdAt: 1710000000, + updatedAt: 1710000012, + }, + }, + }; + const duplicateGoalUpdatedNotification: ServerNotification = { + method: "thread/goal/updated", + params: { + ...goalUpdatedNotification.params, + goal: { + ...goalUpdatedNotification.params.goal, + tokensUsed: 84, + timeUsedSeconds: 24, + updatedAt: 1710000024, + }, + }, + }; + + await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [ + goalUpdatedNotification, + duplicateGoalUpdatedNotification, + ]); + + const events = mockFixture.getAcpConnectionEvents([]); + expect(events).toHaveLength(1); + expect(events[0]!.args[0].update.content.text).toBe("\n\nGoal updated (active): Ship the goal update\n\n"); + }); + + it("should separate completed goal updates from preceding agent text", async () => { + const goalCompletedNotification: ServerNotification = { + method: "thread/goal/updated", + params: { + threadId: sessionId, + turnId: "turn-1", + goal: { + threadId: sessionId, + objective: "tell me a joke", + status: "complete", + tokenBudget: null, + tokensUsed: 42, + timeUsedSeconds: 12, + createdAt: 1710000000, + updatedAt: 1710000012, + }, + }, + }; + + await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [ + { + method: "item/agentMessage/delta", + params: { + threadId: sessionId, + turnId: "turn-1", + itemId: "message-1", + delta: "Because they kept losing interest in `any`.", + }, + }, + goalCompletedNotification, + ]); + + const events = mockFixture.getAcpConnectionEvents([]); + expect(events).toHaveLength(2); + expect(events[1]!.args[0].update.content.text).toBe("\n\nGoal updated (complete): tell me a joke\n\n"); + }); + + it("should suppress duplicate thread goal cleared notifications", async () => { + const goalClearedNotification: ServerNotification = { + method: "thread/goal/cleared", + params: { + threadId: sessionId, + }, + }; + + await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [ + goalClearedNotification, + goalClearedNotification, + ]); + + const events = mockFixture.getAcpConnectionEvents([]); + expect(events).toHaveLength(1); + expect(events[0]!.args[0].update.content.text).toBe("\n\nGoal cleared.\n\n"); + }); + + function createSessionState(): SessionState { + return createTestSessionState({ + sessionId, + currentModelId: "model-id[effort]", + agentMode: AgentMode.DEFAULT_AGENT_MODE, + }); + } }); From 3a3e8f6ef489e96a2a376d4ef93ef8ec66f5a683 Mon Sep 17 00:00:00 2001 From: Luca Dibattista <34377738+LucaDiba@users.noreply.github.com> Date: Mon, 29 Jun 2026 07:07:37 -0700 Subject: [PATCH 19/45] Fix skill listing for session cwd (#251) Use the active ACP session roots when publishing skill commands and handling /skills so Codex app-server does not fall back to the adapter process cwd. Adds a portable e2e regression for repo-local skills under the session workspace. Fixes https://github.com/agentclientprotocol/codex-acp/issues/250 --- src/CodexAcpServer.ts | 8 ++++---- src/CodexCommands.ts | 18 ++++++++++++------ .../CodexACPAgent/CodexAcpClient.test.ts | 19 +++++++++++++++++-- .../CodexACPAgent/e2e/acp-e2e.test.ts | 17 +++++++++++++++++ .../e2e/spawned-agent-fixture.ts | 18 ++++++++++++++++++ 5 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index e80280e2..19030334 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -414,7 +414,7 @@ export class CodexAcpServer { this.publishMcpStartupStatusAsync(sessionId); } - this.publishAvailableCommandsAsync(sessionId); + this.publishAvailableCommandsAsync(sessionState); const sessionModelState: LegacySessionModelState = this.createModelState(models, currentModelId); const sessionModeState: SessionModeState = sessionState.agentMode.toSessionModeState(); @@ -804,8 +804,8 @@ export class CodexAcpServer { return !isJetBrains2026_1Client(this.clientInfo); } - private publishAvailableCommandsAsync(sessionId: string) { - void this.availableCommands.publish(sessionId); + private publishAvailableCommandsAsync(sessionState: SessionState) { + void this.availableCommands.publish(sessionState); } private findCurrentModel(models: Model[], currentModelId: string): Model | undefined { @@ -917,7 +917,7 @@ export class CodexAcpServer { this.publishMcpStartupStatusAsync(sessionId); } - await this.availableCommands.publish(sessionId); + await this.availableCommands.publish(sessionState); const sessionModelState: LegacySessionModelState = this.createModelState(models, currentModelId); const sessionModeState: SessionModeState = sessionState.agentMode.toSessionModeState(); diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index 0698002c..bd3c81e0 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -2,7 +2,7 @@ import type * as acp from "@agentclientprotocol/sdk"; import type {AvailableCommand} from "@agentclientprotocol/sdk"; import {ACPSessionConnection, type AcpClientConnection} from "./ACPSessionConnection"; import type {CodexAcpClient} from "./CodexAcpClient"; -import type {RateLimitSnapshot, ReviewTarget, SkillsListEntry, TurnCompletedNotification} from "./app-server/v2"; +import type {RateLimitSnapshot, ReviewTarget, SkillsListEntry, SkillsListParams, TurnCompletedNotification} from "./app-server/v2"; import type {SessionState} from "./CodexAcpServer"; import type {RateLimitsMap} from "./RateLimitsMap"; import type {TokenCount} from "./TokenCount"; @@ -42,24 +42,30 @@ export class CodexCommands { this.onLogout = onLogout; } - async publish(sessionId: string): Promise { + async publish(sessionState: SessionState): Promise { try { - const skillsResponse = await this.runWithProcessCheck(() => this.codexAcpClient.listSkills()); + const skillsResponse = await this.runWithProcessCheck(() => this.codexAcpClient.listSkills(this.createSkillsListParams(sessionState))); const availableCommands = this.buildAvailableCommands(skillsResponse?.data ?? []); if (availableCommands.length === 0) { return; } - const session = new ACPSessionConnection(this.connection, sessionId); + const session = new ACPSessionConnection(this.connection, sessionState.sessionId); await session.update({ sessionUpdate: "available_commands_update", availableCommands }); } catch (err) { - logger.error(`Failed to publish available commands for session ${sessionId}`, err); + logger.error(`Failed to publish available commands for session ${sessionState.sessionId}`, err); } } + private createSkillsListParams(sessionState: SessionState): SkillsListParams { + return { + cwds: [sessionState.cwd, ...sessionState.additionalDirectories], + }; + } + private buildAvailableCommands(skillsEntries: SkillsListEntry[]): AvailableCommand[] { const commands = new Map(); @@ -221,7 +227,7 @@ export class CodexCommands { return { handled: true }; } case "skills": { - const response = await this.runWithProcessCheck(() => this.codexAcpClient.listSkills()); + const response = await this.runWithProcessCheck(() => this.codexAcpClient.listSkills(this.createSkillsListParams(sessionState))); const skills = (response?.data ?? []).flatMap(entry => entry.skills); const lines = skills.map(skill => { const description = skill.shortDescription ?? skill.description ?? ""; diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 87af33ac..77077e5e 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1250,7 +1250,14 @@ describe('ACP server test', { timeout: 40_000 }, () => { vi.spyOn(mockFixture.getCodexAcpClient(), "listSkills").mockResolvedValue({ data: [] }); // @ts-expect-error - exercising private helper - await codexAcpAgent.availableCommands.publish("session-id"); + await codexAcpAgent.availableCommands.publish(createTestSessionState({ + sessionId: "session-id", + cwd: "/workspace", + })); + + expect(mockFixture.getCodexAcpClient().listSkills).toHaveBeenCalledWith({ + cwds: ["/workspace"], + }); await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/available-commands-build-in.json"); }); @@ -1275,7 +1282,15 @@ describe('ACP server test', { timeout: 40_000 }, () => { }); // @ts-expect-error - exercising private helper - await codexAcpAgent.availableCommands.publish("session-id"); + await codexAcpAgent.availableCommands.publish(createTestSessionState({ + sessionId: "session-id", + cwd: "/workspace", + additionalDirectories: ["/workspace/extra"], + })); + + expect(mockFixture.getCodexAcpClient().listSkills).toHaveBeenCalledWith({ + cwds: ["/workspace", "/workspace/extra"], + }); await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/available-commands-skills.json"); }); diff --git a/src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts b/src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts index 84b8ee6e..9d5284c5 100644 --- a/src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts +++ b/src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts @@ -128,4 +128,21 @@ describeE2E("E2E tests", () => { expect(text).toContain("- session-root-skill: Session root skill"); }); }); + + it("lists repo skills from the session cwd", async () => { + fixture = await createAuthenticatedFixture(); + fixture.writeSkill({ + name: "workspace-skill", + description: "Workspace skill", + body: "This skill exists only in the session workspace.", + }, path.join(fixture.workspaceDir, ".agents", "skills")); + + const session = await fixture.createSession(); + + await fixture.expectAvailableCommand(session.sessionId, "$workspace-skill"); + await fixture.expectPromptText(session.sessionId, "/skills", (text) => { + expect(text).toContain("Available skills:"); + expect(text).toContain("- workspace-skill: Workspace skill"); + }); + }); }); diff --git a/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts b/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts index 583f9501..3ed17c3c 100644 --- a/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts +++ b/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts @@ -25,6 +25,7 @@ export interface SpawnedAgentFixture { restart(): Promise; writeSkill(skill: TestSkill, rootDir?: string): void; setPermissionResponder(responder: PermissionResponder): void; + expectAvailableCommand(sessionId: string, commandName: string, timeoutMs?: number): Promise; expectPromptText( sessionId: string, promptText: string, @@ -103,6 +104,7 @@ class RuntimePaths { class RecordingClient implements acp.Client { private readonly textBySessionId = new Map(); + private readonly availableCommandsBySessionId = new Map(); private readonly permissionRequestsBySessionId = new Map(); private permissionResponder: PermissionResponder = () => ({ outcome: {outcome: "cancelled"}, @@ -123,6 +125,11 @@ class RecordingClient implements acp.Client { } async sessionUpdate(params: acp.SessionNotification): Promise { + if (params.update.sessionUpdate === "available_commands_update") { + this.availableCommandsBySessionId.set(params.sessionId, params.update.availableCommands); + return; + } + if (params.update.sessionUpdate !== "agent_message_chunk" || params.update.content.type !== "text") { return; } @@ -135,6 +142,10 @@ class RecordingClient implements acp.Client { return this.textBySessionId.get(sessionId) ?? ""; } + readAvailableCommands(sessionId: string): acp.AvailableCommand[] { + return this.availableCommandsBySessionId.get(sessionId) ?? []; + } + readPermissionRequests( sessionId: string, toolCallKind: acp.ToolKind, @@ -204,6 +215,13 @@ class SpawnedAgentFixtureImpl implements SpawnedAgentFixture { this.client.setPermissionResponder(responder); } + async expectAvailableCommand(sessionId: string, commandName: string, timeoutMs = 30_000): Promise { + await vi.waitFor(() => { + const commandNames = this.client.readAvailableCommands(sessionId).map(command => command.name); + expect(commandNames).toContain(commandName); + }, {timeout: timeoutMs}); + } + async expectPromptText( sessionId: string, promptText: string, From fbc8f84cb58269c26a2ec620da2ac11a7bfed517 Mon Sep 17 00:00:00 2001 From: Aleksandr Suhinin <95745995+AlexandrSuhinin@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:40:34 +0300 Subject: [PATCH 20/45] fix: remove fast session config option if model doesn't support it (#254) --- src/CodexAcpServer.ts | 4 +++- .../CodexACPAgent/fast-mode-config.test.ts | 22 ++++++++++++++++++- .../session-config-options.test.ts | 3 ++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 19030334..80486fe6 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -784,7 +784,9 @@ export class CodexAcpServer { createReasoningEffortConfigOption(sessionState.supportedReasoningEfforts, currentModelId.effort), ); } - configOptions.push(createFastModeConfigOption(sessionState.fastModeEnabled)); + if (sessionState.currentModelSupportsFast) { + configOptions.push(createFastModeConfigOption(sessionState.fastModeEnabled)); + } return configOptions; } diff --git a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts index 70716c8a..ba8a71da 100644 --- a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts +++ b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts @@ -12,6 +12,7 @@ import { FAST_MODE_OFF, FAST_MODE_ON, } from "../../FastModeConfig"; +import {MODEL_CONFIG_ID} from "../../ModelConfigOption"; describe("Fast mode session config", () => { async function createSession( @@ -25,13 +26,14 @@ describe("Fast mode session config", () => { id: "fast-model", additionalSpeedTiers: ["fast"], }); + const slowModel = createTestModel({id: "slow-model"}); vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false); vi.spyOn(codexAcpClient, "getAccount").mockResolvedValue({account: null, requiresOpenaiAuth: false}); vi.spyOn(codexAcpClient, "newSession").mockResolvedValue({ sessionId: "session-id", currentModelId: "fast-model[medium]", - models: [fastModel], + models: [fastModel, slowModel], currentServiceTier, additionalDirectories: [], }); @@ -183,6 +185,24 @@ describe("Fast mode session config", () => { })); }); + it("removes the Fast mode config option when switching to a non-fast model via session_config", async () => { + const {codexAcpAgent} = await createSession("fast"); + + const fastResponse = await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: MODEL_CONFIG_ID, + value: "fast-model", + }); + expect(fastResponse.configOptions?.some(o => o.id === FAST_MODE_CONFIG_ID)).toBe(true); + + const slowResponse = await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: MODEL_CONFIG_ID, + value: "slow-model", + }); + expect(slowResponse.configOptions?.some(o => o.id === FAST_MODE_CONFIG_ID)).toBe(false); + }); + it("keeps Fast mode selected across model switches but stops applying it for non-fast models", async () => { const {codexAcpAgent, codexAcpClient, fixture} = await createSession("fast"); const slowModel = createTestModel({id: "slow-model"}); diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index 391cc043..7377c909 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -19,6 +19,7 @@ function buildModels(): {fast: Model; slow: Model} { description: "Frontier", supportedReasoningEfforts: [lowEffort, mediumEffort, highEffort], defaultReasoningEffort: "medium", + additionalSpeedTiers: ["fast"], }); const slow = createTestModel({ id: "slow-model", @@ -95,7 +96,7 @@ describe("Session config options", () => { const {codexAcpAgent, response} = await createSession("custom-model[high]", [fast, slow]); const ids = response.configOptions?.map(o => o.id); - expect(ids).toEqual([MODE_CONFIG_ID, MODEL_CONFIG_ID, "fast-mode"]); + expect(ids).toEqual([MODE_CONFIG_ID, MODEL_CONFIG_ID]); const modelOption = response.configOptions?.find(o => o.id === MODEL_CONFIG_ID); expect(modelOption).toMatchObject({ From 5506fbae85878013c6eb40ae540ea21a607d9334 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Jun 2026 15:43:51 +0000 Subject: [PATCH 21/45] Release v1.0.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 329fe14d..069e62a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentclientprotocol/codex-acp", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentclientprotocol/codex-acp", - "version": "1.0.1", + "version": "1.0.2", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.0.0", diff --git a/package.json b/package.json index f2ad46e0..29e94719 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.0.1", + "version": "1.0.2", "description": "", "main": "dist/index.js", "bin": { From 7fb6ddda17959a476d7f5d3fc51e5ae06966c76d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 30 Jun 2026 21:09:28 +0200 Subject: [PATCH 22/45] Update ACP SDK v1.1 and dev tools (#257) * Update ACP SDK v1.1 and dev tools * Update diff dependency to v9 --- package-lock.json | 872 ++++++++++++++++++++++++---------------------- package.json | 10 +- 2 files changed, 467 insertions(+), 415 deletions(-) diff --git a/package-lock.json b/package-lock.json index 069e62a5..e3d90cfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "1.0.2", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "^1.0.0", + "@agentclientprotocol/sdk": "^1.1.0", "@openai/codex": "^0.142.4", - "diff": "^8.0.3", + "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.0.0" @@ -20,39 +20,39 @@ "codex-acp": "dist/index.js" }, "devDependencies": { - "@types/node": "^24.10.1", + "@types/node": "^26.0.1", "esbuild": "^0.28.1", "mcp-hello-world": "^1.1.2", "tsx": "^4.20.6", - "typescript": "^5.9.3", - "vitest": "^4.0.10" + "typescript": "^6.0.3", + "vitest": "^4.1.9" } }, "node_modules/@agentclientprotocol/sdk": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.0.0.tgz", - "integrity": "sha512-5FHgLbLZhSAq9F+9SU7AOCXTDzaw/IYx/AvMjrZDs/YFZ9ZPxkdn9ti3fSkny0huHfj2hrqF6pqdns9gN5/Fig==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.1.0.tgz", + "integrity": "sha512-NT2KqphUJ3w6EksUL51ZhJgIYgq/ZLGcBPkyMKgRSO5PMVwe9DnKKX+Htnvk6KHh6dUuh34UHK4gKp+4te1Mdg==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -61,9 +61,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -589,21 +589,21 @@ } }, "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -613,10 +613,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, "license": "MIT", "engines": { @@ -637,6 +651,24 @@ "node": ">=6.6.0" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@modelcontextprotocol/sdk/node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -780,6 +812,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -838,29 +877,47 @@ } }, "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -994,9 +1051,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", "dev": true, "license": "MIT", "funding": { @@ -1004,9 +1061,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", "cpu": [ "arm64" ], @@ -1021,9 +1078,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", "cpu": [ "arm64" ], @@ -1038,9 +1095,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", "cpu": [ "x64" ], @@ -1055,9 +1112,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", "cpu": [ "x64" ], @@ -1072,9 +1129,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", "cpu": [ "arm" ], @@ -1089,13 +1146,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1106,13 +1166,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1123,13 +1186,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1140,13 +1206,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1157,13 +1226,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1174,13 +1246,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1191,9 +1266,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", "cpu": [ "arm64" ], @@ -1208,9 +1283,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", "cpu": [ "wasm32" ], @@ -1218,18 +1293,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", "cpu": [ "arm64" ], @@ -1244,9 +1319,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", "cpu": [ "x64" ], @@ -1275,9 +1350,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -1311,54 +1386,81 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~8.3.0" } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.9", "pathe": "^2.0.3" }, "funding": { @@ -1366,14 +1468,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1382,9 +1484,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true, "license": "MIT", "funding": { @@ -1392,15 +1494,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1497,23 +1599,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/body-parser/node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", @@ -1677,27 +1762,19 @@ } }, "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "ms": "2.0.0" } }, "node_modules/default-browser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", - "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -1766,9 +1843,9 @@ } }, "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -1827,16 +1904,16 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", + "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -1929,9 +2006,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "dev": true, "license": "MIT", "engines": { @@ -1939,9 +2016,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2014,23 +2091,6 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2039,9 +2099,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "dev": true, "funding": [ { @@ -2092,23 +2152,6 @@ "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2220,9 +2263,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -2233,9 +2276,9 @@ } }, "node_modules/hono": { - "version": "4.12.25", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", - "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", "dev": true, "license": "MIT", "engines": { @@ -2356,9 +2399,9 @@ "license": "MIT" }, "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -2378,9 +2421,9 @@ "license": "ISC" }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "dev": true, "license": "MIT", "funding": { @@ -2544,6 +2587,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2565,6 +2611,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2586,6 +2635,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2607,6 +2659,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2778,16 +2833,16 @@ } }, "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -2837,9 +2892,9 @@ } }, "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -2958,9 +3013,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -3013,13 +3068,14 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -3072,9 +3128,9 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "dev": true, "license": "MIT", "peer": true, @@ -3083,9 +3139,9 @@ } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "dev": true, "license": "MIT", "peer": true, @@ -3093,7 +3149,7 @@ "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.7" } }, "node_modules/require-from-string": { @@ -3107,13 +3163,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3123,21 +3179,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" } }, "node_modules/router": { @@ -3157,6 +3213,31 @@ "node": ">= 18" } }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/router/node_modules/path-to-regexp": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", @@ -3241,20 +3322,10 @@ "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, @@ -3305,15 +3376,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -3325,14 +3396,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -3517,9 +3588,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3531,9 +3602,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, @@ -3567,190 +3638,171 @@ "node": ">= 0.8" } }, - "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "node_modules/vite": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz", + "integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", - "why-is-node-running": "^2.3.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" }, "bin": { - "vitest": "vitest.mjs" + "vite": "bin/vite.js" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "@edge-runtime/vm": { + "@types/node": { "optional": true }, - "@opentelemetry/api": { + "@vitejs/devtools": { "optional": true }, - "@types/node": { + "esbuild": { "optional": true }, - "@vitest/browser-playwright": { + "jiti": { "optional": true }, - "@vitest/browser-preview": { + "less": { "optional": true }, - "@vitest/browser-webdriverio": { + "sass": { "optional": true }, - "@vitest/ui": { + "sass-embedded": { "optional": true }, - "happy-dom": { + "stylus": { "optional": true }, - "jsdom": { + "sugarss": { "optional": true }, - "vite": { - "optional": false - } - } - }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.0", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { + "terser": { "optional": true }, - "vite": { + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } }, - "node_modules/vitest/node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" }, "bin": { - "vite": "bin/vite.js" + "vitest": "vitest.mjs" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "@types/node": { + "@edge-runtime/vm": { "optional": true }, - "@vitejs/devtools": { + "@opentelemetry/api": { "optional": true }, - "esbuild": { + "@types/node": { "optional": true }, - "jiti": { + "@vitest/browser-playwright": { "optional": true }, - "less": { + "@vitest/browser-preview": { "optional": true }, - "sass": { + "@vitest/browser-webdriverio": { "optional": true }, - "sass-embedded": { + "@vitest/coverage-istanbul": { "optional": true }, - "stylus": { + "@vitest/coverage-v8": { "optional": true }, - "sugarss": { + "@vitest/ui": { "optional": true }, - "terser": { + "happy-dom": { "optional": true }, - "tsx": { + "jsdom": { "optional": true }, - "yaml": { - "optional": true + "vite": { + "optional": false } } }, @@ -3804,9 +3856,9 @@ "license": "ISC" }, "node_modules/wsl-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.0.tgz", - "integrity": "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", "license": "MIT", "dependencies": { "is-wsl": "^3.1.0", @@ -3829,13 +3881,13 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "dev": true, "license": "ISC", "peerDependencies": { - "zod": "^3.25 || ^4" + "zod": "^3.25.28 || ^4" } } } diff --git a/package.json b/package.json index 29e94719..d588a1ea 100644 --- a/package.json +++ b/package.json @@ -53,17 +53,17 @@ "license": "Apache-2.0", "type": "module", "devDependencies": { - "@types/node": "^24.10.1", + "@types/node": "^26.0.1", "esbuild": "^0.28.1", "mcp-hello-world": "^1.1.2", "tsx": "^4.20.6", - "typescript": "^5.9.3", - "vitest": "^4.0.10" + "typescript": "^6.0.3", + "vitest": "^4.1.9" }, "dependencies": { - "@agentclientprotocol/sdk": "^1.0.0", + "@agentclientprotocol/sdk": "^1.1.0", "@openai/codex": "^0.142.4", - "diff": "^8.0.3", + "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.0.0" From a5d223cb630a8108a6d00dc0bf8c333b5ba0154e Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 1 Jul 2026 10:54:46 +0200 Subject: [PATCH 23/45] Handle completed image generation items (#262) Closes #259 --- src/CodexEventHandler.ts | 2 +- src/CodexToolCallMapper.ts | 23 ++++++++++++++++--- src/StdUtils.ts | 5 ++-- .../data/image-generation-completed-only.json | 2 +- .../data/image-generation-flow.json | 2 +- .../CodexACPAgent/image-events.test.ts | 4 ++-- tsconfig.json | 1 + 7 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 4ed0cdbe..2e177152 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -429,7 +429,7 @@ export class CodexEventHandler { if (this.activeImageGenerationItems.delete(event.item.id)) { return createImageGenerationCompleteUpdate(event.item); } - return createImageGenerationUpdate(event.item); + return createImageGenerationUpdate(event.item, { terminalStatus: true }); case "reasoning": if (this.seenReasoningDeltaItemIds.delete(event.item.id)) { return null; diff --git a/src/CodexToolCallMapper.ts b/src/CodexToolCallMapper.ts index 61060c7a..3e2fd4e7 100644 --- a/src/CodexToolCallMapper.ts +++ b/src/CodexToolCallMapper.ts @@ -197,21 +197,24 @@ export function createImageGenerationCompleteUpdate( return { sessionUpdate: "tool_call_update", toolCallId: item.id, - status: imageGenerationToolStatus(item.status), + status: imageGenerationTerminalStatus(item.status), content: imageGenerationContent(item), rawOutput: imageGenerationRawOutput(item), }; } export function createImageGenerationUpdate( - item: ThreadItem & { type: "imageGeneration" } + item: ThreadItem & { type: "imageGeneration" }, + options?: { terminalStatus?: boolean }, ): UpdateSessionEvent { return { sessionUpdate: "tool_call", toolCallId: item.id, kind: "other", title: "Image generation", - status: imageGenerationToolStatus(item.status), + status: options?.terminalStatus + ? imageGenerationTerminalStatus(item.status) + : imageGenerationToolStatus(item.status), content: imageGenerationContent(item), rawOutput: imageGenerationRawOutput(item), }; @@ -624,6 +627,20 @@ function imageGenerationToolStatus(status: string): AcpToolCallStatus { } } +function imageGenerationTerminalStatus(status: string): AcpToolCallStatus { + switch (status) { + case "failed": + return "failed"; + case "completed": + case "generating": + case "in_progress": + case "inProgress": + case "incomplete": + default: + return "completed"; + } +} + function imageGenerationContent( item: ThreadItem & { type: "imageGeneration" } ): ToolCallContent[] { diff --git a/src/StdUtils.ts b/src/StdUtils.ts index 82e20bb2..99423433 100644 --- a/src/StdUtils.ts +++ b/src/StdUtils.ts @@ -1,8 +1,7 @@ import {Readable, Writable} from "node:stream"; import {Disposable} from "vscode-jsonrpc"; -import type {PartialMessageInfo} from "vscode-jsonrpc/lib/common/messageReader"; import {Emitter, Message, MessageReader, MessageWriter} from "vscode-jsonrpc/node"; -import type {DataCallback} from "vscode-jsonrpc/node"; +import type {DataCallback, PartialMessageInfo} from "vscode-jsonrpc/node"; import * as acp from "@agentclientprotocol/sdk"; //TODO ask to include proper jsonrpc field and remove @@ -70,4 +69,4 @@ export function createJsonStream(readable: Readable, writable: Writable){ const input = Writable.toWeb(writable); const output = Readable.toWeb(readable) as ReadableStream; return acp.ndJsonStream(input, output); -} \ No newline at end of file +} diff --git a/src/__tests__/CodexACPAgent/data/image-generation-completed-only.json b/src/__tests__/CodexACPAgent/data/image-generation-completed-only.json index d20df233..73d9bf87 100644 --- a/src/__tests__/CodexACPAgent/data/image-generation-completed-only.json +++ b/src/__tests__/CodexACPAgent/data/image-generation-completed-only.json @@ -20,7 +20,7 @@ } ], "rawOutput": { - "status": "completed", + "status": "generating", "revisedPrompt": null, "result": "iVBORw0KGgo=" } diff --git a/src/__tests__/CodexACPAgent/data/image-generation-flow.json b/src/__tests__/CodexACPAgent/data/image-generation-flow.json index 204fe7f2..176aefdd 100644 --- a/src/__tests__/CodexACPAgent/data/image-generation-flow.json +++ b/src/__tests__/CodexACPAgent/data/image-generation-flow.json @@ -44,7 +44,7 @@ } ], "rawOutput": { - "status": "completed", + "status": "generating", "revisedPrompt": "A tiny blue square", "result": "iVBORw0KGgo=", "savedPath": "/tmp/codex/generated-blue-square.png" diff --git a/src/__tests__/CodexACPAgent/image-events.test.ts b/src/__tests__/CodexACPAgent/image-events.test.ts index bb7718aa..8eb8102e 100644 --- a/src/__tests__/CodexACPAgent/image-events.test.ts +++ b/src/__tests__/CodexACPAgent/image-events.test.ts @@ -45,7 +45,7 @@ describe("CodexEventHandler - image events", () => { item: { type: "imageGeneration", id: "image-generation-1", - status: "completed", + status: "generating", revisedPrompt: "A tiny blue square", result: "iVBORw0KGgo=", savedPath: "/tmp/codex/generated-blue-square.png", @@ -71,7 +71,7 @@ describe("CodexEventHandler - image events", () => { item: { type: "imageGeneration", id: "image-generation-completed-only", - status: "completed", + status: "generating", revisedPrompt: null, result: "iVBORw0KGgo=", }, diff --git a/tsconfig.json b/tsconfig.json index 1d21f510..39a4fd81 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "module": "ES2022", "moduleResolution": "Bundler", "target": "ES2022", + "types": ["node"], "sourceMap": true, "declaration": true, From 2753785fcf098f0912ad1c07f7924842fdba56ce Mon Sep 17 00:00:00 2001 From: "acp-release-bot[bot]" <246668977+acp-release-bot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:55:04 +0200 Subject: [PATCH 24/45] Update codex to 0.142.5 (#261) * Update codex to 0.142.5 * Fix types and tests after Codex update --------- Co-authored-by: github-actions[bot] --- package-lock.json | 86 +++++++++++++++-------------------------------- package.json | 2 +- 2 files changed, 29 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3d90cfd..87e6bc69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.1.0", - "@openai/codex": "^0.142.4", + "@openai/codex": "^0.142.5", "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", @@ -929,9 +929,9 @@ } }, "node_modules/@openai/codex": { - "version": "0.142.4", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4.tgz", - "integrity": "sha512-BW9+3Cs7tChgkc0QhUsZIDm6XgpSDSeJkljRSHhJZqDwlaxDnNVhRP/sZwhgrTn3f2jPn3jc4WTKfLOTk+g5pA==", + "version": "0.142.5", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5.tgz", + "integrity": "sha512-WQEpD7l3k68eIAP0aq28EdR18ENBAf8DyprzFhzNwCOQJSv4nHzpwT8Fl30IJacprko2ZCmUBZjM2u941l2yLw==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -940,19 +940,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.4-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.4-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.4-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.142.4-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.4-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.142.4-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.5-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.5-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.5-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.142.5-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.5-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.142.5-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.142.4-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-darwin-arm64.tgz", - "integrity": "sha512-CfXp5jilBOxD8zLuG6Nk7w5ZPP3roE3s1rsr60za9PQKI7kaJ/5G+oP3Z0N1CoB4mpy+5JHkMLTZAZZPGFlSgQ==", + "version": "0.142.5-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-darwin-arm64.tgz", + "integrity": "sha512-l43p8xv+Z/2/b6fCUc7/FmcQZsaPB7RFizLponGwHAnFOWe3i9Vky69p+up3BUam9AetoQQUv7Mo+2KdaFEqhA==", "cpu": [ "arm64" ], @@ -967,9 +967,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.142.4-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-darwin-x64.tgz", - "integrity": "sha512-H9ia31pkJFtBk+UrREAfJiTUfcvgactKbG4W4SF2LFfJOTRsrsJZGtscHQ8/lLGeKEfOk/psrbSOPDyrZnxvnQ==", + "version": "0.142.5-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-darwin-x64.tgz", + "integrity": "sha512-yk6A06/VmW7NFsa48OVPaj//g/zeSpd79wjuqfXZwW8ZKRYQm3+wCd3hWjPl79F3QnXvDvM2j3JMIBL3m3GXXg==", "cpu": [ "x64" ], @@ -984,9 +984,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.142.4-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-linux-arm64.tgz", - "integrity": "sha512-/bBcEm+OKmlGHH0c88jrVgYMRyaNC/hGOW/znP/PiHQEeIf4UdsUGQ7x4FAZm648140cgaeKYIBoRBbHizS+5w==", + "version": "0.142.5-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-linux-arm64.tgz", + "integrity": "sha512-77ka5PSnm5HdxdBT99IwntCasmbqevlS0eiC0AtEb6ZXCLkim2gm0AWm+jNYy0EhbssvNK+KghayWo34HMgXeA==", "cpu": [ "arm64" ], @@ -1001,9 +1001,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.142.4-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-linux-x64.tgz", - "integrity": "sha512-t3tNOkDdiO5/gRqkTmuMcAj5JjCbeWotOJzxV7pPkD2u2KvzVRrk7jPAjuy+85wPc12lsIupYgxHlK7CkVLOjA==", + "version": "0.142.5-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-linux-x64.tgz", + "integrity": "sha512-pxY+d3NgNE57Y/MApD3/TZUAygxJN6I9h3ZeDUwe67mxWjUxsuapxMRFTKSznCalYbRAeZp752+AAXmUbmguEg==", "cpu": [ "x64" ], @@ -1018,9 +1018,9 @@ }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.142.4-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-win32-arm64.tgz", - "integrity": "sha512-QBGeoYk3pmVscVIxeW21cvPJIjsnmD5dOTqMAzypXGik5soNIKHbps2Kj1LBKOjVbbx5qoDEbTwDfYmnjfqgdQ==", + "version": "0.142.5-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-win32-arm64.tgz", + "integrity": "sha512-65BEqGbUZ7r0ayunIHdBjo5crwgbwKX/6puOcO+VCswUw/dXvDsN2IGcbXB52+bS9U5+FxP783cUHfTT6m40DQ==", "cpu": [ "arm64" ], @@ -1035,9 +1035,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.142.4-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.4-win32-x64.tgz", - "integrity": "sha512-W0rSPT0t7FnKlJYNu9/HpHzaSOutuSSYjIVunKorYmrScXk9pQxBEhEuyn16JOtywc/2nX8nHEmWsuj2sd8suw==", + "version": "0.142.5-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-win32-x64.tgz", + "integrity": "sha512-a+wI4PEx9a2fg6V5ueTTDkOkr1XpEvA5RFXIbo/L2hOfzMmGtyRnbG24bCGu5Q2RSgVxSQV0aLkdb3vdYMNH9A==", "cpu": [ "x64" ], @@ -1153,9 +1153,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1173,9 +1170,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1193,9 +1187,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1213,9 +1204,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1233,9 +1221,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1253,9 +1238,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2587,9 +2569,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2611,9 +2590,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2635,9 +2611,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2659,9 +2632,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index d588a1ea..575a7819 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^1.1.0", - "@openai/codex": "^0.142.4", + "@openai/codex": "^0.142.5", "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^8.2.1", From a744380e4deadda111a26ccfba4cf1c892da017a Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 1 Jul 2026 10:57:11 +0200 Subject: [PATCH 25/45] Emit goal changes as session metadata (#263) These were quite noisy and not always helpful. We can revisit with a new status api in ACP --- src/CodexAcpServer.ts | 3 +- src/CodexEventHandler.ts | 42 +++------------- .../CodexACPAgent/CodexAcpClient.test.ts | 17 ++++++- .../data/thread-goal-cleared.json | 9 ++-- .../data/thread-goal-updated-multiline.json | 13 +++-- .../data/thread-goal-updated.json | 13 +++-- .../CodexACPAgent/thread-goal-events.test.ts | 50 ++++++++++++++++--- 7 files changed, 90 insertions(+), 57 deletions(-) diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 80486fe6..e953f604 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -13,6 +13,7 @@ import type { Model, ReasoningEffortOption, Thread, + ThreadGoalStatus, ThreadItem, TurnCompletedNotification, UserInput @@ -69,7 +70,7 @@ import {resolveTerminalOutputMode, type TerminalOutputMode} from "./TerminalOutp export interface ThreadGoalSnapshot { objective: string; - status: string; + status: ThreadGoalStatus; tokenBudget: number | null; } diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 2e177152..97bd9880 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -273,35 +273,9 @@ export class CodexEventHandler { } this.sessionState.currentGoal = goalSnapshot; - const status = this.formatThreadGoalStatus(event.goal.status); - const objective = goalSnapshot.objective; - const text = objective.includes("\n") - ? `Goal updated (${status}):\n${objective}` - : `Goal updated (${status}): ${objective}`; - return { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `\n\n${text}\n\n`, - }, - }; - } - - private formatThreadGoalStatus(status: ThreadGoalUpdatedNotification["goal"]["status"]): string { - switch (status) { - case "active": - return "active"; - case "paused": - return "paused"; - case "budgetLimited": - return "budget limited"; - case "blocked": - return "blocked"; - case "usageLimited": - return "usage limited"; - case "complete": - return "complete"; - } + return this.createCodexSessionInfoUpdate({ + goal: goalSnapshot, + }); } private createThreadGoalClearedEvent(_event: ThreadGoalClearedNotification): UpdateSessionEvent | null { @@ -310,13 +284,9 @@ export class CodexEventHandler { } this.sessionState.currentGoal = null; - return { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: "\n\nGoal cleared.\n\n", - }, - }; + return this.createCodexSessionInfoUpdate({ + goal: null, + }); } private createThreadGoalSnapshot(event: ThreadGoalUpdatedNotification): ThreadGoalSnapshot { diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 77077e5e..3f98b757 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1660,7 +1660,22 @@ describe('ACP server test', { timeout: 40_000 }, () => { await expect(promptPromise).resolves.toEqual(expect.objectContaining({ stopReason: "end_turn", })); - expect(mockFixture.getAcpConnectionDump([])).toContain("Goal updated (active): Ship the migration and keep tests green"); + expect(mockFixture.getAcpConnectionEvents([])).toContainEqual(expect.objectContaining({ + args: [expect.objectContaining({ + update: { + sessionUpdate: "session_info_update", + _meta: { + codex: { + goal: { + objective: "Ship the migration and keep tests green", + status: "active", + tokenBudget: null, + }, + }, + }, + }, + })], + })); }); it('completes goal slash command when a turn routes after the goal update', async () => { diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json b/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json index c6fc87f1..acf61a68 100644 --- a/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json +++ b/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json @@ -4,10 +4,11 @@ { "sessionId": "test-session-id", "update": { - "sessionUpdate": "agent_message_chunk", - "content": { - "type": "text", - "text": "\n\nGoal cleared.\n\n" + "sessionUpdate": "session_info_update", + "_meta": { + "codex": { + "goal": null + } } } } diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json b/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json index b9336936..c524584c 100644 --- a/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json +++ b/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json @@ -4,10 +4,15 @@ { "sessionId": "test-session-id", "update": { - "sessionUpdate": "agent_message_chunk", - "content": { - "type": "text", - "text": "\n\nGoal updated (budget limited):\nFirst task\nSecond task\n\n" + "sessionUpdate": "session_info_update", + "_meta": { + "codex": { + "goal": { + "objective": "First task\nSecond task", + "status": "budgetLimited", + "tokenBudget": 1000 + } + } } } } diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-updated.json b/src/__tests__/CodexACPAgent/data/thread-goal-updated.json index 264c1e34..bc14e9cb 100644 --- a/src/__tests__/CodexACPAgent/data/thread-goal-updated.json +++ b/src/__tests__/CodexACPAgent/data/thread-goal-updated.json @@ -4,10 +4,15 @@ { "sessionId": "test-session-id", "update": { - "sessionUpdate": "agent_message_chunk", - "content": { - "type": "text", - "text": "\n\nGoal updated (active): Ship the goal update\n\n" + "sessionUpdate": "session_info_update", + "_meta": { + "codex": { + "goal": { + "objective": "Ship the goal update", + "status": "active", + "tokenBudget": null + } + } } } } diff --git a/src/__tests__/CodexACPAgent/thread-goal-events.test.ts b/src/__tests__/CodexACPAgent/thread-goal-events.test.ts index 2db982de..659b0892 100644 --- a/src/__tests__/CodexACPAgent/thread-goal-events.test.ts +++ b/src/__tests__/CodexACPAgent/thread-goal-events.test.ts @@ -18,7 +18,7 @@ describe("CodexEventHandler - thread goal events", () => { vi.clearAllMocks(); }); - it("should send thread goal updates as agent messages", async () => { + it("should send thread goal updates as session metadata", async () => { const goalUpdatedNotification: ServerNotification = { method: "thread/goal/updated", params: { @@ -44,7 +44,7 @@ describe("CodexEventHandler - thread goal events", () => { ); }); - it("should format multiline thread goal updates", async () => { + it("should trim multiline thread goal objectives in session metadata", async () => { const goalUpdatedNotification: ServerNotification = { method: "thread/goal/updated", params: { @@ -70,7 +70,7 @@ describe("CodexEventHandler - thread goal events", () => { ); }); - it("should send thread goal cleared as an agent message", async () => { + it("should send thread goal cleared as session metadata", async () => { const goalClearedNotification: ServerNotification = { method: "thread/goal/cleared", params: { @@ -123,10 +123,21 @@ describe("CodexEventHandler - thread goal events", () => { const events = mockFixture.getAcpConnectionEvents([]); expect(events).toHaveLength(1); - expect(events[0]!.args[0].update.content.text).toBe("\n\nGoal updated (active): Ship the goal update\n\n"); + expect(events[0]!.args[0].update).toEqual({ + sessionUpdate: "session_info_update", + _meta: { + codex: { + goal: { + objective: "Ship the goal update", + status: "active", + tokenBudget: null, + }, + }, + }, + }); }); - it("should separate completed goal updates from preceding agent text", async () => { + it("should not append completed goal updates to preceding agent text", async () => { const goalCompletedNotification: ServerNotification = { method: "thread/goal/updated", params: { @@ -160,7 +171,25 @@ describe("CodexEventHandler - thread goal events", () => { const events = mockFixture.getAcpConnectionEvents([]); expect(events).toHaveLength(2); - expect(events[1]!.args[0].update.content.text).toBe("\n\nGoal updated (complete): tell me a joke\n\n"); + expect(events[0]!.args[0].update).toEqual({ + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: "Because they kept losing interest in `any`.", + }, + }); + expect(events[1]!.args[0].update).toEqual({ + sessionUpdate: "session_info_update", + _meta: { + codex: { + goal: { + objective: "tell me a joke", + status: "complete", + tokenBudget: null, + }, + }, + }, + }); }); it("should suppress duplicate thread goal cleared notifications", async () => { @@ -178,7 +207,14 @@ describe("CodexEventHandler - thread goal events", () => { const events = mockFixture.getAcpConnectionEvents([]); expect(events).toHaveLength(1); - expect(events[0]!.args[0].update.content.text).toBe("\n\nGoal cleared.\n\n"); + expect(events[0]!.args[0].update).toEqual({ + sessionUpdate: "session_info_update", + _meta: { + codex: { + goal: null, + }, + }, + }); }); function createSessionState(): SessionState { From ef5e169dd44db15ea2b9e55ca97f192e1411fe79 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 2 Jul 2026 13:27:37 +0200 Subject: [PATCH 26/45] Support boolean Fast mode config options (#255) * Support boolean Fast mode config options If the client expresses support for boolean config options, we can provide it. * Set fast mode config category to model config --- src/CodexAcpServer.ts | 40 +++++++++---- src/FastModeConfig.ts | 21 ++++++- .../CodexACPAgent/fast-mode-config.test.ts | 59 ++++++++++++++++++- 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index e953f604..9283af88 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -57,6 +57,7 @@ import { formatWebSearchTitle, } from "./CodexToolCallMapper"; import { + clientSupportsBooleanConfigOptions, createFastModeConfigOption, FAST_MODE_CONFIG_ID, FAST_MODE_OFF, @@ -139,6 +140,7 @@ export class CodexAcpServer { private readonly availableCommands: CodexCommands; private clientInfo: acp.Implementation | null; private terminalOutputMode: TerminalOutputMode; + private booleanConfigOptionsSupported: boolean; private readonly sessions: Map; private readonly pendingMcpStartupSessions: Map; @@ -169,6 +171,7 @@ export class CodexAcpServer { this.getRecentStderr = getRecentStderr ?? (() => ""); this.clientInfo = null; this.terminalOutputMode = "terminal_output_delta"; + this.booleanConfigOptionsSupported = false; this.availableCommands = new CodexCommands( connection, codexAcpClient, @@ -183,6 +186,7 @@ export class CodexAcpServer { logger.log("Initialize request received"); this.clientInfo = _params.clientInfo ?? null; this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities); + this.booleanConfigOptionsSupported = clientSupportsBooleanConfigOptions(_params.clientCapabilities); await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params)); return { protocolVersion: acp.PROTOCOL_VERSION, @@ -658,23 +662,18 @@ export class CodexAcpServer { const sessionState = this.sessions.get(params.sessionId); if (!sessionState) throw new Error(`Session ${params.sessionId} not found`); - if (typeof params.value !== "string") { - throw RequestError.invalidParams(); - } - const value = params.value; - switch (params.configId) { case FAST_MODE_CONFIG_ID: - this.applyFastModeChange(sessionState, value); + this.applyFastModeChange(sessionState, params); break; case MODE_CONFIG_ID: - this.applyModeChange(sessionState, value); + this.applyModeChange(sessionState, this.stringConfigValue(params)); break; case MODEL_CONFIG_ID: - this.applyModelChange(sessionState, value); + this.applyModelChange(sessionState, this.stringConfigValue(params)); break; case REASONING_EFFORT_CONFIG_ID: - this.applyReasoningEffortChange(sessionState, value); + this.applyReasoningEffortChange(sessionState, this.stringConfigValue(params)); break; default: throw RequestError.invalidParams(); @@ -685,13 +684,25 @@ export class CodexAcpServer { }; } - private applyFastModeChange(sessionState: SessionState, value: string): void { + private applyFastModeChange(sessionState: SessionState, params: acp.SetSessionConfigOptionRequest): void { + const value = params.value; + if (typeof value === "boolean") { + sessionState.fastModeEnabled = value; + return; + } if (value !== FAST_MODE_ON && value !== FAST_MODE_OFF) { throw RequestError.invalidParams(); } sessionState.fastModeEnabled = value === FAST_MODE_ON; } + private stringConfigValue(params: acp.SetSessionConfigOptionRequest): string { + if (typeof params.value !== "string") { + throw RequestError.invalidParams(); + } + return params.value; + } + private applyModeChange(sessionState: SessionState, value: string): void { const newMode = AgentMode.find(value); if (!newMode) { @@ -785,9 +796,12 @@ export class CodexAcpServer { createReasoningEffortConfigOption(sessionState.supportedReasoningEfforts, currentModelId.effort), ); } - if (sessionState.currentModelSupportsFast) { - configOptions.push(createFastModeConfigOption(sessionState.fastModeEnabled)); - } + if (sessionState.currentModelSupportsFast) { + configOptions.push(createFastModeConfigOption( + sessionState.fastModeEnabled, + this.booleanConfigOptionsSupported, + )); + } return configOptions; } diff --git a/src/FastModeConfig.ts b/src/FastModeConfig.ts index 4187aaa3..8e681c24 100644 --- a/src/FastModeConfig.ts +++ b/src/FastModeConfig.ts @@ -1,8 +1,10 @@ import type {SessionConfigOption} from "@agentclientprotocol/sdk"; +import type * as acp from "@agentclientprotocol/sdk"; import type {ServiceTier} from "./app-server"; import type {Model} from "./app-server/v2"; export const FAST_MODE_CONFIG_ID = "fast-mode"; +export const FAST_MODE_CATEGORY = "model_config"; export const FAST_MODE_ON = "on"; export const FAST_MODE_OFF = "off"; @@ -16,12 +18,27 @@ export function resolveFastServiceTier(fastModeEnabled: boolean, currentModelSup return fastModeEnabled && currentModelSupportsFast ? "fast" : null; } -export function createFastModeConfigOption(fastModeEnabled: boolean): SessionConfigOption { +export function clientSupportsBooleanConfigOptions(clientCapabilities?: acp.ClientCapabilities | null): boolean { + return clientCapabilities?.session?.configOptions?.boolean != null; +} + +export function createFastModeConfigOption(fastModeEnabled: boolean, useBooleanConfigOption = false): SessionConfigOption { + if (useBooleanConfigOption) { + return { + id: FAST_MODE_CONFIG_ID, + name: "Fast mode", + description: FAST_MODE_DESCRIPTION, + category: FAST_MODE_CATEGORY, + type: "boolean", + currentValue: fastModeEnabled, + }; + } + return { id: FAST_MODE_CONFIG_ID, name: "Fast mode", description: FAST_MODE_DESCRIPTION, - category: FAST_MODE_CONFIG_ID, + category: FAST_MODE_CATEGORY, type: "select", currentValue: fastModeEnabled ? FAST_MODE_ON : FAST_MODE_OFF, options: [ diff --git a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts index ba8a71da..07d42444 100644 --- a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts +++ b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts @@ -15,9 +15,18 @@ import { import {MODEL_CONFIG_ID} from "../../ModelConfigOption"; describe("Fast mode session config", () => { + const booleanConfigCapabilities: acp.ClientCapabilities = { + session: { + configOptions: { + boolean: {}, + }, + }, + }; + async function createSession( currentServiceTier: "fast" | "flex" | null = null, - clientInfo: acp.Implementation | null = null + clientInfo: acp.Implementation | null = null, + clientCapabilities?: acp.ClientCapabilities, ) { const fixture = createCodexMockTestFixture(); const codexAcpAgent = fixture.getCodexAcpAgent(); @@ -41,6 +50,7 @@ describe("Fast mode session config", () => { await codexAcpAgent.initialize({ protocolVersion: acp.PROTOCOL_VERSION, clientInfo, + ...(clientCapabilities ? {clientCapabilities} : {}), }); const response = await codexAcpAgent.newSession({cwd: "/test/cwd", mcpServers: []}); @@ -63,6 +73,31 @@ describe("Fast mode session config", () => { expect(response.configOptions).toContainEqual(createFastModeConfigOption(false)); }); + it("returns the Fast mode config option as a boolean when the client supports it", async () => { + const {response} = await createSession(null, null, booleanConfigCapabilities); + + expect(response.configOptions).toContainEqual(createFastModeConfigOption(false, true)); + const option = response.configOptions?.find(option => option.id === FAST_MODE_CONFIG_ID); + expect(option).toMatchObject({ + id: FAST_MODE_CONFIG_ID, + type: "boolean", + currentValue: false, + }); + expect(option).not.toHaveProperty("options"); + }); + + it("keeps the Fast mode select option when boolean support is explicitly absent", async () => { + const {response} = await createSession(null, null, { + session: { + configOptions: { + boolean: null, + }, + }, + }); + + expect(response.configOptions).toContainEqual(createFastModeConfigOption(false)); + }); + it("initializes Fast mode as On when the app-server session tier is fast", async () => { const {response, codexAcpAgent} = await createSession("fast"); @@ -139,6 +174,28 @@ describe("Fast mode session config", () => { expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(false); }); + it("toggles Fast mode through boolean session config options", async () => { + const {codexAcpAgent} = await createSession(null, null, booleanConfigCapabilities); + + const onResponse = await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: FAST_MODE_CONFIG_ID, + type: "boolean", + value: true, + }); + expect(onResponse.configOptions).toContainEqual(createFastModeConfigOption(true, true)); + expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(true); + + const offResponse = await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: FAST_MODE_CONFIG_ID, + type: "boolean", + value: false, + }); + expect(offResponse.configOptions).toContainEqual(createFastModeConfigOption(false, true)); + expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(false); + }); + it("rejects unknown Fast mode config ids and values", async () => { const {codexAcpAgent} = await createSession(); From 8309bc26e0e04ac16ffa6629e4854f034ac41404 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 2 Jul 2026 13:32:12 +0200 Subject: [PATCH 27/45] feat: Add message IDs to text session chunks (#256) --- src/CodexAcpServer.ts | 64 ++++++++++------ src/CodexCommands.ts | 42 ++-------- src/CodexEventHandler.ts | 76 ++++--------------- src/ContentChunks.ts | 52 +++++++++++++ src/ResponseItemHistoryFallback.ts | 2 +- .../data/follow-up-no-duplicates.json | 3 + .../data/load-session-history.json | 4 + ...ession-response-item-history-fallback.json | 3 + .../CodexACPAgent/data/multiple-sessions.json | 2 + .../CodexACPAgent/data/output-acp-events.json | 3 + .../data/reasoning-completed-parts.json | 1 + .../reasoning-deltas-and-section-break.json | 3 + .../CodexACPAgent/thread-goal-events.test.ts | 1 + 13 files changed, 136 insertions(+), 120 deletions(-) create mode 100644 src/ContentChunks.ts diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 9283af88..6454ca86 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -68,6 +68,11 @@ import { import packageJson from "../package.json"; import {isJetBrains2026_1Client} from "./JBUtils"; import {resolveTerminalOutputMode, type TerminalOutputMode} from "./TerminalOutputMode"; +import { + createAgentTextMessageChunk, + createAgentTextThoughtChunk, + createUserMessageChunk, +} from "./ContentChunks"; export interface ThreadGoalSnapshot { objective: string; @@ -981,6 +986,7 @@ export class CodexAcpServer { case "agentMessage": return [{ sessionUpdate: "agent_message_chunk", + messageId: item.id, content: { type: "text", text: item.text }, }]; case "reasoning": @@ -1020,13 +1026,11 @@ export class CodexAcpServer { private createUserMessageUpdates(item: ThreadItem & { type: "userMessage" }): UpdateSessionEvent[] { const updates: UpdateSessionEvent[] = []; + const messageId = item.id; for (const input of item.content) { const blocks = this.userInputToContentBlocks(input); for (const block of blocks) { - updates.push({ - sessionUpdate: "user_message_chunk", - content: block, - }); + updates.push(createUserMessageChunk(block, messageId)); } } return updates; @@ -1034,10 +1038,8 @@ export class CodexAcpServer { private createReasoningUpdates(item: ThreadItem & { type: "reasoning" }): UpdateSessionEvent[] { const parts = item.summary.length > 0 ? item.summary : item.content; - return parts.map((text) => ({ - sessionUpdate: "agent_thought_chunk", - content: { type: "text", text: text }, - })); + const messageId = item.id; + return parts.map((text) => createAgentTextThoughtChunk(text, messageId)); } private createWebSearchUpdate( @@ -1604,13 +1606,7 @@ export class CodexAcpServer { } await this.connection.notify(acp.methods.client.session.update, { sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: "*Conversation interrupted*" - } - } + update: createAgentTextMessageChunk("*Conversation interrupted*"), }); } @@ -1689,26 +1685,33 @@ function mergeHistoryUpdates( merged.push(update); }; - const flushFallbackThrough = (targetKey: string): boolean => { + const flushFallbackBeforeMatchingDuplicate = (targetUpdate: UpdateSessionEvent): void => { + const targetKey = historyUpdateKey(targetUpdate); + const targetContentKey = historyUpdateContentKey(targetUpdate); + if (!targetKey && !targetContentKey) { + return; + } + const matchIndex = responseItemFallbackUpdates.findIndex((update, index) => ( - index >= fallbackIndex && historyUpdateKey(update) === targetKey + index >= fallbackIndex + && ( + (targetKey !== null && historyUpdateKey(update) === targetKey) + || (targetContentKey !== null && historyUpdateContentKey(update) === targetContentKey) + ) )); if (matchIndex === -1) { - return false; + return; } - while (fallbackIndex <= matchIndex) { + while (fallbackIndex < matchIndex) { pushUpdate(responseItemFallbackUpdates[fallbackIndex]!); fallbackIndex += 1; } - return true; + fallbackIndex += 1; }; for (const update of threadUpdates) { - const key = historyUpdateKey(update); - if (key && flushFallbackThrough(key)) { - continue; - } + flushFallbackBeforeMatchingDuplicate(update); pushUpdate(update); } @@ -1725,7 +1728,7 @@ function historyUpdateKey(update: UpdateSessionEvent): string | null { case "user_message_chunk": case "agent_message_chunk": case "agent_thought_chunk": - return `${update.sessionUpdate}:${JSON.stringify(update.content)}`; + return `${update.sessionUpdate}:${update.messageId ?? ""}:${JSON.stringify(update.content)}`; case "tool_call": return `tool_call:${update.toolCallId}:start`; case "tool_call_update": @@ -1735,6 +1738,17 @@ function historyUpdateKey(update: UpdateSessionEvent): string | null { } } +function historyUpdateContentKey(update: UpdateSessionEvent): string | null { + switch (update.sessionUpdate) { + case "user_message_chunk": + case "agent_message_chunk": + case "agent_thought_chunk": + return `${update.sessionUpdate}:${JSON.stringify(update.content)}`; + default: + return historyUpdateKey(update); + } +} + function getRequestedMcpServerNames(mcpServers: Array): Array { return Array.from(new Set(mcpServers.map(server => sanitizeMcpServerName(server.name)))); } diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index bd3c81e0..57df6e94 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -7,6 +7,7 @@ import type {SessionState} from "./CodexAcpServer"; import type {RateLimitsMap} from "./RateLimitsMap"; import type {TokenCount} from "./TokenCount"; import {logger} from "./Logger"; +import {createAgentTextMessageChunk} from "./ContentChunks"; type ParsedSlashCommand = { name: string; @@ -210,20 +211,14 @@ export class CodexCommands { case "status": { const session = new ACPSessionConnection(this.connection, sessionId); const message = this.buildStatusMessage(sessionState); - await session.update({ - sessionUpdate: "agent_message_chunk", - content: { type: "text", text: message } - }); + await session.update(createAgentTextMessageChunk(message)); return { handled: true }; } case "logout": { await this.runWithProcessCheck(() => this.codexAcpClient.logout()); await this.onLogout(); const session = new ACPSessionConnection(this.connection, sessionId); - await session.update({ - sessionUpdate: "agent_message_chunk", - content: { type: "text", text: "Logged out from Codex account." } - }); + await session.update(createAgentTextMessageChunk("Logged out from Codex account.")); return { handled: true }; } case "skills": { @@ -237,10 +232,7 @@ export class CodexCommands { ? ["Available skills:", ...lines].join("\n") : "No skills configured."; const session = new ACPSessionConnection(this.connection, sessionId); - await session.update({ - sessionUpdate: "agent_message_chunk", - content: { type: "text", text } - }); + await session.update(createAgentTextMessageChunk(text)); return { handled: true }; } case "mcp": { @@ -258,10 +250,7 @@ export class CodexCommands { ? ["Configured MCP servers:", ...lines].join("\n") : "No MCP servers configured."; const session = new ACPSessionConnection(this.connection, sessionId); - await session.update({ - sessionUpdate: "agent_message_chunk", - content: { type: "text", text } - }); + await session.update(createAgentTextMessageChunk(text)); return { handled: true }; } default: @@ -316,13 +305,7 @@ export class CodexCommands { if (argument.length > 4000) { const session = new ACPSessionConnection(this.connection, sessionId); - await session.update({ - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: 'Command "/goal" requires goal text of at most 4000 characters.' - } - }); + await session.update(createAgentTextMessageChunk('Command "/goal" requires goal text of at most 4000 characters.')); return { handled: true }; } @@ -371,13 +354,7 @@ export class CodexCommands { private async sendCommandUsageMessage(name: string, inputHint: string, sessionId: string): Promise { const session = new ACPSessionConnection(this.connection, sessionId); - await session.update({ - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `Command "/${name}" requires ${inputHint}.` - } - }); + await session.update(createAgentTextMessageChunk(`Command "/${name}" requires ${inputHint}.`)); } private async sendUnknownCommandMessage(name: string, sessionId: string): Promise { @@ -390,10 +367,7 @@ export class CodexCommands { text.push(...lines); } const session = new ACPSessionConnection(this.connection, sessionId); - await session.update({ - sessionUpdate: "agent_message_chunk", - content: { type: "text", text: text.join("\n") } - }); + await session.update(createAgentTextMessageChunk(text.join("\n"))); } private buildStatusMessage(sessionState: SessionState): string { diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 97bd9880..5ed8ad79 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -55,6 +55,10 @@ import { } from "./CodexToolCallMapper"; import { stripShellPrefix } from "./CommandUtils"; import {createTerminalOutputMeta, type TerminalOutputMode} from "./TerminalOutputMode"; +import { + createAgentTextMessageChunk, + createAgentTextThoughtChunk, +} from "./ContentChunks"; export { stripShellPrefix }; @@ -226,44 +230,20 @@ export class CodexEventHandler { } private async createTextEvent(event: AgentMessageDeltaNotification): Promise { - return { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: event.delta - } - } + return createAgentTextMessageChunk(event.delta, event.itemId); } private async createConfigWarningEvent(event: ConfigWarningNotification): Promise { const detailsText = event.details ? `\n\n${event.details}` : ""; - return { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `Config warning: ${event.summary}${detailsText}\n\n` - } - } + return createAgentTextMessageChunk(`Config warning: ${event.summary}${detailsText}\n\n`); } private createWarningEvent(event: WarningNotification): UpdateSessionEvent { - return { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `Warning: ${event.message}\n\n` - } - }; + return createAgentTextMessageChunk(`Warning: ${event.message}\n\n`); } private createModelReroutedEvent(event: ModelReroutedNotification): UpdateSessionEvent { - return { - sessionUpdate: "agent_thought_chunk", - content: { - type: "text", - text: `Model rerouted from ${event.fromModel} to ${event.toModel} (${event.reason}).\n\n` - } - }; + return createAgentTextThoughtChunk(`Model rerouted from ${event.fromModel} to ${event.toModel} (${event.reason}).\n\n`); } private createThreadGoalUpdatedEvent(event: ThreadGoalUpdatedNotification): UpdateSessionEvent | null { @@ -312,22 +292,16 @@ export class CodexEventHandler { event: ReasoningSummaryTextDeltaNotification | ReasoningTextDeltaNotification ): UpdateSessionEvent { this.seenReasoningDeltaItemIds.add(event.itemId); - return this.createAgentThoughtEvent(event.delta); + return this.createAgentThoughtEvent(event.delta, event.itemId); } private createReasoningSectionBreakEvent(event: ReasoningSummaryPartAddedNotification): UpdateSessionEvent { this.seenReasoningDeltaItemIds.add(event.itemId); - return this.createAgentThoughtEvent("\n\n"); + return this.createAgentThoughtEvent("\n\n", event.itemId); } - private createAgentThoughtEvent(text: string): UpdateSessionEvent { - return { - sessionUpdate: "agent_thought_chunk", - content: { - type: "text", - text, - } - }; + private createAgentThoughtEvent(text: string, messageId: string): UpdateSessionEvent { + return createAgentTextThoughtChunk(text, messageId); } private async createItemEvent(event: ItemStartedNotification): Promise { @@ -432,7 +406,7 @@ export class CodexEventHandler { if (text.length === 0) { return null; } - return this.createAgentThoughtEvent(text); + return this.createAgentThoughtEvent(text, item.id); } private createExitedReviewModeEvent(item: ThreadItem & { type: "exitedReviewMode" }): UpdateSessionEvent | null { @@ -440,23 +414,11 @@ export class CodexEventHandler { if (text.length === 0) { return null; } - return { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text, - } - }; + return createAgentTextMessageChunk(text); } private createContextCompactedEvent(): UpdateSessionEvent { - return { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: "*Context compacted to fit the model's context window.*\n\n" - } - }; + return createAgentTextMessageChunk("*Context compacted to fit the model's context window.*\n\n"); } private createCommandOutputDeltaEvent(event: CommandExecutionOutputDeltaNotification): UpdateSessionEvent { @@ -599,13 +561,7 @@ export class CodexEventHandler { ? RequestError.internalError(this.createTurnErrorData(params.error)) : RequestError.authRequired(this.createTurnErrorData(params.error), params.error.message); } - return { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `${params.error.message}\n\n` - } - } + return createAgentTextMessageChunk(`${params.error.message}\n\n`); } private isAuthenticationRequiredError(error: CodexErrorInfo | null): boolean { diff --git a/src/ContentChunks.ts b/src/ContentChunks.ts new file mode 100644 index 00000000..b111d347 --- /dev/null +++ b/src/ContentChunks.ts @@ -0,0 +1,52 @@ +import type {ContentBlock} from "@agentclientprotocol/sdk"; +import type {UpdateSessionEvent} from "./ACPSessionConnection"; + +export function createUserMessageChunk(content: ContentBlock, messageId?: string): UpdateSessionEvent { + if (messageId) { + return { + sessionUpdate: "user_message_chunk", + messageId, + content, + }; + } + return { + sessionUpdate: "user_message_chunk", + content, + }; +} + +export function createAgentMessageChunk(content: ContentBlock, messageId?: string): UpdateSessionEvent { + if (messageId) { + return { + sessionUpdate: "agent_message_chunk", + messageId, + content, + }; + } + return { + sessionUpdate: "agent_message_chunk", + content, + }; +} + +export function createAgentThoughtChunk(content: ContentBlock, messageId?: string): UpdateSessionEvent { + if (messageId) { + return { + sessionUpdate: "agent_thought_chunk", + messageId, + content, + }; + } + return { + sessionUpdate: "agent_thought_chunk", + content, + }; +} + +export function createAgentTextMessageChunk(text: string, messageId?: string): UpdateSessionEvent { + return createAgentMessageChunk({type: "text", text}, messageId); +} + +export function createAgentTextThoughtChunk(text: string, messageId?: string): UpdateSessionEvent { + return createAgentThoughtChunk({type: "text", text}, messageId); +} diff --git a/src/ResponseItemHistoryFallback.ts b/src/ResponseItemHistoryFallback.ts index 2591c6ad..11e49d68 100644 --- a/src/ResponseItemHistoryFallback.ts +++ b/src/ResponseItemHistoryFallback.ts @@ -27,7 +27,7 @@ function historyFallbackUpdateKey(update: UpdateSessionEvent): string | null { case "user_message_chunk": case "agent_message_chunk": case "agent_thought_chunk": - return `${update.sessionUpdate}:${JSON.stringify(update.content)}`; + return `${update.sessionUpdate}:${update.messageId ?? ""}:${JSON.stringify(update.content)}`; case "tool_call": return `tool_call:${update.toolCallId}:start`; case "tool_call_update": diff --git a/src/__tests__/CodexACPAgent/data/follow-up-no-duplicates.json b/src/__tests__/CodexACPAgent/data/follow-up-no-duplicates.json index efd040d7..d992b603 100644 --- a/src/__tests__/CodexACPAgent/data/follow-up-no-duplicates.json +++ b/src/__tests__/CodexACPAgent/data/follow-up-no-duplicates.json @@ -5,6 +5,7 @@ "sessionId": "id", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "string", "content": { "type": "text", "text": "He" @@ -20,6 +21,7 @@ "sessionId": "id", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "string", "content": { "type": "text", "text": "ll" @@ -35,6 +37,7 @@ "sessionId": "id", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "string", "content": { "type": "text", "text": "o!" diff --git a/src/__tests__/CodexACPAgent/data/load-session-history.json b/src/__tests__/CodexACPAgent/data/load-session-history.json index bb272cf7..454bcf7d 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-history.json +++ b/src/__tests__/CodexACPAgent/data/load-session-history.json @@ -71,6 +71,7 @@ "sessionId": "session-1", "update": { "sessionUpdate": "user_message_chunk", + "messageId": "item-user-1", "content": { "type": "text", "text": "Hi" @@ -86,6 +87,7 @@ "sessionId": "session-1", "update": { "sessionUpdate": "user_message_chunk", + "messageId": "item-user-1", "content": { "type": "text", "text": "[@image](https://example.com/image.png)" @@ -101,6 +103,7 @@ "sessionId": "session-1", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "item-agent-1", "content": { "type": "text", "text": "Hello!" @@ -116,6 +119,7 @@ "sessionId": "session-1", "update": { "sessionUpdate": "agent_thought_chunk", + "messageId": "item-reason-1", "content": { "type": "text", "text": "Thinking..." diff --git a/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json b/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json index c5d03f99..3fd4f914 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json +++ b/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json @@ -71,6 +71,7 @@ "sessionId": "session-legacy", "update": { "sessionUpdate": "user_message_chunk", + "messageId": "item-user-1", "content": { "type": "text", "text": "List the files" @@ -86,6 +87,7 @@ "sessionId": "session-legacy", "update": { "sessionUpdate": "agent_thought_chunk", + "messageId": "item-reasoning-1", "content": { "type": "text", "text": "Need to inspect the directory." @@ -245,6 +247,7 @@ "sessionId": "session-legacy", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "item-agent-1", "content": { "type": "text", "text": "The directory contains README.md and src." diff --git a/src/__tests__/CodexACPAgent/data/multiple-sessions.json b/src/__tests__/CodexACPAgent/data/multiple-sessions.json index 895ec176..4c884302 100644 --- a/src/__tests__/CodexACPAgent/data/multiple-sessions.json +++ b/src/__tests__/CodexACPAgent/data/multiple-sessions.json @@ -5,6 +5,7 @@ "sessionId": "session-1", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "string", "content": { "type": "text", "text": "Hello-1" @@ -20,6 +21,7 @@ "sessionId": "session-2", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "string", "content": { "type": "text", "text": "Hello-2" diff --git a/src/__tests__/CodexACPAgent/data/output-acp-events.json b/src/__tests__/CodexACPAgent/data/output-acp-events.json index efd040d7..d992b603 100644 --- a/src/__tests__/CodexACPAgent/data/output-acp-events.json +++ b/src/__tests__/CodexACPAgent/data/output-acp-events.json @@ -5,6 +5,7 @@ "sessionId": "id", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "string", "content": { "type": "text", "text": "He" @@ -20,6 +21,7 @@ "sessionId": "id", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "string", "content": { "type": "text", "text": "ll" @@ -35,6 +37,7 @@ "sessionId": "id", "update": { "sessionUpdate": "agent_message_chunk", + "messageId": "string", "content": { "type": "text", "text": "o!" diff --git a/src/__tests__/CodexACPAgent/data/reasoning-completed-parts.json b/src/__tests__/CodexACPAgent/data/reasoning-completed-parts.json index 1a8241ec..0f8d7b46 100644 --- a/src/__tests__/CodexACPAgent/data/reasoning-completed-parts.json +++ b/src/__tests__/CodexACPAgent/data/reasoning-completed-parts.json @@ -5,6 +5,7 @@ "sessionId": "test-session-id", "update": { "sessionUpdate": "agent_thought_chunk", + "messageId": "reasoning-2", "content": { "type": "text", "text": "First summary\n\nSecond summary" diff --git a/src/__tests__/CodexACPAgent/data/reasoning-deltas-and-section-break.json b/src/__tests__/CodexACPAgent/data/reasoning-deltas-and-section-break.json index 43ff8453..894bff9d 100644 --- a/src/__tests__/CodexACPAgent/data/reasoning-deltas-and-section-break.json +++ b/src/__tests__/CodexACPAgent/data/reasoning-deltas-and-section-break.json @@ -5,6 +5,7 @@ "sessionId": "test-session-id", "update": { "sessionUpdate": "agent_thought_chunk", + "messageId": "reasoning-1", "content": { "type": "text", "text": "First thought" @@ -20,6 +21,7 @@ "sessionId": "test-session-id", "update": { "sessionUpdate": "agent_thought_chunk", + "messageId": "reasoning-1", "content": { "type": "text", "text": "\n\n" @@ -35,6 +37,7 @@ "sessionId": "test-session-id", "update": { "sessionUpdate": "agent_thought_chunk", + "messageId": "reasoning-1", "content": { "type": "text", "text": "Raw reasoning detail" diff --git a/src/__tests__/CodexACPAgent/thread-goal-events.test.ts b/src/__tests__/CodexACPAgent/thread-goal-events.test.ts index 659b0892..fc690994 100644 --- a/src/__tests__/CodexACPAgent/thread-goal-events.test.ts +++ b/src/__tests__/CodexACPAgent/thread-goal-events.test.ts @@ -173,6 +173,7 @@ describe("CodexEventHandler - thread goal events", () => { expect(events).toHaveLength(2); expect(events[0]!.args[0].update).toEqual({ sessionUpdate: "agent_message_chunk", + messageId: "message-1", content: { type: "text", text: "Because they kept losing interest in `any`.", From 259c16df4115d67358ab755cce8af0b0c526caf3 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 2 Jul 2026 13:49:30 +0200 Subject: [PATCH 28/45] Upgrade vscode-jsonrpc to v9 (#258) --- package-lock.json | 8 ++++---- package.json | 2 +- src/StdUtils.ts | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87e6bc69..6d3c226a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@openai/codex": "^0.142.5", "diff": "^9.0.0", "open": "^11.0.0", - "vscode-jsonrpc": "^8.2.1", + "vscode-jsonrpc": "^9.0.1", "zod": "^4.0.0" }, "bin": { @@ -3777,9 +3777,9 @@ } }, "node_modules/vscode-jsonrpc": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", - "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", + "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", "license": "MIT", "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index 575a7819..241d20d8 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@openai/codex": "^0.142.5", "diff": "^9.0.0", "open": "^11.0.0", - "vscode-jsonrpc": "^8.2.1", + "vscode-jsonrpc": "^9.0.1", "zod": "^4.0.0" } } diff --git a/src/StdUtils.ts b/src/StdUtils.ts index 99423433..3d9e2159 100644 --- a/src/StdUtils.ts +++ b/src/StdUtils.ts @@ -1,7 +1,6 @@ import {Readable, Writable} from "node:stream"; -import {Disposable} from "vscode-jsonrpc"; -import {Emitter, Message, MessageReader, MessageWriter} from "vscode-jsonrpc/node"; -import type {DataCallback, PartialMessageInfo} from "vscode-jsonrpc/node"; +import {Emitter} from "vscode-jsonrpc/node"; +import type {DataCallback, Disposable, Message, MessageReader, MessageWriter, PartialMessageInfo} from "vscode-jsonrpc/node"; import * as acp from "@agentclientprotocol/sdk"; //TODO ask to include proper jsonrpc field and remove From c4e55d5c4b4671e9c3636b02be51c1237ffc6c00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Jul 2026 11:50:31 +0000 Subject: [PATCH 29/45] Release v1.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d3c226a..0212b29e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentclientprotocol/codex-acp", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentclientprotocol/codex-acp", - "version": "1.0.2", + "version": "1.1.0", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.1.0", diff --git a/package.json b/package.json index 241d20d8..77807e32 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.0.2", + "version": "1.1.0", "description": "", "main": "dist/index.js", "bin": { From c516b9a9b4e7fb77f70f939de116ce56eb95f003 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 7 Jul 2026 10:46:09 +0200 Subject: [PATCH 30/45] Update ACP SDK to 1.2.0 (#265) --- package-lock.json | 314 +++++++++++++++++++++++++--------------------- package.json | 8 +- 2 files changed, 176 insertions(+), 146 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0212b29e..235a2e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.0", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "^1.1.0", + "@agentclientprotocol/sdk": "^1.2.0", "@openai/codex": "^0.142.5", "diff": "^9.0.0", "open": "^11.0.0", @@ -20,18 +20,18 @@ "codex-acp": "dist/index.js" }, "devDependencies": { - "@types/node": "^26.0.1", + "@types/node": "^26.1.0", "esbuild": "^0.28.1", "mcp-hello-world": "^1.1.2", - "tsx": "^4.20.6", + "tsx": "^4.23.0", "typescript": "^6.0.3", - "vitest": "^4.1.9" + "vitest": "^4.1.10" } }, "node_modules/@agentclientprotocol/sdk": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.1.0.tgz", - "integrity": "sha512-NT2KqphUJ3w6EksUL51ZhJgIYgq/ZLGcBPkyMKgRSO5PMVwe9DnKKX+Htnvk6KHh6dUuh34UHK4gKp+4te1Mdg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.2.0.tgz", + "integrity": "sha512-rJ3766M2eouZsNCaK21JbPD++JLBZ0PWq38dzj1sA4/i0njaQEkiC2F5v7MlizFW6akX+qu5Ks8R00Xw+YMHKA==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" @@ -746,9 +746,9 @@ } }, "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1051,9 +1051,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", - "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", "funding": { @@ -1061,9 +1061,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", - "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -1078,9 +1078,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", - "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -1095,9 +1095,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", - "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -1112,9 +1112,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", - "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -1129,9 +1129,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", - "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -1146,13 +1146,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", - "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1163,13 +1166,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", - "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1180,13 +1186,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", - "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1197,13 +1206,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", - "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1214,13 +1226,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", - "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1231,13 +1246,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", - "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1248,9 +1266,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", - "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -1265,9 +1283,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", - "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ "wasm32" ], @@ -1284,9 +1302,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", - "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ "arm64" ], @@ -1301,9 +1319,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", - "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -1368,9 +1386,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", - "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "dev": true, "license": "MIT", "dependencies": { @@ -1378,16 +1396,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1396,13 +1414,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1423,9 +1441,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1436,13 +1454,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -1450,14 +1468,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1466,9 +1484,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -1476,13 +1494,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1886,9 +1904,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", - "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", "dev": true, "license": "MIT" }, @@ -2258,9 +2276,9 @@ } }, "node_modules/hono": { - "version": "4.12.27", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", - "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "version": "4.12.28", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.28.tgz", + "integrity": "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA==", "dev": true, "license": "MIT", "engines": { @@ -2569,6 +2587,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2590,6 +2611,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2611,6 +2635,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2632,6 +2659,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2960,9 +2990,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -3081,9 +3111,9 @@ } }, "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3133,13 +3163,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", - "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.137.0", + "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3149,21 +3179,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.3", - "@rolldown/binding-darwin-arm64": "1.1.3", - "@rolldown/binding-darwin-x64": "1.1.3", - "@rolldown/binding-freebsd-x64": "1.1.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", - "@rolldown/binding-linux-arm64-gnu": "1.1.3", - "@rolldown/binding-linux-arm64-musl": "1.1.3", - "@rolldown/binding-linux-ppc64-gnu": "1.1.3", - "@rolldown/binding-linux-s390x-gnu": "1.1.3", - "@rolldown/binding-linux-x64-gnu": "1.1.3", - "@rolldown/binding-linux-x64-musl": "1.1.3", - "@rolldown/binding-openharmony-arm64": "1.1.3", - "@rolldown/binding-wasm32-wasi": "1.1.3", - "@rolldown/binding-win32-arm64-msvc": "1.1.3", - "@rolldown/binding-win32-x64-msvc": "1.1.3" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/router": { @@ -3525,9 +3555,9 @@ "optional": true }, "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", "dev": true, "license": "MIT", "dependencies": { @@ -3609,9 +3639,9 @@ } }, "node_modules/vite": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz", - "integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "dependencies": { @@ -3687,19 +3717,19 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -3727,12 +3757,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 77807e32..0c534f2f 100644 --- a/package.json +++ b/package.json @@ -53,15 +53,15 @@ "license": "Apache-2.0", "type": "module", "devDependencies": { - "@types/node": "^26.0.1", + "@types/node": "^26.1.0", "esbuild": "^0.28.1", "mcp-hello-world": "^1.1.2", - "tsx": "^4.20.6", + "tsx": "^4.23.0", "typescript": "^6.0.3", - "vitest": "^4.1.9" + "vitest": "^4.1.10" }, "dependencies": { - "@agentclientprotocol/sdk": "^1.1.0", + "@agentclientprotocol/sdk": "^1.2.0", "@openai/codex": "^0.142.5", "diff": "^9.0.0", "open": "^11.0.0", From 6a314a520c3bfcd8441abd586fb237113494255f Mon Sep 17 00:00:00 2001 From: "Ilia.Shulgin" Date: Wed, 8 Jul 2026 09:56:03 +0200 Subject: [PATCH 31/45] build: Don't create empty commits for Codex update --- .github/workflows/codex-update.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codex-update.yml b/.github/workflows/codex-update.yml index 30ff1218..f144323c 100644 --- a/.github/workflows/codex-update.yml +++ b/.github/workflows/codex-update.yml @@ -92,7 +92,8 @@ jobs: -c sandbox_workspace_write.writable_roots=["${{ github.workspace }}/.git"] prompt: > Finalize the update using codex-update-compat skill. - Commit the changes, the message should mention that types or/and tests after the update were fixed. + Commit only tracked file changes, and never create an empty commit. + If there are changes, the commit message should mention that types or/and tests after the update were fixed. When creating the final message do not mention: * Validation run details. * Commits. From f0d6ff2656d7186b3b03324e0bec15ec6103f17f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Jul 2026 07:58:19 +0000 Subject: [PATCH 32/45] Update codex to 0.143.0 --- package-lock.json | 86 ++++++------------- package.json | 2 +- src/app-server/MultiAgentMode.ts | 8 +- src/app-server/ResponseItem.ts | 2 +- src/app-server/ThreadId.ts | 5 ++ src/app-server/v2/AppInfo.ts | 2 +- src/app-server/v2/AskForApproval.ts | 2 +- src/app-server/v2/CapabilityRootLocation.ts | 6 +- src/app-server/v2/CodexErrorInfo.ts | 2 +- src/app-server/v2/ConfigRequirements.ts | 3 +- ...onsumeAccountRateLimitResetCreditParams.ts | 7 +- ...cpServerOauthLoginCompletedNotification.ts | 2 +- .../v2/McpServerOauthLoginParams.ts | 2 +- .../v2/McpServerStartupFailureReason.ts | 5 ++ .../v2/McpServerStatusUpdatedNotification.ts | 3 +- src/app-server/v2/McpToolCallAppContext.ts | 2 +- src/app-server/v2/MigrationDetails.ts | 3 +- src/app-server/v2/ModelsRequirements.ts | 6 ++ src/app-server/v2/NewThreadModelDefaults.ts | 6 ++ .../v2/PluginInstallPolicySource.ts | 5 ++ src/app-server/v2/PluginSource.ts | 10 ++- src/app-server/v2/PluginSummary.ts | 7 +- src/app-server/v2/RateLimitResetCredit.ts | 27 ++++++ .../v2/RateLimitResetCreditStatus.ts | 5 ++ .../v2/RateLimitResetCreditsSummary.ts | 11 ++- src/app-server/v2/RateLimitResetType.ts | 5 ++ src/app-server/v2/SkillMigration.ts | 5 ++ src/app-server/v2/Thread.ts | 65 ++++++-------- src/app-server/v2/ThreadExtra.ts | 8 ++ src/app-server/v2/ThreadForkParams.ts | 6 ++ src/app-server/v2/ThreadHistoryMode.ts | 5 ++ src/app-server/v2/ThreadItem.ts | 2 +- src/app-server/v2/ThreadRollbackParams.ts | 3 + src/app-server/v2/Turn.ts | 6 +- src/app-server/v2/index.ts | 10 +++ 35 files changed, 212 insertions(+), 122 deletions(-) create mode 100644 src/app-server/v2/McpServerStartupFailureReason.ts create mode 100644 src/app-server/v2/ModelsRequirements.ts create mode 100644 src/app-server/v2/NewThreadModelDefaults.ts create mode 100644 src/app-server/v2/PluginInstallPolicySource.ts create mode 100644 src/app-server/v2/RateLimitResetCredit.ts create mode 100644 src/app-server/v2/RateLimitResetCreditStatus.ts create mode 100644 src/app-server/v2/RateLimitResetType.ts create mode 100644 src/app-server/v2/SkillMigration.ts create mode 100644 src/app-server/v2/ThreadExtra.ts create mode 100644 src/app-server/v2/ThreadHistoryMode.ts diff --git a/package-lock.json b/package-lock.json index 235a2e4b..43f04caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.2.0", - "@openai/codex": "^0.142.5", + "@openai/codex": "^0.143.0", "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^9.0.1", @@ -929,9 +929,9 @@ } }, "node_modules/@openai/codex": { - "version": "0.142.5", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5.tgz", - "integrity": "sha512-WQEpD7l3k68eIAP0aq28EdR18ENBAf8DyprzFhzNwCOQJSv4nHzpwT8Fl30IJacprko2ZCmUBZjM2u941l2yLw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0.tgz", + "integrity": "sha512-6h53sNtESIYncWVwU7zEjdVajwcad/0H94MOrgGqhwBMa9RRUDVG6DU9E9euC7yRdtrsKDAkJkz/m5moZ6MU3A==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -940,19 +940,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.142.5-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.142.5-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.142.5-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.142.5-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.142.5-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.142.5-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.143.0-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.143.0-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.143.0-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.143.0-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.143.0-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.143.0-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.142.5-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-darwin-arm64.tgz", - "integrity": "sha512-l43p8xv+Z/2/b6fCUc7/FmcQZsaPB7RFizLponGwHAnFOWe3i9Vky69p+up3BUam9AetoQQUv7Mo+2KdaFEqhA==", + "version": "0.143.0-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-darwin-arm64.tgz", + "integrity": "sha512-hs9bPkE1c0+EtbHxxVfhaaGl1u6/LVdhEtOX5t9N+ri54yjMUkzKJLc7MOBa1DYRMDTWT+lPEfeB+8Fv9coTKg==", "cpu": [ "arm64" ], @@ -967,9 +967,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.142.5-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-darwin-x64.tgz", - "integrity": "sha512-yk6A06/VmW7NFsa48OVPaj//g/zeSpd79wjuqfXZwW8ZKRYQm3+wCd3hWjPl79F3QnXvDvM2j3JMIBL3m3GXXg==", + "version": "0.143.0-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-darwin-x64.tgz", + "integrity": "sha512-xHbrb/Qkj+ZZqYkAuio3mcFxpqlg/NNpBDz5iMKlGa7XfK3miUu6R50c9cRW1ZPQVRfRnFhs4l7OhI5u5r+HvA==", "cpu": [ "x64" ], @@ -984,9 +984,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.142.5-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-linux-arm64.tgz", - "integrity": "sha512-77ka5PSnm5HdxdBT99IwntCasmbqevlS0eiC0AtEb6ZXCLkim2gm0AWm+jNYy0EhbssvNK+KghayWo34HMgXeA==", + "version": "0.143.0-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-linux-arm64.tgz", + "integrity": "sha512-1TMx1a/YBEY0SBhPGn8z9HjOpaDssw+WJiIi/+r0CB5uZf6qnALN02TrZTZl2PSUI8olnBf/VccjnwVCrzR38w==", "cpu": [ "arm64" ], @@ -1001,9 +1001,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.142.5-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-linux-x64.tgz", - "integrity": "sha512-pxY+d3NgNE57Y/MApD3/TZUAygxJN6I9h3ZeDUwe67mxWjUxsuapxMRFTKSznCalYbRAeZp752+AAXmUbmguEg==", + "version": "0.143.0-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-linux-x64.tgz", + "integrity": "sha512-B6v6oUgBbjt1bL7yBbRp7e0vLiVOjRdz9Wykotve52Aq2H2TeiCqxM5BSOaFkmNO6VxE0seW7hTi2UZF9x8LLA==", "cpu": [ "x64" ], @@ -1018,9 +1018,9 @@ }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.142.5-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-win32-arm64.tgz", - "integrity": "sha512-65BEqGbUZ7r0ayunIHdBjo5crwgbwKX/6puOcO+VCswUw/dXvDsN2IGcbXB52+bS9U5+FxP783cUHfTT6m40DQ==", + "version": "0.143.0-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-win32-arm64.tgz", + "integrity": "sha512-8ntYPI3IQWZuayL214tYanuYhom23RxHUWRkmay45hGymEW4TwwFqzppKXEzcBXrCF5uSWlOMMG+B2elN6kWuQ==", "cpu": [ "arm64" ], @@ -1035,9 +1035,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.142.5-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.142.5-win32-x64.tgz", - "integrity": "sha512-a+wI4PEx9a2fg6V5ueTTDkOkr1XpEvA5RFXIbo/L2hOfzMmGtyRnbG24bCGu5Q2RSgVxSQV0aLkdb3vdYMNH9A==", + "version": "0.143.0-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-win32-x64.tgz", + "integrity": "sha512-d60HLkzSQ7rdL35xPxH1x3g8DuJjEbhEx1UOK5ivA1PBD9eWvP46rtdHkSvCZyqHUhaHFr5We2SugE9+Y8HhVA==", "cpu": [ "x64" ], @@ -1153,9 +1153,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1173,9 +1170,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1193,9 +1187,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1213,9 +1204,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1233,9 +1221,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1253,9 +1238,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2587,9 +2569,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2611,9 +2590,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2635,9 +2611,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2659,9 +2632,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index 0c534f2f..e8b60dd8 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^1.2.0", - "@openai/codex": "^0.142.5", + "@openai/codex": "^0.143.0", "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^9.0.1", diff --git a/src/app-server/MultiAgentMode.ts b/src/app-server/MultiAgentMode.ts index b59be94f..7784a6f5 100644 --- a/src/app-server/MultiAgentMode.ts +++ b/src/app-server/MultiAgentMode.ts @@ -3,9 +3,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. /** - * Controls whether the model receives multi-agent delegation instructions and, - * when it does, whether it should only spawn sub-agents after an explicit user - * request or may delegate proactively when doing so would help. `none` leaves - * the multi-agent tools available without injecting delegation instructions. + * Controls the effective multi-agent delegation instructions for a turn. `custom` means the + * configured mode hint defines the policy instead of a built-in policy. */ -export type MultiAgentMode = "none" | "explicitRequestOnly" | "proactive"; +export type MultiAgentMode = { "custom": string } | "explicitRequestOnly" | "proactive"; diff --git a/src/app-server/ResponseItem.ts b/src/app-server/ResponseItem.ts index 62547d3e..769b1b08 100644 --- a/src/app-server/ResponseItem.ts +++ b/src/app-server/ResponseItem.ts @@ -20,4 +20,4 @@ id?: string, /** * Set when using the Responses API. */ -call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" }; +call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, namespace?: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" }; diff --git a/src/app-server/ThreadId.ts b/src/app-server/ThreadId.ts index bfb3b4b4..801ffb35 100644 --- a/src/app-server/ThreadId.ts +++ b/src/app-server/ThreadId.ts @@ -2,4 +2,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +/** + * Identifier for a Codex thread. + * + * Codex-generated thread IDs are UUIDv7, and some use cases rely on that. + */ export type ThreadId = string; diff --git a/src/app-server/v2/AppInfo.ts b/src/app-server/v2/AppInfo.ts index ef1f54aa..7145ce9a 100644 --- a/src/app-server/v2/AppInfo.ts +++ b/src/app-server/v2/AppInfo.ts @@ -7,7 +7,7 @@ import type { AppMetadata } from "./AppMetadata"; /** * EXPERIMENTAL - app metadata returned by app-list APIs. */ -export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, distributionChannel: string | null, branding: AppBranding | null, appMetadata: AppMetadata | null, labels: { [key in string]?: string } | null, installUrl: string | null, isAccessible: boolean, +export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, iconAssets: { [key in string]?: string } | null, iconDarkAssets: { [key in string]?: string } | null, distributionChannel: string | null, branding: AppBranding | null, appMetadata: AppMetadata | null, labels: { [key in string]?: string } | null, installUrl: string | null, isAccessible: boolean, /** * Whether this app is enabled in config.toml. * Example: diff --git a/src/app-server/v2/AskForApproval.ts b/src/app-server/v2/AskForApproval.ts index 8d41214e..1d605501 100644 --- a/src/app-server/v2/AskForApproval.ts +++ b/src/app-server/v2/AskForApproval.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type AskForApproval = "untrusted" | "on-failure" | "on-request" | { "granular": { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, } } | "never"; +export type AskForApproval = "untrusted" | "on-request" | { "granular": { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, } } | "never"; diff --git a/src/app-server/v2/CapabilityRootLocation.ts b/src/app-server/v2/CapabilityRootLocation.ts index 6266101e..6c2ac908 100644 --- a/src/app-server/v2/CapabilityRootLocation.ts +++ b/src/app-server/v2/CapabilityRootLocation.ts @@ -5,4 +5,8 @@ /** * Location used to resolve a selected capability root. */ -export type CapabilityRootLocation = { "type": "environment", environmentId: string, path: string, }; +export type CapabilityRootLocation = { "type": "environment", environmentId: string, +/** + * Absolute path for the root in the selected environment. + */ +path: string, }; diff --git a/src/app-server/v2/CodexErrorInfo.ts b/src/app-server/v2/CodexErrorInfo.ts index 6e975abf..ec50328e 100644 --- a/src/app-server/v2/CodexErrorInfo.ts +++ b/src/app-server/v2/CodexErrorInfo.ts @@ -9,4 +9,4 @@ import type { NonSteerableTurnKind } from "./NonSteerableTurnKind"; * 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. */ -export type CodexErrorInfo = "contextWindowExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other"; +export type CodexErrorInfo = "contextWindowExceeded" | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other"; diff --git a/src/app-server/v2/ConfigRequirements.ts b/src/app-server/v2/ConfigRequirements.ts index 3eb9a65a..e2fd573a 100644 --- a/src/app-server/v2/ConfigRequirements.ts +++ b/src/app-server/v2/ConfigRequirements.ts @@ -4,8 +4,9 @@ import type { WebSearchMode } from "../WebSearchMode"; import type { AskForApproval } from "./AskForApproval"; import type { ComputerUseRequirements } from "./ComputerUseRequirements"; +import type { ModelsRequirements } from "./ModelsRequirements"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, models: ModelsRequirements | null}; diff --git a/src/app-server/v2/ConsumeAccountRateLimitResetCreditParams.ts b/src/app-server/v2/ConsumeAccountRateLimitResetCreditParams.ts index c3cef64f..f1c5bf35 100644 --- a/src/app-server/v2/ConsumeAccountRateLimitResetCreditParams.ts +++ b/src/app-server/v2/ConsumeAccountRateLimitResetCreditParams.ts @@ -7,4 +7,9 @@ export type ConsumeAccountRateLimitResetCreditParams = { * Identifies one logical reset attempt. A UUID is recommended; reuse the same value when * retrying that attempt. */ -idempotencyKey: string, }; +idempotencyKey: string, +/** + * Opaque reset-credit identifier to redeem. When omitted, the backend selects the next + * available credit. + */ +creditId?: string | null, }; diff --git a/src/app-server/v2/McpServerOauthLoginCompletedNotification.ts b/src/app-server/v2/McpServerOauthLoginCompletedNotification.ts index 592860ae..cfa66030 100644 --- a/src/app-server/v2/McpServerOauthLoginCompletedNotification.ts +++ b/src/app-server/v2/McpServerOauthLoginCompletedNotification.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type McpServerOauthLoginCompletedNotification = { name: string, success: boolean, error?: string, }; +export type McpServerOauthLoginCompletedNotification = { name: string, threadId: string | null, success: boolean, error?: string, }; diff --git a/src/app-server/v2/McpServerOauthLoginParams.ts b/src/app-server/v2/McpServerOauthLoginParams.ts index a61c3046..ff088b8d 100644 --- a/src/app-server/v2/McpServerOauthLoginParams.ts +++ b/src/app-server/v2/McpServerOauthLoginParams.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type McpServerOauthLoginParams = { name: string, scopes?: Array | null, timeoutSecs?: bigint | null, }; +export type McpServerOauthLoginParams = { name: string, threadId?: string | null, scopes?: Array | null, timeoutSecs?: bigint | null, }; diff --git a/src/app-server/v2/McpServerStartupFailureReason.ts b/src/app-server/v2/McpServerStartupFailureReason.ts new file mode 100644 index 00000000..0373e544 --- /dev/null +++ b/src/app-server/v2/McpServerStartupFailureReason.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerStartupFailureReason = "reauthenticationRequired"; diff --git a/src/app-server/v2/McpServerStatusUpdatedNotification.ts b/src/app-server/v2/McpServerStatusUpdatedNotification.ts index b5c0a905..fd192f22 100644 --- a/src/app-server/v2/McpServerStatusUpdatedNotification.ts +++ b/src/app-server/v2/McpServerStatusUpdatedNotification.ts @@ -1,6 +1,7 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerStartupFailureReason } from "./McpServerStartupFailureReason"; import type { McpServerStartupState } from "./McpServerStartupState"; -export type McpServerStatusUpdatedNotification = { threadId: string | null, name: string, status: McpServerStartupState, error: string | null, }; +export type McpServerStatusUpdatedNotification = { threadId: string | null, name: string, status: McpServerStartupState, error: string | null, failureReason: McpServerStartupFailureReason | null, }; diff --git a/src/app-server/v2/McpToolCallAppContext.ts b/src/app-server/v2/McpToolCallAppContext.ts index 0a834342..e4bc5a11 100644 --- a/src/app-server/v2/McpToolCallAppContext.ts +++ b/src/app-server/v2/McpToolCallAppContext.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type McpToolCallAppContext = { connectorId: string, linkId: string | null, resourceUri: string | null, }; +export type McpToolCallAppContext = { connectorId: string, linkId: string | null, resourceUri: string | null, appName: string | null, templateId: string | null, actionName: string | null, }; diff --git a/src/app-server/v2/MigrationDetails.ts b/src/app-server/v2/MigrationDetails.ts index 4fe87eab..21f77984 100644 --- a/src/app-server/v2/MigrationDetails.ts +++ b/src/app-server/v2/MigrationDetails.ts @@ -6,6 +6,7 @@ import type { HookMigration } from "./HookMigration"; import type { McpServerMigration } from "./McpServerMigration"; import type { PluginsMigration } from "./PluginsMigration"; import type { SessionMigration } from "./SessionMigration"; +import type { SkillMigration } from "./SkillMigration"; import type { SubagentMigration } from "./SubagentMigration"; -export type MigrationDetails = { plugins: Array, sessions: Array, mcpServers: Array, hooks: Array, subagents: Array, commands: Array, }; +export type MigrationDetails = { plugins: Array, skills: Array, sessions: Array, mcpServers: Array, hooks: Array, subagents: Array, commands: Array, }; diff --git a/src/app-server/v2/ModelsRequirements.ts b/src/app-server/v2/ModelsRequirements.ts new file mode 100644 index 00000000..9041fff8 --- /dev/null +++ b/src/app-server/v2/ModelsRequirements.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NewThreadModelDefaults } from "./NewThreadModelDefaults"; + +export type ModelsRequirements = { newThread: NewThreadModelDefaults | null, }; diff --git a/src/app-server/v2/NewThreadModelDefaults.ts b/src/app-server/v2/NewThreadModelDefaults.ts new file mode 100644 index 00000000..fed8b25e --- /dev/null +++ b/src/app-server/v2/NewThreadModelDefaults.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReasoningEffort } from "../ReasoningEffort"; + +export type NewThreadModelDefaults = { model: string | null, modelReasoningEffort: ReasoningEffort | null, serviceTier: string | null, }; diff --git a/src/app-server/v2/PluginInstallPolicySource.ts b/src/app-server/v2/PluginInstallPolicySource.ts new file mode 100644 index 00000000..caa39628 --- /dev/null +++ b/src/app-server/v2/PluginInstallPolicySource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginInstallPolicySource = "WORKSPACE_SETTING" | "IMPLICIT_CANONICAL_APP"; diff --git a/src/app-server/v2/PluginSource.ts b/src/app-server/v2/PluginSource.ts index f6e86719..c7ba3bcf 100644 --- a/src/app-server/v2/PluginSource.ts +++ b/src/app-server/v2/PluginSource.ts @@ -3,4 +3,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -export type PluginSource = { "type": "local", path: AbsolutePathBuf, } | { "type": "git", url: string, path: string | null, refName: string | null, sha: string | null, } | { "type": "remote" }; +export type PluginSource = { "type": "local", path: AbsolutePathBuf, } | { "type": "git", url: string, path: string | null, refName: string | null, sha: string | null, } | { "type": "npm", package: string, +/** + * Optional npm version or version range. + */ +version: string | null, +/** + * Optional HTTPS registry URL. Authentication stays in the user's npm config. + */ +registry: string | null, } | { "type": "remote" }; diff --git a/src/app-server/v2/PluginSummary.ts b/src/app-server/v2/PluginSummary.ts index 268349cb..10fd5aa2 100644 --- a/src/app-server/v2/PluginSummary.ts +++ b/src/app-server/v2/PluginSummary.ts @@ -4,6 +4,7 @@ import type { PluginAuthPolicy } from "./PluginAuthPolicy"; import type { PluginAvailability } from "./PluginAvailability"; import type { PluginInstallPolicy } from "./PluginInstallPolicy"; +import type { PluginInstallPolicySource } from "./PluginInstallPolicySource"; import type { PluginInterface } from "./PluginInterface"; import type { PluginShareContext } from "./PluginShareContext"; import type { PluginSource } from "./PluginSource"; @@ -13,6 +14,10 @@ export type PluginSummary = { id: string, * Backend remote plugin identifier when available. */ remotePluginId: string | null, +/** + * Version advertised by the remote marketplace backend when available. + */ +version: string | null, /** * Version of the locally materialized plugin package when available. */ @@ -20,7 +25,7 @@ localVersion: string | null, name: string, /** * Remote sharing context associated with this plugin when available. */ -shareContext: PluginShareContext | null, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, authPolicy: PluginAuthPolicy, +shareContext: PluginShareContext | null, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, installPolicySource: PluginInstallPolicySource | null, authPolicy: PluginAuthPolicy, /** * Availability state for installing and using the plugin. */ diff --git a/src/app-server/v2/RateLimitResetCredit.ts b/src/app-server/v2/RateLimitResetCredit.ts new file mode 100644 index 00000000..514c1558 --- /dev/null +++ b/src/app-server/v2/RateLimitResetCredit.ts @@ -0,0 +1,27 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus"; +import type { RateLimitResetType } from "./RateLimitResetType"; + +export type RateLimitResetCredit = { +/** + * Opaque backend identifier for this reset credit. + */ +id: string, resetType: RateLimitResetType, status: RateLimitResetCreditStatus, +/** + * Unix timestamp in seconds when the credit was granted. + */ +grantedAt: number, +/** + * Unix timestamp in seconds when the credit expires, or `null` if it does not expire. + */ +expiresAt: number | null, +/** + * Backend-provided display title for this credit, or `null` when unavailable. + */ +title: string | null, +/** + * Backend-provided display description for this credit, or `null` when unavailable. + */ +description: string | null, }; diff --git a/src/app-server/v2/RateLimitResetCreditStatus.ts b/src/app-server/v2/RateLimitResetCreditStatus.ts new file mode 100644 index 00000000..fa15861b --- /dev/null +++ b/src/app-server/v2/RateLimitResetCreditStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimitResetCreditStatus = "available" | "redeeming" | "redeemed" | "unknown"; diff --git a/src/app-server/v2/RateLimitResetCreditsSummary.ts b/src/app-server/v2/RateLimitResetCreditsSummary.ts index e42dd8a7..46a8eee2 100644 --- a/src/app-server/v2/RateLimitResetCreditsSummary.ts +++ b/src/app-server/v2/RateLimitResetCreditsSummary.ts @@ -1,5 +1,14 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitResetCredit } from "./RateLimitResetCredit"; -export type RateLimitResetCreditsSummary = { availableCount: bigint, }; +export type RateLimitResetCreditsSummary = { availableCount: bigint, +/** + * Detail rows for available reset credits, when the backend provides them. + * + * `null` means only `availableCount` is known, while an empty array means details were fetched + * and no available credits were returned. The backend may cap this list, so its length can be + * less than `availableCount`. + */ +credits: Array | null, }; diff --git a/src/app-server/v2/RateLimitResetType.ts b/src/app-server/v2/RateLimitResetType.ts new file mode 100644 index 00000000..718145bf --- /dev/null +++ b/src/app-server/v2/RateLimitResetType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimitResetType = "codexRateLimits" | "unknown"; diff --git a/src/app-server/v2/SkillMigration.ts b/src/app-server/v2/SkillMigration.ts new file mode 100644 index 00000000..0555ffc8 --- /dev/null +++ b/src/app-server/v2/SkillMigration.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillMigration = { name: string, }; diff --git a/src/app-server/v2/Thread.ts b/src/app-server/v2/Thread.ts index 1c288ccb..49e2411f 100644 --- a/src/app-server/v2/Thread.ts +++ b/src/app-server/v2/Thread.ts @@ -8,87 +8,70 @@ import type { ThreadSource } from "./ThreadSource"; import type { ThreadStatus } from "./ThreadStatus"; import type { Turn } from "./Turn"; -export type Thread = { id: string, -/** +export type Thread = {/** + * Identifier for this thread. Codex-generated thread IDs are UUIDv7. + */ +id: string, /** * Session id shared by threads that belong to the same session tree. */ -sessionId: string, -/** +sessionId: string, /** * Source thread id when this thread was created by forking another thread. */ -forkedFromId: string | null, -/** +forkedFromId: string | null, /** * The ID of the parent thread. This will only be set if this thread is a subagent. */ -parentThreadId: string | null, -/** +parentThreadId: string | null, /** * Usually the first user message in the thread, if available. */ -preview: string, -/** +preview: string, /** * Whether the thread is ephemeral and should not be materialized on disk. */ -ephemeral: boolean, -/** +ephemeral: boolean, /** * Model provider used for this thread (for example, 'openai'). */ -modelProvider: string, -/** +modelProvider: string, /** * Unix timestamp (in seconds) when the thread was created. */ -createdAt: number, -/** +createdAt: number, /** * Unix timestamp (in seconds) when the thread was last updated. */ -updatedAt: number, -/** +updatedAt: number, /** * Unix timestamp (in seconds) used for thread recency ordering. */ -recencyAt: number | null, -/** +recencyAt: number | null, /** * Current runtime status for the thread. */ -status: ThreadStatus, -/** +status: ThreadStatus, /** * [UNSTABLE] Path to the thread on disk. */ -path: string | null, -/** +path: string | null, /** * Working directory captured for the thread. */ -cwd: AbsolutePathBuf, -/** +cwd: AbsolutePathBuf, /** * Version of the CLI that created the thread. */ -cliVersion: string, -/** +cliVersion: string, /** * Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). */ -source: SessionSource, -/** +source: SessionSource, /** * Optional analytics source classification for this thread. */ -threadSource: ThreadSource | null, -/** +threadSource: ThreadSource | null, /** * Optional random unique nickname assigned to an AgentControl-spawned sub-agent. */ -agentNickname: string | null, -/** +agentNickname: string | null, /** * Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. */ -agentRole: string | null, -/** +agentRole: string | null, /** * Optional Git metadata captured when the thread was created. */ -gitInfo: GitInfo | null, -/** +gitInfo: GitInfo | null, /** * Optional user-facing thread title. */ -name: string | null, -/** +name: string | null, /** * Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` * (when `includeTurns` is true) responses. * For all other responses and notifications returning a Thread, * the turns field will be an empty list. */ -turns: Array, }; +turns: Array}; diff --git a/src/app-server/v2/ThreadExtra.ts b/src/app-server/v2/ThreadExtra.ts new file mode 100644 index 00000000..aa35e45f --- /dev/null +++ b/src/app-server/v2/ThreadExtra.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Extra app-server data for a thread. + */ +export type ThreadExtra = Record; diff --git a/src/app-server/v2/ThreadForkParams.ts b/src/app-server/v2/ThreadForkParams.ts index c5109b2c..3ace4d44 100644 --- a/src/app-server/v2/ThreadForkParams.ts +++ b/src/app-server/v2/ThreadForkParams.ts @@ -18,6 +18,12 @@ import type { ThreadSource } from "./ThreadSource"; * Prefer using thread_id whenever possible. */ export type ThreadForkParams = {threadId: string, /** + * Optional last turn id to fork through, inclusive. + * + * When specified, turns after `last_turn_id` are omitted from the fork. + * The referenced turn cannot be in progress. + */ +lastTurnId?: string | null, /** * Configuration overrides for the forked thread, if any. */ model?: string | null, modelProvider?: string | null, serviceTier?: string | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** diff --git a/src/app-server/v2/ThreadHistoryMode.ts b/src/app-server/v2/ThreadHistoryMode.ts new file mode 100644 index 00000000..db0f2d82 --- /dev/null +++ b/src/app-server/v2/ThreadHistoryMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadHistoryMode = "legacy" | "paginated"; diff --git a/src/app-server/v2/ThreadItem.ts b/src/app-server/v2/ThreadItem.ts index eb8c6486..c611ab07 100644 --- a/src/app-server/v2/ThreadItem.ts +++ b/src/app-server/v2/ThreadItem.ts @@ -105,4 +105,4 @@ reasoningEffort: ReasoningEffort | null, /** * Last known status of the target agents, when available. */ -agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; +agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; diff --git a/src/app-server/v2/ThreadRollbackParams.ts b/src/app-server/v2/ThreadRollbackParams.ts index 1c938e3b..af416d18 100644 --- a/src/app-server/v2/ThreadRollbackParams.ts +++ b/src/app-server/v2/ThreadRollbackParams.ts @@ -2,6 +2,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +/** + * DEPRECATED: `thread/rollback` will be removed soon. + */ export type ThreadRollbackParams = { threadId: string, /** * The number of turns to drop from the end of the thread. Must be >= 1. diff --git a/src/app-server/v2/Turn.ts b/src/app-server/v2/Turn.ts index 6505ec34..b8680256 100644 --- a/src/app-server/v2/Turn.ts +++ b/src/app-server/v2/Turn.ts @@ -6,7 +6,11 @@ import type { TurnError } from "./TurnError"; import type { TurnItemsView } from "./TurnItemsView"; import type { TurnStatus } from "./TurnStatus"; -export type Turn = { id: string, +export type Turn = { +/** + * Identifier for this turn. Codex-generated turn IDs are UUIDv7. + */ +id: string, /** * Thread items currently included in this turn payload. */ diff --git a/src/app-server/v2/index.ts b/src/app-server/v2/index.ts index bc31a10a..8c0cc055 100644 --- a/src/app-server/v2/index.ts +++ b/src/app-server/v2/index.ts @@ -261,6 +261,7 @@ export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthL export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams"; export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse"; export type { McpServerRefreshResponse } from "./McpServerRefreshResponse"; +export type { McpServerStartupFailureReason } from "./McpServerStartupFailureReason"; export type { McpServerStartupState } from "./McpServerStartupState"; export type { McpServerStatus } from "./McpServerStatus"; export type { McpServerStatusDetail } from "./McpServerStatusDetail"; @@ -289,6 +290,7 @@ export type { ModelServiceTier } from "./ModelServiceTier"; export type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; export type { ModelVerification } from "./ModelVerification"; export type { ModelVerificationNotification } from "./ModelVerificationNotification"; +export type { ModelsRequirements } from "./ModelsRequirements"; export type { NetworkAccess } from "./NetworkAccess"; export type { NetworkApprovalContext } from "./NetworkApprovalContext"; export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; @@ -297,6 +299,7 @@ export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; export type { NetworkRequirements } from "./NetworkRequirements"; export type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission"; +export type { NewThreadModelDefaults } from "./NewThreadModelDefaults"; export type { NonSteerableTurnKind } from "./NonSteerableTurnKind"; export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; @@ -319,6 +322,7 @@ export type { PluginDetail } from "./PluginDetail"; export type { PluginHookSummary } from "./PluginHookSummary"; export type { PluginInstallParams } from "./PluginInstallParams"; export type { PluginInstallPolicy } from "./PluginInstallPolicy"; +export type { PluginInstallPolicySource } from "./PluginInstallPolicySource"; export type { PluginInstallResponse } from "./PluginInstallResponse"; export type { PluginInstalledParams } from "./PluginInstalledParams"; export type { PluginInstalledResponse } from "./PluginInstalledResponse"; @@ -362,7 +366,10 @@ export type { ProcessTerminalSize } from "./ProcessTerminalSize"; export type { ProductSurface } from "./ProductSurface"; export type { ProfileV2 } from "./ProfileV2"; export type { RateLimitReachedType } from "./RateLimitReachedType"; +export type { RateLimitResetCredit } from "./RateLimitResetCredit"; +export type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus"; export type { RateLimitResetCreditsSummary } from "./RateLimitResetCreditsSummary"; +export type { RateLimitResetType } from "./RateLimitResetType"; export type { RateLimitSnapshot } from "./RateLimitSnapshot"; export type { RateLimitWindow } from "./RateLimitWindow"; export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification"; @@ -399,6 +406,7 @@ export type { SkillDependencies } from "./SkillDependencies"; export type { SkillErrorInfo } from "./SkillErrorInfo"; export type { SkillInterface } from "./SkillInterface"; export type { SkillMetadata } from "./SkillMetadata"; +export type { SkillMigration } from "./SkillMigration"; export type { SkillRequestApprovalParams } from "./SkillRequestApprovalParams"; export type { SkillRequestApprovalResponse } from "./SkillRequestApprovalResponse"; export type { SkillScope } from "./SkillScope"; @@ -440,6 +448,7 @@ export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; export type { ThreadDeleteParams } from "./ThreadDeleteParams"; export type { ThreadDeleteResponse } from "./ThreadDeleteResponse"; export type { ThreadDeletedNotification } from "./ThreadDeletedNotification"; +export type { ThreadExtra } from "./ThreadExtra"; export type { ThreadForkParams } from "./ThreadForkParams"; export type { ThreadForkResponse } from "./ThreadForkResponse"; export type { ThreadGoal } from "./ThreadGoal"; @@ -452,6 +461,7 @@ export type { ThreadGoalSetParams } from "./ThreadGoalSetParams"; export type { ThreadGoalSetResponse } from "./ThreadGoalSetResponse"; export type { ThreadGoalStatus } from "./ThreadGoalStatus"; export type { ThreadGoalUpdatedNotification } from "./ThreadGoalUpdatedNotification"; +export type { ThreadHistoryMode } from "./ThreadHistoryMode"; export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams"; export type { ThreadInjectItemsResponse } from "./ThreadInjectItemsResponse"; export type { ThreadItem } from "./ThreadItem"; From 80691adec1b3e9d077bbaeed37303f5321172341 Mon Sep 17 00:00:00 2001 From: "Ilia.Shulgin" Date: Wed, 8 Jul 2026 10:03:14 +0200 Subject: [PATCH 33/45] build: Delete branches for obsolete Codex update PRs --- .github/workflows/codex-update.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codex-update.yml b/.github/workflows/codex-update.yml index f144323c..ee00607f 100644 --- a/.github/workflows/codex-update.yml +++ b/.github/workflows/codex-update.yml @@ -142,5 +142,6 @@ jobs: fi gh pr close "$pr_number" \ + --delete-branch \ --comment "Closing obsolete update PR" done From 27cb818b20fb663642766ba0c268c021c1dddf29 Mon Sep 17 00:00:00 2001 From: "Ilia.Shulgin" Date: Wed, 8 Jul 2026 10:08:00 +0200 Subject: [PATCH 34/45] test: Wrap expected content in file approval e2e test prompt --- src/__tests__/CodexACPAgent/e2e/acp-e2e-file-approval.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/CodexACPAgent/e2e/acp-e2e-file-approval.test.ts b/src/__tests__/CodexACPAgent/e2e/acp-e2e-file-approval.test.ts index c2a6fee2..51182971 100644 --- a/src/__tests__/CodexACPAgent/e2e/acp-e2e-file-approval.test.ts +++ b/src/__tests__/CodexACPAgent/e2e/acp-e2e-file-approval.test.ts @@ -89,7 +89,7 @@ async function editFileDirectly( sessionId, prompt: [{ type: "text", - text: `Create ${filePath} by editing files directly. Content must be exactly: ${FILE_CONTENT}. Do not use shell commands.`, + text: `Create ${filePath} by editing files directly. Content must be exactly: '${FILE_CONTENT}'. Do not use shell commands.`, }], }); if (expectSuccess) { From 843a504004328abf8a45f0b9c2c2d46021851f7a Mon Sep 17 00:00:00 2001 From: nikita-ashihmin Date: Wed, 8 Jul 2026 13:58:11 +0400 Subject: [PATCH 35/45] Expose Codex agent message phases in ACP chunks (#267) --- src/CodexAcpServer.ts | 6 +- src/CodexEventHandler.ts | 17 +++- src/ContentChunks.ts | 29 +++++-- src/ResponseItemHistoryFallback.ts | 9 +- .../agent-message-events.test.ts | 85 +++++++++++++++++++ .../data/agent-message-phases.json | 42 +++++++++ .../response-item-history-fallback.test.ts | 28 ++++++ 7 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 src/__tests__/CodexACPAgent/agent-message-events.test.ts create mode 100644 src/__tests__/CodexACPAgent/data/agent-message-phases.json diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 6454ca86..22d5e703 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -69,6 +69,7 @@ import packageJson from "../package.json"; import {isJetBrains2026_1Client} from "./JBUtils"; import {resolveTerminalOutputMode, type TerminalOutputMode} from "./TerminalOutputMode"; import { + createCodexMessagePhaseMeta, createAgentTextMessageChunk, createAgentTextThoughtChunk, createUserMessageChunk, @@ -983,12 +984,15 @@ export class CodexAcpServer { case "subAgentActivity": case "sleep": return []; - case "agentMessage": + case "agentMessage": { + const meta = createCodexMessagePhaseMeta(item.phase); return [{ sessionUpdate: "agent_message_chunk", messageId: item.id, content: { type: "text", text: item.text }, + ...(meta ? { _meta: meta } : {}), }]; + } case "reasoning": return this.createReasoningUpdates(item); case "fileChange": diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 5ed8ad79..c2a4d672 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -56,6 +56,7 @@ import { import { stripShellPrefix } from "./CommandUtils"; import {createTerminalOutputMeta, type TerminalOutputMode} from "./TerminalOutputMode"; import { + createCodexMessagePhaseMeta, createAgentTextMessageChunk, createAgentTextThoughtChunk, } from "./ContentChunks"; @@ -74,6 +75,7 @@ export class CodexEventHandler { private readonly seenReasoningDeltaItemIds = new Set(); private readonly terminalCommandIds = new Set(); private readonly terminalCommandOutputIds = new Set(); + private readonly agentMessagePhases = new Map(); constructor(connection: AcpClientConnection, sessionState: SessionState) { this.connection = connection; @@ -230,7 +232,8 @@ export class CodexEventHandler { } private async createTextEvent(event: AgentMessageDeltaNotification): Promise { - return createAgentTextMessageChunk(event.delta, event.itemId); + const phase = this.agentMessagePhases.get(event.itemId) ?? null; + return createAgentTextMessageChunk(event.delta, event.itemId, createCodexMessagePhaseMeta(phase)); } private async createConfigWarningEvent(event: ConfigWarningNotification): Promise { @@ -331,11 +334,13 @@ export class CodexEventHandler { return createImageGenerationStartUpdate(event.item); case "collabAgentToolCall": return createCollabAgentToolCallUpdate(event.item); + case "agentMessage": + this.rememberAgentMessagePhase(event.item); + return null; case "subAgentActivity": case "sleep": case "userMessage": case "hookPrompt": - case "agentMessage": case "reasoning": case "enteredReviewMode": case "exitedReviewMode": @@ -383,6 +388,9 @@ export class CodexEventHandler { return createWebSearchCompleteUpdate(event.item); case "collabAgentToolCall": return createCollabAgentToolCallCompleteUpdate(event.item); + case "agentMessage": + this.rememberAgentMessagePhase(event.item); + return null; case "exitedReviewMode": return this.createExitedReviewModeEvent(event.item); case "contextCompaction": @@ -392,7 +400,6 @@ export class CodexEventHandler { case "sleep": case "userMessage": case "hookPrompt": - case "agentMessage": case "enteredReviewMode": case "plan": return null; @@ -400,6 +407,10 @@ export class CodexEventHandler { } } + private rememberAgentMessagePhase(item: ThreadItem & { type: "agentMessage" }): void { + this.agentMessagePhases.set(item.id, item.phase); + } + private createCompletedReasoningEvent(item: ThreadItem & { type: "reasoning" }): UpdateSessionEvent | null { const parts = item.summary.length > 0 ? item.summary : item.content; const text = parts.filter(part => part.length > 0).join("\n\n"); diff --git a/src/ContentChunks.ts b/src/ContentChunks.ts index b111d347..2bef82e8 100644 --- a/src/ContentChunks.ts +++ b/src/ContentChunks.ts @@ -1,52 +1,67 @@ import type {ContentBlock} from "@agentclientprotocol/sdk"; import type {UpdateSessionEvent} from "./ACPSessionConnection"; -export function createUserMessageChunk(content: ContentBlock, messageId?: string): UpdateSessionEvent { +type AcpMeta = Record; + +export function createCodexMessagePhaseMeta(phase: string | null | undefined): AcpMeta | undefined { + if (!phase) { + return undefined; + } + return { codex: { phase } }; +} + +export function createUserMessageChunk(content: ContentBlock, messageId?: string, meta?: AcpMeta): UpdateSessionEvent { if (messageId) { return { sessionUpdate: "user_message_chunk", messageId, content, + ...(meta ? { _meta: meta } : {}), }; } return { sessionUpdate: "user_message_chunk", content, + ...(meta ? { _meta: meta } : {}), }; } -export function createAgentMessageChunk(content: ContentBlock, messageId?: string): UpdateSessionEvent { +export function createAgentMessageChunk(content: ContentBlock, messageId?: string, meta?: AcpMeta): UpdateSessionEvent { if (messageId) { return { sessionUpdate: "agent_message_chunk", messageId, content, + ...(meta ? { _meta: meta } : {}), }; } return { sessionUpdate: "agent_message_chunk", content, + ...(meta ? { _meta: meta } : {}), }; } -export function createAgentThoughtChunk(content: ContentBlock, messageId?: string): UpdateSessionEvent { +export function createAgentThoughtChunk(content: ContentBlock, messageId?: string, meta?: AcpMeta): UpdateSessionEvent { if (messageId) { return { sessionUpdate: "agent_thought_chunk", messageId, content, + ...(meta ? { _meta: meta } : {}), }; } return { sessionUpdate: "agent_thought_chunk", content, + ...(meta ? { _meta: meta } : {}), }; } -export function createAgentTextMessageChunk(text: string, messageId?: string): UpdateSessionEvent { - return createAgentMessageChunk({type: "text", text}, messageId); +export function createAgentTextMessageChunk(text: string, messageId?: string, meta?: AcpMeta): UpdateSessionEvent { + return createAgentMessageChunk({type: "text", text}, messageId, meta); } -export function createAgentTextThoughtChunk(text: string, messageId?: string): UpdateSessionEvent { - return createAgentThoughtChunk({type: "text", text}, messageId); +export function createAgentTextThoughtChunk(text: string, messageId?: string, meta?: AcpMeta): UpdateSessionEvent { + return createAgentThoughtChunk({type: "text", text}, messageId, meta); } diff --git a/src/ResponseItemHistoryFallback.ts b/src/ResponseItemHistoryFallback.ts index 11e49d68..0730455f 100644 --- a/src/ResponseItemHistoryFallback.ts +++ b/src/ResponseItemHistoryFallback.ts @@ -6,6 +6,7 @@ import { stripShellPrefix } from "./CommandUtils"; import type { CommandAction, Thread, ThreadItem } from "./app-server/v2"; import { createCommandActionEvent } from "./CodexToolCallMapper"; import { createTerminalOutputMeta, type TerminalOutputMode } from "./TerminalOutputMode"; +import { createAgentMessageChunk, createCodexMessagePhaseMeta } from "./ContentChunks"; type JsonRecord = Record; type AcpToolCallEvent = Extract; @@ -234,10 +235,10 @@ function createMessageUpdates(item: JsonRecord): UpdateSessionEvent[] { return []; } - return contentBlocksFromResponseContent(item["content"]).map((content) => ({ - sessionUpdate: "agent_message_chunk", - content, - })); + const phase = stringValue(item["phase"]); + return contentBlocksFromResponseContent(item["content"]).map((content) => ( + createAgentMessageChunk(content, undefined, createCodexMessagePhaseMeta(phase)) + )); } function createEventMsgUpdates(record: JsonRecord): UpdateSessionEvent[] | null { diff --git a/src/__tests__/CodexACPAgent/agent-message-events.test.ts b/src/__tests__/CodexACPAgent/agent-message-events.test.ts new file mode 100644 index 00000000..e4147d5f --- /dev/null +++ b/src/__tests__/CodexACPAgent/agent-message-events.test.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ServerNotification } from "../../app-server"; +import type { SessionState } from "../../CodexAcpServer"; +import { AgentMode } from "../../AgentMode"; +import { + createCodexMockTestFixture, + createTestSessionState, + setupPromptAndSendNotifications, + type CodexMockTestFixture +} from "../acp-test-utils"; + +describe("CodexEventHandler - agent message events", () => { + let mockFixture: CodexMockTestFixture; + const sessionId = "test-session-id"; + + beforeEach(() => { + mockFixture = createCodexMockTestFixture(); + vi.clearAllMocks(); + }); + + const sessionState: SessionState = createTestSessionState({ + sessionId, + currentModelId: "model-id[effort]", + agentMode: AgentMode.DEFAULT_AGENT_MODE + }); + + it("includes Codex message phase metadata on streamed agent message chunks", async () => { + const notifications: ServerNotification[] = [ + { + method: "item/started", + params: { + threadId: sessionId, + turnId: "turn-1", + startedAtMs: 0, + item: { + type: "agentMessage", + id: "commentary-message", + text: "", + phase: "commentary", + memoryCitation: null, + }, + }, + }, + { + method: "item/agentMessage/delta", + params: { + threadId: sessionId, + turnId: "turn-1", + itemId: "commentary-message", + delta: "Checking the relevant event mapping.", + }, + }, + { + method: "item/started", + params: { + threadId: sessionId, + turnId: "turn-1", + startedAtMs: 10, + item: { + type: "agentMessage", + id: "final-message", + text: "", + phase: "final_answer", + memoryCitation: null, + }, + }, + }, + { + method: "item/agentMessage/delta", + params: { + threadId: sessionId, + turnId: "turn-1", + itemId: "final-message", + delta: "Yes, here is the answer.", + }, + }, + ]; + + await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, notifications); + + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( + "data/agent-message-phases.json" + ); + }); +}); diff --git a/src/__tests__/CodexACPAgent/data/agent-message-phases.json b/src/__tests__/CodexACPAgent/data/agent-message-phases.json new file mode 100644 index 00000000..8f1a4b1e --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/agent-message-phases.json @@ -0,0 +1,42 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "agent_message_chunk", + "messageId": "commentary-message", + "content": { + "type": "text", + "text": "Checking the relevant event mapping." + }, + "_meta": { + "codex": { + "phase": "commentary" + } + } + } + } + ] +} +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "agent_message_chunk", + "messageId": "final-message", + "content": { + "type": "text", + "text": "Yes, here is the answer." + }, + "_meta": { + "codex": { + "phase": "final_answer" + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts b/src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts index 72301794..aaeef097 100644 --- a/src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts +++ b/src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts @@ -55,6 +55,26 @@ describe("ResponseItemHistoryFallback", () => { expect(thoughtTexts(updates)).toEqual(["Need to inspect the directory."]); }); + it("preserves assistant message phase metadata from response items", () => { + const updates = parseResponseItemHistoryFallback(jsonl([ + { + type: "response_item", + payload: { + type: "message", + role: "assistant", + content: [{ type: "output_text", text: "Final answer text." }], + phase: "final_answer", + }, + }, + functionCall("call-missing", "ls"), + functionCallOutput("call-missing", "Chunk ID: missing\nProcess exited with code 0\nOutput:\nREADME.md\n"), + ]), "terminal_output"); + + expect(agentMessageMetas(updates)).toEqual([ + { codex: { phase: "final_answer" } }, + ]); + }); + it("marks exec command outputs without exit footers failed when they report command errors", () => { const updates = parseResponseItemHistoryFallback(jsonl([ functionCall("call-read-failed", "cat missing.txt"), @@ -133,3 +153,11 @@ function thoughtTexts(updates: UpdateSessionEvent[] | null): string[] { )) .flatMap((update) => update.content.type === "text" ? [update.content.text] : []); } + +function agentMessageMetas(updates: UpdateSessionEvent[] | null): unknown[] { + return (updates ?? []) + .filter((update): update is Extract => ( + update.sessionUpdate === "agent_message_chunk" + )) + .map((update) => update._meta); +} From 8872bc2b95d3f32afdef6c7b6c02f36f4f60f574 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:54:00 +0200 Subject: [PATCH 36/45] build(deps): bump openai/codex-action in the github-actions group (#273) Bumps the github-actions group with 1 update: [openai/codex-action](https://github.com/openai/codex-action). Updates `openai/codex-action` from 1.9 to 1.11 - [Changelog](https://github.com/openai/codex-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/codex-action/compare/10cb888d2ed3b99867f7e7ccff174a861a75aeb6...52fe01ec70a42f454c9d2ebd47598f9fd6893d56) --- updated-dependencies: - dependency-name: openai/codex-action dependency-version: '1.11' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codex-update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codex-update.yml b/.github/workflows/codex-update.yml index ee00607f..9632f479 100644 --- a/.github/workflows/codex-update.yml +++ b/.github/workflows/codex-update.yml @@ -83,7 +83,7 @@ jobs: - name: Finalize update with Codex id: codex-finalize - uses: openai/codex-action@10cb888d2ed3b99867f7e7ccff174a861a75aeb6 + uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} codex-version: ${{ env.VERSION }} From 3f78868e480dcf5b4965fe277399e83b7dc1134a Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 9 Jul 2026 11:19:25 +0200 Subject: [PATCH 37/45] Support MCP elicitation if the client does (#268) * Support ACP form elicitation capability * Auto-resolve stale user input elicitations * Support Other answers in user input elicitations * Complete accepted URL elicitations on resolution * Disable app-server elicitation opt-in * Remove request_user_input handling --- src/CodexAcpServer.ts | 14 +- src/CodexElicitationHandler.ts | 347 +++++++++++++++++- src/ElicitationCapabilities.ts | 13 + .../CodexACPAgent/elicitation-events.test.ts | 207 +++++++++++ .../CodexACPAgent/initialize.test.ts | 13 + src/__tests__/acp-test-utils.ts | 18 +- 6 files changed, 590 insertions(+), 22 deletions(-) create mode 100644 src/ElicitationCapabilities.ts diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 22d5e703..375a0573 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -145,6 +145,7 @@ export class CodexAcpServer { private readonly getRecentStderr: () => string; private readonly availableCommands: CodexCommands; private clientInfo: acp.Implementation | null; + private clientCapabilities: acp.ClientCapabilities | null; private terminalOutputMode: TerminalOutputMode; private booleanConfigOptionsSupported: boolean; @@ -176,6 +177,7 @@ export class CodexAcpServer { this.getExitCode = getExitCode ?? (() => null); this.getRecentStderr = getRecentStderr ?? (() => ""); this.clientInfo = null; + this.clientCapabilities = null; this.terminalOutputMode = "terminal_output_delta"; this.booleanConfigOptionsSupported = false; this.availableCommands = new CodexCommands( @@ -191,6 +193,7 @@ export class CodexAcpServer { ): Promise { logger.log("Initialize request received"); this.clientInfo = _params.clientInfo ?? null; + this.clientCapabilities = _params.clientCapabilities ?? null; this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities); this.booleanConfigOptionsSupported = clientSupportsBooleanConfigOptions(_params.clientCapabilities); await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params)); @@ -1441,10 +1444,15 @@ export class CodexAcpServer { try { const eventHandler = new CodexEventHandler(this.connection, sessionState); const approvalHandler = new CodexApprovalHandler(this.connection, sessionState, activePrompt.signal); - const elicitationHandler = new CodexElicitationHandler(this.connection, sessionState, activePrompt.signal); + const elicitationHandler = new CodexElicitationHandler( + this.connection, + sessionState, + this.clientCapabilities, + activePrompt.signal, + ); await this.codexAcpClient.subscribeToSessionEvents(params.sessionId, - (event) => { - elicitationHandler.handleNotification(event); + async (event) => { + await elicitationHandler.handleNotification(event); return eventHandler.handleNotification(event); }, approvalHandler, diff --git a/src/CodexElicitationHandler.ts b/src/CodexElicitationHandler.ts index 2a15df04..3b4498e0 100644 --- a/src/CodexElicitationHandler.ts +++ b/src/CodexElicitationHandler.ts @@ -2,6 +2,7 @@ import * as acp from "@agentclientprotocol/sdk"; import type { SessionState } from "./CodexAcpServer"; import type { ElicitationHandler } from "./CodexAppServerClient"; import type { ServerNotification } from "./app-server"; +import type {JsonValue} from "./app-server/serde_json/JsonValue"; import type { ItemCompletedNotification, ItemStartedNotification, @@ -11,6 +12,10 @@ import type { import { logger } from "./Logger"; import { McpApprovalOptionId } from "./McpApprovalOptionId"; import type {AcpClientConnection} from "./ACPSessionConnection"; +import { + clientSupportsFormElicitation, + clientSupportsUrlElicitation, +} from "./ElicitationCapabilities"; // Standard elicitation options (non-tool-call approval). const ELICITATION_OPTIONS: acp.PermissionOption[] = [ @@ -19,6 +24,17 @@ const ELICITATION_OPTIONS: acp.PermissionOption[] = [ ]; type PersistValue = "session" | "always"; +type ToolApprovalPersistValue = PersistValue | "once"; + +type McpElicitationContext = { + isToolApproval: boolean; + persistOptions: Set; + correlatedCallId: string | undefined; +}; +type AcpBackedMcpElicitationParams = Extract< + McpServerElicitationRequestParams, + { mode: "form" } | { mode: "url" } +>; /** * Parses the `persist` field from the elicitation request `_meta`. @@ -48,6 +64,161 @@ function isMcpToolCallApproval(meta: unknown): boolean { ); } +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function normalizeJsonValue(value: unknown): JsonValue { + if (value === null || value === undefined) { + return null; + } + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return value; + } + if (typeof value === "bigint") { + return Number(value); + } + if (Array.isArray(value)) { + return value.map(normalizeJsonValue); + } + if (typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .filter(([, nested]) => nested !== undefined) + .map(([key, nested]) => [key, normalizeJsonValue(nested)]) + ); + } + return String(value); +} + +function normalizeJsonObject(value: Record): Record { + return Object.fromEntries( + Object.entries(value) + .filter(([, nested]) => nested !== undefined) + .map(([key, nested]) => [key, normalizeJsonValue(nested)]) + ); +} + +function normalizeElicitationSchema(value: unknown): acp.ElicitationSchema { + const normalized = normalizeElicitationSchemaValue(value); + if (!isRecord(normalized)) { + return { type: "object", properties: {} }; + } + + return { + ...normalized, + type: "object", + } as acp.ElicitationSchema; +} + +function normalizeElicitationSchemaValue(value: unknown): unknown { + if (typeof value === "bigint") { + return Number(value); + } + if (Array.isArray(value)) { + return value.map(normalizeElicitationSchemaValue); + } + if (!isRecord(value)) { + return value; + } + + const result: Record = Object.fromEntries( + Object.entries(value) + .filter(([, nested]) => nested !== undefined) + .map(([key, nested]) => [key, normalizeElicitationSchemaValue(nested)]) + ); + + if ( + result["type"] === "string" && + Array.isArray(result["enum"]) && + Array.isArray(result["enumNames"]) && + !Array.isArray(result["oneOf"]) + ) { + const values = result["enum"]; + const names = result["enumNames"]; + result["oneOf"] = values.map((value, index) => ({ + const: String(value), + title: String(names[index] ?? value), + })); + delete result["enum"]; + delete result["enumNames"]; + } + + return result; +} + +function metaRecord(meta: unknown): Record | null { + return isRecord(meta) ? meta : null; +} + +function persistChoiceOption(value: ToolApprovalPersistValue): acp.EnumOption { + switch (value) { + case "once": + return { const: "once", title: "Allow once" }; + case "session": + return { const: "session", title: "Allow for this session" }; + case "always": + return { const: "always", title: "Allow and don't ask again" }; + } +} + +function addPersistChoiceToSchema( + schema: acp.ElicitationSchema, + persistOptions: Set +): acp.ElicitationSchema { + if (persistOptions.size === 0) { + return schema; + } + + const choices: ToolApprovalPersistValue[] = ["once"]; + if (persistOptions.has("session")) choices.push("session"); + if (persistOptions.has("always")) choices.push("always"); + + return { + ...schema, + properties: { + ...schema.properties, + persist: { + type: "string", + title: "Approval scope", + oneOf: choices.map(persistChoiceOption), + default: "once", + }, + }, + required: Array.from(new Set([...(schema.required ?? []), "persist"])), + }; +} + +function contentRecord(content: unknown): Record { + return isRecord(content) ? content as Record : {}; +} + +function jsonObjectOrNull( + content: Record +): JsonValue | null { + const entries = Object.entries(content); + if (entries.length === 0) { + return null; + } + return Object.fromEntries(entries.map(([key, value]) => [key, normalizeJsonValue(value)])); +} + +function elicitationResponseMeta( + response: acp.CreateElicitationResponse, + context: McpElicitationContext, + persist: unknown = undefined +): JsonValue | null { + const responseMeta = metaRecord(response._meta); + const meta = responseMeta ? normalizeJsonObject(responseMeta) : {}; + if (context.isToolApproval) { + delete meta["persist"]; + } + if (persist === "session" || persist === "always") { + meta["persist"] = persist; + } + return Object.keys(meta).length === 0 ? null : meta; +} + /** * Builds the ACP permission options for an MCP tool call approval elicitation. * Always includes "Allow Once"; adds session/always persist options when advertised. @@ -69,6 +240,7 @@ function buildToolApprovalOptions(persistOptions: Set): acp.Permis export class CodexElicitationHandler implements ElicitationHandler { private readonly connection: AcpClientConnection; private readonly sessionState: SessionState; + private readonly clientCapabilities: acp.ClientCapabilities | null; private readonly cancellationSignal: AbortSignal | undefined; // In Rust, the MCP elicitation handler receives ElicitationRequestEvent directly from the MCP // protocol layer, where id is set to "mcp_tool_call_approval_" — the call ID is extracted @@ -86,14 +258,23 @@ export class CodexElicitationHandler implements ElicitationHandler { // call's elicitation before starting the next, so there is at most one pending approval per // (threadId, serverName). private readonly pendingMcpApprovals = new Map(); + // The app-server handler exposes URL elicitationId, while serverRequest/resolved only exposes + // threadId here, so accepted URL elicitations are completed at thread scope. + private readonly pendingUrlElicitations = new Map>(); - constructor(connection: AcpClientConnection, sessionState: SessionState, cancellationSignal?: AbortSignal) { + constructor( + connection: AcpClientConnection, + sessionState: SessionState, + clientCapabilities: acp.ClientCapabilities | null = null, + cancellationSignal?: AbortSignal + ) { this.connection = connection; this.sessionState = sessionState; + this.clientCapabilities = clientCapabilities; this.cancellationSignal = cancellationSignal; } - handleNotification(notification: ServerNotification): void { + async handleNotification(notification: ServerNotification): Promise { switch (notification.method) { case "item/started": this.handleItemStarted(notification.params); @@ -103,6 +284,7 @@ export class CodexElicitationHandler implements ElicitationHandler { return; case "serverRequest/resolved": this.clearThread(notification.params.threadId); + await this.completeUrlElicitations(notification.params.threadId); return; default: return; @@ -113,7 +295,22 @@ export class CodexElicitationHandler implements ElicitationHandler { params: McpServerElicitationRequestParams ): Promise { try { - const { request, correlatedCallId } = this.buildPermissionRequest(params); + const context = this.createMcpElicitationContext(params); + if (this.shouldUseAcpElicitation(params)) { + const response = await this.connection.request( + acp.methods.client.elicitation.create, + this.buildElicitationRequest(params, context), + this.requestOptions(), + ); + const result = this.convertElicitationResponse(response, context); + if (params.mode === "url" && result.action === "accept") { + this.trackUrlElicitation(params.threadId, params.elicitationId); + } + await this.publishAcceptedMcpToolApproval(context, result.action === "accept"); + return result; + } + + const { request, correlatedCallId } = this.buildPermissionRequest(params, context); const response = await this.connection.request( acp.methods.client.session.requestPermission, request, @@ -128,7 +325,7 @@ export class CodexElicitationHandler implements ElicitationHandler { }); } } - return this.convertResponse(response); + return this.convertPermissionResponse(response); } catch (error) { logger.error("Error handling MCP elicitation request", error); return { action: "cancel", content: null, _meta: null }; @@ -139,8 +336,66 @@ export class CodexElicitationHandler implements ElicitationHandler { return this.cancellationSignal ? {cancellationSignal: this.cancellationSignal} : undefined; } - private buildPermissionRequest( + private createMcpElicitationContext(params: McpServerElicitationRequestParams): McpElicitationContext { + const isToolApproval = isMcpToolCallApproval(params._meta); + const persistOptions = parsePersistOptions(params._meta); + const correlatedCallId = isToolApproval && (params.mode === "form" || params.mode === "openai/form") + ? this.popPendingApproval(params.threadId, params.serverName) + : undefined; + return { isToolApproval, persistOptions, correlatedCallId }; + } + + private shouldUseAcpElicitation( params: McpServerElicitationRequestParams + ): params is AcpBackedMcpElicitationParams { + switch (params.mode) { + case "form": + return clientSupportsFormElicitation(this.clientCapabilities); + case "url": + return clientSupportsUrlElicitation(this.clientCapabilities); + case "openai/form": + return false; + } + } + + private buildElicitationRequest( + params: AcpBackedMcpElicitationParams, + context: McpElicitationContext + ): acp.CreateElicitationRequest { + const base = { + sessionId: this.sessionState.sessionId, + ...(context.correlatedCallId ? { toolCallId: context.correlatedCallId } : {}), + message: params.message, + _meta: metaRecord(params._meta), + }; + + switch (params.mode) { + case "form": { + const requestedSchema = context.isToolApproval + ? addPersistChoiceToSchema( + normalizeElicitationSchema(params.requestedSchema), + context.persistOptions, + ) + : normalizeElicitationSchema(params.requestedSchema); + return { + ...base, + mode: "form", + requestedSchema, + }; + } + case "url": + return { + ...base, + mode: "url", + url: params.url, + elicitationId: params.elicitationId, + }; + } + } + + private buildPermissionRequest( + params: McpServerElicitationRequestParams, + context: McpElicitationContext ): { request: acp.RequestPermissionRequest; correlatedCallId: string | undefined } { const sessionId = this.sessionState.sessionId; const messageContent: acp.ToolCallContent = { @@ -148,17 +403,12 @@ export class CodexElicitationHandler implements ElicitationHandler { content: { type: "text", text: params.message }, }; - const meta = params._meta; - const isToolApproval = isMcpToolCallApproval(meta); - const options = isToolApproval - ? buildToolApprovalOptions(parsePersistOptions(meta)) + const options = context.isToolApproval + ? buildToolApprovalOptions(context.persistOptions) : ELICITATION_OPTIONS; if (params.mode === "form" || params.mode === "openai/form") { - const correlatedCallId = isToolApproval - ? this.popPendingApproval(params.threadId, params.serverName) - : undefined; - if (correlatedCallId !== undefined) { + if (context.correlatedCallId !== undefined) { // The tool call item is already visible in the IDE conversation history because // item/started was emitted before the elicitation request. Sending content or // rawInput here would duplicate that information in the approval widget. @@ -166,7 +416,7 @@ export class CodexElicitationHandler implements ElicitationHandler { request: { sessionId, toolCall: { - toolCallId: correlatedCallId, + toolCallId: context.correlatedCallId, kind: "execute", status: "pending", // content: [messageContent], — omitted: already rendered via item/started @@ -175,7 +425,7 @@ export class CodexElicitationHandler implements ElicitationHandler { _meta: { is_mcp_tool_approval: true }, options, }, - correlatedCallId, + correlatedCallId: context.correlatedCallId, }; } return { @@ -183,12 +433,12 @@ export class CodexElicitationHandler implements ElicitationHandler { sessionId, toolCall: { toolCallId: `elicitation-${params.serverName}`, - kind: isToolApproval ? "execute" : "other", + kind: context.isToolApproval ? "execute" : "other", status: "pending", content: [messageContent], rawInput: { serverName: params.serverName, schema: params.requestedSchema }, }, - ...(isToolApproval ? { _meta: { is_mcp_tool_approval: true } } : {}), + ...(context.isToolApproval ? { _meta: { is_mcp_tool_approval: true } } : {}), options, }, correlatedCallId: undefined, @@ -211,7 +461,7 @@ export class CodexElicitationHandler implements ElicitationHandler { } } - private convertResponse( + private convertPermissionResponse( response: acp.RequestPermissionResponse ): McpServerElicitationRequestResponse { if (response.outcome.outcome === "cancelled") { @@ -231,6 +481,67 @@ export class CodexElicitationHandler implements ElicitationHandler { return { action: "decline", content: null, _meta: null }; } + private convertElicitationResponse( + response: acp.CreateElicitationResponse, + context: McpElicitationContext + ): McpServerElicitationRequestResponse { + switch (response.action) { + case "accept": { + const content = contentRecord(response.content); + const persist = context.isToolApproval ? content["persist"] : undefined; + if (persist === "session" || persist === "always" || persist === "once") { + delete content["persist"]; + } + return { + action: "accept", + content: jsonObjectOrNull(content), + _meta: elicitationResponseMeta(response, context, persist), + }; + } + case "decline": + return { action: "decline", content: null, _meta: elicitationResponseMeta(response, context) }; + case "cancel": + return { action: "cancel", content: null, _meta: elicitationResponseMeta(response, context) }; + default: + return { action: "cancel", content: null, _meta: null }; + } + } + + private async publishAcceptedMcpToolApproval( + context: McpElicitationContext, + accepted: boolean + ): Promise { + if (!accepted || context.correlatedCallId === undefined) { + return; + } + await this.connection.notify(acp.methods.client.session.update, { + sessionId: this.sessionState.sessionId, + update: { sessionUpdate: "tool_call_update", toolCallId: context.correlatedCallId, status: "in_progress" }, + }); + } + + private trackUrlElicitation(threadId: string, elicitationId: string): void { + const existing = this.pendingUrlElicitations.get(threadId); + if (existing) { + existing.add(elicitationId); + return; + } + this.pendingUrlElicitations.set(threadId, new Set([elicitationId])); + } + + private async completeUrlElicitations(threadId: string): Promise { + const elicitationIds = this.pendingUrlElicitations.get(threadId); + if (!elicitationIds) { + return; + } + this.pendingUrlElicitations.delete(threadId); + for (const elicitationId of elicitationIds) { + await this.connection.notify(acp.methods.client.elicitation.complete, { + elicitationId, + }); + } + } + private handleItemStarted(event: ItemStartedNotification): void { if (event.item.type !== "mcpToolCall") { return; diff --git a/src/ElicitationCapabilities.ts b/src/ElicitationCapabilities.ts new file mode 100644 index 00000000..51030b0d --- /dev/null +++ b/src/ElicitationCapabilities.ts @@ -0,0 +1,13 @@ +import type * as acp from "@agentclientprotocol/sdk"; + +export function clientSupportsFormElicitation( + clientCapabilities?: acp.ClientCapabilities | null +): boolean { + return clientCapabilities?.elicitation?.form != null; +} + +export function clientSupportsUrlElicitation( + clientCapabilities?: acp.ClientCapabilities | null +): boolean { + return clientCapabilities?.elicitation?.url != null; +} diff --git a/src/__tests__/CodexACPAgent/elicitation-events.test.ts b/src/__tests__/CodexACPAgent/elicitation-events.test.ts index e796d8ba..3dd84e1a 100644 --- a/src/__tests__/CodexACPAgent/elicitation-events.test.ts +++ b/src/__tests__/CodexACPAgent/elicitation-events.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as acp from "@agentclientprotocol/sdk"; import type { McpServerElicitationRequestParams } from '../../app-server/v2'; import { createCodexMockTestFixture, createTestSessionState, type CodexMockTestFixture } from '../acp-test-utils'; import type { SessionState } from '../../CodexAcpServer'; @@ -49,7 +50,94 @@ describe('Elicitation Events', () => { }; } + async function setupSessionWithPendingPromptAndCapabilities(clientCapabilities: acp.ClientCapabilities) { + await fixture.getCodexAcpAgent().initialize({ + protocolVersion: acp.PROTOCOL_VERSION, + clientCapabilities, + }); + return setupSessionWithPendingPrompt(); + } + describe('Form mode elicitation', () => { + it('should use ACP form elicitation when the client supports it', async () => { + const { promptPromise, completeTurn } = await setupSessionWithPendingPromptAndCapabilities({ + elicitation: { form: {} }, + }); + fixture.setElicitationResponse({ + action: 'accept', + content: { username: 'octocat' }, + _meta: { source: 'client' }, + }); + + const params: McpServerElicitationRequestParams = { + threadId: sessionId, turnId: 'turn-1', serverName: 'test-server', + mode: 'form', _meta: null, message: 'Please provide your username', + requestedSchema: { type: 'object', properties: { username: { type: 'string' } }, required: ['username'] }, + }; + + const response = await fixture.sendServerRequest('mcpServer/elicitation/request', params); + expect(response).toEqual({ action: 'accept', content: { username: 'octocat' }, _meta: { source: 'client' } }); + + const [elicitationEvent] = fixture.getAcpConnectionEvents([]); + expect(elicitationEvent).toEqual({ + method: 'createElicitation', + args: [{ + sessionId, + mode: 'form', + message: 'Please provide your username', + requestedSchema: { + type: 'object', + properties: { username: { type: 'string' } }, + required: ['username'], + }, + _meta: null, + }], + }); + + completeTurn(); + await promptPromise; + }); + + it('should normalize legacy enumNames schemas for ACP form elicitation', async () => { + const { promptPromise, completeTurn } = await setupSessionWithPendingPromptAndCapabilities({ + elicitation: { form: {} }, + }); + fixture.setElicitationResponse({ + action: 'accept', + content: { color: 'blue' }, + }); + + const params: McpServerElicitationRequestParams = { + threadId: sessionId, turnId: 'turn-1', serverName: 'test-server', + mode: 'form', _meta: null, message: 'Pick a color', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'string', + enum: ['red', 'blue'], + enumNames: ['Red', 'Blue'], + }, + }, + required: ['color'], + }, + }; + + await fixture.sendServerRequest('mcpServer/elicitation/request', params); + + const [elicitationEvent] = fixture.getAcpConnectionEvents([]); + expect(elicitationEvent?.args[0].requestedSchema.properties.color).toEqual({ + type: 'string', + oneOf: [ + { const: 'red', title: 'Red' }, + { const: 'blue', title: 'Blue' }, + ], + }); + + completeTurn(); + await promptPromise; + }); + it('should map accept to accept', async () => { const { promptPromise, completeTurn } = setupSessionWithPendingPrompt(); fixture.setPermissionResponse({ outcome: { outcome: 'selected', optionId: 'accept' } }); @@ -131,6 +219,78 @@ describe('Elicitation Events', () => { }); describe('MCP tool call approval elicitation', () => { + it('should use ACP form elicitation for MCP tool approval when supported', async () => { + const { promptPromise, completeTurn } = await setupSessionWithPendingPromptAndCapabilities({ + elicitation: { form: {} }, + }); + fixture.setElicitationResponse({ + action: 'accept', + content: { persist: 'always' }, + _meta: { source: 'client' }, + }); + + fixture.sendServerNotification({ + method: 'item/started', + params: { + threadId: sessionId, + turnId: 'turn-1', + startedAtMs: 0, + item: { + type: "mcpToolCall", + id: "call-id", + server: "tool-server", + tool: "tool-name", + status: "inProgress", + arguments: { argument: "example" }, + appContext: null, + pluginId: null, + result: null, + error: null, + durationMs: null, + }, + }, + }); + await fixture.getCodexAcpClient().waitForSessionNotifications(sessionId); + fixture.clearAcpConnectionDump(); + + const params: McpServerElicitationRequestParams = { + threadId: sessionId, turnId: 'turn-1', serverName: 'tool-server', + mode: 'form', + _meta: { codex_approval_kind: 'mcp_tool_call', persist: ['session', 'always'] }, + message: 'Allow tool call?', + requestedSchema: { type: 'object', properties: {} }, + }; + + const response = await fixture.sendServerRequest('mcpServer/elicitation/request', params); + expect(response).toEqual({ action: 'accept', content: null, _meta: { source: 'client', persist: 'always' } }); + + const events = fixture.getAcpConnectionEvents(['_meta']); + expect(events[0]).toMatchObject({ + method: 'createElicitation', + args: [{ + sessionId, + toolCallId: 'call-id', + mode: 'form', + message: 'Allow tool call?', + }], + }); + expect(events[0]!.args[0].requestedSchema.properties.persist.oneOf).toEqual([ + { const: 'once', title: 'Allow once' }, + { const: 'session', title: 'Allow for this session' }, + { const: 'always', title: "Allow and don't ask again" }, + ]); + expect(events[1]).toEqual({ + method: 'sessionUpdate', + args: [{ + sessionId, + update: { sessionUpdate: 'tool_call_update', toolCallId: 'call-id', status: 'in_progress' }, + }], + }); + + completeTurn(); + await promptPromise; + }); + it('should show Allow/session/always/Decline options when all persist values advertised', async () => { const { promptPromise, completeTurn } = setupSessionWithPendingPrompt(); fixture.setPermissionResponse({ outcome: { outcome: 'selected', optionId: McpApprovalOptionId.AllowOnce } }); @@ -373,6 +533,53 @@ describe('Elicitation Events', () => { }); describe('URL mode elicitation', () => { + it('should use ACP URL elicitation when the client supports it', async () => { + const { promptPromise, completeTurn } = await setupSessionWithPendingPromptAndCapabilities({ + elicitation: { url: {} }, + }); + fixture.setElicitationResponse({ action: 'accept', _meta: { source: 'client' } }); + + const params: McpServerElicitationRequestParams = { + threadId: sessionId, turnId: 'turn-1', serverName: 'auth-server', + mode: 'url', _meta: null, message: 'Please authorize access', + url: 'https://example.com/authorize', elicitationId: 'elicit-123', + }; + + const response = await fixture.sendServerRequest('mcpServer/elicitation/request', params); + expect(response).toEqual({ action: 'accept', content: null, _meta: { source: 'client' } }); + + const [elicitationEvent] = fixture.getAcpConnectionEvents([]); + expect(elicitationEvent).toEqual({ + method: 'createElicitation', + args: [{ + sessionId, + mode: 'url', + message: 'Please authorize access', + url: 'https://example.com/authorize', + elicitationId: 'elicit-123', + _meta: null, + }], + }); + + fixture.sendServerNotification({ + method: 'serverRequest/resolved', + params: { + threadId: sessionId, + requestId: 'request-1', + }, + }); + await fixture.getCodexAcpClient().waitForSessionNotifications(sessionId); + + const [, completeElicitationEvent] = fixture.getAcpConnectionEvents([]); + expect(completeElicitationEvent).toEqual({ + method: 'completeElicitation', + args: [{ elicitationId: 'elicit-123' }], + }); + + completeTurn(); + await promptPromise; + }); + it('should map accept to accept for URL mode', async () => { const { promptPromise, completeTurn } = setupSessionWithPendingPrompt(); fixture.setPermissionResponse({ outcome: { outcome: 'selected', optionId: 'accept' } }); diff --git a/src/__tests__/CodexACPAgent/initialize.test.ts b/src/__tests__/CodexACPAgent/initialize.test.ts index e7050101..0528629e 100644 --- a/src/__tests__/CodexACPAgent/initialize.test.ts +++ b/src/__tests__/CodexACPAgent/initialize.test.ts @@ -84,6 +84,19 @@ describe('CodexACPAgent - initialize', () => { ])); }); + it('should not opt into experimental app-server capabilities for ACP elicitation support', async () => { + await agent.initialize({ + protocolVersion: acp.PROTOCOL_VERSION, + clientCapabilities: { + elicitation: { form: {}, url: {} }, + }, + }); + + expect(mockCodexConnection.sendRequest).toHaveBeenCalledWith("initialize", expect.objectContaining({ + capabilities: null, + })); + }); + it('should advertise API key auth with the legacy metadata method', () => { expect(getCodexAuthMethods()).toEqual(expect.arrayContaining([ expect.objectContaining({ diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index 2f50d49d..456701f0 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -1,5 +1,5 @@ import * as acp from "@agentclientprotocol/sdk"; -import type {McpServerStdio, RequestPermissionResponse} from "@agentclientprotocol/sdk"; +import type {CreateElicitationResponse, McpServerStdio, RequestPermissionResponse} from "@agentclientprotocol/sdk"; import {CodexAcpClient} from '../CodexAcpClient'; import {CodexAppServerClient, type CodexConnectionEvent} from '../CodexAppServerClient'; import {startCodexConnection} from "../CodexJsonRpcConnection"; @@ -42,6 +42,12 @@ function normalizeAcpConnectionEvent(event: MethodCallEvent): MethodCallEvent { if (event.method === "request" && event.args[0] === acp.methods.client.session.requestPermission) { return {method: "requestPermission", args: [event.args[1]]}; } + if (event.method === "request" && event.args[0] === acp.methods.client.elicitation.create) { + return {method: "createElicitation", args: [event.args[1]]}; + } + if (event.method === "notify" && event.args[0] === acp.methods.client.elicitation.complete) { + return {method: "completeElicitation", args: [event.args[1]]}; + } if (event.method === "notify" && event.args[0] === acp.methods.client.session.update) { return {method: "sessionUpdate", args: [event.args[1]]}; } @@ -238,6 +244,7 @@ export interface CodexMockTestFixture extends TestFixture { sendServerNotification(notification: ServerNotification | Record): void, sendServerRequest(method: string, params: unknown): Promise, setPermissionResponse(response: RequestPermissionResponse): void, + setElicitationResponse(response: CreateElicitationResponse | Promise): void, } /** @@ -255,6 +262,9 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { const permissionState: { response: RequestPermissionResponse } = { response: { outcome: { outcome: 'cancelled' } } }; + const elicitationState: { response: CreateElicitationResponse | Promise } = { + response: { action: 'cancel' } + }; const mockCodexConnection = { sendRequest: () => Promise.resolve(undefined), @@ -276,6 +286,9 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { if (args[0] === acp.methods.client.session.requestPermission) { return permissionState.response; } + if (args[0] === acp.methods.client.elicitation.create) { + return elicitationState.response; + } return { mock: "Mocked return" }; }); returnValues.set('requestPermission', () => permissionState.response); @@ -313,6 +326,9 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { setPermissionResponse(response: RequestPermissionResponse): void { permissionState.response = response; }, + setElicitationResponse(response: CreateElicitationResponse | Promise): void { + elicitationState.response = response; + }, }; } From 8d13a10b3b42763901b17ae5b44ae0d670ec5ff3 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 9 Jul 2026 12:39:58 +0200 Subject: [PATCH 38/45] Update to ACP SDK 1.2.1 (#275) Also uses the elicitation helpers --- package-lock.json | 200 ++++++++++-------- package.json | 2 +- src/CodexElicitationHandler.ts | 43 ++-- .../CodexACPAgent/elicitation-events.test.ts | 44 ++++ 4 files changed, 185 insertions(+), 104 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43f04caa..96f97e7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.0", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "^1.2.0", + "@agentclientprotocol/sdk": "^1.2.1", "@openai/codex": "^0.143.0", "diff": "^9.0.0", "open": "^11.0.0", @@ -29,9 +29,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.2.0.tgz", - "integrity": "sha512-rJ3766M2eouZsNCaK21JbPD++JLBZ0PWq38dzj1sA4/i0njaQEkiC2F5v7MlizFW6akX+qu5Ks8R00Xw+YMHKA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.2.1.tgz", + "integrity": "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" @@ -1051,9 +1051,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.138.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", - "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", "funding": { @@ -1061,9 +1061,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", - "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -1078,9 +1078,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", - "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -1095,9 +1095,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", - "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -1112,9 +1112,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", - "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -1129,9 +1129,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", - "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -1146,13 +1146,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", - "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1163,13 +1166,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", - "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1180,13 +1186,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", - "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1197,13 +1206,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", - "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1214,13 +1226,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", - "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1231,13 +1246,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", - "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1248,9 +1266,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", - "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -1265,9 +1283,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", - "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ "wasm32" ], @@ -1284,9 +1302,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", - "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -1301,9 +1319,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", - "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -1368,9 +1386,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", - "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", "dependencies": { @@ -1557,9 +1575,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "dev": true, "license": "MIT", "dependencies": { @@ -2569,6 +2587,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2590,6 +2611,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2611,6 +2635,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2632,6 +2659,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3133,13 +3163,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", - "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.138.0", + "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3149,21 +3179,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.4", - "@rolldown/binding-darwin-arm64": "1.1.4", - "@rolldown/binding-darwin-x64": "1.1.4", - "@rolldown/binding-freebsd-x64": "1.1.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", - "@rolldown/binding-linux-arm64-gnu": "1.1.4", - "@rolldown/binding-linux-arm64-musl": "1.1.4", - "@rolldown/binding-linux-ppc64-gnu": "1.1.4", - "@rolldown/binding-linux-s390x-gnu": "1.1.4", - "@rolldown/binding-linux-x64-gnu": "1.1.4", - "@rolldown/binding-linux-x64-musl": "1.1.4", - "@rolldown/binding-openharmony-arm64": "1.1.4", - "@rolldown/binding-wasm32-wasi": "1.1.4", - "@rolldown/binding-win32-arm64-msvc": "1.1.4", - "@rolldown/binding-win32-x64-msvc": "1.1.4" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/router": { @@ -3456,9 +3486,9 @@ } }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -3609,16 +3639,16 @@ } }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", + "picomatch": "^4.0.5", "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "bin": { diff --git a/package.json b/package.json index e8b60dd8..52035bce 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "vitest": "^4.1.10" }, "dependencies": { - "@agentclientprotocol/sdk": "^1.2.0", + "@agentclientprotocol/sdk": "^1.2.1", "@openai/codex": "^0.143.0", "diff": "^9.0.0", "open": "^11.0.0", diff --git a/src/CodexElicitationHandler.ts b/src/CodexElicitationHandler.ts index 3b4498e0..b99bb51b 100644 --- a/src/CodexElicitationHandler.ts +++ b/src/CodexElicitationHandler.ts @@ -485,26 +485,33 @@ export class CodexElicitationHandler implements ElicitationHandler { response: acp.CreateElicitationResponse, context: McpElicitationContext ): McpServerElicitationRequestResponse { - switch (response.action) { - case "accept": { - const content = contentRecord(response.content); - const persist = context.isToolApproval ? content["persist"] : undefined; - if (persist === "session" || persist === "always" || persist === "once") { - delete content["persist"]; - } - return { - action: "accept", - content: jsonObjectOrNull(content), - _meta: elicitationResponseMeta(response, context, persist), - }; + if (acp.CreateElicitationResponse.isAccept(response)) { + const content = contentRecord(response.content); + const persist = context.isToolApproval ? content["persist"] : undefined; + if (persist === "session" || persist === "always" || persist === "once") { + delete content["persist"]; } - case "decline": - return { action: "decline", content: null, _meta: elicitationResponseMeta(response, context) }; - case "cancel": - return { action: "cancel", content: null, _meta: elicitationResponseMeta(response, context) }; - default: - return { action: "cancel", content: null, _meta: null }; + return { + action: "accept", + content: jsonObjectOrNull(content), + _meta: elicitationResponseMeta(response, context, persist), + }; + } + + if (acp.CreateElicitationResponse.isDecline(response)) { + return { action: "decline", content: null, _meta: elicitationResponseMeta(response, context) }; + } + + if (acp.CreateElicitationResponse.isCancel(response)) { + return { action: "cancel", content: null, _meta: elicitationResponseMeta(response, context) }; } + + if (acp.CreateElicitationResponse.isCustom(response)) { + return { action: "cancel", content: null, _meta: null }; + } + + // Malformed known variants match none of the SDK guards. + return { action: "cancel", content: null, _meta: null }; } private async publishAcceptedMcpToolApproval( diff --git a/src/__tests__/CodexACPAgent/elicitation-events.test.ts b/src/__tests__/CodexACPAgent/elicitation-events.test.ts index 3dd84e1a..8bd45e54 100644 --- a/src/__tests__/CodexACPAgent/elicitation-events.test.ts +++ b/src/__tests__/CodexACPAgent/elicitation-events.test.ts @@ -138,6 +138,50 @@ describe('Elicitation Events', () => { await promptPromise; }); + it('should map custom ACP elicitation actions to cancel', async () => { + const { promptPromise, completeTurn } = await setupSessionWithPendingPromptAndCapabilities({ + elicitation: { form: {} }, + }); + fixture.setElicitationResponse({ + action: 'defer', + _meta: { source: 'client' }, + }); + + const params: McpServerElicitationRequestParams = { + threadId: sessionId, turnId: 'turn-1', serverName: 'test-server', + mode: 'form', _meta: null, message: 'Please provide your username', + requestedSchema: { type: 'object', properties: { username: { type: 'string' } }, required: ['username'] }, + }; + + const response = await fixture.sendServerRequest('mcpServer/elicitation/request', params); + expect(response).toEqual({ action: 'cancel', content: null, _meta: null }); + + completeTurn(); + await promptPromise; + }); + + it('should not treat malformed ACP elicitation accept responses as accepted', async () => { + const { promptPromise, completeTurn } = await setupSessionWithPendingPromptAndCapabilities({ + elicitation: { form: {} }, + }); + fixture.setElicitationResponse({ + action: 'accept', + content: { username: { nested: 'invalid' } }, + } as acp.CreateElicitationResponse); + + const params: McpServerElicitationRequestParams = { + threadId: sessionId, turnId: 'turn-1', serverName: 'test-server', + mode: 'form', _meta: null, message: 'Please provide your username', + requestedSchema: { type: 'object', properties: { username: { type: 'string' } }, required: ['username'] }, + }; + + const response = await fixture.sendServerRequest('mcpServer/elicitation/request', params); + expect(response).toEqual({ action: 'cancel', content: null, _meta: null }); + + completeTurn(); + await promptPromise; + }); + it('should map accept to accept', async () => { const { promptPromise, completeTurn } = setupSessionWithPendingPrompt(); fixture.setPermissionResponse({ outcome: { outcome: 'selected', optionId: 'accept' } }); From f9f0994a42f0284fe9f213e3185606ea63d40977 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Jul 2026 13:04:06 +0000 Subject: [PATCH 39/45] Release v1.1.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96f97e7f..0df40615 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentclientprotocol/codex-acp", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentclientprotocol/codex-acp", - "version": "1.1.0", + "version": "1.1.1", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.2.1", diff --git a/package.json b/package.json index 52035bce..a6ac6ac1 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.1.0", + "version": "1.1.1", "description": "", "main": "dist/index.js", "bin": { From 6ff8d010daccb5e2426f32b7e0d8cf576647164a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Jul 2026 17:34:46 +0000 Subject: [PATCH 40/45] Update codex to 0.144.0 --- package-lock.json | 86 ++++++++----------------- package.json | 2 +- src/app-server/AuthMode.ts | 2 +- src/app-server/ImageGenerationItem.ts | 3 +- src/app-server/WebSearchItem.ts | 4 +- src/app-server/v2/AppToolApproval.ts | 2 +- src/app-server/v2/LoginAccountParams.ts | 3 +- src/app-server/v2/LoginAppBrand.ts | 5 ++ src/app-server/v2/ThreadItem.ts | 6 +- src/app-server/v2/index.ts | 1 + 10 files changed, 46 insertions(+), 68 deletions(-) create mode 100644 src/app-server/v2/LoginAppBrand.ts diff --git a/package-lock.json b/package-lock.json index 0df40615..872cb841 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.2.1", - "@openai/codex": "^0.143.0", + "@openai/codex": "^0.144.0", "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^9.0.1", @@ -929,9 +929,9 @@ } }, "node_modules/@openai/codex": { - "version": "0.143.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0.tgz", - "integrity": "sha512-6h53sNtESIYncWVwU7zEjdVajwcad/0H94MOrgGqhwBMa9RRUDVG6DU9E9euC7yRdtrsKDAkJkz/m5moZ6MU3A==", + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.0.tgz", + "integrity": "sha512-QFh6f+v5QUx/Vg0HjIl9HB94p7aDLBDkZjc4IXX5RXUcXHPVCZNb6Hl2R49Og/fqW7orgZkeDcgWfRANUa1WoQ==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -940,19 +940,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.143.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.143.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.143.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.143.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.143.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.143.0-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.144.0-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.144.0-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.144.0-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.144.0-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.144.0-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.144.0-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.143.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-darwin-arm64.tgz", - "integrity": "sha512-hs9bPkE1c0+EtbHxxVfhaaGl1u6/LVdhEtOX5t9N+ri54yjMUkzKJLc7MOBa1DYRMDTWT+lPEfeB+8Fv9coTKg==", + "version": "0.144.0-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.0-darwin-arm64.tgz", + "integrity": "sha512-rqFAJdOa2I0VRgepVsSZeLxs96+Y+LXTjccOOvH6894FyaFAYPZ/o+6hgpB1iGHxxdoY/DsGa8jrJC8Leqn9Kg==", "cpu": [ "arm64" ], @@ -967,9 +967,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.143.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-darwin-x64.tgz", - "integrity": "sha512-xHbrb/Qkj+ZZqYkAuio3mcFxpqlg/NNpBDz5iMKlGa7XfK3miUu6R50c9cRW1ZPQVRfRnFhs4l7OhI5u5r+HvA==", + "version": "0.144.0-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.0-darwin-x64.tgz", + "integrity": "sha512-4p2jxRbN+Khg5UQzpkzT9upFj+qkEF/abmdvrtflkkWmVKP6Nt+yi8ospdqv9PDqvQ9SotPvX7iXaFaeUTrtmA==", "cpu": [ "x64" ], @@ -984,9 +984,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.143.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-linux-arm64.tgz", - "integrity": "sha512-1TMx1a/YBEY0SBhPGn8z9HjOpaDssw+WJiIi/+r0CB5uZf6qnALN02TrZTZl2PSUI8olnBf/VccjnwVCrzR38w==", + "version": "0.144.0-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.0-linux-arm64.tgz", + "integrity": "sha512-k++xhZrn9P3laO00Q92APG6mdOFDD66nUBo+8ExCa1NXi2pjLEMLC4+UNJTUUtUT1PEflOZ5pDKxPXgzaiFFFg==", "cpu": [ "arm64" ], @@ -1001,9 +1001,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.143.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-linux-x64.tgz", - "integrity": "sha512-B6v6oUgBbjt1bL7yBbRp7e0vLiVOjRdz9Wykotve52Aq2H2TeiCqxM5BSOaFkmNO6VxE0seW7hTi2UZF9x8LLA==", + "version": "0.144.0-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.0-linux-x64.tgz", + "integrity": "sha512-GmKtQeX+cO9lN7mQD1FEVcXYEMLMgMByHwZdvlluH0bj/+c2ind3hwbRtE3eECFDekNhEiB80Ez0FfbkyFQqoA==", "cpu": [ "x64" ], @@ -1018,9 +1018,9 @@ }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.143.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-win32-arm64.tgz", - "integrity": "sha512-8ntYPI3IQWZuayL214tYanuYhom23RxHUWRkmay45hGymEW4TwwFqzppKXEzcBXrCF5uSWlOMMG+B2elN6kWuQ==", + "version": "0.144.0-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.0-win32-arm64.tgz", + "integrity": "sha512-e2yGSgwdzrT1SoJMoOzWD58WBEsIaAMZpEchuV2VGkE2T955SG7dn7EyVQTQcy7/rdpE8aEDktZ/1eQQfjkdtQ==", "cpu": [ "arm64" ], @@ -1035,9 +1035,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.143.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.143.0-win32-x64.tgz", - "integrity": "sha512-d60HLkzSQ7rdL35xPxH1x3g8DuJjEbhEx1UOK5ivA1PBD9eWvP46rtdHkSvCZyqHUhaHFr5We2SugE9+Y8HhVA==", + "version": "0.144.0-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.0-win32-x64.tgz", + "integrity": "sha512-QiholLCYqNeYvNM77HOmPtrOFrY0rQc/N9nXt+sQGXO3rEGmcWjpLzujY4Oegl3CLRHoieWqlep3EqEvFBjoIA==", "cpu": [ "x64" ], @@ -1153,9 +1153,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1173,9 +1170,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1193,9 +1187,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1213,9 +1204,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1233,9 +1221,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1253,9 +1238,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2587,9 +2569,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2611,9 +2590,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2635,9 +2611,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2659,9 +2632,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index a6ac6ac1..9c57614f 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^1.2.1", - "@openai/codex": "^0.143.0", + "@openai/codex": "^0.144.0", "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^9.0.1", diff --git a/src/app-server/AuthMode.ts b/src/app-server/AuthMode.ts index 77bc037e..b2488761 100644 --- a/src/app-server/AuthMode.ts +++ b/src/app-server/AuthMode.ts @@ -5,4 +5,4 @@ /** * Authentication mode for OpenAI-backed providers. */ -export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey"; +export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "headers" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey"; diff --git a/src/app-server/ImageGenerationItem.ts b/src/app-server/ImageGenerationItem.ts index 0edb7c22..26cd6285 100644 --- a/src/app-server/ImageGenerationItem.ts +++ b/src/app-server/ImageGenerationItem.ts @@ -1,5 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "./AbsolutePathBuf"; -export type ImageGenerationItem = { id: string, status: string, revised_prompt?: string, result: string, saved_path?: string, }; +export type ImageGenerationItem = { id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, }; diff --git a/src/app-server/WebSearchItem.ts b/src/app-server/WebSearchItem.ts index 46b14065..bc1e6d54 100644 --- a/src/app-server/WebSearchItem.ts +++ b/src/app-server/WebSearchItem.ts @@ -1,6 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WebSearchAction } from "./WebSearchAction"; +import type { WebSearchAction } from "./v2/WebSearchAction"; -export type WebSearchItem = { id: string, query: string, action: WebSearchAction, }; +export type WebSearchItem = { id: string, query: string, action: WebSearchAction | null, }; diff --git a/src/app-server/v2/AppToolApproval.ts b/src/app-server/v2/AppToolApproval.ts index e92cd8e2..6704ef0f 100644 --- a/src/app-server/v2/AppToolApproval.ts +++ b/src/app-server/v2/AppToolApproval.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type AppToolApproval = "auto" | "prompt" | "approve"; +export type AppToolApproval = "auto" | "prompt" | "writes" | "approve"; diff --git a/src/app-server/v2/LoginAccountParams.ts b/src/app-server/v2/LoginAccountParams.ts index e6f1e2ed..1606f167 100644 --- a/src/app-server/v2/LoginAccountParams.ts +++ b/src/app-server/v2/LoginAccountParams.ts @@ -1,8 +1,9 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LoginAppBrand } from "./LoginAppBrand"; -export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens", +export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, useHostedLoginSuccessPage?: boolean, appBrand?: LoginAppBrand | null, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens", /** * Access token (JWT) supplied by the client. * This token is used for backend API requests and email extraction. diff --git a/src/app-server/v2/LoginAppBrand.ts b/src/app-server/v2/LoginAppBrand.ts new file mode 100644 index 00000000..c06d12ff --- /dev/null +++ b/src/app-server/v2/LoginAppBrand.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LoginAppBrand = "codex" | "chatgpt"; diff --git a/src/app-server/v2/ThreadItem.ts b/src/app-server/v2/ThreadItem.ts index c611ab07..a4453855 100644 --- a/src/app-server/v2/ThreadItem.ts +++ b/src/app-server/v2/ThreadItem.ts @@ -1,10 +1,11 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { ImageGenerationItem } from "../ImageGenerationItem"; import type { LegacyAppPathString } from "../LegacyAppPathString"; import type { MessagePhase } from "../MessagePhase"; import type { ReasoningEffort } from "../ReasoningEffort"; +import type { WebSearchItem } from "../WebSearchItem"; import type { JsonValue } from "../serde_json/JsonValue"; import type { CollabAgentState } from "./CollabAgentState"; import type { CollabAgentTool } from "./CollabAgentTool"; @@ -24,7 +25,6 @@ import type { MemoryCitation } from "./MemoryCitation"; import type { PatchApplyStatus } from "./PatchApplyStatus"; import type { SubAgentActivityKind } from "./SubAgentActivityKind"; import type { UserInput } from "./UserInput"; -import type { WebSearchAction } from "./WebSearchAction"; export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array, } | { "type": "hookPrompt", id: string, fragments: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, /** @@ -105,4 +105,4 @@ reasoningEffort: ReasoningEffort | null, /** * Last known status of the target agents, when available. */ -agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; +agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; diff --git a/src/app-server/v2/index.ts b/src/app-server/v2/index.ts index 8c0cc055..ab1d684c 100644 --- a/src/app-server/v2/index.ts +++ b/src/app-server/v2/index.ts @@ -216,6 +216,7 @@ export type { ListMcpServersParams } from "./ListMcpServersParams"; export type { ListMcpServersResponse } from "./ListMcpServersResponse"; export type { LoginAccountParams } from "./LoginAccountParams"; export type { LoginAccountResponse } from "./LoginAccountResponse"; +export type { LoginAppBrand } from "./LoginAppBrand"; export type { LogoutAccountResponse } from "./LogoutAccountResponse"; export type { ManagedHooksRequirements } from "./ManagedHooksRequirements"; export type { MarketplaceAddParams } from "./MarketplaceAddParams"; From 5013bd96999d83c66e035a78973fedd02e81a6d7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Jul 2026 17:38:25 +0000 Subject: [PATCH 41/45] Release v1.1.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 872cb841..246d9a21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentclientprotocol/codex-acp", - "version": "1.1.1", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentclientprotocol/codex-acp", - "version": "1.1.1", + "version": "1.1.2", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.2.1", diff --git a/package.json b/package.json index 9c57614f..7a608842 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.1.1", + "version": "1.1.2", "description": "", "main": "dist/index.js", "bin": { From 8aff492d4b033ff2c02ad3b9d591994d57617463 Mon Sep 17 00:00:00 2001 From: "Ilia.Shulgin" Date: Thu, 9 Jul 2026 19:46:48 +0200 Subject: [PATCH 42/45] test: Fix missing model id --- src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts b/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts index 3ed17c3c..32fbbdc3 100644 --- a/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts +++ b/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts @@ -10,7 +10,7 @@ import type {PermissionResponder} from "./permission-responders"; import type {LegacyNewSessionResponse} from "../../../AcpExtensions"; export const DEFAULT_TEST_MODEL_ID = ModelId.create("gpt-5.2", "none"); -export const OTHER_TEST_MODEL_ID = ModelId.create("gpt-5.3-codex", "low"); +export const OTHER_TEST_MODEL_ID = ModelId.create("gpt-5.4-mini", "low"); export interface TestSkill { readonly name: string; From 1632afaae894190ea3fd2ec27006f3985fd5f32a Mon Sep 17 00:00:00 2001 From: Leon Zhao Date: Fri, 10 Jul 2026 08:05:58 +0800 Subject: [PATCH 43/45] docs: design upstream goal cleanup --- ...07-10-upstream-goal-fast-cleanup-design.md | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-10-upstream-goal-fast-cleanup-design.md diff --git a/docs/superpowers/specs/2026-07-10-upstream-goal-fast-cleanup-design.md b/docs/superpowers/specs/2026-07-10-upstream-goal-fast-cleanup-design.md new file mode 100644 index 00000000..717403ac --- /dev/null +++ b/docs/superpowers/specs/2026-07-10-upstream-goal-fast-cleanup-design.md @@ -0,0 +1,96 @@ +# Upstream Goal/Fast Cleanup Design + +## Objective + +Remove the fork-specific goal implementation added by commit `20f61f9` now that upstream implements goal behavior. Keep the fork-only Plan Mode because upstream does not implement it. Keep Fast Mode as an upstream-owned feature and align the ACP SDK dependency with upstream. + +## Historical Boundary + +The fork-specific goal layer was introduced by `20f61f9` after the shared base `37a98b3`. It added ACP extension routes for `thread/goal/get`, `thread/goal/set`, and `thread/goal/clear`; raw goal notifications; a custom no-argument `/goal` view; and corresponding tests. + +Fast Mode was introduced before the fork diverged by upstream commit `64d3bc2`. The fork has no independent Fast Mode implementation to remove. Later upstream commits `fbc8f84` and `ef5e169` own model-capability filtering and boolean config support. + +Upstream does not contain `PlanModeConfig.ts` or the fork's collaboration-mode plumbing, so Plan Mode remains fork-owned. + +## Selected Approach + +Use a symbol-level cleanup based on the historical diff rather than replacing entire files with upstream versions. This removes only the duplicated goal layer while preserving unrelated fork extensions and Plan Mode. + +Whole-file replacement was rejected because files such as `AcpExtensions.ts`, `CodexAcpServer.ts`, and `CodexEventHandler.ts` also contain retained fork behavior for usage, rate limits, proposed plans, packaging identity, and Plan Mode. + +Reverting all of `20f61f9` was rejected because it would remove retained features and create a substantially larger reconstruction diff. + +## Runtime Changes + +### Remove fork goal extensions + +- Remove the `THREAD_GOAL_GET_METHOD`, `THREAD_GOAL_SET_METHOD`, and `THREAD_GOAL_CLEAR_METHOD` ACP extension constants and request types. +- Remove their Zod request parsers and server route registration from `src/index.ts`. +- Restore extension-method dispatch to the upstream-supported authentication and legacy model methods. +- Remove fork-only goal parameter parsing and wrapper methods from `CodexAcpServer` and `CodexAcpClient`. +- Remove only `threadGoalGet` from `CodexAppServerClient`. Retain upstream's `threadGoalSet`, `threadGoalClear`, `runGoalSet`, and `runGoalClear` implementation. + +### Use upstream goal behavior exclusively + +- Restore the upstream `/goal` command behavior, including the no-argument usage message. +- Remove the fork-only goal summary formatting and direct goal lookup. +- Stop forwarding `thread/goal/updated` and `thread/goal/cleared` as raw ACP notifications. +- Retain upstream `session_info_update` goal metadata, duplicate suppression, continuation-turn routing, grace periods, and cancellation behavior. + +### Retain Plan Mode + +- Keep `src/PlanModeConfig.ts` and its tests. +- Keep `planModeEnabled` and `planModeExplicitlySet` session state. +- Keep the `collaborationMode` turn parameter and plan/default mode switching. +- Keep `_acp_ext:codex_proposed_plan` streaming/completion notifications. +- Keep `experimentalApi: true` and `requestAttestation: false` as the existing fork capability configuration required by the retained Plan Mode integration. + +### Retain unrelated fork extensions + +- Keep `_acp_ext:session_usage_update`. +- Keep `_acp_ext:session_rate_limits`. +- Keep authentication extensions, the legacy session-model extension, fork package identity, and fork binary names. + +## Fast Mode and SDK Alignment + +Fast Mode remains enabled and should behave exactly like upstream: + +- expose Fast Mode only for models with a fast speed tier; +- use boolean config options when the ACP client advertises support; +- otherwise use the upstream select option; +- retain the selected preference across model switches but apply the service tier only to supported models. + +`src/FastModeConfig.ts` must have no fork-only behavioral diff from `upstream/main`. Adjacent `CodexAcpServer` changes may differ only where Plan Mode is inserted. + +The dependency range for `@agentclientprotocol/sdk` must match `upstream/main` exactly (`^1.2.1` at design time). The resolved SDK package and transitive dependency graph in `package-lock.json` must follow upstream; only fork root metadata such as package name, version, and binary name may differ. + +## Tests and Fixtures + +- Delete `thread-goal-ext-methods.test.ts`. +- Restore upstream `thread-goal-events.test.ts` expectations and its three goal metadata snapshots. +- Remove the fork-only no-argument goal-view test and restore the upstream missing-input test. +- Keep upstream goal runtime, routing, cancellation, deduplication, and metadata coverage. +- Keep Plan Mode and proposed-plan tests unchanged except for mechanical fixture updates if required. +- Keep upstream Fast Mode tests unchanged. + +## Error Handling and Compatibility + +Requests to the removed fork-only goal extension routes will no longer be registered. Goal operations remain available through upstream's `/goal` command and app-server flow. + +No compatibility shim or deprecation response will be added because the requested outcome is complete removal of the local implementation. + +## Verification + +1. Confirm no fork goal constants, parsers, wrappers, raw notifications, or tests remain. +2. Compare Fast Mode source and behavior with `upstream/main`. +3. Compare the SDK dependency range and resolved package with `upstream/main`. +4. Run targeted goal, Plan Mode, proposed-plan, Fast Mode, and initialization tests. +5. Run `npm run typecheck` and `npm test`. +6. Run `npm run build`, `npm pack --dry-run`, and `npm run bundle:all` with the CI Bun version. + +## Success Criteria + +- Goal behavior is provided only by upstream code paths. +- Fast Mode and the ACP SDK follow upstream. +- Plan Mode and unrelated fork extensions remain functional. +- The repository passes the full verification workflow with a clean working tree after commit. From 51ad48d3306a9f70b2004f7a28fdea57030a5241 Mon Sep 17 00:00:00 2001 From: Leon Zhao Date: Fri, 10 Jul 2026 08:23:19 +0800 Subject: [PATCH 44/45] refactor: remove duplicate goal implementation --- .../2026-07-10-upstream-goal-fast-cleanup.md | 11 ++ src/AcpExtensions.ts | 46 +------- src/CodexAcpClient.ts | 18 --- src/CodexAcpServer.ts | 111 +----------------- src/CodexAppServerClient.ts | 10 +- src/CodexCommands.ts | 39 +----- src/CodexEventHandler.ts | 6 - .../CodexACPAgent/CodexAcpClient.test.ts | 30 +---- .../data/available-commands-build-in.json | 14 +-- .../data/available-commands-skills.json | 14 +-- .../data/load-session-history.json | 14 +-- ...ession-response-item-history-fallback.json | 14 +-- .../data/thread-goal-cleared.json | 16 +++ .../data/thread-goal-updated-multiline.json | 20 ++++ .../data/thread-goal-updated.json | 20 ++++ .../CodexACPAgent/thread-goal-events.test.ts | 92 ++------------- .../thread-goal-ext-methods.test.ts | 78 ------------ src/index.ts | 21 +--- 18 files changed, 127 insertions(+), 447 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-10-upstream-goal-fast-cleanup.md create mode 100644 src/__tests__/CodexACPAgent/data/thread-goal-cleared.json create mode 100644 src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json create mode 100644 src/__tests__/CodexACPAgent/data/thread-goal-updated.json delete mode 100644 src/__tests__/CodexACPAgent/thread-goal-ext-methods.test.ts diff --git a/docs/superpowers/plans/2026-07-10-upstream-goal-fast-cleanup.md b/docs/superpowers/plans/2026-07-10-upstream-goal-fast-cleanup.md new file mode 100644 index 00000000..34872c6a --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-upstream-goal-fast-cleanup.md @@ -0,0 +1,11 @@ +# Upstream Goal/Fast Cleanup Implementation Plan + +1. Remove fork goal ACP extension constants, request types, parsers, route registration, and server/client forwarding wrappers. +2. Preserve upstream goal setters, runtime-effect tracking, session metadata, cancellation, and `/goal` orchestration. +3. Remove raw goal notifications and restore upstream goal event tests/snapshots. +4. Restore upstream no-argument `/goal` behavior and remove fork-only goal formatting/tests. +5. Delete the fork goal extension test suite and scan for remaining fork goal symbols. +6. Verify Plan Mode/proposed-plan code is unchanged. +7. Verify Fast Mode source and behavior match upstream and the ACP SDK dependency/lock resolution matches upstream. +8. Run targeted tests, typecheck, the complete test suite, npm build/package checks, and all CI Bun bundles. +9. Review the final diff and commit the implementation. diff --git a/src/AcpExtensions.ts b/src/AcpExtensions.ts index e6838217..411f5807 100644 --- a/src/AcpExtensions.ts +++ b/src/AcpExtensions.ts @@ -5,16 +5,8 @@ import type { ResumeSessionResponse, SessionId, } from "@agentclientprotocol/sdk"; -import type { - ThreadGoalClearParams, - ThreadGoalGetParams, - ThreadGoalSetParams, -} from "./app-server/v2"; export const LEGACY_SET_SESSION_MODEL_METHOD = "session/set_model"; -export const THREAD_GOAL_GET_METHOD = "thread/goal/get"; -export const THREAD_GOAL_SET_METHOD = "thread/goal/set"; -export const THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear"; export const ACP_EXT_SESSION_USAGE_UPDATE_METHOD = "_acp_ext:session_usage_update"; export const ACP_EXT_SESSION_RATE_LIMITS_METHOD = "_acp_ext:session_rate_limits"; export const ACP_EXT_CODEX_PROPOSED_PLAN_METHOD = "_acp_ext:codex_proposed_plan"; @@ -82,28 +74,11 @@ export type ExtMethodRequest = AuthenticationStatusRequest | AuthenticationLogoutRequest | LegacySetSessionModelExtRequest - | ThreadGoalGetExtRequest - | ThreadGoalSetExtRequest - | ThreadGoalClearExtRequest export function isExtMethodRequest(request: { method: string, params: Record }): request is ExtMethodRequest { - return resolveExtMethod(request.method) !== null; -} - -export type ExtMethodName = ExtMethodRequest["method"]; - -export function resolveExtMethod(method: string): ExtMethodName | null { - switch (method) { - case "authentication/status": - case "authentication/logout": - case LEGACY_SET_SESSION_MODEL_METHOD: - case THREAD_GOAL_GET_METHOD: - case THREAD_GOAL_SET_METHOD: - case THREAD_GOAL_CLEAR_METHOD: - return method; - default: - return null; - } + return request.method === "authentication/status" + || request.method === "authentication/logout" + || request.method === LEGACY_SET_SESSION_MODEL_METHOD; } export type AuthenticationStatusRequest = { method: "authentication/status", params: {} } @@ -117,21 +92,6 @@ export type LegacySetSessionModelExtRequest = { params: LegacySetSessionModelRequest; } -export type ThreadGoalGetExtRequest = { - method: typeof THREAD_GOAL_GET_METHOD; - params: ThreadGoalGetParams; -} - -export type ThreadGoalSetExtRequest = { - method: typeof THREAD_GOAL_SET_METHOD; - params: ThreadGoalSetParams; -} - -export type ThreadGoalClearExtRequest = { - method: typeof THREAD_GOAL_CLEAR_METHOD; - params: ThreadGoalClearParams; -} - export async function legacySetSessionModel( connection: Pick, params: LegacySetSessionModelRequest, diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 93ae0877..e884df56 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -33,12 +33,6 @@ import type { SkillsListParams, SkillsListResponse, SandboxPolicy, - ThreadGoalClearParams, - ThreadGoalClearResponse, - ThreadGoalGetParams, - ThreadGoalGetResponse, - ThreadGoalSetParams, - ThreadGoalSetResponse, Thread, ThreadGoalStatus, ThreadSourceKind, @@ -242,18 +236,6 @@ export class CodexAcpClient { return this.codexClient.accountRead({refreshToken: false}); } - async getThreadGoal(params: ThreadGoalGetParams): Promise { - return await this.codexClient.threadGoalGet(params); - } - - async setThreadGoal(params: ThreadGoalSetParams): Promise { - return await this.codexClient.threadGoalSet(params); - } - - async clearThreadGoal(params: ThreadGoalClearParams): Promise { - return await this.codexClient.threadGoalClear(params); - } - async resumeSession(request: acp.ResumeSessionRequest, onSubscribed?: () => void): Promise { const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta); await this.refreshSkills(request.cwd, additionalDirectories); diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 68a2aec7..06475fc7 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -12,12 +12,6 @@ import type { Account, Model, ReasoningEffortOption, - ThreadGoalClearParams, - ThreadGoalClearResponse, - ThreadGoalGetParams, - ThreadGoalGetResponse, - ThreadGoalSetParams, - ThreadGoalSetResponse, ThreadGoalStatus, Thread, ThreadItem, @@ -48,11 +42,8 @@ import { type LegacySessionModelState, type LegacySetSessionModelRequest, type LegacySetSessionModelResponse, + isExtMethodRequest, LEGACY_SET_SESSION_MODEL_METHOD, - THREAD_GOAL_CLEAR_METHOD, - THREAD_GOAL_GET_METHOD, - THREAD_GOAL_SET_METHOD, - resolveExtMethod, } from "./AcpExtensions"; import { createCollabAgentToolCallUpdate, @@ -148,15 +139,6 @@ interface ActivePrompt { complete: () => void; } -const THREAD_GOAL_STATUSES: ReadonlySet = new Set([ - "active", - "paused", - "blocked", - "usageLimited", - "budgetLimited", - "complete", -]); - export class CodexAcpServer { private static readonly MODEL_NAME_TOKEN_OVERRIDES: Record = { gpt: "GPT", @@ -257,11 +239,11 @@ export class CodexAcpServer { } async extMethod(method: string, params: Record): Promise> { - const extMethod = resolveExtMethod(method); - if (!extMethod) { + const methodRequest = { method: method, params: params }; + if (!isExtMethodRequest(methodRequest)) { return {}; } - switch (extMethod) { + switch (methodRequest.method) { case "authentication/status": return await this.runWithProcessCheck(() => this.codexAcpClient.getAuthenticationStatus()); case "authentication/logout": { @@ -269,13 +251,7 @@ export class CodexAcpServer { return {}; } case LEGACY_SET_SESSION_MODEL_METHOD: - return await this.unstable_setSessionModel(this.parseLegacySetSessionModelParams(params)); - case THREAD_GOAL_GET_METHOD: - return await this.getThreadGoal(this.parseThreadGoalGetParams(params)); - case THREAD_GOAL_SET_METHOD: - return await this.setThreadGoal(this.parseThreadGoalSetParams(params)); - case THREAD_GOAL_CLEAR_METHOD: - return await this.clearThreadGoal(this.parseThreadGoalClearParams(params)); + return await this.unstable_setSessionModel(this.parseLegacySetSessionModelParams(methodRequest.params)); } } @@ -827,18 +803,6 @@ export class CodexAcpServer { return {}; } - async getThreadGoal(params: ThreadGoalGetParams): Promise { - return await this.runWithProcessCheck(() => this.codexAcpClient.getThreadGoal(params)); - } - - async setThreadGoal(params: ThreadGoalSetParams): Promise { - return await this.runWithProcessCheck(() => this.codexAcpClient.setThreadGoal(params)); - } - - async clearThreadGoal(params: ThreadGoalClearParams): Promise { - return await this.runWithProcessCheck(() => this.codexAcpClient.clearThreadGoal(params)); - } - private parseLegacySetSessionModelParams(params: Record): LegacySetSessionModelRequest { const sessionId = params["sessionId"]; const modelId = params["modelId"]; @@ -851,71 +815,6 @@ export class CodexAcpServer { }; } - private parseThreadGoalGetParams(params: Record): ThreadGoalGetParams { - return { - threadId: this.parseThreadGoalThreadId(params), - }; - } - - private parseThreadGoalClearParams(params: Record): ThreadGoalClearParams { - return { - threadId: this.parseThreadGoalThreadId(params), - }; - } - - private parseThreadGoalSetParams(params: Record): ThreadGoalSetParams { - const parsed: ThreadGoalSetParams = { - threadId: this.parseThreadGoalThreadId(params), - }; - - if (Object.prototype.hasOwnProperty.call(params, "objective")) { - parsed.objective = this.parseOptionalString(params["objective"]); - } - if (Object.prototype.hasOwnProperty.call(params, "status")) { - parsed.status = this.parseOptionalThreadGoalStatus(params["status"]); - } - if (Object.prototype.hasOwnProperty.call(params, "tokenBudget")) { - parsed.tokenBudget = this.parseOptionalNumber(params["tokenBudget"]); - } - - return parsed; - } - - private parseThreadGoalThreadId(params: Record): string { - const threadId = params["threadId"]; - if (typeof threadId !== "string") { - throw RequestError.invalidParams(); - } - return threadId; - } - - private parseOptionalString(value: unknown): string | null { - if (value === null || typeof value === "string") { - return value; - } - throw RequestError.invalidParams(); - } - - private parseOptionalNumber(value: unknown): number | null { - if (value === null) { - return null; - } - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - throw RequestError.invalidParams(); - } - - private parseOptionalThreadGoalStatus(value: unknown): ThreadGoalStatus | null { - if (value === null) { - return null; - } - if (typeof value === "string" && THREAD_GOAL_STATUSES.has(value as ThreadGoalStatus)) { - return value as ThreadGoalStatus; - } - throw RequestError.invalidParams(); - } - private createSessionConfigOptions(sessionState: SessionState): Array { const currentModelId = ModelId.fromString(sessionState.currentModelId); const configOptions = [ diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index 73a0b599..d1bc210c 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -37,8 +37,6 @@ import type { ThreadGoalClearedNotification, ThreadGoalClearParams, ThreadGoalClearResponse, - ThreadGoalGetParams, - ThreadGoalGetResponse, ThreadGoalSetParams, ThreadGoalSetResponse, ThreadLoadedListParams, @@ -519,16 +517,12 @@ export class CodexAppServerClient { return await this.sendRequest({ method: "thread/compact/start", params: params }); } - async threadGoalGet(params: ThreadGoalGetParams): Promise { - return await this.sendRequest({ method: "thread/goal/get", params }); - } - async threadGoalSet(params: ThreadGoalSetParams): Promise { - return await this.sendRequest({ method: "thread/goal/set", params }); + return await this.sendRequest({ method: "thread/goal/set", params: params }); } async threadGoalClear(params: ThreadGoalClearParams): Promise { - return await this.sendRequest({ method: "thread/goal/clear", params }); + return await this.sendRequest({ method: "thread/goal/clear", params: params }); } async listMcpServerStatus(params: ListMcpServerStatusParams): Promise { diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index 4fc70ef0..d7057b8e 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -7,7 +7,6 @@ import type { ReviewTarget, SkillsListEntry, SkillsListParams, - ThreadGoal, TurnCompletedNotification, } from "./app-server/v2"; import type {SessionState} from "./CodexAcpServer"; @@ -117,11 +116,6 @@ export class CodexCommands { description: "Display session configuration and token usage.", input: null }, - { - name: "goal", - description: "Set or view the goal for a long-running task.", - input: { hint: "objective | pause | resume | clear" } - }, { name: "review", description: "Review uncommitted changes, or review with custom instructions.", @@ -142,6 +136,11 @@ export class CodexCommands { description: "Summarize conversation to avoid hitting the context limit.", input: null }, + { + name: "goal", + description: "Set, pause, resume, or clear a task goal.", + input: { hint: "[|clear|pause|resume]" } + }, { name: "logout", description: "Sign out of Codex. This option is available when you are logged in via ChatGPT.", @@ -286,11 +285,7 @@ export class CodexCommands { const sessionId = sessionState.sessionId; const argument = rest.trim(); if (argument.length === 0) { - const response = await this.runWithProcessCheck(() => this.codexAcpClient.getThreadGoal({threadId: sessionId})); - const text = response.goal - ? this.formatThreadGoalSummary(response.goal) - : "Usage: /goal \nNo goal is currently set."; - await this.sendCommandMessage(text, sessionId); + await this.sendCommandUsageMessage("goal", "[|clear|pause|resume]", sessionId); return { handled: true }; } @@ -523,28 +518,6 @@ export class CodexCommands { return ` (resets ${timeStr} on ${dateStr})`; } - private formatThreadGoalSummary(goal: ThreadGoal): string { - const usage = goal.tokenBudget === null - ? `tokens used: ${goal.tokensUsed}` - : `tokens used: ${goal.tokensUsed} of ${goal.tokenBudget}`; - return [ - `Goal ${this.formatThreadGoalStatus(goal.status)}: ${goal.objective}`, - usage, - `time used: ${goal.timeUsedSeconds} seconds`, - ].join("\n"); - } - - private formatThreadGoalStatus(status: ThreadGoal["status"]): string { - switch (status) { - case "usageLimited": - return "usage-limited"; - case "budgetLimited": - return "budget-limited"; - default: - return status; - } - } - private formatTokenCount(count: number): string { if (count >= 1000000) { return `${(count / 1000000).toFixed(1)}M`; diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 443c5ce1..13594fb0 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -249,12 +249,6 @@ export class CodexEventHandler { this.createSessionRateLimitsExtNotification(notification.params) ); return; - case "thread/goal/updated": - await this.connection.notify(notification.method, notification.params); - return; - case "thread/goal/cleared": - await this.connection.notify(notification.method, notification.params); - return; case "item/plan/delta": await this.emitCodexProposedPlanDelta(notification.params); return; diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 36d3104c..2347c23f 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1307,31 +1307,6 @@ describe('ACP server test', { timeout: 40_000 }, () => { await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/command-status.json"); }); - it('shows the current goal for the local goal slash command', async () => { - const { mockFixture, turnStartSpy } = setupPromptFixture(); - const getThreadGoalSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "getThreadGoal") - .mockResolvedValueOnce({ goal: null }) - .mockResolvedValueOnce({ goal: createThreadGoal() }); - - await mockFixture.getCodexAcpAgent().prompt({ - sessionId: "session-id", - prompt: [{ type: "text", text: "/goal" }], - }); - expect(mockFixture.getAcpConnectionEvents([]).at(-1)!.args[0].update.content.text) - .toBe("Usage: /goal \nNo goal is currently set."); - - await mockFixture.getCodexAcpAgent().prompt({ - sessionId: "session-id", - prompt: [{ type: "text", text: "/goal" }], - }); - expect(mockFixture.getAcpConnectionEvents([]).at(-1)!.args[0].update.content.text) - .toBe("Goal active: Ship the migration and keep tests green\ntokens used: 0\ntime used: 0 seconds"); - - expect(getThreadGoalSpy).toHaveBeenCalledTimes(2); - expect(getThreadGoalSpy).toHaveBeenCalledWith({ threadId: "session-id" }); - expect(turnStartSpy).not.toHaveBeenCalled(); - }); - it('passes skill slash commands through to Codex', async () => { const { mockFixture, turnStartSpy } = setupPromptFixture(); @@ -2598,9 +2573,8 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(turnStartSpy).not.toHaveBeenCalled(); }); - it('reports an unset goal when the goal slash command has no input', async () => { + it('reports missing goal slash command input', async () => { const { mockFixture } = setupPromptFixture(); - vi.spyOn(mockFixture.getCodexAcpClient(), "getThreadGoal").mockResolvedValue({goal: null}); const goalSetSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadGoalSet") .mockResolvedValue({ goal: createThreadGoal() }); const goalClearSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadGoalClear") @@ -2616,7 +2590,7 @@ describe('ACP server test', { timeout: 40_000 }, () => { const [event] = mockFixture.getAcpConnectionEvents([]); expect(event).toBeDefined(); expect(event!.args[0].update.content.text).toBe( - "Usage: /goal \nNo goal is currently set." + 'Command "/goal" requires [|clear|pause|resume].' ); }); it('returns cancelled promptly when non-interruptible slash command startup is cancelled', async () => { diff --git a/src/__tests__/CodexACPAgent/data/available-commands-build-in.json b/src/__tests__/CodexACPAgent/data/available-commands-build-in.json index 9a9405e1..26e78cbb 100644 --- a/src/__tests__/CodexACPAgent/data/available-commands-build-in.json +++ b/src/__tests__/CodexACPAgent/data/available-commands-build-in.json @@ -21,13 +21,6 @@ "description": "Display session configuration and token usage.", "input": null }, - { - "name": "goal", - "description": "Set or view the goal for a long-running task.", - "input": { - "hint": "objective | pause | resume | clear" - } - }, { "name": "review", "description": "Review uncommitted changes, or review with custom instructions.", @@ -54,6 +47,13 @@ "description": "Summarize conversation to avoid hitting the context limit.", "input": null }, + { + "name": "goal", + "description": "Set, pause, resume, or clear a task goal.", + "input": { + "hint": "[|clear|pause|resume]" + } + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/available-commands-skills.json b/src/__tests__/CodexACPAgent/data/available-commands-skills.json index 1baaedca..59987072 100644 --- a/src/__tests__/CodexACPAgent/data/available-commands-skills.json +++ b/src/__tests__/CodexACPAgent/data/available-commands-skills.json @@ -21,13 +21,6 @@ "description": "Display session configuration and token usage.", "input": null }, - { - "name": "goal", - "description": "Set or view the goal for a long-running task.", - "input": { - "hint": "objective | pause | resume | clear" - } - }, { "name": "review", "description": "Review uncommitted changes, or review with custom instructions.", @@ -54,6 +47,13 @@ "description": "Summarize conversation to avoid hitting the context limit.", "input": null }, + { + "name": "goal", + "description": "Set, pause, resume, or clear a task goal.", + "input": { + "hint": "[|clear|pause|resume]" + } + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/load-session-history.json b/src/__tests__/CodexACPAgent/data/load-session-history.json index 8cf635ac..454bcf7d 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-history.json +++ b/src/__tests__/CodexACPAgent/data/load-session-history.json @@ -21,13 +21,6 @@ "description": "Display session configuration and token usage.", "input": null }, - { - "name": "goal", - "description": "Set or view the goal for a long-running task.", - "input": { - "hint": "objective | pause | resume | clear" - } - }, { "name": "review", "description": "Review uncommitted changes, or review with custom instructions.", @@ -54,6 +47,13 @@ "description": "Summarize conversation to avoid hitting the context limit.", "input": null }, + { + "name": "goal", + "description": "Set, pause, resume, or clear a task goal.", + "input": { + "hint": "[|clear|pause|resume]" + } + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json b/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json index fd22ba03..3fd4f914 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json +++ b/src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json @@ -21,13 +21,6 @@ "description": "Display session configuration and token usage.", "input": null }, - { - "name": "goal", - "description": "Set or view the goal for a long-running task.", - "input": { - "hint": "objective | pause | resume | clear" - } - }, { "name": "review", "description": "Review uncommitted changes, or review with custom instructions.", @@ -54,6 +47,13 @@ "description": "Summarize conversation to avoid hitting the context limit.", "input": null }, + { + "name": "goal", + "description": "Set, pause, resume, or clear a task goal.", + "input": { + "hint": "[|clear|pause|resume]" + } + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json b/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json new file mode 100644 index 00000000..acf61a68 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/thread-goal-cleared.json @@ -0,0 +1,16 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "session_info_update", + "_meta": { + "codex": { + "goal": null + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json b/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json new file mode 100644 index 00000000..c524584c --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/thread-goal-updated-multiline.json @@ -0,0 +1,20 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "session_info_update", + "_meta": { + "codex": { + "goal": { + "objective": "First task\nSecond task", + "status": "budgetLimited", + "tokenBudget": 1000 + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/data/thread-goal-updated.json b/src/__tests__/CodexACPAgent/data/thread-goal-updated.json new file mode 100644 index 00000000..bc14e9cb --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/thread-goal-updated.json @@ -0,0 +1,20 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "session_info_update", + "_meta": { + "codex": { + "goal": { + "objective": "Ship the goal update", + "status": "active", + "tokenBudget": null + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/thread-goal-events.test.ts b/src/__tests__/CodexACPAgent/thread-goal-events.test.ts index 309db27e..fc690994 100644 --- a/src/__tests__/CodexACPAgent/thread-goal-events.test.ts +++ b/src/__tests__/CodexACPAgent/thread-goal-events.test.ts @@ -18,7 +18,7 @@ describe("CodexEventHandler - thread goal events", () => { vi.clearAllMocks(); }); - it("should send thread goal updates as session metadata and the raw extension notification", async () => { + it("should send thread goal updates as session metadata", async () => { const goalUpdatedNotification: ServerNotification = { method: "thread/goal/updated", params: { @@ -39,27 +39,8 @@ describe("CodexEventHandler - thread goal events", () => { await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [goalUpdatedNotification]); - const events = mockFixture.getAcpConnectionEvents([]); - expect(events[0]!.args[0].update).toEqual({ - sessionUpdate: "session_info_update", - _meta: { - codex: { - goal: { - objective: "Ship the goal update", - status: "active", - tokenBudget: null, - }, - }, - }, - }); - expect(events[1]).toEqual( - { - method: "notify", - args: [ - "thread/goal/updated", - goalUpdatedNotification.params, - ], - } + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( + "data/thread-goal-updated.json" ); }); @@ -84,31 +65,12 @@ describe("CodexEventHandler - thread goal events", () => { await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [goalUpdatedNotification]); - const events = mockFixture.getAcpConnectionEvents([]); - expect(events[0]!.args[0].update).toEqual({ - sessionUpdate: "session_info_update", - _meta: { - codex: { - goal: { - objective: "First task\nSecond task", - status: "budgetLimited", - tokenBudget: 1000, - }, - }, - }, - }); - expect(events[1]).toEqual( - { - method: "notify", - args: [ - "thread/goal/updated", - goalUpdatedNotification.params, - ], - } + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( + "data/thread-goal-updated-multiline.json" ); }); - it("should send thread goal cleared as session metadata and the raw extension notification", async () => { + it("should send thread goal cleared as session metadata", async () => { const goalClearedNotification: ServerNotification = { method: "thread/goal/cleared", params: { @@ -118,23 +80,8 @@ describe("CodexEventHandler - thread goal events", () => { await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [goalClearedNotification]); - const events = mockFixture.getAcpConnectionEvents([]); - expect(events[0]!.args[0].update).toEqual({ - sessionUpdate: "session_info_update", - _meta: { - codex: { - goal: null, - }, - }, - }); - expect(events[1]).toEqual( - { - method: "notify", - args: [ - "thread/goal/cleared", - goalClearedNotification.params, - ], - } + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( + "data/thread-goal-cleared.json" ); }); @@ -175,11 +122,8 @@ describe("CodexEventHandler - thread goal events", () => { ]); const events = mockFixture.getAcpConnectionEvents([]); - const sessionUpdates = events.filter(event => event.method === "sessionUpdate"); - const rawNotifications = events.filter(event => event.method === "notify"); - expect(sessionUpdates).toHaveLength(1); - expect(rawNotifications).toHaveLength(2); - expect(sessionUpdates[0]!.args[0].update).toEqual({ + expect(events).toHaveLength(1); + expect(events[0]!.args[0].update).toEqual({ sessionUpdate: "session_info_update", _meta: { codex: { @@ -226,7 +170,7 @@ describe("CodexEventHandler - thread goal events", () => { ]); const events = mockFixture.getAcpConnectionEvents([]); - expect(events).toHaveLength(3); + expect(events).toHaveLength(2); expect(events[0]!.args[0].update).toEqual({ sessionUpdate: "agent_message_chunk", messageId: "message-1", @@ -247,13 +191,6 @@ describe("CodexEventHandler - thread goal events", () => { }, }, }); - expect(events[2]).toEqual({ - method: "notify", - args: [ - "thread/goal/updated", - goalCompletedNotification.params, - ], - }); }); it("should suppress duplicate thread goal cleared notifications", async () => { @@ -270,11 +207,8 @@ describe("CodexEventHandler - thread goal events", () => { ]); const events = mockFixture.getAcpConnectionEvents([]); - const sessionUpdates = events.filter(event => event.method === "sessionUpdate"); - const rawNotifications = events.filter(event => event.method === "notify"); - expect(sessionUpdates).toHaveLength(1); - expect(rawNotifications).toHaveLength(2); - expect(sessionUpdates[0]!.args[0].update).toEqual({ + expect(events).toHaveLength(1); + expect(events[0]!.args[0].update).toEqual({ sessionUpdate: "session_info_update", _meta: { codex: { diff --git a/src/__tests__/CodexACPAgent/thread-goal-ext-methods.test.ts b/src/__tests__/CodexACPAgent/thread-goal-ext-methods.test.ts deleted file mode 100644 index 4f7d515b..00000000 --- a/src/__tests__/CodexACPAgent/thread-goal-ext-methods.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {describe, expect, it, vi} from "vitest"; -import {createCodexMockTestFixture} from "../acp-test-utils"; -import { - THREAD_GOAL_CLEAR_METHOD, - THREAD_GOAL_GET_METHOD, - THREAD_GOAL_SET_METHOD, -} from "../../AcpExtensions"; -import type {ThreadGoal} from "../../app-server/v2"; - -describe("Thread goal ext methods", () => { - const threadId = "thread-id"; - const goal: ThreadGoal = { - threadId, - objective: "Ship goal support", - status: "active", - tokenBudget: 1000, - tokensUsed: 42, - timeUsedSeconds: 12, - createdAt: 1710000000, - updatedAt: 1710000012, - }; - - it("gets the thread goal with the app-server response shape", async () => { - const fixture = createCodexMockTestFixture(); - const threadGoalGet = vi.spyOn(fixture.getCodexAppServerClient(), "threadGoalGet") - .mockResolvedValue({goal}); - - const response = await fixture.getCodexAcpAgent().extMethod(THREAD_GOAL_GET_METHOD, { - threadId, - }); - - expect(threadGoalGet).toHaveBeenCalledWith({threadId}); - expect(response).toEqual({goal}); - }); - - it("sets the thread goal and returns the app-server response", async () => { - const fixture = createCodexMockTestFixture(); - const threadGoalSet = vi.spyOn(fixture.getCodexAppServerClient(), "threadGoalSet") - .mockResolvedValue({goal}); - - const response = await fixture.getCodexAcpAgent().extMethod(THREAD_GOAL_SET_METHOD, { - threadId, - objective: "Ship goal support", - status: "paused", - tokenBudget: 1000, - }); - - expect(threadGoalSet).toHaveBeenCalledWith({ - threadId, - objective: "Ship goal support", - status: "paused", - tokenBudget: 1000, - }); - expect(response).toEqual({goal}); - }); - - it("clears the thread goal with the app-server response shape", async () => { - const fixture = createCodexMockTestFixture(); - const threadGoalClear = vi.spyOn(fixture.getCodexAppServerClient(), "threadGoalClear") - .mockResolvedValue({cleared: true}); - - const response = await fixture.getCodexAcpAgent().extMethod(THREAD_GOAL_CLEAR_METHOD, { - threadId, - }); - - expect(threadGoalClear).toHaveBeenCalledWith({threadId}); - expect(response).toEqual({cleared: true}); - }); - - it("rejects invalid thread goal params", async () => { - const fixture = createCodexMockTestFixture(); - - await expect(fixture.getCodexAcpAgent().extMethod(THREAD_GOAL_SET_METHOD, { - threadId, - status: "not-a-status", - })).rejects.toThrow(); - }); -}); diff --git a/src/index.ts b/src/index.ts index af034e28..5f8a83dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,12 +12,7 @@ import packageJson from "../package.json"; import {logger} from "./Logger"; import {runLoginCommand} from "./login"; import {runCodexCli} from "./CodexCli"; -import { - LEGACY_SET_SESSION_MODEL_METHOD, - THREAD_GOAL_CLEAR_METHOD, - THREAD_GOAL_GET_METHOD, - THREAD_GOAL_SET_METHOD, -} from "./AcpExtensions"; +import {LEGACY_SET_SESSION_MODEL_METHOD} from "./AcpExtensions"; const emptyExtensionParamsParser = z.preprocess( (params) => params ?? {}, @@ -29,17 +24,6 @@ const legacySetSessionModelParamsParser = z.object({ modelId: z.string(), }).passthrough(); -const threadGoalParamsParser = z.object({ - threadId: z.string(), -}).passthrough(); - -const threadGoalSetParamsParser = z.object({ - threadId: z.string(), - objective: z.string().nullable().optional(), - status: z.enum(["active", "paused", "blocked", "usageLimited", "budgetLimited", "complete"]).nullable().optional(), - tokenBudget: z.number().nullable().optional(), -}).passthrough(); - if (process.argv.includes("--version")) { console.log(`${packageJson.name} ${packageJson.version}`); process.exit(0); @@ -145,8 +129,5 @@ function startAcpServer() { .onRequest("authentication/status", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/status", ctx.params)) .onRequest("authentication/logout", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/logout", ctx.params)) .onRequest(LEGACY_SET_SESSION_MODEL_METHOD, legacySetSessionModelParamsParser, (ctx) => getAgent().extMethod(LEGACY_SET_SESSION_MODEL_METHOD, ctx.params)) - .onRequest(THREAD_GOAL_GET_METHOD, threadGoalParamsParser, (ctx) => getAgent().extMethod(THREAD_GOAL_GET_METHOD, ctx.params)) - .onRequest(THREAD_GOAL_SET_METHOD, threadGoalSetParamsParser, (ctx) => getAgent().extMethod(THREAD_GOAL_SET_METHOD, ctx.params)) - .onRequest(THREAD_GOAL_CLEAR_METHOD, threadGoalParamsParser, (ctx) => getAgent().extMethod(THREAD_GOAL_CLEAR_METHOD, ctx.params)) .connect(acpJsonStream); } From 7357481ee7bdf377687ae7cee6f60a928021b82b Mon Sep 17 00:00:00 2001 From: Leon Zhao Date: Fri, 10 Jul 2026 10:22:48 +0800 Subject: [PATCH 45/45] chore: --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index dfc2ce66..2c25cc17 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.0.1", + "version": "1.1.2", "description": "An ACP-compatible coding agent powered by Codex", "main": "dist/index.js", "bin": { @@ -75,4 +75,4 @@ "vscode-jsonrpc": "^9.0.1", "zod": "^4.0.0" } -} +} \ No newline at end of file