From 659b5c516fcec1749e289e820d0fdb617eeb2947 Mon Sep 17 00:00:00 2001 From: Nikita Ashikhmin Date: Sun, 18 Oct 2026 20:31:57 +0400 Subject: [PATCH 1/4] IJAI-2003 an edited message rewinds the same Codex thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AIR edits a message in the session the user is looking at. A fork answers that with a second thread — another id, another transcript — so the edited prompt would run against a conversation the user never saw. `_session/rewind` drops the named user turn and everything after it from the same Codex thread: the adapter resolves the message to the turn it starts and calls `thread/revert` with that turn as the exclusive boundary. The thread id stays the ACP session id, and no session-list entry appears. A legacy-history thread has no such operation and answers `{rewound: false}`, which the contract already spells as "the conversation was left as it was". A steer inside an existing turn is refused: Codex removes whole turns. The prompt-text conversion the history replay sends to the client is now the one the rewind fingerprints, so a prompt the client was shown one way cannot be hashed another and be missed. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- README.md | 1 + docs/air-extensions.md | 39 +++ src/AcpAgentRouter.ts | 15 ++ src/AcpExtensions.ts | 15 +- src/AirExtension.ts | 1 + src/CodexAcpClient.ts | 7 + src/CodexAcpServer.ts | 64 ++--- src/CodexAppServerClient.ts | 6 + src/SessionRewind.ts | 68 +++++ src/UserInputContent.ts | 61 +++++ .../data/initialize-v2-response.json | 3 +- .../data/session-index-capabilities.json | 4 +- .../CodexACPAgent/data/session-index-v2.json | 1 + .../CodexACPAgent/initialize.test.ts | 2 +- src/__tests__/CodexACPAgent/providers.test.ts | 64 +++++ src/__tests__/SessionRewind.test.ts | 253 ++++++++++++++++++ .../air/agent-message-and-reasoning.jsonl | 2 +- .../data/air/background-terminal.jsonl | 2 +- .../scenarios/data/air/collab-agent.jsonl | 2 +- .../scenarios/data/air/collab-controls.jsonl | 2 +- .../scenarios/data/air/command-approval.jsonl | 2 +- .../scenarios/data/air/command-failed.jsonl | 2 +- .../data/air/command-output-stdin.jsonl | 2 +- .../air/command-without-streamed-output.jsonl | 2 +- .../data/air/context-compaction.jsonl | 2 +- .../scenarios/data/air/dynamic-tool.jsonl | 2 +- .../data/air/file-change-approval.jsonl | 2 +- .../scenarios/data/air/file-changes.jsonl | 2 +- .../data/air/fuzzy-file-search.jsonl | 2 +- .../scenarios/data/air/goal-update.jsonl | 2 +- .../scenarios/data/air/guardian-review.jsonl | 2 +- .../scenarios/data/air/history-replay.jsonl | 2 +- .../scenarios/data/air/image-generation.jsonl | 2 +- .../scenarios/data/air/image-view.jsonl | 2 +- .../data/air/late-subagent-update.jsonl | 2 +- .../scenarios/data/air/mcp-elicitation.jsonl | 2 +- .../data/air/mcp-startup-failure.jsonl | 2 +- .../data/air/mcp-tool-approval.jsonl | 2 +- .../scenarios/data/air/mcp-tool.jsonl | 2 +- .../data/air/mcp-url-elicitation.jsonl | 2 +- .../data/air/native-subagent-session.jsonl | 2 +- .../data/air/nested-subagent-session.jsonl | 2 +- .../air/network-and-sandbox-approval.jsonl | 2 +- .../data/air/plan-deltas-and-turn-plan.jsonl | 2 +- .../data/air/plan-review-permission.jsonl | 2 +- .../scenarios/data/air/plan-stream.jsonl | 2 +- .../scenarios/data/air/read-search-list.jsonl | 2 +- .../data/air/subagent-activity.jsonl | 2 +- .../scenarios/data/air/web-search.jsonl | 2 +- src/__tests__/scenarios/scenario-harness.ts | 1 + src/index.ts | 1 + 51 files changed, 592 insertions(+), 80 deletions(-) create mode 100644 src/SessionRewind.ts create mode 100644 src/UserInputContent.ts create mode 100644 src/__tests__/SessionRewind.test.ts diff --git a/README.md b/README.md index 0c4ba6678..2556a97be 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Use [OpenAI Codex](https://github.com/openai/codex) from [Agent Client Protocol] - [Native ACP subagent sessions](docs/subagent-sessions.md) (after capability negotiation) with separate child histories and root-routed permissions; a legacy tool-call fallback otherwise. - [Background terminal tasks](docs/air-extensions.md#async-tasks) in AIR, with task status and targeted stop support after capability negotiation. - Session-scoped long-running goals in AIR through the [goal extension](docs/air-extensions.md#goal). +- In-place message editing in AIR through the [session rewind extension](docs/air-extensions.md#session-rewind), without a provider fork. - Typed warnings and errors through the opt-in [AIR session failure extension](docs/air-extensions.md#session-failure). - All AIR extensions, capabilities, and `_meta` keys: [AIR extensions](docs/air-extensions.md). - A per-turn [agent file-change report](docs/air-extensions.md#agent-file-change-report) after capability negotiation. diff --git a/docs/air-extensions.md b/docs/air-extensions.md index d7ed720b5..7280753b9 100644 --- a/docs/air-extensions.md +++ b/docs/air-extensions.md @@ -27,6 +27,7 @@ It describes only this adapter. - [Session index](#session-index) - [Context compaction](#context-compaction) - [Session fork point](#session-fork-point) +- [Session rewind](#session-rewind) - [Presentation hints](#presentation-hints) - [Removed keys](#removed-keys) @@ -1327,6 +1328,44 @@ It adds this object to the `session/fork` request: - When no message matches, the request fails with `invalidParams`. - The fork keeps the history up to the turn that holds the message. +## Session rewind + +Standard ACP forks a session, which gives a second thread with another id. +AIR edits a message in the session the user is looking at, so it removes a history suffix from the same Codex thread +instead, through `_session/rewind`. + +The adapter advertises `sessionRewind` in the AIR capability list of `initialize`. +A client sends the method only when the capability is there. + +```json +{ + "sessionId": "thread-1", + "beforeMessage": { "messageId": "item-12", "messageFingerprint": "sha256:<64 hex>", "messageOccurrence": 1 }, + "resumeAtMessage": { "messageId": "item-11", "messageFingerprint": "sha256:<64 hex>", "messageOccurrence": 1 } +} +``` + +- `beforeMessage` is the first user message to remove, and it is excluded from the retained history. +- `resumeAtMessage` is the last agent message to retain. It is absent when the client rewinds the first user turn. + The turn boundary already fixes the cut, so the adapter does not need it. +- A history point is the ACP message id, the `sha256:` digest of the message text, and the one-based occurrence of + that digest among messages of the same role. + The adapter matches the id first, and the fingerprint occurrence when restored Codex history carries other ids. + An id with a `:segment:` suffix also matches the message without the suffix. +- The response is `{"rewound": true}` only after Codex accepted the rewind. + `{"rewound": false}` means the conversation was left as it was, and the client then keeps its own transcript too. +- When no message matches, the request fails with `invalidParams`. +- A message that is a steer inside an existing turn fails with `invalidParams`: + Codex removes whole turns, not a suffix of one. + +Codex mapping: + +- The adapter reads the thread history and resolves `beforeMessage` to the turn it starts. +- Paginated history calls `thread/revert` with that turn as the exclusive boundary. + Legacy history has no such operation and answers `{"rewound": false}`. +- The Codex thread id stays the ACP session id. No `thread/fork`, no new thread, no session-list entry. +- `thread/revert` changes the persisted conversation only. It does not revert file changes. + ## Presentation hints The adapter sends these keys only to AIR: diff --git a/src/AcpAgentRouter.ts b/src/AcpAgentRouter.ts index 86559d213..558b3b1e8 100644 --- a/src/AcpAgentRouter.ts +++ b/src/AcpAgentRouter.ts @@ -10,6 +10,7 @@ import { SESSION_STEERING_METHOD, } from "./AcpExtensions"; import {ASYNC_TASK_STOP_METHOD} from "./async-tasks/AsyncTaskExtension"; +import {SESSION_REWIND_METHOD} from "./SessionRewind"; import {CODEX_HOOKS_LIST_METHOD, CODEX_HOOKS_TRUST_METHOD} from "./CodexHookTrust"; import { SESSION_ARCHIVE_METHOD, @@ -54,6 +55,18 @@ const asyncTaskStopParamsParser = z.object({ asyncTaskId: z.string().trim().min(1), }).passthrough(); +const sessionHistoryPointParser = z.object({ + messageId: z.string().trim().min(1), + messageFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/), + messageOccurrence: z.number().int().positive(), +}); + +const sessionRewindParamsParser = z.object({ + sessionId: z.string().trim().min(1), + beforeMessage: sessionHistoryPointParser, + resumeAtMessage: sessionHistoryPointParser.optional(), +}).passthrough(); + const hooksListParamsParser = z.object({cwd: z.string().trim().min(1)}); const hooksTrustParamsParser = z.object({ cwd: z.string().trim().min(1), @@ -150,6 +163,7 @@ export function createAcpAgentRouter( .onRequest(LEGACY_SET_SESSION_MODEL_METHOD, legacySetSessionModelParamsParser, (ctx) => getAgent().extMethod(LEGACY_SET_SESSION_MODEL_METHOD, ctx.params)) .onRequest(SESSION_STEERING_METHOD, sessionSteerParamsParser, (ctx) => getAgent().extMethod(SESSION_STEERING_METHOD, ctx.params)) .onRequest(ASYNC_TASK_STOP_METHOD, asyncTaskStopParamsParser, (ctx) => getAgent().extMethod(ASYNC_TASK_STOP_METHOD, ctx.params)) + .onRequest(SESSION_REWIND_METHOD, sessionRewindParamsParser, (ctx) => getAgent().extMethod(SESSION_REWIND_METHOD, ctx.params)) .onRequest(CODEX_HOOKS_LIST_METHOD, hooksListParamsParser, (ctx) => getAgent().listHooks(ctx.params.cwd)) .onRequest(CODEX_HOOKS_TRUST_METHOD, hooksTrustParamsParser, (ctx) => getAgent().trustHooks(ctx.params.cwd, ctx.params.hooks)) .onRequest(GOAL_CONTROL_METHOD, goalControlParamsParser, (ctx) => getAgent().extMethod(GOAL_CONTROL_METHOD, ctx.params)) @@ -182,6 +196,7 @@ export function createAcpAgentRouter( .onRequest(SESSION_STEERING_METHOD, sessionSteerParamsParser, (ctx) => getAgent().extMethod(SESSION_STEERING_METHOD, ctx.params)) .onRequest(GOAL_CONTROL_METHOD, goalControlParamsParser, (ctx) => getAgent().extMethod(GOAL_CONTROL_METHOD, ctx.params)) .onRequest(ASYNC_TASK_STOP_METHOD, asyncTaskStopParamsParser, (ctx) => getAgent().extMethod(ASYNC_TASK_STOP_METHOD, ctx.params)) + .onRequest(SESSION_REWIND_METHOD, sessionRewindParamsParser, (ctx) => getAgent().extMethod(SESSION_REWIND_METHOD, ctx.params)) .onRequest(SESSION_RENAME_METHOD, sessionRenameParamsParser, (ctx) => getAgent().sessionIndex.rename(ctx.params)) .onRequest(SESSION_ARCHIVE_METHOD, sessionArchiveParamsParser, (ctx) => getAgent().sessionIndex.setArchived(ctx.params, true)) .onRequest(SESSION_UNARCHIVE_METHOD, sessionArchiveParamsParser, (ctx) => getAgent().sessionIndex.setArchived(ctx.params, false)) diff --git a/src/AcpExtensions.ts b/src/AcpExtensions.ts index b450c8bd0..cca4297fd 100644 --- a/src/AcpExtensions.ts +++ b/src/AcpExtensions.ts @@ -15,6 +15,10 @@ import { ASYNC_TASK_STOP_METHOD, type AsyncTaskStopExtRequest, } from "./async-tasks/AsyncTaskExtension"; +import { + SESSION_REWIND_METHOD, + type SessionRewindRequest, +} from "./SessionRewind"; export { AUTH_STATUS_META_KEY, @@ -79,6 +83,7 @@ export type ExtMethodRequest = | SessionSteeringExtRequest | GoalControlExtRequest | AsyncTaskStopExtRequest + | SessionRewindExtRequest export function isExtMethodRequest(request: { method: string, params: Record }): request is ExtMethodRequest { return request.method === "authentication/status" @@ -87,7 +92,8 @@ export function isExtMethodRequest(request: { method: string, params: Record, params: SessionSteerRequest, diff --git a/src/AirExtension.ts b/src/AirExtension.ts index a121986ea..cb427771a 100644 --- a/src/AirExtension.ts +++ b/src/AirExtension.ts @@ -19,6 +19,7 @@ export const AIR_CUSTOM_INSTRUCTIONS_KEY = "customInstructions"; export const AIR_NATIVE_SUBAGENT_SESSIONS_KEY = "nativeSubagentSessions"; export const AIR_ASYNC_TASKS_KEY = "asyncTasks"; export const AIR_RECOMMENDED_CONFIG_VALUE_KEY = "recommendedValue"; +export const AIR_SESSION_REWIND_KEY = "sessionRewind"; export const AIR_ASYNC_TASKS_BACKGROUNDED_KEY = "backgrounded"; export const AIR_AGENT_FILE_CHANGE_REPORT_REQUEST_KEY = "agentFileChangeReportRequest"; export const AIR_MESSAGE_PHASE_KEY = "phase"; diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 07a4acc7f..7dbead54c 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -65,6 +65,7 @@ import type {ModeKind} from "./app-server/ModeKind"; import {arePathBasenamesEqual, arePathsEqual, isAbsolutePathLike} from "./PathUtils"; import {CodexSubagentSubscriptions} from "./subagents/CodexSubagentSubscriptions"; import {forkSession as runForkSession} from "./SessionFork"; +import {rewindSession as runRewindSession, type SessionRewindRequest} from "./SessionRewind"; import type {SessionMetadata, SessionMetadataWithThread} from "./SessionMetadata"; import { isMissingRolloutError, @@ -729,6 +730,12 @@ export class CodexAcpClient { }); } + async rewindSession(request: SessionRewindRequest): Promise<{rewound: boolean}> { + const response = await runRewindSession(request, this.codexClient); + await this.waitForSessionNotifications(request.sessionId); + return response; + } + async loadSession(request: WithAcpMcpServers, 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 d7d466ad1..59ed66b85 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -1,7 +1,8 @@ import * as acp from "@agentclientprotocol/sdk"; import {RequestError, type SessionId, type SessionModeState} from "@agentclientprotocol/sdk"; import {CodexEventHandler, type CompletedPlan, failureWasShownAsMessage} from "./CodexEventHandler"; -import {attachmentFileUri, desktopAttachmentHistory} from "./DesktopAttachmentHistory"; +import {attachmentFileUri} from "./DesktopAttachmentHistory"; +import {userInputToContentBlocks} from "./UserInputContent"; import {CodexApprovalHandler} from "./permissions/CodexApprovalHandler"; import {PermissionLifecycleContext} from "./permissions/lifecycle"; import {CodexElicitationHandler} from "./CodexElicitationHandler"; @@ -122,6 +123,8 @@ import { type LegacySetSessionModelRequest, type LegacySetSessionModelResponse, SESSION_STEERING_METHOD, + SESSION_REWIND_METHOD, + type SessionRewindRequest, type SessionSteeringResponse, type SessionSteerRequest, } from "./AcpExtensions"; @@ -191,6 +194,7 @@ import { AIR_PLAN_CONTENT_DELTA_KEY, AIR_RAW_INPUT_RENDERING_KEY, AIR_RECOMMENDED_CONFIG_VALUE_KEY, + AIR_SESSION_REWIND_KEY, AIR_EXTENSION_CAPABILITIES_KEY, AIR_EXTENSION_VERSION, AIR_GOAL_KEY, @@ -755,6 +759,7 @@ export class CodexAcpServer { AIR_RAW_INPUT_RENDERING_KEY, AIR_PLAN_CONTENT_DELTA_KEY, AIR_CODEX_HOOKS_KEY, + AIR_SESSION_REWIND_KEY, ...this.sessionIndex.agentCapabilities(), ] : [ @@ -764,6 +769,7 @@ export class CodexAcpServer { AIR_NATIVE_SUBAGENT_SESSIONS_KEY, AIR_ASYNC_TASKS_KEY, AIR_RECOMMENDED_CONFIG_VALUE_KEY, + AIR_SESSION_REWIND_KEY, ...this.sessionIndex.agentCapabilities(), ]; return { @@ -830,6 +836,19 @@ export class CodexAcpServer { ), }; } + case SESSION_REWIND_METHOD: + if (this.providerUpdate !== null) { + await this.providerUpdate; + } + if (!this.sessions.has(methodRequest.params.sessionId)) { + throw RequestError.invalidParams( + undefined, + `Unknown session: ${methodRequest.params.sessionId}`, + ); + } + return await this.runWithProcessCheck( + () => this.codexAcpClient.rewindSession(methodRequest.params as SessionRewindRequest), + ); case GOAL_CONTROL_METHOD: case LEGACY_GOAL_CONTROL_METHOD: { const sessionState = this.sessions.get(methodRequest.params.sessionId); @@ -3531,7 +3550,7 @@ export class CodexAcpServer { // provided then (`clientId`), so it round-trips as the same message on reconnect; v1 has // no such client-minted id and keeps using Codex's own item id. const messageId = this.protocolVersion === 2 ? (item.clientId ?? item.id) : item.id; - const contentBlocks = item.content.map(input => this.userInputToContentBlocks(input)); + const contentBlocks = item.content.map(input => userInputToContentBlocks(input)); const attachmentUris = new Set(contentBlocks.flatMap((blocks, index) => item.content[index]?.type === "text" ? blocks.flatMap(block => block.type === "resource_link" ? [block.uri] : []) @@ -3591,47 +3610,6 @@ export class CodexAcpServer { ); } - private userInputToContentBlocks(input: UserInput): acp.ContentBlock[] { - switch (input.type) { - case "text": - return desktopAttachmentHistory(input.text) - ?? (input.text.length > 0 ? [{ type: "text", text: input.text }] : []); - case "image": - return [{ - type: "text", - text: "url" in input - ? this.formatUriAsLink("image", input.url) - : `image:${input.fileId}`, - }]; - case "localImage": - case "localAudio": - case "mention": { - const uri = attachmentFileUri(input.path); - const fileName = input.path.split(/[\\/]/).pop() || input.type; - const name = input.type === "mention" && input.name.trim().length > 0 ? input.name : fileName; - return uri !== null - ? [{type: "resource_link", name, uri}] - : [{type: "text", text: this.formatUriAsLink(name, input.path)}]; - } - case "skill": - return [{ type: "text", text: `skill:${input.name} (${input.path})` }]; - case "audio": - return [{type: "text", text: this.formatUriAsLink("audio", input.url)}]; - } - } - - private formatUriAsLink(name: string | null, uri: string): string { - if (name && name.length > 0) { - return `[@${name}](${uri})`; - } - if (uri.startsWith("file://")) { - const path = uri.replace("file://", ""); - const fileName = path.split("/").pop() ?? path; - return `[@${fileName}](${uri})`; - } - return uri; - } - getSessionState(sessionId: string): SessionState { const sessionState = this.sessions.get(sessionId); if (!sessionState) { diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index 210331054..638aeb849 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -75,6 +75,8 @@ import type { Turn, ThreadResumeParams, ThreadResumeResponse, + ThreadRevertParams, + ThreadRevertResponse, ThreadSettings, ThreadStartParams, ThreadStartResponse, @@ -684,6 +686,10 @@ export class CodexAppServerClient { return await this.sendRequest({ method: "thread/fork", params: params }); } + async threadRevert(params: ThreadRevertParams): Promise { + return await this.sendRequest({method: "thread/revert", params}); + } + getThreadSettings(threadId: string): ThreadSettings | undefined { return this.threadSettings.get(threadId); } diff --git a/src/SessionRewind.ts b/src/SessionRewind.ts new file mode 100644 index 000000000..000db8009 --- /dev/null +++ b/src/SessionRewind.ts @@ -0,0 +1,68 @@ +import {createHash} from "node:crypto"; +import {RequestError} from "@agentclientprotocol/sdk"; +import type {CodexAppServerClient} from "./CodexAppServerClient"; +import {userInputVisibleText} from "./UserInputContent"; + +export const SESSION_REWIND_METHOD = "_session/rewind"; +export const SESSION_REWIND_CAPABILITY = "sessionRewind"; + +export type SessionHistoryPoint = { + messageId: string; + messageFingerprint: string; + messageOccurrence: number; +}; + +export type SessionRewindRequest = { + sessionId: string; + beforeMessage: SessionHistoryPoint; + resumeAtMessage?: SessionHistoryPoint; +}; + +export type SessionRewindResponse = {rewound: boolean}; + +export async function rewindSession( + request: SessionRewindRequest, + client: CodexAppServerClient, +): Promise { + const history = await client.threadReadWithHistory(request.sessionId); + const userTurns = history.thread.turns.flatMap((turn, turnIndex) => turn.items + .flatMap((item, itemIndex) => item.type === "userMessage" + ? [{turn, turnIndex, item, itemIndex}] + : [])); + const candidates = messageIdCandidates(request.beforeMessage.messageId); + const exact = candidates + .map(candidate => userTurns.find(({item}) => item.id === candidate)) + .find(match => match !== undefined); + const match = exact ?? userTurns.filter(({item}) => + fingerprint(userInputVisibleText(item.content)) === request.beforeMessage.messageFingerprint, + )[request.beforeMessage.messageOccurrence - 1]; + if (!match) { + throw RequestError.invalidParams( + {messageId: request.beforeMessage.messageId}, + `Rewind message ${request.beforeMessage.messageId} was not found in session ${request.sessionId}`, + ); + } + if (match.itemIndex !== 0) { + throw RequestError.invalidParams( + {messageId: request.beforeMessage.messageId}, + `Rewind message ${request.beforeMessage.messageId} does not start a turn`, + ); + } + // `thread/revert` replaces a paginated thread's durable history. A legacy + // thread has no such operation, so the only honest answer is that the + // conversation was left as it was and the client keeps its own transcript. + if (history.thread.historyMode === "legacy") { + return {rewound: false}; + } + await client.threadRevert({threadId: request.sessionId, beforeTurnId: match.turn.id}); + return {rewound: true}; +} + +function fingerprint(text: string): string { + return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`; +} + +function messageIdCandidates(messageId: string): string[] { + const protocolMessageId = messageId.replace(/:segment:\d+$/, ""); + return protocolMessageId === messageId ? [messageId] : [messageId, protocolMessageId]; +} diff --git a/src/UserInputContent.ts b/src/UserInputContent.ts new file mode 100644 index 000000000..bc89e54c5 --- /dev/null +++ b/src/UserInputContent.ts @@ -0,0 +1,61 @@ +import * as acp from "@agentclientprotocol/sdk"; +import type {UserInput} from "./app-server/v2"; +import {attachmentFileUri, desktopAttachmentHistory} from "./DesktopAttachmentHistory"; + +/** + * The content blocks of one persisted user input. + * + * One conversion for both readers of a stored prompt: the history replay, which + * sends these blocks to the client, and the rewind, which fingerprints the text + * the client was sent. Two conversions would make a prompt the client shows one + * way and the rewind hashes another, and the rewind would then miss it. + */ +export function userInputToContentBlocks(input: UserInput): acp.ContentBlock[] { + switch (input.type) { + case "text": + return desktopAttachmentHistory(input.text) + ?? (input.text.length > 0 ? [{type: "text", text: input.text}] : []); + case "image": + return [{ + type: "text", + text: "url" in input + ? formatUriAsLink("image", input.url) + : `image:${input.fileId}`, + }]; + case "localImage": + case "localAudio": + case "mention": { + const uri = attachmentFileUri(input.path); + const fileName = input.path.split(/[\\/]/).pop() || input.type; + const name = input.type === "mention" && input.name.trim().length > 0 ? input.name : fileName; + return uri !== null + ? [{type: "resource_link", name, uri}] + : [{type: "text", text: formatUriAsLink(name, input.path)}]; + } + case "skill": + return [{type: "text", text: `skill:${input.name} (${input.path})`}]; + case "audio": + return [{type: "text", text: formatUriAsLink("audio", input.url)}]; + } +} + +/** The text of a stored prompt as the client was shown it, which is what the + * client fingerprints when it names the message a rewind drops. */ +export function userInputVisibleText(content: UserInput[]): string { + return content.flatMap(userInputToContentBlocks) + .filter((block): block is Extract => block.type === "text") + .map(block => block.text) + .join(""); +} + +function formatUriAsLink(name: string | null, uri: string): string { + if (name && name.length > 0) { + return `[@${name}](${uri})`; + } + if (uri.startsWith("file://")) { + const path = uri.replace("file://", ""); + const fileName = path.split("/").pop() ?? path; + return `[@${fileName}](${uri})`; + } + return uri; +} diff --git a/src/__tests__/CodexACPAgent/data/initialize-v2-response.json b/src/__tests__/CodexACPAgent/data/initialize-v2-response.json index 1125848c2..816c8fac9 100644 --- a/src/__tests__/CodexACPAgent/data/initialize-v2-response.json +++ b/src/__tests__/CodexACPAgent/data/initialize-v2-response.json @@ -86,7 +86,8 @@ "customInstructions", "nativeSubagentSessions", "asyncTasks", - "recommendedValue" + "recommendedValue", + "sessionRewind" ] } } diff --git a/src/__tests__/CodexACPAgent/data/session-index-capabilities.json b/src/__tests__/CodexACPAgent/data/session-index-capabilities.json index 73ebec3cc..316832e08 100644 --- a/src/__tests__/CodexACPAgent/data/session-index-capabilities.json +++ b/src/__tests__/CodexACPAgent/data/session-index-capabilities.json @@ -10,6 +10,7 @@ "rawInputRendering", "planContentDelta", "codexHooks", + "sessionRewind", "sessionIndex", "sessionArchive", "sessionRename", @@ -26,7 +27,8 @@ "recommendedValue", "rawInputRendering", "planContentDelta", - "codexHooks" + "codexHooks", + "sessionRewind" ], "plain": null } diff --git a/src/__tests__/CodexACPAgent/data/session-index-v2.json b/src/__tests__/CodexACPAgent/data/session-index-v2.json index 377eb6aa8..45fe8941b 100644 --- a/src/__tests__/CodexACPAgent/data/session-index-v2.json +++ b/src/__tests__/CodexACPAgent/data/session-index-v2.json @@ -6,6 +6,7 @@ "nativeSubagentSessions", "asyncTasks", "recommendedValue", + "sessionRewind", "sessionIndex", "sessionArchive", "sessionRename", diff --git a/src/__tests__/CodexACPAgent/initialize.test.ts b/src/__tests__/CodexACPAgent/initialize.test.ts index ad567e9c3..16773f09b 100644 --- a/src/__tests__/CodexACPAgent/initialize.test.ts +++ b/src/__tests__/CodexACPAgent/initialize.test.ts @@ -91,7 +91,7 @@ describe('CodexACPAgent - initialize', () => { controlMethod: "_session/goal", actions: ["set", "pause", "resume", "clear"], }, - capabilities: ["sessionFailure", "diffPatch", "agentFileChangeReport", "customInstructions", "nativeSubagentSessions", "asyncTasks", "recommendedValue", "rawInputRendering", "planContentDelta", "codexHooks"], + capabilities: ["sessionFailure", "diffPatch", "agentFileChangeReport", "customInstructions", "nativeSubagentSessions", "asyncTasks", "recommendedValue", "rawInputRendering", "planContentDelta", "codexHooks", "sessionRewind"], }, }, }); diff --git a/src/__tests__/CodexACPAgent/providers.test.ts b/src/__tests__/CodexACPAgent/providers.test.ts index 50d45d520..185470af1 100644 --- a/src/__tests__/CodexACPAgent/providers.test.ts +++ b/src/__tests__/CodexACPAgent/providers.test.ts @@ -2,6 +2,15 @@ import {describe, expect, it, vi} from "vitest"; import * as acp from "@agentclientprotocol/sdk"; import {createCodexMockTestFixture, createTestSessionState, stubGatewayModelListing} from "../acp-test-utils"; import {CodexAcpClient, CUSTOM_GATEWAY_PROVIDER_ID, OPENAI_PROVIDER_ID} from "../../CodexAcpClient"; +import {SESSION_REWIND_METHOD} from "../../SessionRewind"; + +function deferred(): {promise: Promise, resolve: (value: T) => void} { + let resolve!: (value: T) => void; + const promise = new Promise(resolvePromise => { + resolve = resolvePromise; + }); + return {promise, resolve}; +} async function expectInvalidParams(fn: () => unknown): Promise { const caught = await Promise.resolve().then(fn).catch((err: unknown) => err); @@ -306,6 +315,61 @@ describe("Configurable LLM providers (providers/*)", () => { }); }); + it("waits for an in-flight provider update before rewinding", async () => { + const replacement = createCodexMockTestFixture().getCodexAcpClient(); + const resumeStarted = deferred(); + const resume = deferred(); + vi.spyOn(replacement, "initialize").mockResolvedValue(); + vi.spyOn(replacement, "resumeSession").mockImplementation(async () => { + resumeStarted.resolve(); + return await resume.promise; + }); + const replacementRewind = vi.spyOn(replacement, "rewindSession").mockResolvedValue({rewound: true}); + const fixture = createCodexMockTestFixture(vi.fn().mockResolvedValue(replacement)); + const agent = fixture.getCodexAcpAgent(); + await agent.initialize({protocolVersion: acp.PROTOCOL_VERSION}); + const sessions = (agent as unknown as {sessions: Map>}).sessions; + sessions.set("thread-1", createTestSessionState({sessionId: "thread-1", cwd: "/workspace"})); + + const providerUpdate = agent.setProvider({ + providerId: OPENAI_PROVIDER_ID, + apiType: "openai", + baseUrl: "https://gateway.example/v1", + }); + await resumeStarted.promise; + const rewind = agent.extMethod(SESSION_REWIND_METHOD, { + sessionId: "thread-1", + beforeMessage: { + messageId: "user-1", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + }); + + await Promise.resolve(); + expect(replacementRewind).not.toHaveBeenCalled(); + + resume.resolve({models: []} as never); + await providerUpdate; + await expect(rewind).resolves.toEqual({rewound: true}); + expect(replacementRewind).toHaveBeenCalledOnce(); + }); + + it("rejects rewind for a thread that is not a loaded ACP session", async () => { + const fixture = createCodexMockTestFixture(); + const rewind = vi.spyOn(fixture.getCodexAcpClient(), "rewindSession"); + + await expect(fixture.getCodexAcpAgent().extMethod(SESSION_REWIND_METHOD, { + sessionId: "persisted-but-not-loaded", + beforeMessage: { + messageId: "user-1", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + })).rejects.toThrow("Unknown session: persisted-but-not-loaded"); + expect(rewind).not.toHaveBeenCalled(); + }); + it("shares state with the legacy gateway auth method", async () => { const fixture = createCodexMockTestFixture(); const codexAcpClient = fixture.getCodexAcpClient(); diff --git a/src/__tests__/SessionRewind.test.ts b/src/__tests__/SessionRewind.test.ts new file mode 100644 index 000000000..44694036d --- /dev/null +++ b/src/__tests__/SessionRewind.test.ts @@ -0,0 +1,253 @@ +import {describe, expect, it, vi} from "vitest"; +import type {CodexAppServerClient} from "../CodexAppServerClient"; +import {CodexAcpClient} from "../CodexAcpClient"; +import {rewindSession} from "../SessionRewind"; + +describe("session rewind", () => { + it("reverts the same Codex thread before the selected user turn", async () => { + const client = { + threadReadWithHistory: vi.fn().mockResolvedValue({ + thread: { + turns: [ + {id: "turn-1", items: [{type: "userMessage", id: "user-1", content: [{type: "text", text: "one"}]}]}, + {id: "turn-2", items: [{type: "userMessage", id: "user-2", content: [{type: "text", text: "two"}]}]}, + ], + }, + }), + threadRevert: vi.fn().mockResolvedValue({}), + } as unknown as CodexAppServerClient; + + const result = await rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "user-2", + messageFingerprint: "sha256:3fc4ccfe745870e2c0d99f71f30ff0656c8d1ed5d3f3b71b17a64d1c0d9a4f5f", + messageOccurrence: 1, + }, + }, client); + + expect(result).toEqual({rewound: true}); + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); + }); + + it("resolves a restored message through its fingerprint occurrence", async () => { + const client = { + threadReadWithHistory: vi.fn().mockResolvedValue({ + thread: { + turns: [ + {id: "turn-1", items: [{type: "userMessage", id: "new-1", content: [{type: "text", text: "repeat"}]}]}, + {id: "turn-2", items: [{type: "userMessage", id: "new-2", content: [{type: "text", text: "repeat"}]}]}, + ], + }, + }), + threadRevert: vi.fn().mockResolvedValue({}), + } as unknown as CodexAppServerClient; + + const result = await rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "stale-id", + messageFingerprint: "sha256:25e2b6b106523880e27763084ffa6a0756335be0d7106022535365b9ad39b4b1", + messageOccurrence: 2, + }, + }, client); + + expect(result).toEqual({rewound: true}); + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); + }); + + it("prefers the exact segmented message id over its protocol id fallback", async () => { + const client = { + threadReadWithHistory: vi.fn().mockResolvedValue({ + thread: { + turns: [ + {id: "turn-1", items: [{type: "userMessage", id: "user-1", content: [{type: "text", text: "fallback"}]}]}, + {id: "turn-2", items: [{type: "userMessage", id: "user-1:segment:0", content: [{type: "text", text: "exact"}]}]}, + ], + }, + }), + threadRevert: vi.fn().mockResolvedValue({}), + } as unknown as CodexAppServerClient; + + await rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "user-1:segment:0", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + }, client); + + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); + }); + + it("does not fingerprint history when an exact message id is found", async () => { + const unreadableItem = Object.defineProperty({type: "userMessage", id: "other"}, "content", { + enumerable: true, + get: () => { + throw new Error("fingerprint fallback should not run"); + }, + }); + const client = { + threadReadWithHistory: vi.fn().mockResolvedValue({ + thread: { + turns: [ + {id: "turn-1", items: [unreadableItem]}, + {id: "turn-2", items: [{type: "userMessage", id: "exact", content: [{type: "text", text: "selected"}]}]}, + ], + }, + }), + threadRevert: vi.fn().mockResolvedValue({}), + } as unknown as CodexAppServerClient; + + await rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "exact", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + }, client); + + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); + }); + + it("uses the visible replay text when fingerprinting multimodal and skill inputs", async () => { + const client = { + threadReadWithHistory: vi.fn().mockResolvedValue({ + thread: { + historyMode: "paginated", + turns: [{ + id: "turn-1", + items: [{ + type: "userMessage", + id: "new-id", + content: [ + {type: "text", text: "look"}, + {type: "image", url: "https://example.com/image.png"}, + {type: "skill", name: "review", path: "/tmp/SKILL.md"}, + ], + }], + }], + }, + }), + threadRevert: vi.fn().mockResolvedValue({}), + } as unknown as CodexAppServerClient; + + await rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "stale-id", + messageFingerprint: "sha256:d0425f232dd6a5d6a18eee0fb305ff976368b93fb5ad3da67a4b41d919f7e2de", + messageOccurrence: 1, + }, + }, client); + + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-1"}); + }); + + it("leaves a legacy-history thread as it is", async () => { + const client = { + threadReadWithHistory: vi.fn().mockResolvedValue({ + thread: { + historyMode: "legacy", + turns: [ + {id: "turn-1", items: [{type: "userMessage", id: "user-1", content: [{type: "text", text: "one"}]}]}, + {id: "turn-2", items: [{type: "userMessage", id: "user-2", content: [{type: "text", text: "two"}]}]}, + {id: "turn-3", items: [{type: "userMessage", id: "user-3", content: [{type: "text", text: "three"}]}]}, + ], + }, + }), + threadRevert: vi.fn(), + } as unknown as CodexAppServerClient; + + const result = await rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "user-2", + messageFingerprint: "sha256:3fc4ccfe745870e2c0d99f71f30ff0656c8d1ed5d3f3b71b17a64d1c0d9a4f5f", + messageOccurrence: 1, + }, + }, client); + + expect(result).toEqual({rewound: false}); + expect(client.threadRevert).not.toHaveBeenCalled(); + }); + + it("rejects rewinding a steer inside an existing turn", async () => { + const client = { + threadReadWithHistory: vi.fn().mockResolvedValue({ + thread: { + historyMode: "paginated", + turns: [{ + id: "turn-1", + items: [ + {type: "userMessage", id: "user-1", content: [{type: "text", text: "first"}]}, + {type: "agentMessage", id: "assistant-1", text: "working"}, + {type: "userMessage", id: "steer-1", content: [{type: "text", text: "steer"}]}, + ], + }], + }, + }), + threadRevert: vi.fn(), + } as unknown as CodexAppServerClient; + + await expect(rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "steer-1", + messageFingerprint: "sha256:57fce44d7c6df51ad8525da1580a246e9d1142d79d1d1f176b1d29643d61ed44", + messageOccurrence: 1, + }, + resumeAtMessage: { + messageId: "assistant-1", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + }, client)).rejects.toThrow("does not start a turn"); + expect(client.threadRevert).not.toHaveBeenCalled(); + }); + + it("does not revert when the selected message is absent", async () => { + const client = { + threadReadWithHistory: vi.fn().mockResolvedValue({thread: {turns: []}}), + threadRevert: vi.fn(), + } as unknown as CodexAppServerClient; + + await expect(rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "missing", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + }, client)).rejects.toThrow("Rewind message missing was not found"); + expect(client.threadRevert).not.toHaveBeenCalled(); + }); + + it("drains queued session notifications before acknowledging rewind", async () => { + const appServerClient = { + threadReadWithHistory: vi.fn().mockResolvedValue({ + thread: { + historyMode: "paginated", + turns: [{id: "turn-1", items: [{type: "userMessage", id: "user-1", content: [{type: "text", text: "one"}]}]}], + }, + }), + threadRevert: vi.fn().mockResolvedValue({}), + } as unknown as CodexAppServerClient; + const client = new CodexAcpClient(appServerClient); + const waitForNotifications = vi.spyOn(client, "waitForSessionNotifications").mockResolvedValue(); + + await client.rewindSession({ + sessionId: "thread-1", + beforeMessage: { + messageId: "user-1", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + }); + + expect(waitForNotifications).toHaveBeenCalledWith("thread-1"); + expect(appServerClient.threadRevert).toHaveBeenCalledBefore(waitForNotifications); + }); +}); diff --git a/src/__tests__/scenarios/data/air/agent-message-and-reasoning.jsonl b/src/__tests__/scenarios/data/air/agent-message-and-reasoning.jsonl index e145bc278..aec503baf 100644 --- a/src/__tests__/scenarios/data/air/agent-message-and-reasoning.jsonl +++ b/src/__tests__/scenarios/data/air/agent-message-and-reasoning.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/background-terminal.jsonl b/src/__tests__/scenarios/data/air/background-terminal.jsonl index dfb2dd265..c3164d50f 100644 --- a/src/__tests__/scenarios/data/air/background-terminal.jsonl +++ b/src/__tests__/scenarios/data/air/background-terminal.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/collab-agent.jsonl b/src/__tests__/scenarios/data/air/collab-agent.jsonl index 5776128ea..1d33aa24a 100644 --- a/src/__tests__/scenarios/data/air/collab-agent.jsonl +++ b/src/__tests__/scenarios/data/air/collab-agent.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/collab-controls.jsonl b/src/__tests__/scenarios/data/air/collab-controls.jsonl index e231ae372..04b8d7b82 100644 --- a/src/__tests__/scenarios/data/air/collab-controls.jsonl +++ b/src/__tests__/scenarios/data/air/collab-controls.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/command-approval.jsonl b/src/__tests__/scenarios/data/air/command-approval.jsonl index 3a9a295eb..f0cf90572 100644 --- a/src/__tests__/scenarios/data/air/command-approval.jsonl +++ b/src/__tests__/scenarios/data/air/command-approval.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/command-failed.jsonl b/src/__tests__/scenarios/data/air/command-failed.jsonl index f362f218c..e03111936 100644 --- a/src/__tests__/scenarios/data/air/command-failed.jsonl +++ b/src/__tests__/scenarios/data/air/command-failed.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/command-output-stdin.jsonl b/src/__tests__/scenarios/data/air/command-output-stdin.jsonl index b799a78ad..dcd8d09b3 100644 --- a/src/__tests__/scenarios/data/air/command-output-stdin.jsonl +++ b/src/__tests__/scenarios/data/air/command-output-stdin.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/command-without-streamed-output.jsonl b/src/__tests__/scenarios/data/air/command-without-streamed-output.jsonl index 62ed2c03a..d8cbaa0fd 100644 --- a/src/__tests__/scenarios/data/air/command-without-streamed-output.jsonl +++ b/src/__tests__/scenarios/data/air/command-without-streamed-output.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/context-compaction.jsonl b/src/__tests__/scenarios/data/air/context-compaction.jsonl index e7ce2cbbd..aa8741906 100644 --- a/src/__tests__/scenarios/data/air/context-compaction.jsonl +++ b/src/__tests__/scenarios/data/air/context-compaction.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/dynamic-tool.jsonl b/src/__tests__/scenarios/data/air/dynamic-tool.jsonl index 639264187..06d2bf72d 100644 --- a/src/__tests__/scenarios/data/air/dynamic-tool.jsonl +++ b/src/__tests__/scenarios/data/air/dynamic-tool.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/file-change-approval.jsonl b/src/__tests__/scenarios/data/air/file-change-approval.jsonl index 06fe6a97c..9ae49ebdb 100644 --- a/src/__tests__/scenarios/data/air/file-change-approval.jsonl +++ b/src/__tests__/scenarios/data/air/file-change-approval.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/file-changes.jsonl b/src/__tests__/scenarios/data/air/file-changes.jsonl index 2eb2ee2af..778c5e628 100644 --- a/src/__tests__/scenarios/data/air/file-changes.jsonl +++ b/src/__tests__/scenarios/data/air/file-changes.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/fuzzy-file-search.jsonl b/src/__tests__/scenarios/data/air/fuzzy-file-search.jsonl index aba2ef5a9..98c964066 100644 --- a/src/__tests__/scenarios/data/air/fuzzy-file-search.jsonl +++ b/src/__tests__/scenarios/data/air/fuzzy-file-search.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/goal-update.jsonl b/src/__tests__/scenarios/data/air/goal-update.jsonl index d9a271e9a..64882f877 100644 --- a/src/__tests__/scenarios/data/air/goal-update.jsonl +++ b/src/__tests__/scenarios/data/air/goal-update.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/guardian-review.jsonl b/src/__tests__/scenarios/data/air/guardian-review.jsonl index 37e35e388..286b073cb 100644 --- a/src/__tests__/scenarios/data/air/guardian-review.jsonl +++ b/src/__tests__/scenarios/data/air/guardian-review.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/history-replay.jsonl b/src/__tests__/scenarios/data/air/history-replay.jsonl index 09f8186ca..7b5fc49f2 100644 --- a/src/__tests__/scenarios/data/air/history-replay.jsonl +++ b/src/__tests__/scenarios/data/air/history-replay.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/load","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/image-generation.jsonl b/src/__tests__/scenarios/data/air/image-generation.jsonl index 4197ce556..2098e44c3 100644 --- a/src/__tests__/scenarios/data/air/image-generation.jsonl +++ b/src/__tests__/scenarios/data/air/image-generation.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/image-view.jsonl b/src/__tests__/scenarios/data/air/image-view.jsonl index 6a40d5c5e..3e3b2677a 100644 --- a/src/__tests__/scenarios/data/air/image-view.jsonl +++ b/src/__tests__/scenarios/data/air/image-view.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/late-subagent-update.jsonl b/src/__tests__/scenarios/data/air/late-subagent-update.jsonl index e79ae803c..64b6bc222 100644 --- a/src/__tests__/scenarios/data/air/late-subagent-update.jsonl +++ b/src/__tests__/scenarios/data/air/late-subagent-update.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/mcp-elicitation.jsonl b/src/__tests__/scenarios/data/air/mcp-elicitation.jsonl index 65ee9aeaa..608802f55 100644 --- a/src/__tests__/scenarios/data/air/mcp-elicitation.jsonl +++ b/src/__tests__/scenarios/data/air/mcp-elicitation.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/mcp-startup-failure.jsonl b/src/__tests__/scenarios/data/air/mcp-startup-failure.jsonl index a95fab572..5065da3af 100644 --- a/src/__tests__/scenarios/data/air/mcp-startup-failure.jsonl +++ b/src/__tests__/scenarios/data/air/mcp-startup-failure.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"content":[{"content":{"text":"[codex-acp forwarded startup error] MCP server `db` failed to start: boom","type":"text"},"type":"content"}],"kind":"other","sessionUpdate":"tool_call","status":"failed","title":"mcp__db__startup","toolCallId":"mcp_startup.db."}}} diff --git a/src/__tests__/scenarios/data/air/mcp-tool-approval.jsonl b/src/__tests__/scenarios/data/air/mcp-tool-approval.jsonl index 2651a559a..2a19bd31c 100644 --- a/src/__tests__/scenarios/data/air/mcp-tool-approval.jsonl +++ b/src/__tests__/scenarios/data/air/mcp-tool-approval.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/mcp-tool.jsonl b/src/__tests__/scenarios/data/air/mcp-tool.jsonl index 40e8fa3bf..4abc99c90 100644 --- a/src/__tests__/scenarios/data/air/mcp-tool.jsonl +++ b/src/__tests__/scenarios/data/air/mcp-tool.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/mcp-url-elicitation.jsonl b/src/__tests__/scenarios/data/air/mcp-url-elicitation.jsonl index b13da4a5c..3efff59db 100644 --- a/src/__tests__/scenarios/data/air/mcp-url-elicitation.jsonl +++ b/src/__tests__/scenarios/data/air/mcp-url-elicitation.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/native-subagent-session.jsonl b/src/__tests__/scenarios/data/air/native-subagent-session.jsonl index d615abd7a..b178a80f3 100644 --- a/src/__tests__/scenarios/data/air/native-subagent-session.jsonl +++ b/src/__tests__/scenarios/data/air/native-subagent-session.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/nested-subagent-session.jsonl b/src/__tests__/scenarios/data/air/nested-subagent-session.jsonl index 68ed64971..0adcb23e2 100644 --- a/src/__tests__/scenarios/data/air/nested-subagent-session.jsonl +++ b/src/__tests__/scenarios/data/air/nested-subagent-session.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/network-and-sandbox-approval.jsonl b/src/__tests__/scenarios/data/air/network-and-sandbox-approval.jsonl index 56f5aa178..3bdc0304e 100644 --- a/src/__tests__/scenarios/data/air/network-and-sandbox-approval.jsonl +++ b/src/__tests__/scenarios/data/air/network-and-sandbox-approval.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/plan-deltas-and-turn-plan.jsonl b/src/__tests__/scenarios/data/air/plan-deltas-and-turn-plan.jsonl index 05e8acf72..3973d0dd6 100644 --- a/src/__tests__/scenarios/data/air/plan-deltas-and-turn-plan.jsonl +++ b/src/__tests__/scenarios/data/air/plan-deltas-and-turn-plan.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/plan-review-permission.jsonl b/src/__tests__/scenarios/data/air/plan-review-permission.jsonl index 60b368ecc..e9663eb32 100644 --- a/src/__tests__/scenarios/data/air/plan-review-permission.jsonl +++ b/src/__tests__/scenarios/data/air/plan-review-permission.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"plan","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/plan-stream.jsonl b/src/__tests__/scenarios/data/air/plan-stream.jsonl index 66a79c3d8..6056c7641 100644 --- a/src/__tests__/scenarios/data/air/plan-stream.jsonl +++ b/src/__tests__/scenarios/data/air/plan-stream.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/read-search-list.jsonl b/src/__tests__/scenarios/data/air/read-search-list.jsonl index 5a926020b..d78280bcf 100644 --- a/src/__tests__/scenarios/data/air/read-search-list.jsonl +++ b/src/__tests__/scenarios/data/air/read-search-list.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/subagent-activity.jsonl b/src/__tests__/scenarios/data/air/subagent-activity.jsonl index 96c9820c9..981317c31 100644 --- a/src/__tests__/scenarios/data/air/subagent-activity.jsonl +++ b/src/__tests__/scenarios/data/air/subagent-activity.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/data/air/web-search.jsonl b/src/__tests__/scenarios/data/air/web-search.jsonl index 34ee899c6..208b1be57 100644 --- a/src/__tests__/scenarios/data/air/web-search.jsonl +++ b/src/__tests__/scenarios/data/air/web-search.jsonl @@ -1,4 +1,4 @@ -{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} +{"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks","sessionRewind"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} {"direction":"response","method":"session/new","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"},"sessionId":"session-1"}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} diff --git a/src/__tests__/scenarios/scenario-harness.ts b/src/__tests__/scenarios/scenario-harness.ts index fdbfe0bf2..4691150ac 100644 --- a/src/__tests__/scenarios/scenario-harness.ts +++ b/src/__tests__/scenarios/scenario-harness.ts @@ -30,6 +30,7 @@ export const AIR_CAPABILITY_NAMES = [ "rawInputRendering", "planContentDelta", "codexHooks", + "sessionRewind", ]; /** The client capabilities of each client profile. */ diff --git a/src/index.ts b/src/index.ts index 58b0432c2..f5727eb3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import {prepareCodexHookConfig} from "./CodexHookConfig"; import {createAcpAgentRouter} from "./AcpAgentRouter"; import type {AcpClientConnection, AcpV2Connection} from "./ACPSessionConnection"; + if (process.argv.includes("--version")) { console.log(`${packageJson.name} ${packageJson.version}`); process.exit(0); From 3c9c3dd1beedc1617618f5794881ce15baea92da Mon Sep 17 00:00:00 2001 From: Nikita Ashikhmin Date: Sun, 18 Oct 2026 20:47:35 +0400 Subject: [PATCH 2/4] IJAI-2003 a legacy-history thread says it cannot be rewound A silent refusal reads to the client as a rewind that changed nothing, and Codex has no revert operation for legacy history at all, so the request fails with `invalidRequest` and `data: {sessionId, historyMode}`. A client that gets it knows the thread is the reason and can offer the fork instead of leaving the user with an edit that quietly did not happen. `{rewound: false}` is left to the one case where the conversation really was left as it was: a turn is running, and that turn writes the history the rewind would replace. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- docs/air-extensions.md | 8 ++++++-- src/CodexAcpServer.ts | 9 +++++++-- src/SessionRewind.ts | 10 +++++++--- src/__tests__/CodexACPAgent/providers.test.ts | 18 ++++++++++++++++++ src/__tests__/SessionRewind.test.ts | 7 +++---- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/docs/air-extensions.md b/docs/air-extensions.md index 7280753b9..89f9e7d10 100644 --- a/docs/air-extensions.md +++ b/docs/air-extensions.md @@ -1353,16 +1353,20 @@ A client sends the method only when the capability is there. The adapter matches the id first, and the fingerprint occurrence when restored Codex history carries other ids. An id with a `:segment:` suffix also matches the message without the suffix. - The response is `{"rewound": true}` only after Codex accepted the rewind. - `{"rewound": false}` means the conversation was left as it was, and the client then keeps its own transcript too. +- `{"rewound": false}` means the conversation was left as it was, and the client then keeps its own transcript too. + The adapter answers it while a turn is running: that turn writes the history the rewind would replace. - When no message matches, the request fails with `invalidParams`. - A message that is a steer inside an existing turn fails with `invalidParams`: Codex removes whole turns, not a suffix of one. +- A session created with legacy thread history fails with `invalidRequest` and + `data: {sessionId, historyMode: "legacy"}`. Only threads with paginated history can be rewound, + and a client that gets this error can offer the fork instead. Codex mapping: - The adapter reads the thread history and resolves `beforeMessage` to the turn it starts. - Paginated history calls `thread/revert` with that turn as the exclusive boundary. - Legacy history has no such operation and answers `{"rewound": false}`. + Legacy history has no such operation in the app-server protocol at all, which is the `invalidRequest` above. - The Codex thread id stays the ACP session id. No `thread/fork`, no new thread, no session-list entry. - `thread/revert` changes the persisted conversation only. It does not revert file changes. diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 59ed66b85..2d5c64495 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -836,19 +836,24 @@ export class CodexAcpServer { ), }; } - case SESSION_REWIND_METHOD: + case SESSION_REWIND_METHOD: { if (this.providerUpdate !== null) { await this.providerUpdate; } - if (!this.sessions.has(methodRequest.params.sessionId)) { + const sessionState = this.sessions.get(methodRequest.params.sessionId); + if (!sessionState) { throw RequestError.invalidParams( undefined, `Unknown session: ${methodRequest.params.sessionId}`, ); } + // A running turn writes the history the rewind would replace, so the + // conversation is left as it is and the client keeps its own transcript. + if (sessionState.currentTurnId !== null) return {rewound: false}; return await this.runWithProcessCheck( () => this.codexAcpClient.rewindSession(methodRequest.params as SessionRewindRequest), ); + } case GOAL_CONTROL_METHOD: case LEGACY_GOAL_CONTROL_METHOD: { const sessionState = this.sessions.get(methodRequest.params.sessionId); diff --git a/src/SessionRewind.ts b/src/SessionRewind.ts index 000db8009..8f2add128 100644 --- a/src/SessionRewind.ts +++ b/src/SessionRewind.ts @@ -49,10 +49,14 @@ export async function rewindSession( ); } // `thread/revert` replaces a paginated thread's durable history. A legacy - // thread has no such operation, so the only honest answer is that the - // conversation was left as it was and the client keeps its own transcript. + // thread has no such operation at all, so the client is told the thread + // cannot be rewound rather than being left to read a silent refusal as a + // rewind that changed nothing. if (history.thread.historyMode === "legacy") { - return {rewound: false}; + throw RequestError.invalidRequest( + {sessionId: request.sessionId, historyMode: history.thread.historyMode}, + `Session ${request.sessionId} was created with legacy history, which Codex cannot rewind`, + ); } await client.threadRevert({threadId: request.sessionId, beforeTurnId: match.turn.id}); return {rewound: true}; diff --git a/src/__tests__/CodexACPAgent/providers.test.ts b/src/__tests__/CodexACPAgent/providers.test.ts index 185470af1..9ead12792 100644 --- a/src/__tests__/CodexACPAgent/providers.test.ts +++ b/src/__tests__/CodexACPAgent/providers.test.ts @@ -355,6 +355,24 @@ describe("Configurable LLM providers (providers/*)", () => { expect(replacementRewind).toHaveBeenCalledOnce(); }); + it("leaves the conversation as it is when a turn is running", async () => { + const fixture = createCodexMockTestFixture(); + const rewind = vi.spyOn(fixture.getCodexAcpClient(), "rewindSession"); + const agent = fixture.getCodexAcpAgent(); + const sessions = (agent as unknown as {sessions: Map>}).sessions; + sessions.set("thread-1", createTestSessionState({sessionId: "thread-1", cwd: "/workspace", currentTurnId: "turn-9"})); + + await expect(agent.extMethod(SESSION_REWIND_METHOD, { + sessionId: "thread-1", + beforeMessage: { + messageId: "user-1", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + })).resolves.toEqual({rewound: false}); + expect(rewind).not.toHaveBeenCalled(); + }); + it("rejects rewind for a thread that is not a loaded ACP session", async () => { const fixture = createCodexMockTestFixture(); const rewind = vi.spyOn(fixture.getCodexAcpClient(), "rewindSession"); diff --git a/src/__tests__/SessionRewind.test.ts b/src/__tests__/SessionRewind.test.ts index 44694036d..4b16f2032 100644 --- a/src/__tests__/SessionRewind.test.ts +++ b/src/__tests__/SessionRewind.test.ts @@ -146,7 +146,7 @@ describe("session rewind", () => { expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-1"}); }); - it("leaves a legacy-history thread as it is", async () => { + it("reports that a legacy-history thread cannot be rewound", async () => { const client = { threadReadWithHistory: vi.fn().mockResolvedValue({ thread: { @@ -161,16 +161,15 @@ describe("session rewind", () => { threadRevert: vi.fn(), } as unknown as CodexAppServerClient; - const result = await rewindSession({ + await expect(rewindSession({ sessionId: "thread-1", beforeMessage: { messageId: "user-2", messageFingerprint: "sha256:3fc4ccfe745870e2c0d99f71f30ff0656c8d1ed5d3f3b71b17a64d1c0d9a4f5f", messageOccurrence: 1, }, - }, client); + }, client)).rejects.toThrow("legacy history, which Codex cannot rewind"); - expect(result).toEqual({rewound: false}); expect(client.threadRevert).not.toHaveBeenCalled(); }); From 9715feb6a5e309af999d9a3f11adafa4f1a3107d Mon Sep 17 00:00:00 2001 From: Aleksandr Slapoguzov Date: Fri, 6 Nov 2026 13:38:58 +0100 Subject: [PATCH 3/4] IJAI-2003 a prompt sent during a rewind waits for it instead of being cancelled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `thread/revert` cancels a turn that starts while it runs, so a `session/prompt` sent right after `_session/rewind` ended with `stopReason: "cancelled"` and no error. The rewind now takes the session's turn-start slot before its first await, and the router tries `_session/rewind` before `session/prompt`, so a prompt sent right after it starts its turn on the retained history. A rewind that finds a turn running or a prompt waiting still answers `rewound: false`. It also waits for a provider update and restarts a crashed app-server, as fork and prompt do. The history point now resolves the way the client sees the message: - the fingerprinted text is the text that history replay sends, so a file mention counts as its `[@foo.txt](file://…)` link text and a native attachment sent as a resource link does not; - a v2 message also matches the client id it is replayed under; - the occurrence counts the session's own user messages; Codex subagents run in their own threads, as in the Claude adapter; - the cut is the first user message of a turn, not the turn's first item. Legacy history is refused from the thread metadata, before the turns are read. The docs say that Codex removed the deprecated `thread/rollback` rather than that it never existed. Co-Authored-By: Claude Opus 5.5 --- docs/air-extensions.md | 21 +- src/AcpAgentRouter.ts | 8 +- src/CodexAcpClient.ts | 4 +- src/CodexAcpServer.ts | 77 ++-- src/CodexAppServerClient.ts | 7 +- src/SessionRewind.ts | 44 +-- src/UserInputContent.ts | 22 +- .../CodexACPAgent/session-rewind.test.ts | 185 ++++++++++ src/__tests__/SessionRewind.test.ts | 330 ++++++++---------- src/index.ts | 1 - 10 files changed, 434 insertions(+), 265 deletions(-) create mode 100644 src/__tests__/CodexACPAgent/session-rewind.test.ts diff --git a/docs/air-extensions.md b/docs/air-extensions.md index 1f017cbd0..1336f9ef3 100644 --- a/docs/air-extensions.md +++ b/docs/air-extensions.md @@ -137,7 +137,8 @@ The `initialize` response carries the agent side of the extension only when the "recommendedValue", "rawInputRendering", "planContentDelta", - "codexHooks" + "codexHooks", + "sessionRewind" ] } } @@ -180,6 +181,8 @@ Five agent capabilities come with `sessionIndex` and are listed exactly when it The goal extension has no client capability. The agent advertises the `goal` object, and the client uses the control method when it wants to. +The session rewind extension has no client capability either. +The agent lists `sessionRewind`, and the client sends `_session/rewind` when it wants to; see [Session rewind](#session-rewind). ### Hook trust @@ -1426,10 +1429,18 @@ A client sends the method only when the capability is there. - A history point is the ACP message id, the `sha256:` digest of the message text, and the one-based occurrence of that digest among messages of the same role. The adapter matches the id first, and the fingerprint occurrence when restored Codex history carries other ids. + The id is the one that history replay sends: the Codex item id on v1, and the client message id on v2. An id with a `:segment:` suffix also matches the message without the suffix. +- The text of a user message is its text blocks as history replay sends them, joined without a separator. + A file mention of a prompt is a text block such as `[@foo.txt](file:///work/foo.txt)`, so it is part of the text. + A `resource_link` block that replay sends for a native attachment is not. +- The occurrence counts the user messages of the session itself. + A Codex subagent runs in its own thread, so its messages are not counted, as in the Claude adapter. - The response is `{"rewound": true}` only after Codex accepted the rewind. - `{"rewound": false}` means the conversation was left as it was, and the client then keeps its own transcript too. - The adapter answers it while a turn is running: that turn writes the history the rewind would replace. + The adapter answers it while a turn is running or a prompt waits to start one: that turn writes the history the + rewind would replace. +- A `session/prompt` sent while a rewind runs starts its turn after the rewind, on the retained history. - When no message matches, the request fails with `invalidParams`. - A message that is a steer inside an existing turn fails with `invalidParams`: Codex removes whole turns, not a suffix of one. @@ -1439,9 +1450,11 @@ A client sends the method only when the capability is there. Codex mapping: -- The adapter reads the thread history and resolves `beforeMessage` to the turn it starts. +- The adapter reads the thread metadata, refuses legacy history, then reads the turns and resolves `beforeMessage` + to the turn it starts. - Paginated history calls `thread/revert` with that turn as the exclusive boundary. - Legacy history has no such operation in the app-server protocol at all, which is the `invalidRequest` above. + Legacy history had only the deprecated `thread/rollback`, which Codex has since removed, so it gets the + `invalidRequest` above. - The Codex thread id stays the ACP session id. No `thread/fork`, no new thread, no session-list entry. - `thread/revert` changes the persisted conversation only. It does not revert file changes. diff --git a/src/AcpAgentRouter.ts b/src/AcpAgentRouter.ts index ac40e3eae..cd8a918f9 100644 --- a/src/AcpAgentRouter.ts +++ b/src/AcpAgentRouter.ts @@ -213,6 +213,9 @@ function createRouter( .onRequest(acp.methods.agent.providers.list, (ctx) => getAgent().listProviders(ctx.params)) .onRequest(acp.methods.agent.providers.set, (ctx) => getAgent().setProvider(ctx.params)) .onRequest(acp.methods.agent.providers.disable, (ctx) => getAgent().disableProvider(ctx.params)) + // Before `session/prompt`: the SDK tries the handlers in this order, so a prompt sent right after a + // rewind reaches the agent after it, and waits for it. + .onRequest(SESSION_REWIND_METHOD, sessionRewindParamsParser, (ctx) => getAgent(ctx.params).extMethod(SESSION_REWIND_METHOD, ctx.params)) .onRequest(acp.methods.agent.session.prompt, (ctx) => getAgent(ctx.params).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)) @@ -220,7 +223,6 @@ function createRouter( .onRequest(LEGACY_SET_SESSION_MODEL_METHOD, legacySetSessionModelParamsParser, (ctx) => getAgent(ctx.params).extMethod(LEGACY_SET_SESSION_MODEL_METHOD, ctx.params)) .onRequest(SESSION_STEERING_METHOD, sessionSteerParamsParser, (ctx) => getAgent(ctx.params).extMethod(SESSION_STEERING_METHOD, ctx.params)) .onRequest(ASYNC_TASK_STOP_METHOD, asyncTaskStopParamsParser, (ctx) => getAgent(ctx.params).extMethod(ASYNC_TASK_STOP_METHOD, ctx.params)) - .onRequest(SESSION_REWIND_METHOD, sessionRewindParamsParser, (ctx) => getAgent(ctx.params).extMethod(SESSION_REWIND_METHOD, ctx.params)) .onRequest(CODEX_HOOKS_LIST_METHOD, hooksListParamsParser, (ctx) => getAgent().listHooks(ctx.params.cwd)) .onRequest(CODEX_HOOKS_TRUST_METHOD, hooksTrustParamsParser, (ctx) => getAgent().trustHooks(ctx.params.cwd, ctx.params.hooks)) .onRequest(GOAL_CONTROL_METHOD, goalControlParamsParser, (ctx) => getAgent(ctx.params).extMethod(GOAL_CONTROL_METHOD, ctx.params)) @@ -247,6 +249,9 @@ function createRouter( // The v1-only `authentication/status|logout` extensions are not registered on v2. .onRequest(acpV2.methods.agent.auth.login, (ctx) => getAgent().authenticateV2(ctx.params, ctx.requestId, ctx.signal)) .onRequest(acpV2.methods.agent.auth.logout, (ctx) => getAgent().logoutV2(ctx.params)) + // Before `session/prompt`: the SDK tries the handlers in this order, so a prompt sent right after a + // rewind reaches the agent after it, and waits for it. + .onRequest(SESSION_REWIND_METHOD, sessionRewindParamsParser, (ctx) => getAgent(ctx.params).extMethod(SESSION_REWIND_METHOD, ctx.params)) .onRequest(acpV2.methods.agent.session.prompt, (ctx) => getAgent(ctx.params).promptV2(ctx.params, ctx.signal)) .onNotification(acpV2.methods.agent.session.cancel, (ctx) => getAgent().cancel(ctx.params)) .onRequest(acpV2.methods.agent.providers.list, (ctx) => getAgent().listProviders(ctx.params)) @@ -255,7 +260,6 @@ function createRouter( .onRequest(SESSION_STEERING_METHOD, sessionSteerParamsParser, (ctx) => getAgent(ctx.params).extMethod(SESSION_STEERING_METHOD, ctx.params)) .onRequest(GOAL_CONTROL_METHOD, goalControlParamsParser, (ctx) => getAgent(ctx.params).extMethod(GOAL_CONTROL_METHOD, ctx.params)) .onRequest(ASYNC_TASK_STOP_METHOD, asyncTaskStopParamsParser, (ctx) => getAgent(ctx.params).extMethod(ASYNC_TASK_STOP_METHOD, ctx.params)) - .onRequest(SESSION_REWIND_METHOD, sessionRewindParamsParser, (ctx) => getAgent(ctx.params).extMethod(SESSION_REWIND_METHOD, ctx.params)) .onRequest(SESSION_RENAME_METHOD, sessionRenameParamsParser, (ctx) => getAgent(ctx.params).sessionIndex.rename(ctx.params)) .onRequest(SESSION_BATCH_GET_METHOD, sessionBatchGetParamsParser, (ctx) => getAgent().sessionIndex.batchGet(ctx.params)) .onRequest(SESSION_ARCHIVE_METHOD, sessionArchiveParamsParser, (ctx) => getAgent(ctx.params).sessionIndex.setArchived(ctx.params, true)) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index f4166a7df..4bcd220fc 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -70,7 +70,7 @@ import type {ModeKind} from "./app-server/ModeKind"; import {arePathBasenamesEqual, arePathsEqual, isAbsolutePathLike, isAbsoluteProtocolPath} from "./PathUtils"; import {CodexSubagentSubscriptions} from "./subagents/CodexSubagentSubscriptions"; import {forkSession as runForkSession} from "./SessionFork"; -import {rewindSession as runRewindSession, type SessionRewindRequest} from "./SessionRewind"; +import {rewindSession as runRewindSession, type SessionRewindRequest, type SessionRewindResponse} from "./SessionRewind"; import type {SessionMetadata, SessionMetadataWithThread} from "./SessionMetadata"; import {runAccountLogin} from "./CodexAccountLogin"; import {throwIfRequestCancelled, withRequestCancellation} from "./RequestCancellation"; @@ -748,7 +748,7 @@ export class CodexAcpClient { }, instructions); } - async rewindSession(request: SessionRewindRequest): Promise<{rewound: boolean}> { + async rewindSession(request: SessionRewindRequest): Promise { const response = await runRewindSession(request, this.codexClient); await this.waitForSessionNotifications(request.sessionId); return response; diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 5fce79cfd..766cbe724 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -1,8 +1,7 @@ import * as acp from "@agentclientprotocol/sdk/unstable"; import {RequestError, type SessionId, type SessionModeState} from "@agentclientprotocol/sdk/unstable"; import {CodexEventHandler, type CompletedPlan, failureWasShownAsMessage} from "./CodexEventHandler"; -import {attachmentFileUri} from "./DesktopAttachmentHistory"; -import {userInputToContentBlocks} from "./UserInputContent"; +import {userMessageContentBlocks} from "./UserInputContent"; import {CodexApprovalHandler} from "./permissions/CodexApprovalHandler"; import {isRecord} from "./permissions/json"; import {PermissionLifecycleContext} from "./permissions/lifecycle"; @@ -135,6 +134,7 @@ import { SESSION_STEERING_METHOD, SESSION_REWIND_METHOD, type SessionRewindRequest, + type SessionRewindResponse, type SessionSteeringResponse, type SessionSteerRequest, } from "./AcpExtensions"; @@ -1020,24 +1020,8 @@ export class CodexAcpServer { ), }; } - case SESSION_REWIND_METHOD: { - if (this.providerUpdate !== null) { - await this.providerUpdate; - } - const sessionState = this.sessions.get(methodRequest.params.sessionId); - if (!sessionState) { - throw RequestError.invalidParams( - undefined, - `Unknown session: ${methodRequest.params.sessionId}`, - ); - } - // A running turn writes the history the rewind would replace, so the - // conversation is left as it is and the client keeps its own transcript. - if (sessionState.currentTurnId !== null) return {rewound: false}; - return await this.runWithProcessCheck( - () => this.codexAcpClient.rewindSession(methodRequest.params as SessionRewindRequest), - ); - } + case SESSION_REWIND_METHOD: + return await this.rewindSession(methodRequest.params); case GOAL_CONTROL_METHOD: case LEGACY_GOAL_CONTROL_METHOD: { const sessionState = this.sessions.get(methodRequest.params.sessionId); @@ -1155,6 +1139,38 @@ export class CodexAcpServer { } } + /** Whether a turn of the session runs, or a turn starter holds or waits for its turn-start slot. */ + private hasTurnWork(sessionId: string): boolean { + return this.isSessionBusy(sessionId) || this.activePrompts.has(sessionId) + || this.pendingTurnStarts.has(sessionId) || this.turnStartQueueTail.get(sessionId)?.settled === false; + } + + private async rewindSession(params: SessionRewindRequest): Promise { + const sessionId = params.sessionId; + const unknownSession = () => RequestError.invalidParams(undefined, `Unknown session: ${sessionId}`); + if (!this.sessions.has(sessionId)) throw unknownSession(); + // A running turn writes the history the rewind would replace, so the + // conversation is left as it is and the client keeps its own transcript. + if (this.hasTurnWork(sessionId) || this.sessions.get(sessionId)!.currentTurnId !== null) return {rewound: false}; + // Taken before the first await: a prompt sent meanwhile waits for the rewind + // instead of starting a turn that `thread/revert` would cancel. + const reservation = this.acquireTurnStartReservation(sessionId); + try { + if (this.providerUpdate !== null) await this.waitForProviderUpdate(); + const appServer = this.ensureAppServer(); + if (appServer) await appServer; + const sessionState = this.sessions.get(sessionId); + if (!sessionState) throw unknownSession(); + // A turn that Codex started itself, such as a goal continuation, also writes the history. + if (this.isCodexTurnRunning(sessionId)) return {rewound: false}; + const sessionReady = this.ensureSessionReady(sessionState); + if (sessionReady) await sessionReady; + return await this.runWithProcessCheck(() => this.codexAcpClient.rewindSession(params)); + } finally { + reservation.release(); + } + } + private async withSystemPromptUpdate( request: {sessionId: string; systemPrompt?: unknown; _meta?: unknown}, action: () => Promise, @@ -1162,8 +1178,7 @@ export class CodexAcpServer { this.assertInstructionsReady(request.sessionId); if (!changesSystemPrompt(request, this.sessions.get(request.sessionId)?.instructions)) return action(); const sessionId = request.sessionId; - if (this.isSessionBusy(sessionId) || this.activePrompts.has(sessionId) - || this.pendingTurnStarts.has(sessionId) || this.turnStartQueueTail.get(sessionId)?.settled === false) { + if (this.hasTurnWork(sessionId)) { throw RequestError.invalidParams(undefined, "Cannot change systemPrompt while session work is in progress; retry when idle"); } this.instructionUpdates.add(sessionId); @@ -4165,27 +4180,11 @@ export class CodexAcpServer { } private createUserMessageUpdates(item: ThreadItem & { type: "userMessage" }): UpdateSessionEvent[] { - const updates: UpdateSessionEvent[] = []; // On v2, a message inserted via `session/prompt` is replayed under the id the client // provided then (`clientId`), so it round-trips as the same message on reconnect; v1 has // no such client-minted id and keeps using Codex's own item id. const messageId = this.protocolVersion === 2 ? (item.clientId ?? item.id) : item.id; - const contentBlocks = item.content.map(input => userInputToContentBlocks(input)); - const attachmentUris = new Set(contentBlocks.flatMap((blocks, index) => - item.content[index]?.type === "text" - ? blocks.flatMap(block => block.type === "resource_link" ? [block.uri] : []) - : [])); - for (const [index, input] of item.content.entries()) { - const nativePath = input.type === "localImage" || input.type === "localAudio" || input.type === "mention" - ? input.path - : (input.type === "image" || input.type === "audio") && "url" in input ? input.url : null; - if (nativePath !== null && attachmentUris.has(attachmentFileUri(nativePath) ?? "")) continue; - const blocks = contentBlocks[index]!; - for (const block of blocks) { - updates.push(createUserMessageChunk(block, messageId)); - } - } - return updates; + return userMessageContentBlocks(item.content).map(block => createUserMessageChunk(block, messageId)); } private createReasoningUpdates(item: ThreadItem & { type: "reasoning" }): UpdateSessionEvent[] { diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index 220b595a2..9ed05c9da 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -787,11 +787,16 @@ export class CodexAppServerClient { if (response.thread.historyMode === "legacy") { return await this.threadRead({threadId, includeTurns: true}); } + return {...response, thread: {...response.thread, turns: await this.threadPaginatedTurns(threadId)}}; + } + + /** The full turns of a paginated thread, oldest first. */ + async threadPaginatedTurns(threadId: string): Promise { const turns: Turn[] = []; for await (const page of this.threadHistoryPages(threadId)) { turns.push(...page); } - return {...response, thread: {...response.thread, turns}}; + return turns; } /** diff --git a/src/SessionRewind.ts b/src/SessionRewind.ts index d7056be40..ee21281d4 100644 --- a/src/SessionRewind.ts +++ b/src/SessionRewind.ts @@ -4,7 +4,6 @@ import type {CodexAppServerClient} from "./CodexAppServerClient"; import {userInputVisibleText} from "./UserInputContent"; export const SESSION_REWIND_METHOD = "_session/rewind"; -export const SESSION_REWIND_CAPABILITY = "sessionRewind"; export type SessionHistoryPoint = { messageId: string; @@ -24,16 +23,28 @@ export async function rewindSession( request: SessionRewindRequest, client: CodexAppServerClient, ): Promise { - const history = await client.threadReadWithHistory(request.sessionId); - const userTurns = history.thread.turns.flatMap((turn, turnIndex) => turn.items - .flatMap((item, itemIndex) => item.type === "userMessage" - ? [{turn, turnIndex, item, itemIndex}] - : [])); - const candidates = messageIdCandidates(request.beforeMessage.messageId); - const exact = candidates - .map(candidate => userTurns.find(({item}) => item.id === candidate)) + const {thread} = await client.threadRead({threadId: request.sessionId}); + // `thread/revert` replaces only a paginated thread's history, and Codex removed + // the deprecated `thread/rollback` that legacy history had. The client is told the + // thread cannot be rewound rather than being left to read a silent refusal as a + // rewind that changed nothing. + if (thread.historyMode === "legacy") { + throw RequestError.invalidRequest( + {sessionId: request.sessionId, historyMode: thread.historyMode}, + `Session ${request.sessionId} was created with legacy history, which Codex cannot rewind`, + ); + } + const turns = await client.threadPaginatedTurns(request.sessionId); + const userMessages = turns.flatMap(turn => turn.items.flatMap(item => item.type === "userMessage" + ? [{turn, item}] + : [])); + // v1 replays a message under Codex's item id, v2 under the client id it was sent with. + const exact = messageIdCandidates(request.beforeMessage.messageId) + .map(candidate => userMessages.find(({item}) => item.id === candidate || item.clientId === candidate)) .find(match => match !== undefined); - const match = exact ?? userTurns.filter(({item}) => + // The occurrence counts the user messages of this thread only. Subagents run in their + // own threads, so their messages are not counted, as in the Claude adapter. + const match = exact ?? userMessages.filter(({item}) => fingerprint(userInputVisibleText(item.content)) === request.beforeMessage.messageFingerprint, )[request.beforeMessage.messageOccurrence - 1]; if (!match) { @@ -42,22 +53,13 @@ export async function rewindSession( `Rewind message ${request.beforeMessage.messageId} was not found in session ${request.sessionId}`, ); } - if (match.itemIndex !== 0) { + // Codex removes whole turns, so only the message that started a turn can be the cut. + if (match.turn.items.find(item => item.type === "userMessage") !== match.item) { throw RequestError.invalidParams( {messageId: request.beforeMessage.messageId}, `Rewind message ${request.beforeMessage.messageId} does not start a turn`, ); } - // `thread/revert` replaces a paginated thread's durable history. A legacy - // thread has no such operation at all, so the client is told the thread - // cannot be rewound rather than being left to read a silent refusal as a - // rewind that changed nothing. - if (history.thread.historyMode === "legacy") { - throw RequestError.invalidRequest( - {sessionId: request.sessionId, historyMode: history.thread.historyMode}, - `Session ${request.sessionId} was created with legacy history, which Codex cannot rewind`, - ); - } await client.threadRevert({threadId: request.sessionId, beforeTurnId: match.turn.id}); return {rewound: true}; } diff --git a/src/UserInputContent.ts b/src/UserInputContent.ts index 129188c47..b2c013d4c 100644 --- a/src/UserInputContent.ts +++ b/src/UserInputContent.ts @@ -4,14 +4,30 @@ import {attachmentFileUri, desktopAttachmentHistory} from "./DesktopAttachmentHi import {renderAsyncQuestionReply} from "./CodexAsyncQuestions"; /** - * The content blocks of one persisted user input. + * The content blocks that history replay sends for one persisted user message. * * One conversion for both readers of a stored prompt: the history replay, which * sends these blocks to the client, and the rewind, which fingerprints the text * the client was sent. Two conversions would make a prompt the client shows one * way and the rewind hashes another, and the rewind would then miss it. */ -export function userInputToContentBlocks(input: UserInput): acp.ContentBlock[] { +export function userMessageContentBlocks(content: UserInput[]): acp.ContentBlock[] { + const contentBlocks = content.map(userInputToContentBlocks); + // A native attachment that the Desktop envelope of a text input already lists is sent once. + const attachmentUris = new Set(contentBlocks.flatMap((blocks, index) => + content[index]?.type === "text" + ? blocks.flatMap(block => block.type === "resource_link" ? [block.uri] : []) + : [])); + return content.flatMap((input, index) => { + const nativePath = input.type === "localImage" || input.type === "localAudio" || input.type === "mention" + ? input.path + : (input.type === "image" || input.type === "audio") && "url" in input ? input.url : null; + return nativePath !== null && attachmentUris.has(attachmentFileUri(nativePath) ?? "") ? [] : contentBlocks[index]!; + }); +} + +/** The content blocks of one persisted user input. */ +function userInputToContentBlocks(input: UserInput): acp.ContentBlock[] { switch (input.type) { case "text": { const asyncQuestionReply = renderAsyncQuestionReply(input.text); @@ -46,7 +62,7 @@ export function userInputToContentBlocks(input: UserInput): acp.ContentBlock[] { /** The text of a stored prompt as the client was shown it, which is what the * client fingerprints when it names the message a rewind drops. */ export function userInputVisibleText(content: UserInput[]): string { - return content.flatMap(userInputToContentBlocks) + return userMessageContentBlocks(content) .filter((block): block is Extract => block.type === "text") .map(block => block.text) .join(""); diff --git a/src/__tests__/CodexACPAgent/session-rewind.test.ts b/src/__tests__/CodexACPAgent/session-rewind.test.ts new file mode 100644 index 000000000..7fc541146 --- /dev/null +++ b/src/__tests__/CodexACPAgent/session-rewind.test.ts @@ -0,0 +1,185 @@ +import {createHash} from "node:crypto"; +import * as acp from "@agentclientprotocol/sdk/unstable"; +import * as acpV2 from "@agentclientprotocol/sdk/experimental/v2/unstable"; +import {afterEach, describe, expect, it, vi} from "vitest"; +import {createAcpAgentRouter} from "../../AcpAgentRouter"; +import {CodexAcpClient} from "../../CodexAcpClient"; +import {CodexAcpServer} from "../../CodexAcpServer"; +import {CodexAppServerClient} from "../../CodexAppServerClient"; +import {SESSION_REWIND_METHOD} from "../../SessionRewind"; +import {fakeAppServer} from "../app-server-recovery/fake-app-server"; +import { + createRecoveryFixture, + defaultAnswer, + initialize, + type RecoveryFixture, + requestsOf, +} from "../app-server-recovery/recovery-fixture"; + +function fingerprint(text: string): string { + return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`; +} + +function paginatedThread(threadId: string) { + return (defaultAnswer("thread/resume", {threadId}) as {thread: Record}).thread; +} + +function completedTurn(id: string, items: unknown[]) { + return {id, items, itemsView: "full", status: "completed", error: null, startedAt: null, completedAt: null, durationMs: null}; +} + +/** An app-server whose thread holds `turns`, and whose turns end right after they start. */ +function answerHistory(fixture: Pick, turns: unknown[]): void { + fixture.answers.set("thread/read", params => ({thread: paginatedThread((params as {threadId: string}).threadId)})); + fixture.answers.set("thread/turns/list", params => { + const {sortDirection, limit} = params as {sortDirection: string, limit: number}; + const ordered = sortDirection === "desc" ? [...turns].reverse() : turns; + return {data: ordered.slice(0, limit), nextCursor: null, backwardsCursor: null}; + }); + let started = 0; + fixture.answers.set("turn/start", params => { + const {threadId, input, clientUserMessageId} = params as {threadId: string, input: unknown[], clientUserMessageId?: string}; + const turn = {id: `turn-new-${++started}`, items: [], status: "inProgress", error: null, startedAt: null, completedAt: null, durationMs: null}; + const item = {type: "userMessage", id: `item-new-${started}`, clientId: clientUserMessageId ?? null, content: input}; + setTimeout(() => { + const rpc = fixture.current().rpc; + rpc.notify({method: "turn/started", params: {threadId, turn}}); + rpc.notify({method: "item/completed", params: {threadId, turnId: turn.id, item}}); + rpc.notify({method: "turn/completed", params: {threadId, turn: {...turn, status: "completed"}}}); + }, 1); + return {turn}; + }); +} + +function rewindParams(sessionId: string, beforeMessage: {messageId: string, messageFingerprint?: string}) { + return { + sessionId, + beforeMessage: {messageFingerprint: `sha256:${"0".repeat(64)}`, messageOccurrence: 1, ...beforeMessage}, + }; +} + +const HISTORY = [ + completedTurn("turn-1", [{type: "userMessage", id: "user-1", clientId: null, content: [{type: "text", text: "one", text_elements: []}]}]), + completedTurn("turn-2", [{type: "userMessage", id: "user-2", clientId: null, content: [{type: "text", text: "two", text_elements: []}]}]), +]; + +describe("_session/rewind", () => { + it("starts a prompt sent during the rewind only after Codex reverted the thread", async () => { + const fixture = createRecoveryFixture(); + answerHistory(fixture, HISTORY); + fixture.answers.set("thread/revert", () => undefined); + await initialize(fixture, true); + const {sessionId} = await fixture.agent.newSession({cwd: "/work", mcpServers: []}); + + const rewind = fixture.agent.extMethod(SESSION_REWIND_METHOD, rewindParams(sessionId, {messageId: "user-2"})); + const prompt = fixture.agent.prompt({sessionId, prompt: [{type: "text", text: "edited"}]}); + await vi.waitFor(() => expect(requestsOf(fixture.current(), "thread/revert")).toHaveLength(1)); + expect(requestsOf(fixture.current(), "turn/start").filter(params => (params as {threadId: string}).threadId === sessionId)).toHaveLength(0); + + fixture.current().rpc.pending.find(request => request.method === "thread/revert")!.resolve({}); + + await expect(rewind).resolves.toEqual({rewound: true}); + await expect(prompt).resolves.toMatchObject({stopReason: "end_turn"}); + expect(requestsOf(fixture.current(), "thread/revert")).toEqual([{threadId: sessionId, beforeTurnId: "turn-2"}]); + }); + + it("leaves the conversation as it is while a prompt runs", async () => { + const fixture = createRecoveryFixture(); + answerHistory(fixture, HISTORY); + fixture.answers.set("turn/start", () => undefined); + await initialize(fixture, true); + const {sessionId} = await fixture.agent.newSession({cwd: "/work", mcpServers: []}); + + void fixture.agent.prompt({sessionId, prompt: [{type: "text", text: "long task"}]}).catch(() => undefined); + + await expect(fixture.agent.extMethod(SESSION_REWIND_METHOD, rewindParams(sessionId, {messageId: "user-2"}))) + .resolves.toEqual({rewound: false}); + expect(requestsOf(fixture.current(), "thread/revert")).toHaveLength(0); + }); + + it("resolves a prompt with a file mention through the fingerprint of the text that history replay sent", async () => { + const live = createRecoveryFixture(); + answerHistory(live, []); + await initialize(live, true); + const {sessionId} = await live.agent.newSession({cwd: "/work", mcpServers: []}); + await live.agent.prompt({ + sessionId, + prompt: [ + {type: "text", text: "look at "}, + {type: "resource_link", name: "foo.txt", uri: "file:///work/foo.txt"}, + {type: "text", text: " please"}, + ], + }); + // Codex stores the input of `turn/start` as the content of the user message. + const {input} = (requestsOf(live.current(), "turn/start") as Array<{input: unknown[]}>)[0]!; + const userMessage = {type: "userMessage", id: "restored-id", clientId: null, content: input}; + + const restored = createRecoveryFixture(); + answerHistory(restored, [completedTurn("turn-1", [userMessage])]); + restored.answers.set("thread/resume", params => ({ + ...defaultAnswer("thread/resume", params) as object, + materialized: true, + itemsBackwardsCursor: "cursor-1", + })); + restored.answers.set("thread/items/list", () => ({ + data: [{turnId: "turn-1", item: userMessage, startedAtMs: null, completedAtMs: null}], + nextCursor: null, + backwardsCursor: null, + })); + await initialize(restored, true); + await restored.agent.loadSession({sessionId, cwd: "/work", mcpServers: []}); + const replayedText = restored.updates() + .filter(({update}) => update["sessionUpdate"] === "user_message_chunk") + .map(({update}) => (update["content"] as acp.ContentBlock & {type: "text"}).text) + .join(""); + expect(replayedText).toBe("look at [@foo.txt](file:///work/foo.txt) please"); + + await expect(restored.agent.extMethod(SESSION_REWIND_METHOD, rewindParams(sessionId, { + messageId: "live-id-the-adapter-never-saw", + messageFingerprint: fingerprint(replayedText), + }))).resolves.toEqual({rewound: true}); + expect(requestsOf(restored.current(), "thread/revert")).toEqual([{threadId: sessionId, beforeTurnId: "turn-1"}]); + }); +}); + +const cleanup: (() => void)[] = []; +afterEach(() => cleanup.splice(0).reverse().forEach(dispose => dispose())); + +describe.each([1, 2] as const)("_session/rewind over ACP v%i", version => { + it("serves a prompt sent right after a rewind after the rewind", async () => { + const server = fakeAppServer(); + const answers = new Map unknown>(); + server.rpc.answer = (method, params) => (answers.get(method) ?? (() => defaultAnswer(method, params)))(params); + answerHistory({answers, current: () => server} as never, HISTORY); + answers.set("thread/revert", () => undefined); + const router = createAcpAgentRouter(connection => + new CodexAcpServer(connection, new CodexAcpClient(new CodexAppServerClient(server.rpc.connection)))); + const incoming = new TransformStream(); + const outgoing = new TransformStream(); + router.connect(acp.ndJsonStream(outgoing.writable, incoming.readable)); + const connection = (version === 1 ? acp : acpV2).client({name: "rewind-test"}) + .connect(acp.ndJsonStream(incoming.writable, outgoing.readable)); + cleanup.push(() => connection.close()); + const request = (method: string, params: unknown) => (connection.agent.request as any)(method, params); + await request("initialize", version === 1 + ? {protocolVersion: 1, clientCapabilities: {}} + : {protocolVersion: 2, info: {name: "rewind-test", version: "1"}, capabilities: {}}); + const {sessionId} = await request("session/new", {cwd: "/work", mcpServers: []}); + + // Sent back to back, without waiting for the rewind response. + const rewind = request(SESSION_REWIND_METHOD, rewindParams(sessionId, {messageId: "user-2"})); + const prompt = request("session/prompt", {sessionId, prompt: [{type: "text", text: "edited"}]}); + await vi.waitFor(() => expect(requestsOf(server, "thread/revert")).toHaveLength(1)); + await new Promise(resolve => setTimeout(resolve, 20)); + const sessionTurnStarts = () => requestsOf(server, "turn/start").filter(params => (params as {threadId: string}).threadId === sessionId); + expect(sessionTurnStarts()).toHaveLength(0); + + server.rpc.pending.find(pending => pending.method === "thread/revert")!.resolve({}); + + await expect(rewind).resolves.toEqual({rewound: true}); + // v1 answers when the turn ends, v2 once Codex recorded the user message. + await expect(prompt).resolves.toMatchObject(version === 1 ? {stopReason: "end_turn"} : {messageId: expect.any(String)}); + expect(sessionTurnStarts()).toHaveLength(1); + await new Promise(resolve => setTimeout(resolve, 20)); + }); +}); diff --git a/src/__tests__/SessionRewind.test.ts b/src/__tests__/SessionRewind.test.ts index 4b16f2032..bb6a17ddb 100644 --- a/src/__tests__/SessionRewind.test.ts +++ b/src/__tests__/SessionRewind.test.ts @@ -1,249 +1,195 @@ +import {createHash} from "node:crypto"; import {describe, expect, it, vi} from "vitest"; import type {CodexAppServerClient} from "../CodexAppServerClient"; import {CodexAcpClient} from "../CodexAcpClient"; -import {rewindSession} from "../SessionRewind"; +import {rewindSession, type SessionHistoryPoint} from "../SessionRewind"; + +function fingerprint(text: string): string { + return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`; +} + +const UNKNOWN_FINGERPRINT = `sha256:${"0".repeat(64)}`; + +function userMessage(id: string, content: unknown[], clientId: string | null = null) { + return {type: "userMessage", id, clientId, content}; +} + +function text(value: string) { + return {type: "text", text: value, text_elements: []}; +} + +/** An app-server client whose thread has `turns`, with paginated history unless `historyMode` says otherwise. */ +function historyClient(turns: unknown[], historyMode: "paginated" | "legacy" = "paginated") { + const client = { + threadRead: vi.fn().mockResolvedValue({thread: {historyMode, turns: []}}), + threadPaginatedTurns: vi.fn().mockResolvedValue(turns), + threadRevert: vi.fn().mockResolvedValue({}), + }; + return client as typeof client & CodexAppServerClient; +} + +function rewind(client: CodexAppServerClient, beforeMessage: Partial, resumeAtMessage?: SessionHistoryPoint) { + return rewindSession({ + sessionId: "thread-1", + beforeMessage: {messageId: "stale-id", messageFingerprint: UNKNOWN_FINGERPRINT, messageOccurrence: 1, ...beforeMessage}, + ...(resumeAtMessage ? {resumeAtMessage} : {}), + }, client); +} describe("session rewind", () => { it("reverts the same Codex thread before the selected user turn", async () => { - const client = { - threadReadWithHistory: vi.fn().mockResolvedValue({ - thread: { - turns: [ - {id: "turn-1", items: [{type: "userMessage", id: "user-1", content: [{type: "text", text: "one"}]}]}, - {id: "turn-2", items: [{type: "userMessage", id: "user-2", content: [{type: "text", text: "two"}]}]}, - ], - }, - }), - threadRevert: vi.fn().mockResolvedValue({}), - } as unknown as CodexAppServerClient; - - const result = await rewindSession({ - sessionId: "thread-1", - beforeMessage: { - messageId: "user-2", - messageFingerprint: "sha256:3fc4ccfe745870e2c0d99f71f30ff0656c8d1ed5d3f3b71b17a64d1c0d9a4f5f", - messageOccurrence: 1, - }, - }, client); + const client = historyClient([ + {id: "turn-1", items: [userMessage("user-1", [text("one")])]}, + {id: "turn-2", items: [userMessage("user-2", [text("two")])]}, + ]); - expect(result).toEqual({rewound: true}); + await expect(rewind(client, {messageId: "user-2"})).resolves.toEqual({rewound: true}); expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); }); it("resolves a restored message through its fingerprint occurrence", async () => { - const client = { - threadReadWithHistory: vi.fn().mockResolvedValue({ - thread: { - turns: [ - {id: "turn-1", items: [{type: "userMessage", id: "new-1", content: [{type: "text", text: "repeat"}]}]}, - {id: "turn-2", items: [{type: "userMessage", id: "new-2", content: [{type: "text", text: "repeat"}]}]}, - ], - }, - }), - threadRevert: vi.fn().mockResolvedValue({}), - } as unknown as CodexAppServerClient; - - const result = await rewindSession({ - sessionId: "thread-1", - beforeMessage: { - messageId: "stale-id", - messageFingerprint: "sha256:25e2b6b106523880e27763084ffa6a0756335be0d7106022535365b9ad39b4b1", - messageOccurrence: 2, - }, - }, client); + const client = historyClient([ + {id: "turn-1", items: [userMessage("new-1", [text("repeat")])]}, + {id: "turn-2", items: [userMessage("new-2", [text("repeat")])]}, + ]); + + await rewind(client, {messageFingerprint: fingerprint("repeat"), messageOccurrence: 2}); + + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); + }); + + it("matches the client message id that v2 replays a message under", async () => { + const client = historyClient([ + {id: "turn-1", items: [userMessage("item-1", [text("one")], "client-1")]}, + {id: "turn-2", items: [userMessage("item-2", [text("two")], "client-2")]}, + ]); + + await rewind(client, {messageId: "client-2"}); - expect(result).toEqual({rewound: true}); expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); }); it("prefers the exact segmented message id over its protocol id fallback", async () => { - const client = { - threadReadWithHistory: vi.fn().mockResolvedValue({ - thread: { - turns: [ - {id: "turn-1", items: [{type: "userMessage", id: "user-1", content: [{type: "text", text: "fallback"}]}]}, - {id: "turn-2", items: [{type: "userMessage", id: "user-1:segment:0", content: [{type: "text", text: "exact"}]}]}, - ], - }, - }), - threadRevert: vi.fn().mockResolvedValue({}), - } as unknown as CodexAppServerClient; - - await rewindSession({ - sessionId: "thread-1", - beforeMessage: { - messageId: "user-1:segment:0", - messageFingerprint: `sha256:${"0".repeat(64)}`, - messageOccurrence: 1, - }, - }, client); + const client = historyClient([ + {id: "turn-1", items: [userMessage("user-1", [text("fallback")])]}, + {id: "turn-2", items: [userMessage("user-1:segment:0", [text("exact")])]}, + ]); + + await rewind(client, {messageId: "user-1:segment:0"}); expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); }); it("does not fingerprint history when an exact message id is found", async () => { - const unreadableItem = Object.defineProperty({type: "userMessage", id: "other"}, "content", { + const unreadableItem = Object.defineProperty({type: "userMessage", id: "other", clientId: null}, "content", { enumerable: true, get: () => { throw new Error("fingerprint fallback should not run"); }, }); - const client = { - threadReadWithHistory: vi.fn().mockResolvedValue({ - thread: { - turns: [ - {id: "turn-1", items: [unreadableItem]}, - {id: "turn-2", items: [{type: "userMessage", id: "exact", content: [{type: "text", text: "selected"}]}]}, - ], - }, - }), - threadRevert: vi.fn().mockResolvedValue({}), - } as unknown as CodexAppServerClient; - - await rewindSession({ - sessionId: "thread-1", - beforeMessage: { - messageId: "exact", - messageFingerprint: `sha256:${"0".repeat(64)}`, - messageOccurrence: 1, - }, - }, client); + const client = historyClient([ + {id: "turn-1", items: [unreadableItem]}, + {id: "turn-2", items: [userMessage("exact", [text("selected")])]}, + ]); + + await rewind(client, {messageId: "exact"}); expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); }); it("uses the visible replay text when fingerprinting multimodal and skill inputs", async () => { - const client = { - threadReadWithHistory: vi.fn().mockResolvedValue({ - thread: { - historyMode: "paginated", - turns: [{ - id: "turn-1", - items: [{ - type: "userMessage", - id: "new-id", - content: [ - {type: "text", text: "look"}, - {type: "image", url: "https://example.com/image.png"}, - {type: "skill", name: "review", path: "/tmp/SKILL.md"}, - ], - }], - }], - }, - }), - threadRevert: vi.fn().mockResolvedValue({}), - } as unknown as CodexAppServerClient; - - await rewindSession({ - sessionId: "thread-1", - beforeMessage: { - messageId: "stale-id", - messageFingerprint: "sha256:d0425f232dd6a5d6a18eee0fb305ff976368b93fb5ad3da67a4b41d919f7e2de", - messageOccurrence: 1, - }, - }, client); + const client = historyClient([{ + id: "turn-1", + items: [userMessage("new-id", [ + text("look"), + {type: "image", url: "https://example.com/image.png"}, + {type: "skill", name: "review", path: "/tmp/SKILL.md"}, + ])], + }]); + + await rewind(client, { + messageFingerprint: fingerprint("look[@image](https://example.com/image.png)skill:review (/tmp/SKILL.md)"), + }); expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-1"}); }); - it("reports that a legacy-history thread cannot be rewound", async () => { - const client = { - threadReadWithHistory: vi.fn().mockResolvedValue({ - thread: { - historyMode: "legacy", - turns: [ - {id: "turn-1", items: [{type: "userMessage", id: "user-1", content: [{type: "text", text: "one"}]}]}, - {id: "turn-2", items: [{type: "userMessage", id: "user-2", content: [{type: "text", text: "two"}]}]}, - {id: "turn-3", items: [{type: "userMessage", id: "user-3", content: [{type: "text", text: "three"}]}]}, - ], - }, - }), - threadRevert: vi.fn(), - } as unknown as CodexAppServerClient; - - await expect(rewindSession({ - sessionId: "thread-1", - beforeMessage: { - messageId: "user-2", - messageFingerprint: "sha256:3fc4ccfe745870e2c0d99f71f30ff0656c8d1ed5d3f3b71b17a64d1c0d9a4f5f", - messageOccurrence: 1, - }, - }, client)).rejects.toThrow("legacy history, which Codex cannot rewind"); + it("fingerprints a file mention of a prompt as the link text that replay sends", async () => { + // A prompt `look at please` is stored as three text inputs. + const client = historyClient([{ + id: "turn-1", + items: [userMessage("new-id", [text("look at "), text("[@foo.txt](file:///work/foo.txt)"), text(" please")])], + }]); + + await rewind(client, {messageFingerprint: fingerprint("look at [@foo.txt](file:///work/foo.txt) please")}); + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-1"}); + }); + + it("leaves a native attachment that replay sends as a resource link out of the fingerprint", async () => { + const client = historyClient([{ + id: "turn-1", + items: [userMessage("new-id", [text("read this"), {type: "mention", name: "foo.txt", path: "/work/foo.txt"}])], + }]); + + await rewind(client, {messageFingerprint: fingerprint("read this")}); + + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-1"}); + }); + + it("reports that a legacy-history thread cannot be rewound without reading its turns", async () => { + const client = historyClient([], "legacy"); + + await expect(rewind(client, {messageId: "user-2"})).rejects.toThrow("legacy history, which Codex cannot rewind"); + + expect(client.threadPaginatedTurns).not.toHaveBeenCalled(); expect(client.threadRevert).not.toHaveBeenCalled(); }); it("rejects rewinding a steer inside an existing turn", async () => { - const client = { - threadReadWithHistory: vi.fn().mockResolvedValue({ - thread: { - historyMode: "paginated", - turns: [{ - id: "turn-1", - items: [ - {type: "userMessage", id: "user-1", content: [{type: "text", text: "first"}]}, - {type: "agentMessage", id: "assistant-1", text: "working"}, - {type: "userMessage", id: "steer-1", content: [{type: "text", text: "steer"}]}, - ], - }], - }, - }), - threadRevert: vi.fn(), - } as unknown as CodexAppServerClient; - - await expect(rewindSession({ - sessionId: "thread-1", - beforeMessage: { - messageId: "steer-1", - messageFingerprint: "sha256:57fce44d7c6df51ad8525da1580a246e9d1142d79d1d1f176b1d29643d61ed44", - messageOccurrence: 1, - }, - resumeAtMessage: { - messageId: "assistant-1", - messageFingerprint: `sha256:${"0".repeat(64)}`, - messageOccurrence: 1, - }, - }, client)).rejects.toThrow("does not start a turn"); + const client = historyClient([{ + id: "turn-1", + items: [ + userMessage("user-1", [text("first")]), + {type: "agentMessage", id: "assistant-1", text: "working"}, + userMessage("steer-1", [text("steer")]), + ], + }]); + + await expect(rewind(client, {messageId: "steer-1"}, { + messageId: "assistant-1", + messageFingerprint: UNKNOWN_FINGERPRINT, + messageOccurrence: 1, + })).rejects.toThrow("does not start a turn"); expect(client.threadRevert).not.toHaveBeenCalled(); }); + it("rewinds a turn whose first user message follows other items", async () => { + const client = historyClient([ + {id: "turn-1", items: [userMessage("user-1", [text("one")])]}, + {id: "turn-2", items: [{type: "contextCompaction", id: "compact-1"}, userMessage("user-2", [text("two")])]}, + ]); + + await rewind(client, {messageId: "user-2"}); + + expect(client.threadRevert).toHaveBeenCalledWith({threadId: "thread-1", beforeTurnId: "turn-2"}); + }); + it("does not revert when the selected message is absent", async () => { - const client = { - threadReadWithHistory: vi.fn().mockResolvedValue({thread: {turns: []}}), - threadRevert: vi.fn(), - } as unknown as CodexAppServerClient; + const client = historyClient([]); - await expect(rewindSession({ - sessionId: "thread-1", - beforeMessage: { - messageId: "missing", - messageFingerprint: `sha256:${"0".repeat(64)}`, - messageOccurrence: 1, - }, - }, client)).rejects.toThrow("Rewind message missing was not found"); + await expect(rewind(client, {messageId: "missing"})).rejects.toThrow("Rewind message missing was not found"); expect(client.threadRevert).not.toHaveBeenCalled(); }); it("drains queued session notifications before acknowledging rewind", async () => { - const appServerClient = { - threadReadWithHistory: vi.fn().mockResolvedValue({ - thread: { - historyMode: "paginated", - turns: [{id: "turn-1", items: [{type: "userMessage", id: "user-1", content: [{type: "text", text: "one"}]}]}], - }, - }), - threadRevert: vi.fn().mockResolvedValue({}), - } as unknown as CodexAppServerClient; + const appServerClient = historyClient([{id: "turn-1", items: [userMessage("user-1", [text("one")])]}]); const client = new CodexAcpClient(appServerClient); const waitForNotifications = vi.spyOn(client, "waitForSessionNotifications").mockResolvedValue(); await client.rewindSession({ sessionId: "thread-1", - beforeMessage: { - messageId: "user-1", - messageFingerprint: `sha256:${"0".repeat(64)}`, - messageOccurrence: 1, - }, + beforeMessage: {messageId: "user-1", messageFingerprint: UNKNOWN_FINGERPRINT, messageOccurrence: 1}, }); expect(waitForNotifications).toHaveBeenCalledWith("thread-1"); diff --git a/src/index.ts b/src/index.ts index a40542eff..fac623a79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,6 @@ import {prepareCodexHookConfig} from "./CodexHookConfig"; import {createAcpAgentRouter} from "./AcpAgentRouter"; import type {AcpClientConnection, AcpV2Connection} from "./ACPSessionConnection"; - if (process.argv.includes("--version")) { console.log(`${packageJson.name} ${packageJson.version}`); process.exit(0); From 7036eafb65b57dc95672d23983bde94950122acb Mon Sep 17 00:00:00 2001 From: Aleksandr Slapoguzov Date: Tue, 10 Nov 2026 18:08:54 +0100 Subject: [PATCH 4/4] IJAI-2003 a provider update waits for a pending rewind A provider restart waited only for active prompts, so providers/set during a pending thread/revert replaced the app-server under it. A rewind is now tracked once no provider update is pending, and a later update waits for it. Co-Authored-By: Claude Opus 5.5 --- src/CodexAcpServer.ts | 47 +++++++++++++++---- src/__tests__/CodexACPAgent/providers.test.ts | 41 ++++++++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 5a1c5ecc5..9bc94b319 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -535,6 +535,8 @@ export class CodexAcpServer { private readonly mcpSessionStartup: McpSessionStartup; private readonly pendingTurnStarts: Map; private readonly activePrompts: Map; + /** Rewinds that passed the provider update; a provider restart waits for them, as for active prompts. */ + private readonly activeRewinds = new Set>(); /** Tail of the per-session turn-start FIFO; see `acquireTurnStartReservation`. */ private readonly turnStartQueueTail: Map; settled: boolean}>; private readonly steeringQueues: Map; @@ -1157,21 +1159,41 @@ export class CodexAcpServer { // instead of starting a turn that `thread/revert` would cancel. const reservation = this.acquireTurnStartReservation(sessionId); try { - if (this.providerUpdate !== null) await this.waitForProviderUpdate(); - const appServer = this.ensureAppServer(); - if (appServer) await appServer; - const sessionState = this.sessions.get(sessionId); - if (!sessionState) throw unknownSession(); - // A turn that Codex started itself, such as a goal continuation, also writes the history. - if (this.isCodexTurnRunning(sessionId)) return {rewound: false}; - const sessionReady = this.ensureSessionReady(sessionState); - if (sessionReady) await sessionReady; - return await this.runWithProcessCheck(() => this.codexAcpClient.rewindSession(params)); + // Every provider update queued so far restarts the app-server before the rewind runs. + for (let pending = this.providerUpdate; pending !== null; pending = this.providerUpdate) { + await this.waitForProviderUpdate(); + if (this.providerUpdate === pending) break; + } + // Registered with no provider update pending, so a later update waits for the revert + // and its response instead of restarting the app-server underneath it. + const rewind = this.rewindOnCurrentAppServer(params, unknownSession); + this.activeRewinds.add(rewind); + try { + return await rewind; + } finally { + this.activeRewinds.delete(rewind); + } } finally { reservation.release(); } } + private async rewindOnCurrentAppServer( + params: SessionRewindRequest, + unknownSession: () => RequestError, + ): Promise { + const sessionId = params.sessionId; + const appServer = this.ensureAppServer(); + if (appServer) await appServer; + const sessionState = this.sessions.get(sessionId); + if (!sessionState) throw unknownSession(); + // A turn that Codex started itself, such as a goal continuation, also writes the history. + if (this.isCodexTurnRunning(sessionId)) return {rewound: false}; + const sessionReady = this.ensureSessionReady(sessionState); + if (sessionReady) await sessionReady; + return await this.runWithProcessCheck(() => this.codexAcpClient.rewindSession(params)); + } + private async withSystemPromptUpdate( request: {sessionId: string; systemPrompt?: unknown; _meta?: unknown}, action: () => Promise, @@ -2394,6 +2416,11 @@ export class CodexAcpServer { logger.log("Waiting for active prompts before provider restart", {count: activePrompts.length}); await Promise.all(activePrompts); } + const activeRewinds = [...this.activeRewinds]; + if (activeRewinds.length > 0) { + logger.log("Waiting for rewinds before provider restart", {count: activeRewinds.length}); + await Promise.allSettled(activeRewinds); + } logger.log("Restarting Codex app-server for provider update", {sessionCount: this.sessions.size}); for (const session of this.sessions.values()) { diff --git a/src/__tests__/CodexACPAgent/providers.test.ts b/src/__tests__/CodexACPAgent/providers.test.ts index f42489ea9..04227c0ae 100644 --- a/src/__tests__/CodexACPAgent/providers.test.ts +++ b/src/__tests__/CodexACPAgent/providers.test.ts @@ -355,6 +355,47 @@ describe("Configurable LLM providers (providers/*)", () => { expect(replacementRewind).toHaveBeenCalledOnce(); }); + it("restarts the app-server for a provider update only after a pending rewind settles", async () => { + const replacement = createCodexMockTestFixture().getCodexAcpClient(); + vi.spyOn(replacement, "initialize").mockResolvedValue(); + vi.spyOn(replacement, "resumeSession").mockResolvedValue({models: []} as never); + const restart = vi.fn().mockResolvedValue(replacement); + const fixture = createCodexMockTestFixture(restart); + const revert = deferred<{rewound: boolean}>(); + const revertStarted = deferred(); + vi.spyOn(fixture.getCodexAcpClient(), "rewindSession").mockImplementation(async () => { + revertStarted.resolve(); + return await revert.promise; + }); + const agent = fixture.getCodexAcpAgent(); + await agent.initialize({protocolVersion: acp.PROTOCOL_VERSION}); + const sessions = (agent as unknown as {sessions: Map>}).sessions; + sessions.set("thread-1", createTestSessionState({sessionId: "thread-1", cwd: "/workspace"})); + + const rewind = agent.extMethod(SESSION_REWIND_METHOD, { + sessionId: "thread-1", + beforeMessage: { + messageId: "user-1", + messageFingerprint: `sha256:${"0".repeat(64)}`, + messageOccurrence: 1, + }, + }); + await revertStarted.promise; + const providerUpdate = agent.setProvider({ + providerId: OPENAI_PROVIDER_ID, + apiType: "openai", + baseUrl: "https://gateway.example/v1", + }); + + await new Promise(resolve => setTimeout(resolve, 20)); + expect(restart).not.toHaveBeenCalled(); + + revert.resolve({rewound: true}); + await expect(rewind).resolves.toEqual({rewound: true}); + await expect(providerUpdate).resolves.toEqual({}); + expect(restart).toHaveBeenCalledOnce(); + }); + it("leaves the conversation as it is when a turn is running", async () => { const fixture = createCodexMockTestFixture(); const rewind = vi.spyOn(fixture.getCodexAcpClient(), "rewindSession");