src/vs/platform/agentHost/node/codex/codexMapAppServerEvents.ts

1060 LOC · 1004 covered · 56 uncovered · 154 ranges · 112 concepts · 72 introducers · 59 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- codexMapAppServerEvents.ts ×32
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { generateUuid } from '../../../../base/common/uuid.js';
7 > import { toToolCallMeta } from '../../common/meta/agentToolCallMeta.js';
8 > import { ActionType, type SessionAction, type ChatAction } from '../../common/state/sessionActions.js';
9 > import { MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallContributorKind, ToolResultContentType, TurnState } from '../../common/state/sessionState.js';
10 > import { extractForwardedErrorInfo } from '../shared/forwardedChatError.js';
11 > import { getServerToolDisplay } from '../shared/serverToolGroups.js';
12 > import { ActiveClientToolSet } from '../activeClientState.js';
13 > import { unwrapShellInvocation } from './codexShellCommand.js';
14 > import type { AgentMessageDeltaNotification } from './protocol/generated/v2/AgentMessageDeltaNotification.js';
15 > import type { CommandExecutionOutputDeltaNotification } from './protocol/generated/v2/CommandExecutionOutputDeltaNotification.js';
16 > import type { FileChangeOutputDeltaNotification } from './protocol/generated/v2/FileChangeOutputDeltaNotification.js';
17 > import type { FileChangePatchUpdatedNotification } from './protocol/generated/v2/FileChangePatchUpdatedNotification.js';
18 > import type { FileUpdateChange } from './protocol/generated/v2/FileUpdateChange.js';
19 > import type { ItemCompletedNotification } from './protocol/generated/v2/ItemCompletedNotification.js';
20 > import type { ItemStartedNotification } from './protocol/generated/v2/ItemStartedNotification.js';
21 > import type { McpToolCallProgressNotification } from './protocol/generated/v2/McpToolCallProgressNotification.js';
22 > import type { McpToolCallResult } from './protocol/generated/v2/McpToolCallResult.js';
23 > import type { ReasoningSummaryPartAddedNotification } from './protocol/generated/v2/ReasoningSummaryPartAddedNotification.js';
24 > import type { ReasoningSummaryTextDeltaNotification } from './protocol/generated/v2/ReasoningSummaryTextDeltaNotification.js';
25 > import type { ReasoningTextDeltaNotification } from './protocol/generated/v2/ReasoningTextDeltaNotification.js';
26 > import type { ThreadTokenUsageUpdatedNotification } from './protocol/generated/v2/ThreadTokenUsageUpdatedNotification.js';
27 > import type { TurnCompletedNotification } from './protocol/generated/v2/TurnCompletedNotification.js';
28 > import type { TurnStartedNotification } from './protocol/generated/v2/TurnStartedNotification.js';
29 > import type { UserInput } from './protocol/generated/v2/UserInput.js';
30 > import type { WebSearchAction } from './protocol/generated/v2/WebSearchAction.js';
31 > import type { DynamicToolCallOutputContentItem } from './protocol/generated/v2/DynamicToolCallOutputContentItem.js';
32 > import type { JsonValue } from './protocol/generated/serde_json/JsonValue.js';
33 > import type { CollabAgentTool } from './protocol/generated/v2/CollabAgentTool.js';
34 > import type { CollabAgentState } from './protocol/generated/v2/CollabAgentState.js';
35 >
36 > /**
37 > * Per-session mutable state held by the mapper. Carries the bookkeeping
38 > * needed to glue codex's item-stream (each `agentMessage` item has its
39 > * own id) to the agent host protocol (each markdown part has its own id).
40 > *
41 > * Phase 2 tracks only `itemId → partId` for agent messages. Phase 4
42 > * extends this with tool-call correlation; Phase 6 adds reasoning parts.
43 > */
44 > export interface ICodexSessionMapState {
45 > /** Stable codex `itemId` → our markdown response part id. */
46 > readonly itemToPartId: Map<string, string>;
47 > /**
48 > * Stable codex `itemId` → tool-call bookkeeping. Phase 4 tracks
49 > * `commandExecution` here so completion/approval handlers can find
50 > * the right toolCallId/turnId for each item.
51 > */
52 > readonly itemToToolCall: Map<string, ICodexToolCallEntry>;
53 > /** Stable codex reasoning item/index → our reasoning response part id. */
54 > readonly itemToReasoningPartId: Map<string, string>;
55 > /** Current turn id (per `turn/started`). */
56 > currentTurnId: string | undefined;
57 > /**
58 > * Live registry of the session's client-provided (`dynamicTools`) tools,
59 > * keyed by contributing workbench client. A `dynamicToolCall` tool-call
60 > * start is stamped with the owning client (so the workbench routes
61 > * execution back to it) resolved via {@link ActiveClientToolSet.ownerOf}.
62 > */
63 > clientToolSet: ActiveClientToolSet;
64 > /**
65 > * Names of the agent host's server tools (executed in-process). A
66 > * `dynamicToolCall` for one of these omits the `Client` contributor so the
67 > * workbench does not try to route execution to a client — the agent host
68 > * answers the `item/tool/call` directly.
69 > */
70 > serverToolNames: ReadonlySet<string>;
71 > /**
72 > * Server name → customization id for the session's MCP servers, used to
73 > * stamp the {@link ToolCallContributorKind.MCP} contributor on `mcpToolCall`
74 > * starts so clients can correlate the call with its originating server
75 > * customization. Owned and populated by the agent (mirrors
76 > * {@link clientToolSet}); empty until the agent first applies the inventory.
77 > */
78 > readonly mcpCustomizationIds: Map<string, string>;
79 > /**
80 > * Tool call ids the host declined at the approval prompt. Codex reports the
81 > * resulting `item/completed` as a generic failure, so the completion handler
82 > * consults this set to emit a `userCancelled` (`error.code = 'denied'`)
83 > * result instead. Drained on completion and cleared per turn.
84 > */
85 > readonly declinedToolCalls: Set<string>;
86 > /**
87 > * A `commandExecution` that completed successfully with NO output is
88 > * potentially a sandbox pre-flight. When Codex runs a network (or otherwise
89 > * escalated) command under `on-request` + `workspace-write` it first attempts
90 > * it inside the sandbox — which completes instantly with no output because
91 > * the sandbox blocked it — then re-runs the SAME command as a separate
92 > * `commandExecution` item guarded by an approval request. Rendering both
93 > * items draws the command box twice. To coalesce them we defer the
94 > * pre-flight's completion here: if the next `commandExecution` in the turn
95 > * re-runs the same command it reuses this (still-open) tool call for a single
96 > * box; otherwise the deferred completion is flushed (on the next item or at
97 > * turn end) so a genuinely output-less command still finalizes.
98 > */
99 > pendingPreflight: ICodexPendingPreflight | undefined;
100 > }
101 >
102 > /**
103 > * A deferred `commandExecution` completion held back to coalesce a sandbox
104 > * pre-flight with its approval-guarded re-run. See
105 > * {@link ICodexSessionMapState.pendingPreflight}.
106 > */
107 > interface ICodexPendingPreflight {
108 > readonly toolCallId: string;
109 > readonly turnId: string;
110 > /** Unwrapped command text, used to match the re-run. */
111 > readonly command: string;
112 > /** The `ChatToolCallComplete` action to emit if the pre-flight is not reused. */
113 > readonly completion: (SessionAction | ChatAction)[];
114 > }
115 >
116 > export interface ICodexToolCallEntry {
117 > readonly toolCallId: string;
118 > readonly turnId: string;
119 > readonly toolName: string;
120 > output: string;
121 > }
122 >
123 > export function createCodexSessionMapState(serverToolNames: ReadonlySet<string> = new Set(), clientToolSet: ActiveClientToolSet = new ActiveClientToolSet()): ICodexSessionMapState {
125 > itemToPartId: new Map(),
126 > itemToToolCall: new Map(),
127 > itemToReasoningPartId: new Map(),
128 > currentTurnId: undefined,
129 > clientToolSet,
130 > serverToolNames,
131 > mcpCustomizationIds: new Map(),
132 > declinedToolCalls: new Set(),
133 > pendingPreflight: undefined,
134 > };
135 > }
137 > /**
138 > * Clear the per-turn bookkeeping maps so streamed parts, tool-calls, and
139 > * reasoning parts from a finished (or preempted) turn don't bleed into the
140 > * next one. Does NOT touch {@link ICodexSessionMapState.currentTurnId},
141 > * which tracks the codex app-server turn id and is owned by the
142 > * turn/started + turn/completed handlers.
143 > */
144 > export function resetCodexTurnMapState(state: ICodexSessionMapState): void {
145 > state.itemToPartId.clear(); codexMapAppServerEvents.ts ×1
146 > state.itemToToolCall.clear();
147 > state.itemToReasoningPartId.clear();
148 > state.declinedToolCalls.clear();
149 > state.pendingPreflight = undefined;
150 > }
152 > /**
153 > * Emit and clear any deferred sandbox pre-flight completion (see
154 > * {@link ICodexSessionMapState.pendingPreflight}). Returns `[]` when nothing is
155 > * pending, so callers can unconditionally prepend the result.
156 > */
157 > function flushPendingPreflight(state: ICodexSessionMapState): (SessionAction | ChatAction)[] { codexMapAppServerEvents.ts ×1
158 > const pending = state.pendingPreflight;
159 > if (!pending) {
160 > return [];
161 > }
162 > state.pendingPreflight = undefined; codexMapAppServerEvents.ts ×1
163 > return pending.completion;
164 > }
166 > /**
167 > * Collect the plain-text portions of a codex `userMessage` item's
168 > * `content` (an array of {@link UserInput}). Non-text inputs (images,
169 > * skills, mentions) are ignored. Multiple text parts are joined with a
170 > * blank line, mirroring {@link mapTurnStarted}'s reconstruction.
171 > */
172 > export function extractUserInputText(content: readonly UserInput[]): string {
173 > const collected: string[] = []; codexMapAppServerEvents.ts ×1
174 > for (const c of content) {
175 > if (c.type === 'text') {
176 > collected.push(c.text);
177 > }
178 > }
179 > return collected.join('\n\n');
180 > }
182 > function reasoningKey(itemId: string, kind: 'summary' | 'text', index: number): string { codexMapAppServerEvents.ts ×3
183 > return `${itemId}:${kind}:${index}`;
184 > }
186 > function ensureReasoningPart(state: ICodexSessionMapState, turnId: string, key: string): { readonly partId: string; readonly actions: (SessionAction | ChatAction)[] } { codexMapAppServerEvents.ts ×3
187 > const existing = state.itemToReasoningPartId.get(key);
188 > if (existing) {
189 > return { partId: existing, actions: [] }; codexMapAppServerEvents.ts ×3
190 > }
191 > const partId = generateUuid(); codexMapAppServerEvents.ts ×3
192 > state.itemToReasoningPartId.set(key, partId);
193 > return {
194 > partId,
195 > actions: [{
196 > type: ActionType.ChatResponsePart,
197 > turnId,
198 > part: { kind: ResponsePartKind.Reasoning, id: partId, content: '' },
199 > }],
200 > };
201 > }
203 > export function describeWebSearch(query: string, action: WebSearchAction | null): string {
204 > if (action?.type === 'search') { codexMapAppServerEvents.ts ×4
205 > return action.queries?.join(', ') ?? action.query ?? query;
206 > }
207 > if (action?.type === 'openPage') {
208 return action.url ?? query;
209 }
210 > if (action?.type === 'findInPage') { codexMapAppServerEvents.ts ×4
211 return [action.pattern, action.url].filter(Boolean).join(' in ') || query;
212 }
213 return query;
214 }
216 > export function describeFileChange(changes: readonly FileUpdateChange[]): string {
217 > return changes.map(change => { codexMapAppServerEvents.ts ×7
218 > const kind = change.kind.type === 'update' && change.kind.move_path
219 ? `rename from ${change.kind.move_path}`
220 > : change.kind.type; codexMapAppServerEvents.ts ×7
221 > return `${kind}: ${change.path}`;
222 > }).join('\n');
223 > }
225 > export function fileChangeOutput(changes: readonly FileUpdateChange[]): string {
226 > return changes.map(change => `${describeFileChange([change])}\n${change.diff}`.trim()).join('\n\n'); codexMapAppServerEvents.ts ×7
227 > }
229 > function jsonValueToText(value: JsonValue): string { codexMapAppServerEvents.ts ×4
230 > return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
231 > }
233 > function toolInputText(value: JsonValue): string { codexMapAppServerEvents.ts ×1
234 > return JSON.stringify(value, null, 2);
235 > }
237 > function dynamicToolOutput(contentItems: readonly DynamicToolCallOutputContentItem[] | null): string { codexMapAppServerEvents.ts ×3
238 > return contentItems?.map(item => item.type === 'inputText' ? item.text : item.imageUrl).join('\n') ?? '';
239 > }
241 > function mcpToolOutput(result: McpToolCallResult | null, errorMessage?: string): string { codexMapAppServerEvents.ts ×4
242 > if (errorMessage) {
243 return errorMessage;
244 }
245 > if (!result) { codexMapAppServerEvents.ts ×4
247 > }
248 > const content = result.content.map(jsonValueToText).join('\n'); codexMapAppServerEvents.ts ×4
249 > const structuredContent = result.structuredContent !== null ? jsonValueToText(result.structuredContent) : ''; codexMapAppServerEvents.ts ×4
250 > return [content, structuredContent].filter(Boolean).join('\n');
251 > }
253 > /**
254 > * Human labels for a Codex collab-agent (subagent) tool call, mirroring the
255 > * reference client's phrasing. Codex surfaces subagent orchestration as
256 > * `collabAgentToolCall` items on the parent thread, but each spawned agent
257 > * ALSO runs as its own child thread that emits a full `turn/*` + `item/*`
258 > * event stream. The host ({@link CodexAgent}) renders that child stream in a
259 > * read-only peer chat and attaches a discovery block to the parent
260 > * `spawnAgent` tool call; the lifecycle collab tools (`wait`, `closeAgent`,
261 > * `sendInput`, …) render as plain tool calls in the parent chat.
262 > */
263 > function collabAgentToolLabels(tool: CollabAgentTool): { readonly displayName: string; readonly present: string; readonly past: string } { codexMapAppServerEvents.ts ×3
264 > switch (tool) {
265 > case 'spawnAgent': return { displayName: 'Spawn agent', present: 'Spawning agent', past: 'Spawned agent' };
266 > case 'sendInput': return { displayName: 'Send input to agent', present: 'Sending input to agent', past: 'Sent input to agent' };
267 > case 'resumeAgent': return { displayName: 'Resume agent', present: 'Resuming agent', past: 'Resumed agent' };
268 > case 'wait': return { displayName: 'Wait for agents', present: 'Waiting for agents', past: 'Finished waiting' };
269 > case 'closeAgent': return { displayName: 'Close agent', present: 'Closing agent', past: 'Closed agent' };
270 > default: return { displayName: tool, present: tool, past: tool };
271 > }
272 > }
274 > /** One-line summary of a spawned agent's state — the subagent's result. */
275 > function collabAgentStateSummary(state: CollabAgentState): string { codexMapAppServerEvents.ts ×5
276 > switch (state.status) {
277 > case 'completed': return state.message ? `Completed — ${state.message}` : 'Completed';
278 > case 'errored': return state.message ? `Errored — ${state.message}` : 'Errored';
279 > case 'running': return state.message ? `Running — ${state.message}` : 'Running';
280 > case 'interrupted': return state.message ? `Interrupted — ${state.message}` : 'Interrupted';
281 > case 'pendingInit': return 'Pending init';
282 > case 'shutdown': return 'Shutdown';
283 > case 'notFound': return 'Not found';
284 > default: return state.status;
285 > }
286 > }
288 > /**
289 > * Render the per-agent result block for a completed collab tool call. Prefers
290 > * the receiver order, then appends any other agents present in `agentsStates`.
291 > * The completed message carries the subagent's actual output.
292 > */
293 > function collabAgentResultOutput(receiverThreadIds: readonly string[], agentsStates: { readonly [key: string]: CollabAgentState | undefined }): string { codexMapAppServerEvents.ts ×5
294 > const seen = new Set<string>();
295 > const states: CollabAgentState[] = [];
296 > for (const id of receiverThreadIds) {
297 > const state = agentsStates[id];
298 > if (state) {
299 > states.push(state);
300 > seen.add(id);
301 > }
302 > }
303 > for (const id of Object.keys(agentsStates).sort()) {
304 > if (seen.has(id)) {
305 > continue;
306 > }
307 const state = agentsStates[id];
308 if (state) {
309 states.push(state);
310 }
312 > if (states.length === 0) {
313 return '';
314 }
315 > if (states.length === 1) { codexMapAppServerEvents.ts ×5
316 > return collabAgentStateSummary(states[0]); codexMapAppServerEvents.ts ×1
317 > }
318 > return states.map((state, index) => `Agent ${index + 1}: ${collabAgentStateSummary(state)}`).join('\n'); codexMapAppServerEvents.ts ×1
319 > }
321 > /**
322 > * Translate `turn/started` into a `ChatTurnStarted` action.
323 > *
324 > * Codex's `turn/started.turn.items[0]` SHOULD be the userMessage that
325 > * kicked off the turn; we reconstruct the user message from it. If
326 > * codex didn't include items (it may not), we synthesize an empty user
327 > * message so the agent host can still create the turn shell — the actual
328 > * prompt text was sent via `turn/start` and is already known by the host
329 > * via the prior `sendMessage` call.
330 > */
331 > export function mapTurnStarted(
332 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×2
333 > params: TurnStartedNotification,
334 > fallbackUserText: string,
335 > ): (SessionAction | ChatAction)[] {
336 > state.currentTurnId = params.turn.id;
337 > resetCodexTurnMapState(state);
338 > let userText = fallbackUserText;
339 > const first = params.turn.items?.[0];
340 > if (first && first.type === 'userMessage') {
341 > const collected = extractUserInputText(first.content); codexMapAppServerEvents.ts ×1
342 > if (collected.length > 0) {
343 > userText = collected;
344 > }
345 > }
347 > {
348 > type: ActionType.ChatTurnStarted,
349 > turnId: params.turn.id,
350 > startedAt: typeof params.turn.startedAt === 'number' ? new Date(params.turn.startedAt * 1000).toISOString() : new Date().toISOString(),
351 > message: { text: userText, origin: { kind: MessageKind.User } },
352 > },
353 > ];
354 > }
356 > export function mapReasoningSummaryPartAdded(
357 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×3
358 > params: ReasoningSummaryPartAddedNotification,
359 > ): (SessionAction | ChatAction)[] {
360 > return ensureReasoningPart(state, params.turnId, reasoningKey(params.itemId, 'summary', params.summaryIndex)).actions;
361 > }
363 > export function mapReasoningSummaryTextDelta(
364 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×3
365 > params: ReasoningSummaryTextDeltaNotification,
366 > ): (SessionAction | ChatAction)[] {
367 > const ensured = ensureReasoningPart(state, params.turnId, reasoningKey(params.itemId, 'summary', params.summaryIndex));
368 > return [
369 > ...ensured.actions,
370 > { type: ActionType.ChatReasoning, turnId: params.turnId, partId: ensured.partId, content: params.delta },
371 > ];
372 > }
374 > export function mapReasoningTextDelta(
375 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×1
376 > params: ReasoningTextDeltaNotification,
377 > ): (SessionAction | ChatAction)[] {
378 > const ensured = ensureReasoningPart(state, params.turnId, reasoningKey(params.itemId, 'text', params.contentIndex));
379 > return [
380 > ...ensured.actions,
381 > { type: ActionType.ChatReasoning, turnId: params.turnId, partId: ensured.partId, content: params.delta },
382 > ];
383 > }
385 > export function clearReasoningForItem(state: ICodexSessionMapState, itemId: string): void {
386 for (const key of [...state.itemToReasoningPartId.keys()]) {
387 if (key.startsWith(`${itemId}:`)) {
388 state.itemToReasoningPartId.delete(key);
389 }
390 }
391 }
393 > export function mapTokenUsageUpdated(params: ThreadTokenUsageUpdatedNotification): (SessionAction | ChatAction)[] {
394 > const last = params.tokenUsage.last; codexMapAppServerEvents.ts ×1
395 > return [{
396 > type: ActionType.ChatUsage,
397 > turnId: params.turnId,
398 > usage: {
399 > inputTokens: last.inputTokens,
400 > outputTokens: last.outputTokens,
401 > cacheReadTokens: last.cachedInputTokens,
402 > _meta: {
403 > reasoningOutputTokens: last.reasoningOutputTokens,
404 > modelContextWindow: params.tokenUsage.modelContextWindow,
405 > },
406 > },
407 > }];
408 > }
410 > /**
411 > * `item/started` for an `agentMessage` becomes a `ChatResponsePart`
412 > * action with an empty `MarkdownResponsePart` shell. Subsequent
413 > * `item/agentMessage/delta` notifications append to that part.
414 > *
415 > * Other item types are ignored in Phase 2 — they'll be picked up by
416 > * Phase 6's tool-call mapper.
417 > */
418 > export function mapItemStarted(
419 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×3
420 > params: ItemStartedNotification,
421 > ): (SessionAction | ChatAction)[] {
422 > // Coalesce a sandbox pre-flight with its approval-guarded re-run: if the
423 > // immediately-preceding commandExecution in this turn ran the same command
424 > // and completed with no output (deferred as a pending pre-flight), reuse its
425 > // still-open tool call instead of opening a second box. The escalation's
426 > // `requestApproval` / `item/completed` then drive that box to its final
427 > // state, so nothing new is emitted here.
428 > if (params.item.type === 'commandExecution') {
429 > const pending = state.pendingPreflight; codexMapAppServerEvents.ts ×3
430 > if (pending && pending.turnId === params.turnId && pending.command === unwrapShellInvocation(params.item.command ?? '')) {
431 > state.pendingPreflight = undefined; codexMapAppServerEvents.ts ×1
432 > state.itemToToolCall.set(params.item.id, {
433 > toolCallId: pending.toolCallId,
434 > turnId: params.turnId,
435 > toolName: 'shell',
436 > output: '',
437 > });
438 > return [];
439 > }
441 > // Any other item supersedes a deferred pre-flight: finalize it first so a codexMapAppServerEvents.ts ×3
442 > // genuinely output-less command still renders promptly as a single box.
443 > const flushed = flushPendingPreflight(state);
444 > const body = mapItemStartedBody(state, params);
445 > return flushed.length === 0 ? body : [...flushed, ...body];
446 > }
448 > function mapItemStartedBody( codexMapAppServerEvents.ts ×3
449 > state: ICodexSessionMapState,
450 > params: ItemStartedNotification,
451 > ): (SessionAction | ChatAction)[] {
452 > if (params.item.type === 'agentMessage') {
453 > const partId = generateUuid(); codexMapAppServerEvents.ts ×1
454 > state.itemToPartId.set(params.item.id, partId);
455 > return [
456 > {
457 > type: ActionType.ChatResponsePart,
458 > turnId: params.turnId,
459 > part: {
460 > kind: ResponsePartKind.Markdown,
461 > id: partId,
462 > content: params.item.text ?? '',
463 > },
464 > },
465 > ];
466 > }
467 > if (params.item.type === 'commandExecution') { codexMapAppServerEvents.ts ×1
468 > // Phase 4: surface shell commands as tool calls. We allocate a codexMapAppServerEvents.ts ×3
469 > // fresh toolCallId; the `commandExecution` item id only
470 > // disambiguates the codex side.
471 > const toolCallId = generateUuid();
472 > state.itemToToolCall.set(params.item.id, {
473 > toolCallId,
474 > turnId: params.turnId,
475 > toolName: 'shell',
476 > output: '',
477 > });
478 > const command = unwrapShellInvocation(params.item.command ?? '');
479 > return [
480 > {
481 > type: ActionType.ChatToolCallStart,
482 > turnId: params.turnId,
483 > toolCallId,
484 > toolName: 'shell',
485 > displayName: 'Run shell command',
486 > _meta: toToolCallMeta({ toolKind: 'terminal' }),
487 > },
488 > {
489 > type: ActionType.ChatToolCallDelta,
490 > turnId: params.turnId,
491 > toolCallId,
492 > content: command,
493 > },
494 > {
495 > type: ActionType.ChatToolCallReady,
496 > turnId: params.turnId,
497 > toolCallId,
498 > invocationMessage: command,
499 > toolInput: command,
500 > confirmed: ToolCallConfirmationReason.NotNeeded,
501 > _meta: toToolCallMeta({ toolKind: 'terminal' }),
502 > },
503 > ];
504 > }
505 > if (params.item.type === 'webSearch') { codexMapAppServerEvents.ts ×1
506 > const toolCallId = generateUuid(); codexMapAppServerEvents.ts ×4
507 > state.itemToToolCall.set(params.item.id, {
508 > toolCallId,
509 > turnId: params.turnId,
510 > toolName: 'web_search',
511 > output: '',
512 > });
513 > const query = describeWebSearch(params.item.query, params.item.action);
514 > return [
515 > {
516 > type: ActionType.ChatToolCallStart,
517 > turnId: params.turnId,
518 > toolCallId,
519 > toolName: 'web_search',
520 > displayName: 'Web search',
521 > _meta: toToolCallMeta({ toolKind: 'search' }),
522 > },
523 > {
524 > type: ActionType.ChatToolCallDelta,
525 > turnId: params.turnId,
526 > toolCallId,
527 > content: query,
528 > },
529 > {
530 > type: ActionType.ChatToolCallReady,
531 > turnId: params.turnId,
532 > toolCallId,
533 > invocationMessage: query,
534 > toolInput: query,
535 > confirmed: ToolCallConfirmationReason.NotNeeded,
536 > _meta: toToolCallMeta({ toolKind: 'search' }),
537 > },
538 > ];
539 > }
540 > if (params.item.type === 'fileChange') { codexMapAppServerEvents.ts ×1
541 > const toolCallId = generateUuid(); codexMapAppServerEvents.ts ×7
542 > const output = fileChangeOutput(params.item.changes);
543 > state.itemToToolCall.set(params.item.id, {
544 > toolCallId,
545 > turnId: params.turnId,
546 > toolName: 'file_edit',
547 > output,
548 > });
549 > const summary = describeFileChange(params.item.changes) || 'Apply file changes';
550 > return [
551 > {
552 > type: ActionType.ChatToolCallStart,
553 > turnId: params.turnId,
554 > toolCallId,
555 > toolName: 'file_edit',
556 > displayName: 'Apply file changes',
557 > },
558 > {
559 > type: ActionType.ChatToolCallDelta,
560 > turnId: params.turnId,
561 > toolCallId,
562 > content: summary,
563 > },
564 > {
565 > type: ActionType.ChatToolCallReady,
566 > turnId: params.turnId,
567 > toolCallId,
568 > invocationMessage: summary,
569 > toolInput: summary,
570 > confirmed: ToolCallConfirmationReason.NotNeeded,
571 > },
572 > ...(output ? [{
573 > type: ActionType.ChatToolCallContentChanged,
574 > turnId: params.turnId,
575 > toolCallId,
576 > content: [{ type: ToolResultContentType.Text, text: output }],
577 > } satisfies SessionAction | ChatAction] : []),
578 > ];
579 > }
580 > if (params.item.type === 'mcpToolCall') { codexMapAppServerEvents.ts ×1
581 > const toolCallId = generateUuid(); codexMapAppServerEvents.ts ×1
582 > const toolName = `${params.item.server}.${params.item.tool}`;
583 > const toolInput = toolInputText(params.item.arguments);
584 > const customizationId = state.mcpCustomizationIds.get(params.item.server);
585 > state.itemToToolCall.set(params.item.id, {
586 > toolCallId,
587 > turnId: params.turnId,
588 > toolName,
589 > output: '',
590 > });
591 > return [
592 > {
593 > type: ActionType.ChatToolCallStart,
594 > turnId: params.turnId,
595 > toolCallId,
596 > toolName,
597 > displayName: params.item.tool,
598 > ...(customizationId ? { contributor: { kind: ToolCallContributorKind.MCP, customizationId } } : {}),
599 > },
600 > {
601 > type: ActionType.ChatToolCallDelta,
602 > turnId: params.turnId,
603 > toolCallId,
604 > content: toolInput,
605 > },
606 > {
607 > type: ActionType.ChatToolCallReady,
608 > turnId: params.turnId,
609 > toolCallId,
610 > invocationMessage: `Calling ${toolName}`,
611 > toolInput,
612 > confirmed: ToolCallConfirmationReason.NotNeeded,
613 > },
614 > ];
615 > }
616 > if (params.item.type === 'dynamicToolCall') { codexMapAppServerEvents.ts ×1
617 > const toolCallId = generateUuid(); codexMapAppServerEvents.ts ×3
618 > const toolName = params.item.namespace ? `${params.item.namespace}.${params.item.tool}` : params.item.tool;
619 > const toolInput = toolInputText(params.item.arguments);
620 > const output = dynamicToolOutput(params.item.contentItems);
621 > // Server tools (registered under their bare name) execute in-process, so
622 > // they carry no `Client` contributor; only client-provided tools route
623 > // execution back to the owning workbench client.
624 > const isServerTool = params.item.namespace === null && state.serverToolNames.has(params.item.tool);
625 > const ownerClientId = isServerTool ? undefined : state.clientToolSet.ownerOf(params.item.tool);
626 > const serverDisplay = getServerToolDisplay(params.item.tool, params.item.arguments);
627 > state.itemToToolCall.set(params.item.id, {
628 > toolCallId,
629 > turnId: params.turnId,
630 > toolName,
631 > output,
632 > });
633 > return [
634 > {
635 > type: ActionType.ChatToolCallStart,
636 > turnId: params.turnId,
637 > toolCallId,
638 > toolName,
639 > displayName: serverDisplay?.displayName ?? params.item.tool,
640 > ...(ownerClientId ? { contributor: { kind: ToolCallContributorKind.Client, clientId: ownerClientId } } : {}),
641 > },
642 > {
643 > type: ActionType.ChatToolCallDelta,
644 > turnId: params.turnId,
645 > toolCallId,
646 > content: toolInput,
647 > },
648 > {
649 > type: ActionType.ChatToolCallReady,
650 > turnId: params.turnId,
651 > toolCallId,
652 > invocationMessage: serverDisplay?.invocationMessage ?? `Calling ${toolName}`,
653 > toolInput,
654 > confirmed: ToolCallConfirmationReason.NotNeeded,
655 > },
656 > ...(output ? [{
657 type: ActionType.ChatToolCallContentChanged,
658 turnId: params.turnId,
659 toolCallId,
660 content: [{ type: ToolResultContentType.Text, text: output }],
661 > } satisfies SessionAction | ChatAction] : []), codexMapAppServerEvents.ts ×3
662 > ];
663 > }
664 > if (params.item.type === 'collabAgentToolCall') { codexMapAppServerEvents.ts ×1
665 > const toolCallId = generateUuid(); codexMapAppServerEvents.ts ×3
666 > const labels = collabAgentToolLabels(params.item.tool);
667 > const toolName = `codex.${params.item.tool}`;
668 > state.itemToToolCall.set(params.item.id, {
669 > toolCallId,
670 > turnId: params.turnId,
671 > toolName,
672 > output: '',
673 > });
674 > // `spawnAgent` opens a read-only peer chat for the child thread (the
675 > // host attaches the subagent-discovery block to THIS tool call on
676 > // `subagent_started`), so we deliberately do NOT dump the raw prompt
677 > // into the tool box — it would duplicate the child chat's first user
678 > // message and blow out the tool-call width. The other collab tools
679 > // (`sendInput`, `wait`, `closeAgent`, …) are lifecycle ops with no peer
680 > // chat, so they keep a compact prompt/model summary.
681 > if (params.item.tool === 'spawnAgent') {
683 > {
684 > type: ActionType.ChatToolCallStart,
685 > turnId: params.turnId,
686 > toolCallId,
687 > toolName,
688 > displayName: labels.displayName,
689 > },
690 > {
691 > type: ActionType.ChatToolCallReady,
692 > turnId: params.turnId,
693 > toolCallId,
694 > invocationMessage: labels.present,
695 > confirmed: ToolCallConfirmationReason.NotNeeded,
696 > },
697 > ];
698 > }
699 > const inputParts: string[] = []; codexMapAppServerEvents.ts ×3
700 > if (params.item.prompt) {
701 > inputParts.push(params.item.prompt); codexMapAppServerEvents.ts ×2
702 > }
703 > if (params.item.model) { codexMapAppServerEvents.ts ×3
704 inputParts.push(`Model: ${params.item.model}`);
705 }
706 > const toolInput = inputParts.join('\n\n'); codexMapAppServerEvents.ts ×3
707 > return [
708 > {
709 > type: ActionType.ChatToolCallStart,
710 > turnId: params.turnId,
711 > toolCallId,
712 > toolName,
713 > displayName: labels.displayName,
714 > },
715 > ...(toolInput ? [{
716 > type: ActionType.ChatToolCallDelta, codexMapAppServerEvents.ts ×2
717 > turnId: params.turnId,
718 > toolCallId,
719 > content: toolInput,
720 > } satisfies SessionAction | ChatAction] : []), codexMapAppServerEvents.ts ×3
721 > {
722 > type: ActionType.ChatToolCallReady,
723 > turnId: params.turnId,
724 > toolCallId,
725 > invocationMessage: labels.present,
726 > toolInput,
727 > confirmed: ToolCallConfirmationReason.NotNeeded,
728 > },
729 > ];
730 > }
732 > }
734 > export function mapCommandExecutionOutputDelta(
735 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×2
736 > params: CommandExecutionOutputDeltaNotification,
737 > ): (SessionAction | ChatAction)[] {
738 > const entry = state.itemToToolCall.get(params.itemId);
739 > if (!entry) {
740 return [];
741 }
742 > entry.output += params.delta; codexMapAppServerEvents.ts ×2
743 > return [{
744 > type: ActionType.ChatToolCallContentChanged,
745 > turnId: entry.turnId,
746 > toolCallId: entry.toolCallId,
747 > content: [{ type: ToolResultContentType.Text, text: entry.output }],
748 > }];
749 > }
751 > export function mapFileChangePatchUpdated(
752 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×7
753 > params: FileChangePatchUpdatedNotification,
754 > ): (SessionAction | ChatAction)[] {
755 > const entry = state.itemToToolCall.get(params.itemId);
756 > if (!entry) {
757 return [];
758 }
759 > entry.output = fileChangeOutput(params.changes); codexMapAppServerEvents.ts ×7
760 > return [{
761 > type: ActionType.ChatToolCallContentChanged,
762 > turnId: entry.turnId,
763 > toolCallId: entry.toolCallId,
764 > content: entry.output ? [{ type: ToolResultContentType.Text, text: entry.output }] : [],
765 > }];
766 > }
768 > export function mapFileChangeOutputDelta(
769 state: ICodexSessionMapState,
770 params: FileChangeOutputDeltaNotification,
771 ): (SessionAction | ChatAction)[] {
772 const entry = state.itemToToolCall.get(params.itemId);
773 if (!entry) {
774 return [];
775 }
776 entry.output += params.delta;
777 return [{
778 type: ActionType.ChatToolCallContentChanged,
779 turnId: entry.turnId,
780 toolCallId: entry.toolCallId,
781 content: [{ type: ToolResultContentType.Text, text: entry.output }],
782 }];
783 }
785 > export function mapMcpToolCallProgress(
786 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×4
787 > params: McpToolCallProgressNotification,
788 > ): (SessionAction | ChatAction)[] {
789 > const entry = state.itemToToolCall.get(params.itemId);
790 > if (!entry) {
791 return [];
792 }
793 > entry.output = [entry.output, params.message].filter(Boolean).join('\n'); codexMapAppServerEvents.ts ×4
794 > return [{
795 > type: ActionType.ChatToolCallContentChanged,
796 > turnId: entry.turnId,
797 > toolCallId: entry.toolCallId,
798 > content: [{ type: ToolResultContentType.Text, text: entry.output }],
799 > }];
800 > }
802 > export function mapAgentMessageDelta(
803 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×1
804 > params: AgentMessageDeltaNotification,
805 > ): (SessionAction | ChatAction)[] {
806 > const partId = state.itemToPartId.get(params.itemId);
807 > if (!partId) {
808 > // Got a delta before we saw the corresponding `item/started`. codexMapAppServerEvents.ts ×1
809 > // Drop it — Phase 2 is best-effort and the lost text is replaced
810 > // when `item/completed` arrives with the full `text` field.
811 > return [];
812 > }
814 > {
815 > type: ActionType.ChatDelta,
816 > turnId: params.turnId,
817 > partId,
818 > content: params.delta,
819 > },
820 > ];
821 > }
823 > /**
824 > * `item/completed` for an `agentMessage` — the part is finalized server
825 > * side. For Phase 2 we don't need to emit an extra action: the deltas
826 > * already updated the part's content. We just drop the mapping so the
827 > * memory pressure stays bounded.
828 > *
829 > * For `commandExecution`, emit a synthetic `ChatToolCallReady`
830 > * (auto-confirmed; the codex server already decided to run the command
831 > * — any host-side approval was settled via the `requestApproval`
832 > * server-request handler before we got here) followed by a
833 > * `ChatToolCallComplete` carrying the aggregated output.
834 > */
835 > export function mapItemCompleted(
836 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×1
837 > params: ItemCompletedNotification,
838 > ): (SessionAction | ChatAction)[] {
839 > if (params.item.type === 'agentMessage') {
840 > state.itemToPartId.delete(params.item.id); codexMapAppServerEvents.ts ×1
841 > return [];
842 > }
843 > if (params.item.type === 'reasoning') { codexMapAppServerEvents.ts ×3
844 clearReasoningForItem(state, params.item.id);
845 return [];
846 }
847 > // Every remaining item type is a tool call. Resolve the tracked entry and codexMapAppServerEvents.ts ×3
848 > // drain the host-decline flag here, once, so all completion paths treat a
849 > // declined tool uniformly (reported as `userCancelled` via
850 > // `error.code = 'denied'`) instead of depending on which tool type completed.
851 > const entry = state.itemToToolCall.get(params.item.id);
852 > if (!entry) {
853 return [];
854 }
855 > state.itemToToolCall.delete(params.item.id); codexMapAppServerEvents.ts ×3
856 > const declined = state.declinedToolCalls.delete(entry.toolCallId);
857 > if (params.item.type === 'commandExecution') {
858 > const success = params.item.status === 'completed' && (params.item.exitCode === 0 || params.item.exitCode === null); codexMapAppServerEvents.ts ×4
859 > const output = params.item.aggregatedOutput ?? entry.output;
860 > const command = unwrapShellInvocation(params.item.command ?? '');
861 > const exit = params.item.exitCode;
862 > const pastTense = success
863 > ? `Ran \`${command}\`` codexMapAppServerEvents.ts ×1
864 > : exit !== null codexMapAppServerEvents.ts ×2
865 > ? `Ran \`${command}\` (exit ${exit})` codexMapAppServerEvents.ts ×1
866 > : `Ran \`${command}\` (failed)`; codexMapAppServerEvents.ts ×1
867 > const completion: (SessionAction | ChatAction)[] = [ codexMapAppServerEvents.ts ×4
868 > {
869 > type: ActionType.ChatToolCallComplete,
870 > turnId: entry.turnId,
871 > toolCallId: entry.toolCallId,
872 > result: {
873 > success,
874 > pastTenseMessage: pastTense,
875 > content: output
876 > ? [{ type: ToolResultContentType.Text, text: output }] codexMapAppServerEvents.ts ×1
877 > : undefined, codexMapAppServerEvents.ts ×1
878 > error: success ? undefined : { codexMapAppServerEvents.ts ×4
879 > message: exit !== null ? `Exit code ${exit}` : 'Command failed', codexMapAppServerEvents.ts ×2
880 > ...(declined ? { code: 'denied' } : {}),
881 > },
883 > },
884 > ];
885 > // A successful command that produced NO output may be a sandbox
886 > // pre-flight that Codex will immediately re-run under an approval prompt
887 > // (same command, new item). Defer its completion so the re-run can reuse
888 > // this box; if no re-run arrives, it is flushed on the next item or at
889 > // turn end (see mapItemStarted / mapTurnCompleted).
890 > if (success && !output && !declined) {
891 > const flushed = flushPendingPreflight(state); codexMapAppServerEvents.ts ×1
892 > state.pendingPreflight = { toolCallId: entry.toolCallId, turnId: entry.turnId, command, completion };
893 > return flushed;
894 > }
895 > return [...flushPendingPreflight(state), ...completion]; codexMapAppServerEvents.ts ×1
896 > }
897 > if (params.item.type === 'webSearch') { codexMapAppServerEvents.ts ×1
898 > const query = describeWebSearch(params.item.query, params.item.action); codexMapAppServerEvents.ts ×4
899 > return [{
900 > type: ActionType.ChatToolCallComplete,
901 > turnId: entry.turnId,
902 > toolCallId: entry.toolCallId,
903 > result: {
904 > success: true,
905 > pastTenseMessage: `Searched ${query}`,
906 > },
907 > }];
908 > }
909 > if (params.item.type === 'fileChange') { codexMapAppServerEvents.ts ×1
910 > const output = fileChangeOutput(params.item.changes) || entry.output; codexMapAppServerEvents.ts ×7
911 > const success = params.item.status === 'completed';
912 > const content = output ? [{ type: ToolResultContentType.Text as const, text: output }] : undefined;
913 > const result = {
914 > success,
915 > pastTenseMessage: success ? 'Applied file changes' : 'Failed to apply file changes',
916 > content,
917 > ...(success ? {} : { error: { message: `Patch ${params.item.status}`, ...(declined ? { code: 'denied' } : {}) } }),
918 > };
919 > return [{
920 > type: ActionType.ChatToolCallComplete,
921 > turnId: entry.turnId,
922 > toolCallId: entry.toolCallId,
923 > result,
924 > }];
925 > }
926 > if (params.item.type === 'mcpToolCall') { codexMapAppServerEvents.ts ×1
927 > const success = params.item.status === 'completed' && !params.item.error; codexMapAppServerEvents.ts ×4
928 > const output = mcpToolOutput(params.item.result, params.item.error?.message) || entry.output;
929 > const content = output ? [{ type: ToolResultContentType.Text as const, text: output }] : undefined;
930 > return [{
931 > type: ActionType.ChatToolCallComplete,
932 > turnId: entry.turnId,
933 > toolCallId: entry.toolCallId,
934 > result: {
935 > success,
936 > pastTenseMessage: success ? `Called ${entry.toolName}` : `Failed to call ${entry.toolName}`,
937 > content,
938 > ...(success ? {} : { error: { message: params.item.error?.message ?? `MCP tool ${params.item.status}`, ...(declined ? { code: 'denied' } : {}) } }),
939 > },
940 > }];
941 > }
942 > if (params.item.type === 'dynamicToolCall') { codexMapAppServerEvents.ts ×1
943 > const success = params.item.success === true || params.item.status === 'completed'; codexMapAppServerEvents.ts ×1
944 > const output = dynamicToolOutput(params.item.contentItems) || entry.output;
945 > const content = output ? [{ type: ToolResultContentType.Text as const, text: output }] : undefined;
946 > const serverPastTense = success ? getServerToolDisplay(entry.toolName, params.item.arguments, { text: output, success })?.pastTenseMessage : undefined;
947 > return [{
948 > type: ActionType.ChatToolCallComplete,
949 > turnId: entry.turnId,
950 > toolCallId: entry.toolCallId,
951 > result: {
952 > success,
953 > pastTenseMessage: serverPastTense ?? (success ? `Called ${entry.toolName}` : `Failed to call ${entry.toolName}`),
954 > content,
955 > ...(success ? {} : { error: { message: `Dynamic tool ${params.item.status}`, ...(declined ? { code: 'denied' } : {}) } }),
956 > },
957 > }];
958 > }
959 > if (params.item.type === 'collabAgentToolCall') { codexMapAppServerEvents.ts ×5
960 > const labels = collabAgentToolLabels(params.item.tool);
961 > const success = params.item.status === 'completed';
962 > const output = collabAgentResultOutput(params.item.receiverThreadIds, params.item.agentsStates) || entry.output;
963 > const content = output ? [{ type: ToolResultContentType.Text as const, text: output }] : undefined;
964 > return [{
965 > type: ActionType.ChatToolCallComplete,
966 > turnId: entry.turnId,
967 > toolCallId: entry.toolCallId,
968 > result: {
969 > success,
970 > pastTenseMessage: success ? labels.past : `${labels.displayName} failed`,
971 > content,
972 > ...(success ? {} : { error: { message: `Collab agent ${params.item.status}`, ...(declined ? { code: 'denied' } : {}) } }),
973 > },
974 > }];
975 > }
976 return [];
977 }
979 > /**
980 > * `turn/completed` translates to either a normal complete signal or, when
981 > * the turn ended with `status: 'failed'`, an error followed by the
982 > * complete signal so consumers can react to both.
983 > */
984 > export function mapTurnCompleted(
985 > state: ICodexSessionMapState, codexMapAppServerEvents.ts ×3
986 > params: TurnCompletedNotification,
987 > fallbackDuration?: number,
988 > ): (SessionAction | ChatAction)[] {
989 > state.currentTurnId = undefined;
990 > state.itemToPartId.clear();
991 > state.itemToReasoningPartId.clear();
992 > // Finalize any command whose completion was deferred to coalesce a possible
993 > // sandbox pre-flight (see ICodexSessionMapState.pendingPreflight) — it was
994 > // never reused, so it is a genuine output-less command and must complete.
995 > const preflightFlush = flushPendingPreflight(state);
996 > const orphanedToolCalls = [...state.itemToToolCall.values()];
997 > state.itemToToolCall.clear();
998 > const turnId = params.turn.id;
999 > const status = params.turn.status;
1000 > const duration = typeof params.turn.durationMs === 'number' && Number.isFinite(params.turn.durationMs) && params.turn.durationMs >= 0
1001 > ? params.turn.durationMs codexMapAppServerEvents.ts ×1
1002 > : typeof params.turn.startedAt === 'number' && typeof params.turn.completedAt === 'number' codexMapAppServerEvents.ts ×2
1003 ? Math.max(0, (params.turn.completedAt - params.turn.startedAt) * 1000)
1004 > : typeof fallbackDuration === 'number' && Number.isFinite(fallbackDuration) codexMapAppServerEvents.ts ×2
1005 > ? Math.max(0, fallbackDuration) codexMapAppServerEvents.ts ×2
1007 > const orphanedToolCallActions: (SessionAction | ChatAction)[] = orphanedToolCalls.map(entry => ({ codexMapAppServerEvents.ts ×3
1008 > type: ActionType.ChatToolCallComplete, codexMapAppServerEvents.ts ×2
1009 > turnId: entry.turnId,
1010 > toolCallId: entry.toolCallId,
1011 > result: {
1012 > success: false,
1013 > pastTenseMessage: `Stopped ${entry.toolName}`,
1014 > content: entry.output ? [{ type: ToolResultContentType.Text as const, text: entry.output }] : undefined,
1015 > error: { message: status === 'interrupted' ? 'Turn interrupted before the tool completed' : 'Turn completed before the tool reported completion' },
1016 > },
1018 > if (status === 'failed' && params.turn.error) {
1019 > const errMessage = params.turn.error.message ?? 'Codex turn failed'; codexMapAppServerEvents.ts ×1
1020 > return [
1021 > ...preflightFlush,
1022 > ...orphanedToolCallActions,
1023 > {
1024 > type: ActionType.ChatError,
1025 > turnId,
1026 > duration,
1027 > error: {
1028 > errorType: 'CodexError',
1029 > ...extractForwardedErrorInfo(errMessage),
1030 > },
1031 > },
1032 > {
1033 > type: ActionType.ChatTurnComplete,
1034 > turnId,
1035 > duration,
1036 > },
1037 > ];
1038 > }
1039 > if (status === 'interrupted') { codexMapAppServerEvents.ts ×1
1040 > return [...preflightFlush, ...orphanedToolCallActions, { type: ActionType.ChatTurnCancelled, turnId, duration }]; codexMapAppServerEvents.ts ×1
1041 > }
1042 > return [...preflightFlush, ...orphanedToolCallActions, { type: ActionType.ChatTurnComplete, turnId, duration }]; codexMapAppServerEvents.ts ×1
1043 > }
1045 > /**
1046 > * Build a {@link TurnState} from a codex `Turn.status`. Mostly useful
1047 > * for replay (Phase 3).
1048 > */
1049 > export function turnStateFromStatus(status: string): TurnState {
1050 > switch (status) { codexMapAppServerEvents.ts ×5
1051 > case 'completed':
1052 > return TurnState.Complete; codexMapAppServerEvents.ts ×1
1053 > case 'interrupted': codexMapAppServerEvents.ts ×5
1054 > return TurnState.Cancelled; codexMapAppServerEvents.ts ×2
1055 > case 'failed': codexMapAppServerEvents.ts ×5
1056 > return TurnState.Error; codexMapAppServerEvents.ts ×1
1058 > return TurnState.Complete; codexMapAppServerEvents.ts ×2
1060 > }