diff --git a/examples/combined-app/src/client/hooks/useCodexAgent.ts b/examples/combined-app/src/client/hooks/useCodexAgent.ts index 33fb0b9..44547f1 100644 --- a/examples/combined-app/src/client/hooks/useCodexAgent.ts +++ b/examples/combined-app/src/client/hooks/useCodexAgent.ts @@ -158,6 +158,8 @@ function summarizeApproval(request: ApprovalRequest): string { return "Apply a patch"; case "item/tool/requestUserInput": return "Tool input requested"; + case "mcpServer/elicitation/request": + return request.params.message; case "item/permissions/requestApproval": return "Additional permissions requested"; } diff --git a/sdk/AGENTS.md b/sdk/AGENTS.md index a650704..d61608f 100644 --- a/sdk/AGENTS.md +++ b/sdk/AGENTS.md @@ -241,7 +241,7 @@ await conn.disconnect(); | `startReview(target?, delivery?)` | Start a Codex review (`review/start`); defaults to uncommitted changes | | `compactThread()` | Compact the active thread's context (`thread/compact/start`) | | `readConfig(params?)` | Read the app-server's effective config (`config/read`) | -| `onApprovalRequest(method, handler)` | Handle server-initiated approval requests (returns unsubscribe fn) | +| `onApprovalRequest(method, handler)` | Handle server-initiated approval or MCP elicitation requests (returns unsubscribe fn) | | `threadId` | The active thread id (`string \| undefined`) | | `onAxonEvent(listener)` | Subscribe to all Axon events (returns unsubscribe fn) | | `onTimelineEvent(listener)` | Subscribe to classified timeline events (returns unsubscribe fn) | @@ -385,7 +385,7 @@ create a new instance. - **Auto-reconnect (single retry).** If an SSE stream drops unexpectedly, the SDK re-subscribes once. ACP logs a `console.warn`; Claude logs only when `verbose: true` is set. If the retry also fails, the connection is terminal — create a new instance. - **ACP permissions default to auto-approve** (`allow_always` > `allow_once` > first option). Pass `requestPermission` to customize. - **Claude permissions also auto-approve** all tool use. Register a `"can_use_tool"` handler via `onControlRequest()` to customize. -- **Codex approvals also auto-approve** by default. Register handlers via `onApprovalRequest()` to customize, or mount with `launch_args: ["-c", "approval_policy=never"]` for headless full-auto (no approval traffic at all). +- **Codex approvals auto-approve, while MCP elicitations cancel safely** by default. Register handlers via `onApprovalRequest()` to customize, or mount with `launch_args: ["-c", "approval_policy=never"]` for headless full-auto. - **Explicit `connect()` required:** All connections require `await conn.connect()` first, followed by `initialize()` — ACP, Claude, and Codex alike. - **Node >= 22** required. - **`@runloop/api-client`** is a peer dep — you must install it yourself. diff --git a/sdk/README.md b/sdk/README.md index 14cc5d1..aadbdf9 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -575,7 +575,7 @@ Like ACP/Claude, call `initialize()` after `connect()` — it runs the app-serve |-------|------|-------------| | `verbose` | `boolean` | Emit verbose logs to stderr | | `threadStartParams` | `ThreadStartParams` | Defaults for auto-started threads (cwd, model, `sandbox`, `approvalPolicy`, …) | -| `approvalHandlers` | `Partial>` | Handlers for server-initiated approval requests (see below) | +| `approvalHandlers` | `Partial>` | Handlers for server-initiated approval and elicitation requests (see below) | | `requestTimeoutMs` | `number` | Timeout for request/response correlation and approval handlers (default `60000`) | | `onError` | `(error: unknown) => void` | Error callback (defaults to `console.error`) | | `onDisconnect` | `() => void \| Promise` | Teardown callback invoked by `disconnect()` (e.g. devbox shutdown) | @@ -622,21 +622,31 @@ Like ACP/Claude, call `initialize()` after `connect()` — it runs the app-serve | `onAxonEvent(listener)` | Register an Axon event listener. Returns unsubscribe function. | | `onTimelineEvent(listener)` | Register a classified timeline event listener. Returns unsubscribe function. | | `receiveTimelineEvents()` | Async generator yielding classified `CodexTimelineEvent`s | -| `onApprovalRequest(method, handler)` | Register a handler for a server-initiated approval request. Returns unsubscribe function. | +| `onApprovalRequest(method, handler)` | Register a handler for a server-initiated approval or elicitation request. Returns unsubscribe function. | -### Approval requests +### Approval and elicitation requests -Codex asks the client before running commands, editing files, or escalating permissions — the server sends a JSON-RPC request (`item/commandExecution/requestApproval`, `item/fileChange/requestApproval`, `item/tool/requestUserInput`, `item/permissions/requestApproval`, or the legacy `execCommandApproval` / `applyPatchApproval`) and waits for the client's response. +Codex asks the client before running commands, editing files, escalating permissions, or collecting structured input from an MCP server. The server sends a JSON-RPC request (`item/commandExecution/requestApproval`, `item/fileChange/requestApproval`, `item/tool/requestUserInput`, `mcpServer/elicitation/request`, `item/permissions/requestApproval`, or the legacy `execCommandApproval` / `applyPatchApproval`) and waits for the client's response. -By default the SDK **auto-approves**: command/file approvals are accepted, legacy approvals are approved, user-input answers are empty, and requested permissions are granted for the turn. Register a handler to customize; a handler that doesn't answer within `requestTimeoutMs` is treated as a decline. +By default the SDK **auto-approves approval requests**: command/file approvals are accepted, legacy approvals are approved, user-input answers are empty, and requested permissions are granted for the turn. MCP elicitations default to a safe `cancel` response. Register a handler to customize; a handler that doesn't answer within `requestTimeoutMs` is treated as a decline or cancellation. ```typescript const off = conn.onApprovalRequest("item/commandExecution/requestApproval", (request) => { console.log(`Approve command? (item ${request.params.itemId})`); return { decision: "accept" }; // or { decision: "decline" } }); + +conn.onApprovalRequest("mcpServer/elicitation/request", (request) => { + return { + action: "accept", + content: { city: "Paris" }, + _meta: null, + }; +}); ``` +Handlers receive a second argument with an `AbortSignal`. The signal aborts when app-server resolves the request elsewhere, the handler times out, or the connection closes, allowing applications to discard parked UI state without publishing a late response. Replay also treats `serverRequest/resolved` as terminal, so reconnecting a client does not re-open an elicitation that Codex already cleared. This recovery applies while the Codex process still owns the request. Restarting Codex destroys its in-memory MCP request, so applications must durably reissue the operation rather than replay only the old response. + Whether approvals fire at all depends on the thread's policy — pass `threadStartParams: { approvalPolicy: "on-request", sandbox: "read-only" }` (or per-call `startThread(params)`) to route actions through the client. For headless full-auto use, skip the approval flow entirely by mounting with `launch_args: ["-c", "approval_policy=never"]`. ### Codex Timeline Event Type Guards diff --git a/sdk/src/codex/classify-codex-axon-event.test.ts b/sdk/src/codex/classify-codex-axon-event.test.ts index 622e266..8deae4d 100644 --- a/sdk/src/codex/classify-codex-axon-event.test.ts +++ b/sdk/src/codex/classify-codex-axon-event.test.ts @@ -6,6 +6,7 @@ import { isCodexApprovalRequestEvent, isCodexItemCompletedEvent, isCodexResponseEvent, + isCodexServerRequestResolvedEvent, isCodexThreadStartedEvent, isTurnCompletedEvent, isUnknownTimelineEvent, @@ -115,6 +116,25 @@ describe("classifyCodexAxonEvent", () => { if (isCodexApprovalRequestEvent(event)) expect(event.data.id).toBe(41); }); + it("classifies MCP elicitations and their resolved notification", () => { + const request = classifyCodexAxonEvent( + frame("mcpServer/elicitation/request", { + method: "mcpServer/elicitation/request", + id: 0, + params: { mode: "form", message: "Which city?", requestedSchema: {} }, + }), + ); + expect(isCodexApprovalRequestEvent(request)).toBe(true); + + const resolved = classifyCodexAxonEvent( + frame("serverRequest/resolved", { + method: "serverRequest/resolved", + params: { threadId: "thr-1", requestId: 0 }, + }), + ); + expect(isCodexServerRequestResolvedEvent(resolved)).toBe(true); + }); + it("classifies methodless JSON-RPC responses using the broker response event type", () => { const event = classifyCodexAxonEvent( frame("response", { id: "sdk-1", result: { thread: {} } }), @@ -142,6 +162,8 @@ describe("isCodexProtocolEventType", () => { expect(isCodexProtocolEventType("turn/started")).toBe(true); expect(isCodexProtocolEventType("item/reasoning/textDelta")).toBe(true); expect(isCodexProtocolEventType("applyPatchApproval")).toBe(true); + expect(isCodexProtocolEventType("mcpServer/elicitation/request")).toBe(true); + expect(isCodexProtocolEventType("serverRequest/resolved")).toBe(true); expect(isCodexProtocolEventType("error")).toBe(true); expect(isCodexProtocolEventType("response")).toBe(true); expect(isCodexProtocolEventType("custom/event")).toBe(false); diff --git a/sdk/src/codex/connection.test.ts b/sdk/src/codex/connection.test.ts index b161718..a1ba898 100644 --- a/sdk/src/codex/connection.test.ts +++ b/sdk/src/codex/connection.test.ts @@ -332,6 +332,11 @@ describe("CodexAxonConnection", () => { ["item/commandExecution/requestApproval", {}, { decision: "accept" }], ["item/fileChange/requestApproval", {}, { decision: "accept" }], ["item/tool/requestUserInput", {}, { answers: {} }], + [ + "mcpServer/elicitation/request", + { mode: "form" }, + { action: "cancel", content: null, _meta: null }, + ], [ "item/permissions/requestApproval", { permissions: { network: null, fileSystem: null } }, @@ -367,6 +372,80 @@ describe("CodexAxonConnection", () => { }); }); + it("round-trips an MCP elicitation handler's structured content", async () => { + const { ctrl, mock, conn } = setup(); + conn.onApprovalRequest("mcpServer/elicitation/request", async () => ({ + action: "accept", + content: { city: "Paris" }, + _meta: null, + })); + await conn.connect(); + ctrl.push( + makeAgentEvent("mcpServer/elicitation/request", { + method: "mcpServer/elicitation/request", + id: 0, + params: { mode: "form", message: "Which city?", requestedSchema: {} }, + }), + ); + await tick(); + expect(JSON.parse(mock.published[0]?.payload ?? "null")).toEqual({ + id: 0, + result: { action: "accept", content: { city: "Paris" }, _meta: null }, + }); + }); + + it("stops a parked handler when app-server resolves the request elsewhere", async () => { + const { ctrl, mock, conn } = setup({ requestTimeoutMs: 1_000 }); + let handlerSignal: AbortSignal | undefined; + conn.onApprovalRequest("mcpServer/elicitation/request", (_request, context) => { + handlerSignal = context.signal; + return new Promise(() => undefined); + }); + await conn.connect(); + ctrl.push( + makeAgentEvent("mcpServer/elicitation/request", { + method: "mcpServer/elicitation/request", + id: "elicit-1", + params: { mode: "url", message: "Authorize", url: "https://example.com" }, + }), + ); + await tick(); + ctrl.push( + makeAgentEvent("serverRequest/resolved", { + method: "serverRequest/resolved", + params: { threadId: "thr-1", requestId: "elicit-1" }, + }), + ); + await tick(); + expect(mock.published).toHaveLength(0); + expect(handlerSignal?.aborted).toBe(true); + }); + + it("replaces a stale parked handler when the same request is replayed", async () => { + const { ctrl, mock, conn } = setup({ requestTimeoutMs: 1_000 }); + const signals: AbortSignal[] = []; + conn.onApprovalRequest("mcpServer/elicitation/request", (_request, context) => { + signals.push(context.signal); + return new Promise(() => undefined); + }); + await conn.connect(); + const request = { + method: "mcpServer/elicitation/request", + id: "elicit-replayed", + params: { mode: "form", message: "Which city?", requestedSchema: {} }, + }; + + ctrl.push(makeAgentEvent(request.method, request)); + await tick(); + ctrl.push(makeAgentEvent(request.method, request)); + await tick(); + + expect(signals).toHaveLength(2); + expect(signals[0]?.aborted).toBe(true); + expect(signals[1]?.aborted).toBe(false); + expect(mock.published).toHaveLength(0); + }); + it("answers unsupported server requests with a JSON-RPC error", async () => { const { ctrl, mock, conn } = setup(); await conn.connect(); diff --git a/sdk/src/codex/connection.ts b/sdk/src/codex/connection.ts index 553415d..944838e 100644 --- a/sdk/src/codex/connection.ts +++ b/sdk/src/codex/connection.ts @@ -63,7 +63,16 @@ export type TurnOptions = Omit & { }; export type ApprovalMethod = CodexApprovalRequestMethod; export type ApprovalRequest = Extract; -export type ApprovalHandler = (request: ApprovalRequest) => Promise | unknown; +/** Lifecycle context for an approval or elicitation request handler. */ +export interface ApprovalHandlerContext { + /** Aborted when Codex resolves the request elsewhere, the handler times out, or the connection closes. */ + signal: AbortSignal; +} +export type ApprovalHandler = ( + request: ApprovalRequest, + context: ApprovalHandlerContext, +) => Promise | unknown; +const SERVER_REQUEST_RESOLVED = Symbol("server-request-resolved"); /** * JSON-RPC error returned by the Codex app-server for a client request. * Preserves the wire `code` and `data` alongside the message. @@ -145,6 +154,7 @@ export class CodexAxonConnection { private readonly axonListeners: ListenerSet; private readonly timelineListeners: ListenerSet>; private readonly handlers = new Map(); + private readonly serverRequestResolutionWaiters = new Map void>(); private readonly handleError: (error: unknown) => void; private readonly log; constructor( @@ -315,6 +325,7 @@ export class CodexAxonConnection { this.closed = true; this.abortController.abort(); this.pending.rejectAll(new Error("Client disconnected")); + this.resolveParkedServerRequests(); this.messageQueue.close(); await this.transport?.close(); this.transport = undefined; @@ -356,6 +367,7 @@ export class CodexAxonConnection { }, onTerminalError: (error) => this.pending.rejectAll(error), onFinished: () => { + this.resolveParkedServerRequests(); this.running = false; this.abortController.abort(); this.messageQueue.close(false); @@ -380,9 +392,22 @@ export class CodexAxonConnection { if (frame.method === "turn/started") this._currentTurnId = id; else if (this._currentTurnId === id) this._currentTurnId = undefined; } + private captureServerRequestResolved(frame: CodexFrame): void { + if (frame.method !== "serverRequest/resolved") return; + const requestId = (frame.params as { requestId?: unknown } | undefined)?.requestId; + if (typeof requestId !== "string" && typeof requestId !== "number") return; + this.serverRequestResolutionWaiters.get(requestId)?.(); + } + private resolveParkedServerRequests(): void { + for (const resolveRequest of [...this.serverRequestResolutionWaiters.values()]) { + resolveRequest(); + } + this.serverRequestResolutionWaiters.clear(); + } private route(frame: CodexFrame): void { this.captureThreadStarted(frame); this.captureTurnBoundary(frame); + this.captureServerRequestResolved(frame); if (!frame.method && frame.id != null) { frame.error ? this.pending.reject(frame.id, toRequestError(frame.error)) @@ -405,6 +430,8 @@ export class CodexAxonConnection { return { decision: "approved" }; case "item/tool/requestUserInput": return { answers: {} }; + case "mcpServer/elicitation/request": + return { action: "cancel", content: null, _meta: null }; case "item/permissions/requestApproval": { const permissions = request.params.permissions; return { @@ -427,6 +454,8 @@ export class CodexAxonConnection { return { decision: "denied" }; case "item/tool/requestUserInput": return { answers: {} }; + case "mcpServer/elicitation/request": + return { action: "cancel", content: null, _meta: null }; case "item/permissions/requestApproval": return { permissions: {}, scope: "turn" }; } @@ -435,19 +464,32 @@ export class CodexAxonConnection { request: ApprovalRequest, handler: ApprovalHandler, timeoutMs: number, - ): Promise { + ): Promise { return new Promise((resolve, reject) => { - const timer = setTimeout(() => resolve(this.defaultDecline(request)), timeoutMs); - Promise.resolve(handler(request)).then( - (value) => { - clearTimeout(timer); - resolve(value); - }, - (error) => { - clearTimeout(timer); - reject(error); - }, - ); + let settled = false; + let timer: ReturnType | undefined; + const handlerController = new AbortController(); + const finish = (value: unknown | typeof SERVER_REQUEST_RESOLVED, error?: unknown) => { + if (settled) return; + settled = true; + if (timer) clearTimeout(timer); + if (this.serverRequestResolutionWaiters.get(request.id) === onResolved) { + this.serverRequestResolutionWaiters.delete(request.id); + } + handlerController.abort(); + if (error !== undefined) reject(error); + else resolve(value); + }; + const onResolved = () => finish(SERVER_REQUEST_RESOLVED); + this.serverRequestResolutionWaiters.get(request.id)?.(); + this.serverRequestResolutionWaiters.set(request.id, onResolved); + timer = setTimeout(() => finish(this.defaultDecline(request)), timeoutMs); + Promise.resolve() + .then(() => handler(request, { signal: handlerController.signal })) + .then( + (value) => finish(value), + (error) => finish(undefined, error), + ); }); } private async handleServerRequest(request: ServerRequest): Promise { @@ -466,6 +508,7 @@ export class CodexAxonConnection { const result = handler ? await this.approvalWithTimeout(approval, handler, timeoutMs) : this.defaultApproval(approval); + if (result === SERVER_REQUEST_RESOLVED) return; await this.transport?.write({ id: request.id, result }); } catch (error) { if (this.transport?.isReady()) { @@ -710,10 +753,11 @@ export class CodexAxonConnection { await this.request("turn/steer", { threadId: this._threadId, input }); } /** - * Registers an approval handler. Without one, command/file approvals are - * accepted, legacy approvals are approved, user-input answers are empty, - * and requested permissions are granted for the turn. Handler timeouts are - * declined. Mounting with `approval_policy=never` avoids approval traffic. + * Registers an approval or elicitation handler. Without one, command/file + * approvals are accepted, legacy approvals are approved, user-input answers + * are empty, requested permissions are granted for the turn, and MCP + * elicitations are canceled. Handler timeouts are declined or canceled. + * Mounting with `approval_policy=never` avoids approval traffic. */ onApprovalRequest(method: ApprovalMethod, handler: ApprovalHandler): () => void { this.handlers.set(method, handler); diff --git a/sdk/src/codex/protocol/index.ts b/sdk/src/codex/protocol/index.ts index 113d042..919cd58 100644 --- a/sdk/src/codex/protocol/index.ts +++ b/sdk/src/codex/protocol/index.ts @@ -46,6 +46,7 @@ export const CODEX_NOTIFICATION_METHODS = [ "item/reasoning/summaryTextDelta", "item/reasoning/summaryPartAdded", "item/reasoning/textDelta", + "serverRequest/resolved", "error", ] as const; @@ -53,6 +54,7 @@ export const CODEX_APPROVAL_REQUEST_METHODS = [ "item/commandExecution/requestApproval", "item/fileChange/requestApproval", "item/tool/requestUserInput", + "mcpServer/elicitation/request", "item/permissions/requestApproval", "execCommandApproval", "applyPatchApproval", diff --git a/sdk/src/codex/timeline-event-guards.ts b/sdk/src/codex/timeline-event-guards.ts index d9dcd9c..cd11dce 100644 --- a/sdk/src/codex/timeline-event-guards.ts +++ b/sdk/src/codex/timeline-event-guards.ts @@ -12,6 +12,7 @@ import type { CodexReasoningSummaryTextDeltaTimelineEvent, CodexReasoningTextDeltaTimelineEvent, CodexResponseTimelineEvent, + CodexServerRequestResolvedTimelineEvent, CodexThreadStartedTimelineEvent, CodexTimelineEvent, CodexTurnCompletedTimelineEvent, @@ -86,6 +87,10 @@ export const isCodexReasoningSummaryPartAddedEvent = ( export const isCodexReasoningTextDeltaEvent = ( event: CodexTimelineEvent, ): event is CodexReasoningTextDeltaTimelineEvent => hasEventType(event, "item/reasoning/textDelta"); +export const isCodexServerRequestResolvedEvent = ( + event: CodexTimelineEvent, +): event is CodexServerRequestResolvedTimelineEvent => + hasEventType(event, "serverRequest/resolved"); export const isCodexErrorEvent = (event: CodexTimelineEvent): event is CodexErrorTimelineEvent => hasEventType(event, "error"); export const isCodexResponseEvent = ( diff --git a/sdk/src/codex/transport.test.ts b/sdk/src/codex/transport.test.ts index 0fa1f2e..d775011 100644 --- a/sdk/src/codex/transport.test.ts +++ b/sdk/src/codex/transport.test.ts @@ -67,6 +67,34 @@ describe("CodexAxonTransport", () => { ]); }); + it("does not replay a server request cleared by serverRequest/resolved", async () => { + const ctrl = createControllableStream(true); + const { axon } = createMockAxon(ctrl); + const transport = new CodexAxonTransport(axon as never, { replayTargetSequence: 2 }); + await transport.connect(); + ctrl.push( + makeAgentEvent( + "mcpServer/elicitation/request", + { method: "mcpServer/elicitation/request", id: "elicit-1", params: {} }, + 1, + ), + ); + ctrl.push( + makeAgentEvent( + "serverRequest/resolved", + { + method: "serverRequest/resolved", + params: { threadId: "thr-1", requestId: "elicit-1" }, + }, + 2, + ), + ); + ctrl.end(); + const frames = []; + for await (const frame of transport.readMessages()) frames.push(frame); + expect(frames).toEqual([]); + }); + // Pins current behavior: an answer replayed before its request resolves // nothing, so the request is flushed as unanswered and the connection will // answer it again. Axon sequences are monotonic, so this should only occur diff --git a/sdk/src/codex/transport.ts b/sdk/src/codex/transport.ts index 8e2ef12..886384b 100644 --- a/sdk/src/codex/transport.ts +++ b/sdk/src/codex/transport.ts @@ -52,8 +52,17 @@ export class CodexAxonTransport implements CodexTransport { resolveEventType: (frame) => frame?.method ?? RESPONSE_EVENT_TYPE, isReplayRequest: (event, frame) => isFromAgent(event) && !!frame.method && frame.id != null, requestId: (frame) => frame.id, - isReplayAnswer: (event) => isFromUser(event) && event.event_type === RESPONSE_EVENT_TYPE, - answerId: (frame) => frame.id, + isReplayAnswer: (event) => + (isFromUser(event) && event.event_type === RESPONSE_EVENT_TYPE) || + (isFromAgent(event) && event.event_type === "serverRequest/resolved"), + answerId: (frame) => { + if (frame.id != null) return frame.id; + if (frame.method !== "serverRequest/resolved") return undefined; + const requestId = (frame.params as { requestId?: unknown } | undefined)?.requestId; + return typeof requestId === "string" || typeof requestId === "number" + ? requestId + : undefined; + }, validateOutbound: (frame) => { if (typeof frame.id === "string" && frame.id.startsWith(RESERVED_REQUEST_ID_PREFIX)) throw new Error(`Request IDs beginning with ${RESERVED_REQUEST_ID_PREFIX} are reserved`); diff --git a/sdk/src/codex/types.ts b/sdk/src/codex/types.ts index 328be7b..83ea2d4 100644 --- a/sdk/src/codex/types.ts +++ b/sdk/src/codex/types.ts @@ -64,6 +64,10 @@ export type CodexReasoningTextDeltaTimelineEvent = ProtocolTimelineEvent< "item/reasoning/textDelta", NotificationFrame<"item/reasoning/textDelta"> >; +export type CodexServerRequestResolvedTimelineEvent = ProtocolTimelineEvent< + "serverRequest/resolved", + NotificationFrame<"serverRequest/resolved"> +>; export type CodexErrorTimelineEvent = ProtocolTimelineEvent<"error", NotificationFrame<"error">>; export type CodexResponseTimelineEvent = ProtocolTimelineEvent<"response", CodexResponseFrame>; @@ -73,6 +77,7 @@ export type CodexApprovalRequestTimelineEvent = { | "item/commandExecution/requestApproval" | "item/fileChange/requestApproval" | "item/tool/requestUserInput" + | "mcpServer/elicitation/request" | "item/permissions/requestApproval" | "execCommandApproval" | "applyPatchApproval" @@ -82,6 +87,7 @@ export type CodexApprovalRequestTimelineEvent = { | "item/commandExecution/requestApproval" | "item/fileChange/requestApproval" | "item/tool/requestUserInput" + | "mcpServer/elicitation/request" | "item/permissions/requestApproval" | "execCommandApproval" | "applyPatchApproval" @@ -98,6 +104,7 @@ export type CodexProtocolTimelineEvent = | CodexReasoningSummaryTextDeltaTimelineEvent | CodexReasoningSummaryPartAddedTimelineEvent | CodexReasoningTextDeltaTimelineEvent + | CodexServerRequestResolvedTimelineEvent | CodexApprovalRequestTimelineEvent | CodexErrorTimelineEvent | CodexResponseTimelineEvent;