@gakr-gakr/codex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/autobot.plugin.json +374 -0
  2. package/doctor-contract-api.ts +68 -0
  3. package/harness.ts +72 -0
  4. package/index.ts +124 -0
  5. package/media-understanding-provider.ts +521 -0
  6. package/package.json +40 -0
  7. package/prompt-overlay.ts +21 -0
  8. package/provider-catalog.ts +83 -0
  9. package/provider-discovery.ts +45 -0
  10. package/provider.ts +243 -0
  11. package/src/app-server/app-inventory-cache.ts +324 -0
  12. package/src/app-server/approval-bridge.ts +1211 -0
  13. package/src/app-server/auth-bridge.ts +614 -0
  14. package/src/app-server/capabilities.ts +27 -0
  15. package/src/app-server/client-factory.ts +24 -0
  16. package/src/app-server/client.ts +715 -0
  17. package/src/app-server/compact.ts +512 -0
  18. package/src/app-server/computer-use.ts +683 -0
  19. package/src/app-server/config.ts +1038 -0
  20. package/src/app-server/context-engine-projection.ts +403 -0
  21. package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
  22. package/src/app-server/dynamic-tool-profile.ts +70 -0
  23. package/src/app-server/dynamic-tools.ts +623 -0
  24. package/src/app-server/elicitation-bridge.ts +783 -0
  25. package/src/app-server/event-projector.ts +2065 -0
  26. package/src/app-server/image-payload-sanitizer.ts +167 -0
  27. package/src/app-server/local-runtime-attribution.ts +39 -0
  28. package/src/app-server/managed-binary.ts +193 -0
  29. package/src/app-server/models.ts +172 -0
  30. package/src/app-server/native-hook-relay.ts +150 -0
  31. package/src/app-server/native-subagent-task-mirror.ts +497 -0
  32. package/src/app-server/plugin-activation.ts +283 -0
  33. package/src/app-server/plugin-app-cache-key.ts +74 -0
  34. package/src/app-server/plugin-approval-roundtrip.ts +122 -0
  35. package/src/app-server/plugin-inventory.ts +357 -0
  36. package/src/app-server/plugin-thread-config.ts +455 -0
  37. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
  38. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
  39. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
  40. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
  41. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
  42. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
  43. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
  44. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
  45. package/src/app-server/protocol-validators.ts +203 -0
  46. package/src/app-server/protocol.ts +520 -0
  47. package/src/app-server/rate-limit-cache.ts +48 -0
  48. package/src/app-server/rate-limits.ts +583 -0
  49. package/src/app-server/request.ts +73 -0
  50. package/src/app-server/run-attempt.ts +4862 -0
  51. package/src/app-server/session-binding.ts +398 -0
  52. package/src/app-server/session-history.ts +44 -0
  53. package/src/app-server/shared-client.ts +289 -0
  54. package/src/app-server/side-question.ts +1009 -0
  55. package/src/app-server/test-support.ts +48 -0
  56. package/src/app-server/thread-lifecycle.ts +959 -0
  57. package/src/app-server/timeout.ts +9 -0
  58. package/src/app-server/tool-progress-normalization.ts +77 -0
  59. package/src/app-server/trajectory.ts +368 -0
  60. package/src/app-server/transcript-mirror.ts +208 -0
  61. package/src/app-server/transport-stdio.ts +107 -0
  62. package/src/app-server/transport-websocket.ts +90 -0
  63. package/src/app-server/transport.ts +117 -0
  64. package/src/app-server/user-input-bridge.ts +316 -0
  65. package/src/app-server/version.ts +4 -0
  66. package/src/app-server/vision-tools.ts +12 -0
  67. package/src/command-account.ts +544 -0
  68. package/src/command-formatters.ts +426 -0
  69. package/src/command-handlers.ts +2021 -0
  70. package/src/command-plugins-management.ts +137 -0
  71. package/src/command-rpc.ts +142 -0
  72. package/src/commands.ts +65 -0
  73. package/src/conversation-binding-data.ts +124 -0
  74. package/src/conversation-binding.ts +561 -0
  75. package/src/conversation-control.ts +303 -0
  76. package/src/conversation-turn-collector.ts +186 -0
  77. package/src/conversation-turn-input.ts +106 -0
  78. package/src/migration/apply.ts +501 -0
  79. package/src/migration/helpers.ts +55 -0
  80. package/src/migration/plan.ts +461 -0
  81. package/src/migration/provider.ts +41 -0
  82. package/src/migration/source.ts +643 -0
  83. package/src/migration/targets.ts +25 -0
  84. package/src/node-cli-sessions.ts +711 -0
  85. package/test-api.ts +95 -0
  86. package/tsconfig.json +16 -0
@@ -0,0 +1,2065 @@
1
+ import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
2
+ import {
3
+ classifyAgentHarnessTerminalOutcome,
4
+ embeddedAgentLog,
5
+ emitAgentEvent as emitGlobalAgentEvent,
6
+ formatErrorMessage,
7
+ formatToolAggregate,
8
+ formatToolProgressOutput,
9
+ inferToolMetaFromArgs,
10
+ normalizeUsage,
11
+ runAgentHarnessAfterCompactionHook,
12
+ runAgentHarnessAfterToolCallHook,
13
+ runAgentHarnessBeforeCompactionHook,
14
+ TOOL_PROGRESS_OUTPUT_MAX_CHARS,
15
+ type AgentMessage,
16
+ type EmbeddedRunAttemptParams,
17
+ type EmbeddedRunAttemptResult,
18
+ type HeartbeatToolResponse,
19
+ type MessagingToolSend,
20
+ type MessagingToolSourceReplyPayload,
21
+ type ToolProgressDetailMode,
22
+ } from "autobot/plugin-sdk/agent-harness-runtime";
23
+ import { emitTrustedDiagnosticEvent } from "autobot/plugin-sdk/diagnostic-runtime";
24
+ import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
25
+ import { CodexNativeSubagentTaskMirror } from "./native-subagent-task-mirror.js";
26
+ import { readCodexTurn } from "./protocol-validators.js";
27
+ import {
28
+ isJsonObject,
29
+ type CodexDynamicToolCallOutputContentItem,
30
+ type CodexServerNotification,
31
+ type CodexThreadItem,
32
+ type CodexTurn,
33
+ type JsonObject,
34
+ type JsonValue,
35
+ } from "./protocol.js";
36
+ import { readRecentCodexRateLimits, rememberCodexRateLimits } from "./rate-limit-cache.js";
37
+ import { formatCodexUsageLimitErrorMessage } from "./rate-limits.js";
38
+ import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
39
+ import {
40
+ resolveCodexToolProgressDetailMode,
41
+ sanitizeCodexAgentEventRecord,
42
+ sanitizeCodexToolArguments,
43
+ } from "./tool-progress-normalization.js";
44
+ import type { CodexTrajectoryRecorder } from "./trajectory.js";
45
+ import { attachCodexMirrorIdentity, buildCodexUserPromptMessage } from "./transcript-mirror.js";
46
+
47
+ export type CodexAppServerToolTelemetry = {
48
+ didSendViaMessagingTool: boolean;
49
+ messagingToolSentTexts: string[];
50
+ messagingToolSentMediaUrls: string[];
51
+ messagingToolSentTargets: MessagingToolSend[];
52
+ messagingToolSourceReplyPayloads?: MessagingToolSourceReplyPayload[];
53
+ heartbeatToolResponse?: HeartbeatToolResponse;
54
+ toolMediaUrls?: string[];
55
+ toolAudioAsVoice?: boolean;
56
+ successfulCronAdds?: number;
57
+ };
58
+
59
+ export type CodexAppServerEventProjectorOptions = {
60
+ nativePostToolUseRelayEnabled?: boolean;
61
+ trajectoryRecorder?: CodexTrajectoryRecorder | null;
62
+ };
63
+
64
+ const ZERO_USAGE: Usage = {
65
+ input: 0,
66
+ output: 0,
67
+ cacheRead: 0,
68
+ cacheWrite: 0,
69
+ totalTokens: 0,
70
+ cost: {
71
+ input: 0,
72
+ output: 0,
73
+ cacheRead: 0,
74
+ cacheWrite: 0,
75
+ total: 0,
76
+ },
77
+ };
78
+
79
+ const CURRENT_TOKEN_USAGE_KEYS = [
80
+ "last",
81
+ "current",
82
+ "lastCall",
83
+ "lastCallUsage",
84
+ "lastTokenUsage",
85
+ "last_token_usage",
86
+ ] as const;
87
+
88
+ const CODEX_PROMPT_TOTAL_INPUT_KEYS = [
89
+ "inputTokens",
90
+ "input_tokens",
91
+ "promptTokens",
92
+ "prompt_tokens",
93
+ ] as const;
94
+
95
+ const MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM = 20;
96
+ const TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS = 12_000;
97
+ const TRANSCRIPT_PROGRESS_SUPPRESSED_TOOL_NAMES = new Set([
98
+ "message",
99
+ "messages",
100
+ "reply",
101
+ "send",
102
+ "reaction",
103
+ "react",
104
+ "typing",
105
+ ]);
106
+
107
+ export function shouldEmitTranscriptToolProgress(toolName: unknown, args?: unknown): boolean {
108
+ const normalized = typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
109
+ return Boolean(
110
+ normalized &&
111
+ !TRANSCRIPT_PROGRESS_SUPPRESSED_TOOL_NAMES.has(normalized) &&
112
+ !isActivityLogCommandProgress(normalized, args),
113
+ );
114
+ }
115
+
116
+ type ToolTranscriptCallInput = {
117
+ id: string;
118
+ name: string;
119
+ arguments?: unknown;
120
+ };
121
+
122
+ type ToolTranscriptResultInput = {
123
+ id: string;
124
+ name: string;
125
+ text?: string;
126
+ isError: boolean;
127
+ };
128
+
129
+ export class CodexAppServerEventProjector {
130
+ private readonly assistantTextByItem = new Map<string, string>();
131
+ private readonly assistantItemOrder: string[] = [];
132
+ private readonly assistantPhaseByItem = new Map<string, string>();
133
+ private readonly lastCommentaryProgressTextByItem = new Map<string, string>();
134
+ private readonly reasoningTextByItem = new Map<string, string>();
135
+ private readonly planTextByItem = new Map<string, string>();
136
+ private readonly activeItemIds = new Set<string>();
137
+ private readonly completedItemIds = new Set<string>();
138
+ private readonly activeCompactionItemIds = new Set<string>();
139
+ private readonly toolProgressTexts = new Set<string>();
140
+ private readonly toolResultSummaryItemIds = new Set<string>();
141
+ private readonly toolResultOutputItemIds = new Set<string>();
142
+ private readonly toolResultOutputStreamedItemIds = new Set<string>();
143
+ private readonly transcriptToolProgressSuppressedIds = new Set<string>();
144
+ private readonly toolTranscriptArgumentsById = new Map<string, unknown>();
145
+ private readonly toolResultOutputDeltaState = new Map<
146
+ string,
147
+ { chars: number; messages: number; truncated: boolean }
148
+ >();
149
+ private readonly toolResultOutputTextByItem = new Map<string, string>();
150
+ private readonly toolMetas = new Map<string, { toolName: string; meta?: string }>();
151
+ private readonly toolTranscriptMessages: AgentMessage[] = [];
152
+ private readonly toolTranscriptCallIds = new Set<string>();
153
+ private readonly toolTranscriptResultIds = new Set<string>();
154
+ private readonly transcriptToolProgressCallIds = new Set<string>();
155
+ private lastNativeToolError: EmbeddedRunAttemptResult["lastToolError"];
156
+ private readonly nativeGeneratedMediaUrls = new Set<string>();
157
+ private readonly diagnosticToolStartedAtByItem = new Map<string, number>();
158
+ private readonly afterToolCallObservedItemIds = new Set<string>();
159
+ private assistantStarted = false;
160
+ private reasoningStarted = false;
161
+ private reasoningEnded = false;
162
+ private completedTurn: CodexTurn | undefined;
163
+ private promptError: unknown;
164
+ private promptErrorSource: EmbeddedRunAttemptResult["promptErrorSource"] = null;
165
+ private aborted = false;
166
+ private tokenUsage: ReturnType<typeof normalizeUsage>;
167
+ private guardianReviewCount = 0;
168
+ private completedCompactionCount = 0;
169
+ private latestRateLimits: JsonValue | undefined;
170
+ private readonly nativeSubagentTaskMirror: CodexNativeSubagentTaskMirror;
171
+
172
+ constructor(
173
+ private readonly params: EmbeddedRunAttemptParams,
174
+ private readonly threadId: string,
175
+ private readonly turnId: string,
176
+ private readonly options: CodexAppServerEventProjectorOptions = {},
177
+ ) {
178
+ this.nativeSubagentTaskMirror = new CodexNativeSubagentTaskMirror({
179
+ parentThreadId: threadId,
180
+ requesterSessionKey: params.sessionKey,
181
+ agentId: params.agentId,
182
+ });
183
+ }
184
+
185
+ async handleNotification(notification: CodexServerNotification): Promise<void> {
186
+ const params = isJsonObject(notification.params) ? notification.params : undefined;
187
+ if (!params) {
188
+ return;
189
+ }
190
+ try {
191
+ this.nativeSubagentTaskMirror.handleNotification(notification);
192
+ } catch (error) {
193
+ embeddedAgentLog.warn("Failed to mirror Codex native subagent lifecycle event", {
194
+ method: notification.method,
195
+ error: formatErrorMessage(error),
196
+ });
197
+ }
198
+ if (notification.method === "account/rateLimits/updated") {
199
+ this.latestRateLimits = params;
200
+ rememberCodexRateLimits(params);
201
+ return;
202
+ }
203
+ if (isHookNotificationMethod(notification.method)) {
204
+ if (!this.isHookNotificationForCurrentThread(params)) {
205
+ return;
206
+ }
207
+ } else if (!this.isNotificationForTurn(params)) {
208
+ return;
209
+ }
210
+
211
+ switch (notification.method) {
212
+ case "item/agentMessage/delta":
213
+ await this.handleAssistantDelta(params);
214
+ break;
215
+ case "item/reasoning/summaryTextDelta":
216
+ case "item/reasoning/textDelta":
217
+ await this.handleReasoningDelta(params);
218
+ break;
219
+ case "item/plan/delta":
220
+ this.handlePlanDelta(params);
221
+ break;
222
+ case "turn/plan/updated":
223
+ this.handleTurnPlanUpdated(params);
224
+ break;
225
+ case "item/started":
226
+ await this.handleItemStarted(params);
227
+ break;
228
+ case "item/completed":
229
+ await this.handleItemCompleted(params);
230
+ break;
231
+ case "item/commandExecution/outputDelta":
232
+ this.handleOutputDelta(params, "bash");
233
+ break;
234
+ case "item/fileChange/outputDelta":
235
+ this.handleOutputDelta(params, "apply_patch");
236
+ break;
237
+ case "item/autoApprovalReview/started":
238
+ case "item/autoApprovalReview/completed":
239
+ this.handleGuardianReviewNotification(notification.method, params);
240
+ break;
241
+ case "hook/started":
242
+ case "hook/completed":
243
+ this.handleHookNotification(notification.method, params);
244
+ break;
245
+ case "thread/tokenUsage/updated":
246
+ this.handleTokenUsage(params);
247
+ break;
248
+ case "turn/completed":
249
+ await this.handleTurnCompleted(params);
250
+ break;
251
+ case "rawResponseItem/completed":
252
+ this.handleRawResponseItemCompleted(params);
253
+ break;
254
+ case "error":
255
+ if (readBooleanAlias(params, ["willRetry", "will_retry"]) === true) {
256
+ break;
257
+ }
258
+ this.promptError = this.formatCodexErrorMessage(params) ?? "codex app-server error";
259
+ this.promptErrorSource = "prompt";
260
+ break;
261
+ default:
262
+ break;
263
+ }
264
+ }
265
+
266
+ buildResult(
267
+ toolTelemetry: CodexAppServerToolTelemetry,
268
+ options?: { yieldDetected?: boolean },
269
+ ): EmbeddedRunAttemptResult {
270
+ const assistantTexts = this.collectAssistantTexts();
271
+ const reasoningText = collectTextValues(this.reasoningTextByItem).join("\n\n");
272
+ const planText = collectTextValues(this.planTextByItem).join("\n\n");
273
+ const lastAssistant =
274
+ assistantTexts.length > 0
275
+ ? this.createAssistantMessage(assistantTexts.join("\n\n"))
276
+ : undefined;
277
+ // Each snapshot entry is tagged with a stable mirror identity of the
278
+ // shape `${turnId}:${kind}`. The mirror's idempotency key is derived
279
+ // from this identity rather than from snapshot position or content
280
+ // hash, so:
281
+ // - Re-mirror of the same turn (retry) → same identity → no-op.
282
+ // - Re-emit of a prior turn's entry into a later turn's snapshot
283
+ // (the cross-turn drift mode named in #77012) → original identity
284
+ // is preserved → on-disk key still matches → also a no-op.
285
+ // - Two distinct turns where the user repeats verbatim content →
286
+ // distinct turnIds → distinct identities → both kept.
287
+ const turnId = this.turnId;
288
+ const messagesSnapshot: AgentMessage[] = [
289
+ attachCodexMirrorIdentity(buildCodexUserPromptMessage(this.params), `${turnId}:prompt`),
290
+ ];
291
+ // Codex owns the canonical thread. These mirror records keep enough local
292
+ // context for AutoBot history, search, and future harness switching.
293
+ if (reasoningText) {
294
+ messagesSnapshot.push(
295
+ attachCodexMirrorIdentity(
296
+ this.createAssistantMirrorMessage("Codex reasoning", reasoningText),
297
+ `${turnId}:reasoning`,
298
+ ),
299
+ );
300
+ }
301
+ if (planText) {
302
+ messagesSnapshot.push(
303
+ attachCodexMirrorIdentity(
304
+ this.createAssistantMirrorMessage("Codex plan", planText),
305
+ `${turnId}:plan`,
306
+ ),
307
+ );
308
+ }
309
+ messagesSnapshot.push(...this.toolTranscriptMessages);
310
+ if (lastAssistant) {
311
+ messagesSnapshot.push(attachCodexMirrorIdentity(lastAssistant, `${turnId}:assistant`));
312
+ }
313
+ const turnFailed = this.completedTurn?.status === "failed";
314
+ const turnInterrupted = this.completedTurn?.status === "interrupted";
315
+ const promptError =
316
+ this.promptError ??
317
+ (turnFailed ? (this.completedTurn?.error?.message ?? "codex app-server turn failed") : null);
318
+ const agentHarnessResultClassification = classifyAgentHarnessTerminalOutcome({
319
+ assistantTexts,
320
+ reasoningText,
321
+ planText,
322
+ promptError,
323
+ turnCompleted: Boolean(this.completedTurn),
324
+ });
325
+ return {
326
+ aborted: this.aborted || turnInterrupted,
327
+ externalAbort: false,
328
+ timedOut: false,
329
+ idleTimedOut: false,
330
+ timedOutDuringCompaction: false,
331
+ timedOutDuringToolExecution: false,
332
+ promptError,
333
+ promptErrorSource: promptError ? this.promptErrorSource || "prompt" : null,
334
+ sessionIdUsed: this.params.sessionId,
335
+ ...(agentHarnessResultClassification ? { agentHarnessResultClassification } : {}),
336
+ bootstrapPromptWarningSignaturesSeen: this.params.bootstrapPromptWarningSignaturesSeen,
337
+ bootstrapPromptWarningSignature: this.params.bootstrapPromptWarningSignature,
338
+ messagesSnapshot,
339
+ assistantTexts,
340
+ toolMetas: [...this.toolMetas.values()],
341
+ lastAssistant,
342
+ ...(this.lastNativeToolError ? { lastToolError: this.lastNativeToolError } : {}),
343
+ didSendViaMessagingTool: toolTelemetry.didSendViaMessagingTool,
344
+ messagingToolSentTexts: toolTelemetry.messagingToolSentTexts,
345
+ messagingToolSentMediaUrls: toolTelemetry.messagingToolSentMediaUrls,
346
+ messagingToolSentTargets: toolTelemetry.messagingToolSentTargets,
347
+ messagingToolSourceReplyPayloads: toolTelemetry.messagingToolSourceReplyPayloads ?? [],
348
+ heartbeatToolResponse: toolTelemetry.heartbeatToolResponse,
349
+ toolMediaUrls: this.buildToolMediaUrls(toolTelemetry),
350
+ toolAudioAsVoice: toolTelemetry.toolAudioAsVoice,
351
+ successfulCronAdds: toolTelemetry.successfulCronAdds,
352
+ cloudCodeAssistFormatError: false,
353
+ attemptUsage: this.tokenUsage,
354
+ replayMetadata: {
355
+ hadPotentialSideEffects: toolTelemetry.didSendViaMessagingTool,
356
+ replaySafe: !toolTelemetry.didSendViaMessagingTool,
357
+ },
358
+ itemLifecycle: {
359
+ startedCount: this.activeItemIds.size + this.completedItemIds.size,
360
+ completedCount: this.completedItemIds.size,
361
+ activeCount: this.activeItemIds.size,
362
+ ...(this.completedCompactionCount > 0
363
+ ? { compactionCount: this.completedCompactionCount }
364
+ : {}),
365
+ },
366
+ yieldDetected: options?.yieldDetected || false,
367
+ didSendDeterministicApprovalPrompt: this.guardianReviewCount > 0 ? false : undefined,
368
+ };
369
+ }
370
+
371
+ recordDynamicToolCall(params: { callId: string; tool: string; arguments?: JsonValue }): void {
372
+ this.recordToolTranscriptCall({
373
+ id: params.callId,
374
+ name: params.tool,
375
+ arguments: sanitizeCodexToolArguments(params.arguments),
376
+ });
377
+ }
378
+
379
+ recordDynamicToolResult(params: {
380
+ callId: string;
381
+ tool: string;
382
+ success: boolean;
383
+ contentItems: CodexDynamicToolCallOutputContentItem[];
384
+ }): void {
385
+ this.recordToolTranscriptResult({
386
+ id: params.callId,
387
+ name: params.tool,
388
+ text: collectDynamicToolContentText(params.contentItems),
389
+ isError: !params.success,
390
+ });
391
+ }
392
+
393
+ markTimedOut(): void {
394
+ this.aborted = true;
395
+ this.promptError = "codex app-server attempt timed out";
396
+ this.promptErrorSource = "prompt";
397
+ }
398
+
399
+ markAborted(): void {
400
+ this.aborted = true;
401
+ }
402
+
403
+ isCompacting(): boolean {
404
+ return this.activeCompactionItemIds.size > 0;
405
+ }
406
+
407
+ private async handleAssistantDelta(params: JsonObject): Promise<void> {
408
+ const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "assistant";
409
+ const delta = readString(params, "delta") ?? "";
410
+ if (!delta) {
411
+ return;
412
+ }
413
+ if (!this.assistantStarted) {
414
+ this.assistantStarted = true;
415
+ await this.params.onAssistantMessageStart?.();
416
+ }
417
+ this.rememberAssistantItem(itemId);
418
+ const text = `${this.assistantTextByItem.get(itemId) ?? ""}${delta}`;
419
+ this.assistantTextByItem.set(itemId, text);
420
+ if (this.isCommentaryAssistantItem(itemId)) {
421
+ this.emitCommentaryProgress({ itemId, text });
422
+ }
423
+ // Codex app-server can emit multiple agentMessage items per turn, including
424
+ // intermediate coordination/progress prose. Keep those deltas internal until
425
+ // turn completion chooses the last assistant item as the user-visible reply.
426
+ }
427
+
428
+ private async handleReasoningDelta(params: JsonObject): Promise<void> {
429
+ const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "reasoning";
430
+ const delta = readString(params, "delta") ?? "";
431
+ if (!delta) {
432
+ return;
433
+ }
434
+ this.reasoningStarted = true;
435
+ this.reasoningTextByItem.set(itemId, `${this.reasoningTextByItem.get(itemId) ?? ""}${delta}`);
436
+ await this.params.onReasoningStream?.({ text: delta });
437
+ }
438
+
439
+ private handlePlanDelta(params: JsonObject): void {
440
+ const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "plan";
441
+ const delta = readString(params, "delta") ?? "";
442
+ if (!delta) {
443
+ return;
444
+ }
445
+ const text = `${this.planTextByItem.get(itemId) ?? ""}${delta}`;
446
+ this.planTextByItem.set(itemId, text);
447
+ this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(text) });
448
+ }
449
+
450
+ private handleTurnPlanUpdated(params: JsonObject): void {
451
+ const plan = Array.isArray(params.plan)
452
+ ? params.plan.flatMap((entry) => {
453
+ if (!isJsonObject(entry)) {
454
+ return [];
455
+ }
456
+ const step = readString(entry, "step");
457
+ const status = readString(entry, "status");
458
+ if (!step) {
459
+ return [];
460
+ }
461
+ return status ? [`${step} (${status})`] : [step];
462
+ })
463
+ : undefined;
464
+ this.emitPlanUpdate({
465
+ explanation: readNullableString(params, "explanation"),
466
+ steps: plan,
467
+ });
468
+ }
469
+
470
+ private async handleItemStarted(params: JsonObject): Promise<void> {
471
+ const item = readItem(params.item);
472
+ const itemId = item?.id ?? readString(params, "itemId") ?? readString(params, "id");
473
+ this.rememberAssistantPhase(item);
474
+ if (itemId) {
475
+ this.activeItemIds.add(itemId);
476
+ }
477
+ if (item?.type === "contextCompaction" && itemId) {
478
+ this.activeCompactionItemIds.add(itemId);
479
+ await runAgentHarnessBeforeCompactionHook({
480
+ sessionFile: this.params.sessionFile,
481
+ messages: await this.readMirroredSessionMessages(),
482
+ ctx: {
483
+ runId: this.params.runId,
484
+ agentId: this.params.agentId,
485
+ sessionKey: this.params.sessionKey,
486
+ sessionId: this.params.sessionId,
487
+ workspaceDir: this.params.workspaceDir,
488
+ messageProvider: this.params.messageProvider ?? undefined,
489
+ trigger: this.params.trigger,
490
+ channelId: this.params.messageChannel ?? this.params.messageProvider ?? undefined,
491
+ },
492
+ });
493
+ this.emitAgentEvent({
494
+ stream: "compaction",
495
+ data: {
496
+ phase: "start",
497
+ backend: "codex-app-server",
498
+ threadId: this.threadId,
499
+ turnId: this.turnId,
500
+ itemId,
501
+ },
502
+ });
503
+ }
504
+ this.emitStandardItemEvent({ phase: "start", item });
505
+ this.emitNormalizedToolItemEvent({ phase: "start", item });
506
+ this.recordNativeToolTranscriptCall(item);
507
+ this.emitToolResultSummary(item);
508
+ this.emitAgentEvent({
509
+ stream: "codex_app_server.item",
510
+ data: { phase: "started", itemId, type: item?.type },
511
+ });
512
+ }
513
+
514
+ private async handleItemCompleted(params: JsonObject): Promise<void> {
515
+ const item = readItem(params.item);
516
+ const itemId = item?.id ?? readString(params, "itemId") ?? readString(params, "id");
517
+ if (itemId) {
518
+ this.activeItemIds.delete(itemId);
519
+ this.completedItemIds.add(itemId);
520
+ }
521
+ this.rememberAssistantPhase(item);
522
+ if (item?.type === "agentMessage" && typeof item.text === "string" && item.text) {
523
+ this.rememberAssistantItem(item.id);
524
+ this.assistantTextByItem.set(item.id, item.text);
525
+ if (this.isCommentaryAssistantItem(item.id)) {
526
+ this.emitCommentaryProgress({ itemId: item.id, text: item.text });
527
+ }
528
+ }
529
+ this.recordNativeGeneratedMedia(item);
530
+ if (item?.type === "plan" && typeof item.text === "string" && item.text) {
531
+ this.planTextByItem.set(item.id, item.text);
532
+ this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(item.text) });
533
+ }
534
+ if (item?.type === "contextCompaction" && itemId) {
535
+ this.activeCompactionItemIds.delete(itemId);
536
+ this.completedCompactionCount += 1;
537
+ await runAgentHarnessAfterCompactionHook({
538
+ sessionFile: this.params.sessionFile,
539
+ messages: await this.readMirroredSessionMessages(),
540
+ compactedCount: -1,
541
+ ctx: {
542
+ runId: this.params.runId,
543
+ agentId: this.params.agentId,
544
+ sessionKey: this.params.sessionKey,
545
+ sessionId: this.params.sessionId,
546
+ workspaceDir: this.params.workspaceDir,
547
+ messageProvider: this.params.messageProvider ?? undefined,
548
+ trigger: this.params.trigger,
549
+ channelId: this.params.messageChannel ?? this.params.messageProvider ?? undefined,
550
+ },
551
+ });
552
+ this.emitAgentEvent({
553
+ stream: "compaction",
554
+ data: {
555
+ phase: "end",
556
+ backend: "codex-app-server",
557
+ completed: true,
558
+ threadId: this.threadId,
559
+ turnId: this.turnId,
560
+ itemId,
561
+ },
562
+ });
563
+ }
564
+ this.recordToolMeta(item);
565
+ this.emitStandardItemEvent({ phase: "end", item });
566
+ this.emitNormalizedToolItemEvent({ phase: "result", item });
567
+ this.recordNativeToolTranscriptCall(item);
568
+ this.recordNativeToolTranscriptResult(item);
569
+ this.emitToolResultSummary(item);
570
+ this.emitToolResultOutput(item);
571
+ this.emitAgentEvent({
572
+ stream: "codex_app_server.item",
573
+ data: { phase: "completed", itemId, type: item?.type },
574
+ });
575
+ }
576
+
577
+ private handleTokenUsage(params: JsonObject): void {
578
+ const tokenUsage = isJsonObject(params.tokenUsage) ? params.tokenUsage : undefined;
579
+ const current =
580
+ (tokenUsage ? readFirstJsonObject(tokenUsage, CURRENT_TOKEN_USAGE_KEYS) : undefined) ??
581
+ readFirstJsonObject(params, CURRENT_TOKEN_USAGE_KEYS);
582
+ if (!current) {
583
+ return;
584
+ }
585
+ const usage = normalizeCodexTokenUsage(current);
586
+ if (usage) {
587
+ this.tokenUsage = usage;
588
+ }
589
+ }
590
+
591
+ private handleGuardianReviewNotification(method: string, params: JsonObject): void {
592
+ this.guardianReviewCount += 1;
593
+ const review = isJsonObject(params.review) ? params.review : undefined;
594
+ const action = isJsonObject(params.action) ? params.action : undefined;
595
+ this.emitAgentEvent({
596
+ stream: "codex_app_server.guardian",
597
+ data: {
598
+ method,
599
+ phase: method.endsWith("/started") ? "started" : "completed",
600
+ reviewId: readString(params, "reviewId"),
601
+ targetItemId: readNullableString(params, "targetItemId"),
602
+ decisionSource: readString(params, "decisionSource"),
603
+ status: review ? readString(review, "status") : undefined,
604
+ riskLevel: review ? readString(review, "riskLevel") : undefined,
605
+ userAuthorization: review ? readString(review, "userAuthorization") : undefined,
606
+ rationale: review ? readNullableString(review, "rationale") : undefined,
607
+ actionType: action ? readString(action, "type") : undefined,
608
+ },
609
+ });
610
+ }
611
+
612
+ private handleHookNotification(method: string, params: JsonObject): void {
613
+ const run = isJsonObject(params.run) ? params.run : undefined;
614
+ if (!run) {
615
+ return;
616
+ }
617
+ const durationMs = readNumber(run, "durationMs");
618
+ const entries = readHookOutputEntries(run.entries);
619
+ const hookTurnId = readNullableString(params, "turnId");
620
+ this.emitAgentEvent({
621
+ stream: "codex_app_server.hook",
622
+ data: {
623
+ phase: method === "hook/started" ? "started" : "completed",
624
+ threadId: this.threadId,
625
+ turnId: hookTurnId === undefined ? this.turnId : hookTurnId,
626
+ hookRunId: readString(run, "id"),
627
+ eventName: readString(run, "eventName"),
628
+ handlerType: readString(run, "handlerType"),
629
+ executionMode: readString(run, "executionMode"),
630
+ scope: readString(run, "scope"),
631
+ source: readString(run, "source"),
632
+ sourcePath: readString(run, "sourcePath"),
633
+ status: readString(run, "status"),
634
+ statusMessage: readNullableString(run, "statusMessage"),
635
+ ...(durationMs !== undefined ? { durationMs } : {}),
636
+ ...(entries.length > 0 ? { entries } : {}),
637
+ },
638
+ });
639
+ }
640
+
641
+ private async handleTurnCompleted(params: JsonObject): Promise<void> {
642
+ const turn = readTurn(params.turn);
643
+ if (!turn || turn.id !== this.turnId) {
644
+ return;
645
+ }
646
+ this.completedTurn = turn;
647
+ if (turn.status === "interrupted") {
648
+ this.aborted = true;
649
+ }
650
+ if (turn.status === "failed") {
651
+ this.promptError =
652
+ formatCodexUsageLimitErrorMessage({
653
+ message: turn.error?.message,
654
+ codexErrorInfo: turn.error?.codexErrorInfo as JsonValue | null | undefined,
655
+ rateLimits: this.latestRateLimits ?? readRecentCodexRateLimits(),
656
+ }) ??
657
+ turn.error?.message ??
658
+ "codex app-server turn failed";
659
+ this.promptErrorSource = "prompt";
660
+ }
661
+ for (const item of turn.items ?? []) {
662
+ this.rememberAssistantPhase(item);
663
+ if (item.type === "agentMessage" && typeof item.text === "string" && item.text) {
664
+ this.rememberAssistantItem(item.id);
665
+ this.assistantTextByItem.set(item.id, item.text);
666
+ }
667
+ this.recordNativeGeneratedMedia(item);
668
+ if (item.type === "plan" && typeof item.text === "string" && item.text) {
669
+ this.planTextByItem.set(item.id, item.text);
670
+ this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(item.text) });
671
+ }
672
+ this.recordToolMeta(item);
673
+ this.emitSnapshotOnlyNativeToolProgress(item);
674
+ this.recordNativeToolTranscriptCall(item);
675
+ this.recordNativeToolTranscriptResult(item);
676
+ this.emitAfterToolCallObservation(item);
677
+ this.emitToolResultSummary(item);
678
+ this.emitToolResultOutput(item);
679
+ }
680
+ this.activeCompactionItemIds.clear();
681
+ await this.maybeEndReasoning();
682
+ }
683
+
684
+ private emitSnapshotOnlyNativeToolProgress(item: CodexThreadItem): void {
685
+ if (
686
+ !shouldSynthesizeToolProgressForItem(item) ||
687
+ !this.isCurrentTurnSnapshotItem(item) ||
688
+ this.completedItemIds.has(item.id) ||
689
+ itemStatus(item) === "running"
690
+ ) {
691
+ return;
692
+ }
693
+ const wasStarted = this.activeItemIds.has(item.id);
694
+ if (!wasStarted) {
695
+ this.emitStandardItemEvent({ phase: "start", item });
696
+ this.emitNormalizedToolItemEvent({ phase: "start", item });
697
+ }
698
+ this.activeItemIds.delete(item.id);
699
+ this.emitStandardItemEvent({ phase: "end", item });
700
+ this.emitNormalizedToolItemEvent({ phase: "result", item });
701
+ this.completedItemIds.add(item.id);
702
+ }
703
+
704
+ private isCurrentTurnSnapshotItem(item: CodexThreadItem): boolean {
705
+ const itemTurnId = readItemString(item, "turnId") ?? readItemString(item, "turn_id");
706
+ return itemTurnId === undefined || itemTurnId === this.turnId;
707
+ }
708
+
709
+ private handleOutputDelta(params: JsonObject, toolName: string): void {
710
+ const itemId = readString(params, "itemId");
711
+ const delta = readString(params, "delta");
712
+ if (!itemId || !delta) {
713
+ return;
714
+ }
715
+ appendToolOutputDeltaText(this.toolResultOutputTextByItem, itemId, delta);
716
+ if (!this.shouldEmitToolOutput()) {
717
+ return;
718
+ }
719
+ if (
720
+ this.transcriptToolProgressSuppressedIds.has(itemId) ||
721
+ !shouldEmitTranscriptToolProgress(toolName, this.toolTranscriptArgumentsById.get(itemId))
722
+ ) {
723
+ return;
724
+ }
725
+ const state = this.toolResultOutputDeltaState.get(itemId) ?? {
726
+ chars: 0,
727
+ messages: 0,
728
+ truncated: false,
729
+ };
730
+ if (state.truncated) {
731
+ return;
732
+ }
733
+ const remainingChars = Math.max(0, TOOL_PROGRESS_OUTPUT_MAX_CHARS - state.chars);
734
+ const remainingMessages = Math.max(0, MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM - state.messages);
735
+ if (remainingChars === 0 || remainingMessages === 0) {
736
+ state.truncated = true;
737
+ this.toolResultOutputDeltaState.set(itemId, state);
738
+ this.emitToolResultMessage({
739
+ itemId,
740
+ text: formatToolOutput(toolName, undefined, "(output truncated)"),
741
+ });
742
+ return;
743
+ }
744
+ const chunk = delta.length > remainingChars ? delta.slice(0, remainingChars) : delta;
745
+ state.chars += chunk.length;
746
+ state.messages += 1;
747
+ const reachedLimit =
748
+ delta.length > remainingChars ||
749
+ state.chars >= TOOL_PROGRESS_OUTPUT_MAX_CHARS ||
750
+ state.messages >= MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM;
751
+ if (reachedLimit) {
752
+ state.truncated = true;
753
+ }
754
+ this.toolResultOutputDeltaState.set(itemId, state);
755
+ this.toolResultOutputStreamedItemIds.add(itemId);
756
+ this.emitToolResultMessage({
757
+ itemId,
758
+ text: formatToolOutput(
759
+ toolName,
760
+ undefined,
761
+ reachedLimit ? `${chunk}\n...(truncated)...` : chunk,
762
+ ),
763
+ });
764
+ }
765
+
766
+ private handleRawResponseItemCompleted(params: JsonObject): void {
767
+ const item = isJsonObject(params.item) ? params.item : undefined;
768
+ if (!item || readString(item, "role") !== "assistant") {
769
+ return;
770
+ }
771
+ const text = extractRawAssistantText(item);
772
+ if (!text) {
773
+ return;
774
+ }
775
+ const itemId = readString(item, "id") ?? `raw-assistant-${this.assistantItemOrder.length + 1}`;
776
+ const phase = readString(item, "phase");
777
+ if (phase) {
778
+ this.assistantPhaseByItem.set(itemId, phase);
779
+ }
780
+ this.rememberAssistantItem(itemId);
781
+ this.assistantTextByItem.set(itemId, text);
782
+ if (phase === "commentary") {
783
+ this.emitCommentaryProgress({ itemId, text });
784
+ }
785
+ }
786
+
787
+ private recordNativeGeneratedMedia(item: CodexThreadItem | undefined): void {
788
+ if (item?.type !== "imageGeneration") {
789
+ return;
790
+ }
791
+ const savedPath = readItemString(item, "savedPath")?.trim();
792
+ if (savedPath) {
793
+ this.nativeGeneratedMediaUrls.add(savedPath);
794
+ }
795
+ }
796
+
797
+ private buildToolMediaUrls(toolTelemetry: CodexAppServerToolTelemetry): string[] | undefined {
798
+ const mediaUrls = new Set(
799
+ toolTelemetry.toolMediaUrls?.map((url) => url.trim()).filter(Boolean) ?? [],
800
+ );
801
+ if ((toolTelemetry.messagingToolSentMediaUrls?.length ?? 0) === 0) {
802
+ for (const mediaUrl of this.nativeGeneratedMediaUrls) {
803
+ mediaUrls.add(mediaUrl);
804
+ }
805
+ }
806
+ return mediaUrls.size > 0 ? [...mediaUrls] : toolTelemetry.toolMediaUrls;
807
+ }
808
+
809
+ private async maybeEndReasoning(): Promise<void> {
810
+ if (!this.reasoningStarted || this.reasoningEnded) {
811
+ return;
812
+ }
813
+ this.reasoningEnded = true;
814
+ await this.params.onReasoningEnd?.();
815
+ }
816
+
817
+ private emitPlanUpdate(params: { explanation?: string | null; steps?: string[] }): void {
818
+ if (!params.explanation && (!params.steps || params.steps.length === 0)) {
819
+ return;
820
+ }
821
+ this.emitAgentEvent({
822
+ stream: "plan",
823
+ data: {
824
+ phase: "update",
825
+ title: "Plan updated",
826
+ source: "codex-app-server",
827
+ ...(params.explanation ? { explanation: params.explanation } : {}),
828
+ ...(params.steps && params.steps.length > 0 ? { steps: params.steps } : {}),
829
+ },
830
+ });
831
+ }
832
+
833
+ private rememberAssistantPhase(item: CodexThreadItem | undefined): void {
834
+ if (item?.type !== "agentMessage") {
835
+ return;
836
+ }
837
+ const phase = readItemString(item, "phase");
838
+ if (phase) {
839
+ this.assistantPhaseByItem.set(item.id, phase);
840
+ }
841
+ }
842
+
843
+ private isCommentaryAssistantItem(itemId: string): boolean {
844
+ return this.assistantPhaseByItem.get(itemId) === "commentary";
845
+ }
846
+
847
+ private emitCommentaryProgress(params: { itemId: string; text: string }): void {
848
+ const progressText = params.text.replace(/\s+/g, " ").trim();
849
+ if (
850
+ !progressText ||
851
+ this.lastCommentaryProgressTextByItem.get(params.itemId) === progressText
852
+ ) {
853
+ return;
854
+ }
855
+ this.lastCommentaryProgressTextByItem.set(params.itemId, progressText);
856
+ this.emitAgentEvent({
857
+ stream: "item",
858
+ data: {
859
+ itemId: params.itemId,
860
+ kind: "preamble",
861
+ title: "Preamble",
862
+ phase: "update",
863
+ progressText,
864
+ source: "codex-app-server",
865
+ },
866
+ });
867
+ }
868
+
869
+ private emitStandardItemEvent(params: {
870
+ phase: "start" | "end";
871
+ item: CodexThreadItem | undefined;
872
+ }): void {
873
+ const { item } = params;
874
+ if (!item) {
875
+ return;
876
+ }
877
+ const kind = itemKind(item);
878
+ if (!kind) {
879
+ return;
880
+ }
881
+ const meta = itemMeta(item, this.toolProgressDetailMode());
882
+ const suppressChannelProgress = shouldSuppressChannelProgressForItem(item);
883
+ this.emitAgentEvent({
884
+ stream: "item",
885
+ data: {
886
+ itemId: item.id,
887
+ phase: params.phase,
888
+ kind,
889
+ title: itemTitle(item),
890
+ status: params.phase === "start" ? "running" : itemStatus(item),
891
+ ...(itemName(item) ? { name: itemName(item) } : {}),
892
+ ...(meta ? { meta } : {}),
893
+ ...(suppressChannelProgress ? { suppressChannelProgress: true } : {}),
894
+ },
895
+ });
896
+ }
897
+
898
+ private emitNormalizedToolItemEvent(params: {
899
+ phase: "start" | "result";
900
+ item: CodexThreadItem | undefined;
901
+ }): void {
902
+ const { item } = params;
903
+ if (!item || !shouldSynthesizeToolProgressForItem(item)) {
904
+ return;
905
+ }
906
+ const name = itemName(item);
907
+ if (!name) {
908
+ return;
909
+ }
910
+ const status = params.phase === "result" ? itemStatus(item) : "running";
911
+ const args = itemToolArgs(item);
912
+ const meta = itemMeta(item, this.toolProgressDetailMode());
913
+ this.recordToolTrajectoryEvent({ phase: params.phase, item, name, args, status });
914
+ this.emitDiagnosticToolExecutionEvent({ phase: params.phase, item, name, status });
915
+ if (params.phase === "result") {
916
+ this.recordNativeToolError({ item, name, meta, status });
917
+ }
918
+ if (!shouldEmitTranscriptToolProgress(name, args)) {
919
+ if (params.phase === "result") {
920
+ this.emitAfterToolCallObservation(item);
921
+ }
922
+ return;
923
+ }
924
+ this.emitAgentEvent({
925
+ stream: "tool",
926
+ data: {
927
+ phase: params.phase,
928
+ name,
929
+ itemId: item.id,
930
+ toolCallId: item.id,
931
+ ...(meta ? { meta } : {}),
932
+ ...(params.phase === "start" && args ? { args } : {}),
933
+ ...(params.phase === "result"
934
+ ? {
935
+ status,
936
+ isError: isNonSuccessItemStatus(status),
937
+ ...itemToolResult(item),
938
+ }
939
+ : {}),
940
+ },
941
+ });
942
+ if (params.phase === "result") {
943
+ this.emitAfterToolCallObservation(item);
944
+ }
945
+ }
946
+
947
+ private recordNativeToolError(params: {
948
+ item: CodexThreadItem;
949
+ name: string;
950
+ meta?: string;
951
+ status: ReturnType<typeof itemStatus>;
952
+ }): void {
953
+ if (!isNonSuccessItemStatus(params.status)) {
954
+ if (!this.lastNativeToolError) {
955
+ return;
956
+ }
957
+ if (!this.lastNativeToolError.mutatingAction) {
958
+ this.lastNativeToolError = undefined;
959
+ return;
960
+ }
961
+ const actionFingerprint = nativeToolActionFingerprint(params.item);
962
+ if (
963
+ this.lastNativeToolError.actionFingerprint &&
964
+ actionFingerprint &&
965
+ this.lastNativeToolError.actionFingerprint === actionFingerprint
966
+ ) {
967
+ this.lastNativeToolError = undefined;
968
+ }
969
+ return;
970
+ }
971
+ const error = itemToolError(params.item, params.status, this.toolResultOutputTextByItem);
972
+ const actionFingerprint = nativeToolActionFingerprint(params.item);
973
+ this.lastNativeToolError = {
974
+ toolName: params.name,
975
+ ...(params.meta ? { meta: params.meta } : {}),
976
+ ...(error ? { error } : {}),
977
+ ...(isMutatingNativeToolItem(params.item) ? { mutatingAction: true } : {}),
978
+ ...(actionFingerprint ? { actionFingerprint } : {}),
979
+ };
980
+ }
981
+
982
+ private recordToolTrajectoryEvent(params: {
983
+ phase: "start" | "result";
984
+ item: CodexThreadItem;
985
+ name: string;
986
+ args?: Record<string, unknown>;
987
+ status: ReturnType<typeof itemStatus>;
988
+ }): void {
989
+ if (params.phase === "start") {
990
+ this.options.trajectoryRecorder?.recordEvent("tool.call", {
991
+ threadId: this.threadId,
992
+ turnId: this.turnId,
993
+ itemId: params.item.id,
994
+ toolCallId: params.item.id,
995
+ name: params.name,
996
+ arguments: params.args,
997
+ });
998
+ return;
999
+ }
1000
+ const toolResult = itemToolResult(params.item).result;
1001
+ const output = itemOutputText(params.item, this.toolResultOutputTextByItem);
1002
+ this.options.trajectoryRecorder?.recordEvent("tool.result", {
1003
+ threadId: this.threadId,
1004
+ turnId: this.turnId,
1005
+ itemId: params.item.id,
1006
+ toolCallId: params.item.id,
1007
+ name: params.name,
1008
+ status: params.status,
1009
+ isError: isNonSuccessItemStatus(params.status),
1010
+ ...(toolResult ? { result: toolResult } : {}),
1011
+ ...(output ? { output } : {}),
1012
+ });
1013
+ }
1014
+
1015
+ private emitDiagnosticToolExecutionEvent(params: {
1016
+ phase: "start" | "result";
1017
+ item: CodexThreadItem;
1018
+ name: string;
1019
+ status: ReturnType<typeof itemStatus>;
1020
+ }): void {
1021
+ const base = {
1022
+ runId: this.params.runId,
1023
+ sessionId: this.params.sessionId,
1024
+ sessionKey: this.params.sessionKey,
1025
+ toolName: params.name,
1026
+ toolCallId: params.item.id,
1027
+ };
1028
+ if (params.phase === "start") {
1029
+ this.diagnosticToolStartedAtByItem.set(params.item.id, Date.now());
1030
+ emitTrustedDiagnosticEvent({
1031
+ type: "tool.execution.started",
1032
+ ...base,
1033
+ });
1034
+ return;
1035
+ }
1036
+
1037
+ const startedAt = this.diagnosticToolStartedAtByItem.get(params.item.id);
1038
+ this.diagnosticToolStartedAtByItem.delete(params.item.id);
1039
+ const itemDurationMs =
1040
+ typeof params.item.durationMs === "number" ? params.item.durationMs : undefined;
1041
+ const durationMs =
1042
+ itemDurationMs ?? (startedAt === undefined ? 0 : Math.max(0, Date.now() - startedAt));
1043
+ const terminalEvent =
1044
+ params.status === "blocked"
1045
+ ? {
1046
+ type: "tool.execution.blocked" as const,
1047
+ reason: "codex_native_tool_blocked",
1048
+ deniedReason: "codex_native_tool_blocked",
1049
+ }
1050
+ : params.status === "failed"
1051
+ ? {
1052
+ type: "tool.execution.error" as const,
1053
+ durationMs,
1054
+ errorCategory: "codex_native_tool_error",
1055
+ }
1056
+ : {
1057
+ type: "tool.execution.completed" as const,
1058
+ durationMs,
1059
+ };
1060
+ emitTrustedDiagnosticEvent({ ...base, ...terminalEvent });
1061
+ }
1062
+
1063
+ private emitAfterToolCallObservation(item: CodexThreadItem): void {
1064
+ if (!this.shouldEmitAfterToolCallObservation(item)) {
1065
+ return;
1066
+ }
1067
+ const name = itemName(item);
1068
+ if (!name) {
1069
+ return;
1070
+ }
1071
+ const status = itemStatus(item);
1072
+ if (status === "running") {
1073
+ return;
1074
+ }
1075
+ this.afterToolCallObservedItemIds.add(item.id);
1076
+ const result = itemToolResult(item).result;
1077
+ const error = itemToolError(item, status, this.toolResultOutputTextByItem);
1078
+ const startedAt =
1079
+ typeof item.durationMs === "number" ? Date.now() - Math.max(0, item.durationMs) : undefined;
1080
+ const hookParams = {
1081
+ toolName: name,
1082
+ toolCallId: item.id,
1083
+ runId: this.params.runId,
1084
+ agentId: this.params.agentId,
1085
+ sessionId: this.params.sessionId,
1086
+ sessionKey: this.params.sessionKey,
1087
+ startArgs: itemToolArgs(item) ?? {},
1088
+ ...(result !== undefined ? { result } : {}),
1089
+ ...(error ? { error } : {}),
1090
+ ...(startedAt !== undefined ? { startedAt } : {}),
1091
+ };
1092
+ setImmediate(() => {
1093
+ void runAgentHarnessAfterToolCallHook(hookParams);
1094
+ });
1095
+ }
1096
+
1097
+ private shouldEmitAfterToolCallObservation(item: CodexThreadItem): boolean {
1098
+ if (
1099
+ !shouldSynthesizeToolProgressForItem(item) ||
1100
+ this.afterToolCallObservedItemIds.has(item.id)
1101
+ ) {
1102
+ return false;
1103
+ }
1104
+ if (this.options.nativePostToolUseRelayEnabled && isNativePostToolUseRelayItem(item)) {
1105
+ return false;
1106
+ }
1107
+ return true;
1108
+ }
1109
+
1110
+ private emitToolResultSummary(item: CodexThreadItem | undefined): void {
1111
+ if (!item || !this.params.onToolResult || !this.shouldEmitToolResult()) {
1112
+ return;
1113
+ }
1114
+ const itemId = item.id;
1115
+ if (this.toolResultSummaryItemIds.has(itemId)) {
1116
+ return;
1117
+ }
1118
+ const toolName = itemName(item);
1119
+ if (!toolName) {
1120
+ return;
1121
+ }
1122
+ if (!shouldEmitTranscriptToolProgress(toolName, itemToolArgs(item))) {
1123
+ return;
1124
+ }
1125
+ this.toolResultSummaryItemIds.add(itemId);
1126
+ const meta = itemMeta(item, this.toolProgressDetailMode());
1127
+ this.emitToolResultMessage({
1128
+ itemId,
1129
+ text: formatToolSummary(toolName, meta),
1130
+ });
1131
+ }
1132
+
1133
+ private emitToolResultOutput(item: CodexThreadItem | undefined): void {
1134
+ if (!item || !this.params.onToolResult || !this.shouldEmitToolOutput()) {
1135
+ return;
1136
+ }
1137
+ const itemId = item.id;
1138
+ if (this.toolResultOutputItemIds.has(itemId)) {
1139
+ return;
1140
+ }
1141
+ if (this.toolResultOutputStreamedItemIds.has(itemId)) {
1142
+ return;
1143
+ }
1144
+ const toolName = itemName(item);
1145
+ const output = itemOutputText(item, this.toolResultOutputTextByItem);
1146
+ if (!toolName || !output) {
1147
+ return;
1148
+ }
1149
+ if (!shouldEmitTranscriptToolProgress(toolName, itemToolArgs(item))) {
1150
+ return;
1151
+ }
1152
+ this.emitToolResultMessage({
1153
+ itemId,
1154
+ text: formatToolOutput(toolName, itemMeta(item, this.toolProgressDetailMode()), output),
1155
+ finalOutput: true,
1156
+ isError: isNonSuccessItemStatus(itemStatus(item)),
1157
+ });
1158
+ }
1159
+
1160
+ private emitToolResultMessage(params: {
1161
+ itemId: string;
1162
+ text: string;
1163
+ finalOutput?: boolean;
1164
+ isError?: boolean;
1165
+ }): void {
1166
+ const text = params.text.trim();
1167
+ if (!text) {
1168
+ return;
1169
+ }
1170
+ this.toolProgressTexts.add(text);
1171
+ if (params.finalOutput) {
1172
+ this.toolResultOutputItemIds.add(params.itemId);
1173
+ }
1174
+ try {
1175
+ void Promise.resolve(
1176
+ this.params.onToolResult?.({
1177
+ text,
1178
+ ...(params.isError === true ? { isError: true } : {}),
1179
+ }),
1180
+ ).catch(() => {
1181
+ // Tool progress delivery is best-effort and should not affect the turn.
1182
+ });
1183
+ } catch {
1184
+ // Tool progress delivery is best-effort and should not affect the turn.
1185
+ }
1186
+ }
1187
+
1188
+ private shouldEmitToolResult(): boolean {
1189
+ return typeof this.params.shouldEmitToolResult === "function"
1190
+ ? this.params.shouldEmitToolResult()
1191
+ : this.params.verboseLevel === "on" || this.params.verboseLevel === "full";
1192
+ }
1193
+
1194
+ private shouldEmitToolOutput(): boolean {
1195
+ return typeof this.params.shouldEmitToolOutput === "function"
1196
+ ? this.params.shouldEmitToolOutput()
1197
+ : this.params.verboseLevel === "full";
1198
+ }
1199
+
1200
+ private toolProgressDetailMode(): ToolProgressDetailMode {
1201
+ return resolveCodexToolProgressDetailMode(this.params.toolProgressDetail);
1202
+ }
1203
+
1204
+ private recordToolMeta(item: CodexThreadItem | undefined): void {
1205
+ if (!item) {
1206
+ return;
1207
+ }
1208
+ const toolName = itemName(item);
1209
+ if (!toolName) {
1210
+ return;
1211
+ }
1212
+ const meta = itemMeta(item, this.toolProgressDetailMode());
1213
+ this.toolMetas.set(item.id, {
1214
+ toolName,
1215
+ ...(meta ? { meta } : {}),
1216
+ });
1217
+ }
1218
+
1219
+ private recordNativeToolTranscriptCall(item: CodexThreadItem | undefined): void {
1220
+ if (!item || !shouldSynthesizeToolProgressForItem(item)) {
1221
+ return;
1222
+ }
1223
+ const name = itemName(item);
1224
+ if (!name) {
1225
+ return;
1226
+ }
1227
+ this.recordToolTranscriptCall({
1228
+ id: item.id,
1229
+ name,
1230
+ arguments: itemToolArgs(item),
1231
+ });
1232
+ }
1233
+
1234
+ private recordNativeToolTranscriptResult(item: CodexThreadItem | undefined): void {
1235
+ if (!item || !shouldSynthesizeToolProgressForItem(item)) {
1236
+ return;
1237
+ }
1238
+ const name = itemName(item);
1239
+ if (!name) {
1240
+ return;
1241
+ }
1242
+ this.recordToolTranscriptResult({
1243
+ id: item.id,
1244
+ name,
1245
+ text: itemTranscriptResultText(item, this.toolResultOutputTextByItem),
1246
+ isError: isNonSuccessItemStatus(itemStatus(item)),
1247
+ });
1248
+ }
1249
+
1250
+ private recordToolTranscriptCall(params: ToolTranscriptCallInput): void {
1251
+ if (!params.id || !params.name || this.toolTranscriptCallIds.has(params.id)) {
1252
+ return;
1253
+ }
1254
+ this.toolTranscriptCallIds.add(params.id);
1255
+ this.toolTranscriptArgumentsById.set(params.id, params.arguments);
1256
+ if (!shouldEmitTranscriptToolProgress(params.name, params.arguments)) {
1257
+ this.transcriptToolProgressSuppressedIds.add(params.id);
1258
+ } else {
1259
+ this.transcriptToolProgressSuppressedIds.delete(params.id);
1260
+ }
1261
+ this.emitTranscriptToolCallProgress(params);
1262
+ this.toolTranscriptMessages.push(
1263
+ attachCodexMirrorIdentity(
1264
+ this.createToolCallMessage(params),
1265
+ `${this.turnId}:tool:${params.id}:call`,
1266
+ ),
1267
+ );
1268
+ }
1269
+
1270
+ private recordToolTranscriptResult(params: ToolTranscriptResultInput): void {
1271
+ if (!params.id || !params.name || this.toolTranscriptResultIds.has(params.id)) {
1272
+ return;
1273
+ }
1274
+ this.toolTranscriptResultIds.add(params.id);
1275
+ this.emitTranscriptToolResultProgress(params);
1276
+ this.toolTranscriptMessages.push(
1277
+ attachCodexMirrorIdentity(
1278
+ this.createToolResultMessage(params),
1279
+ `${this.turnId}:tool:${params.id}:result`,
1280
+ ),
1281
+ );
1282
+ }
1283
+
1284
+ private emitTranscriptToolCallProgress(params: ToolTranscriptCallInput): void {
1285
+ if (!shouldEmitTranscriptToolProgress(params.name, params.arguments)) {
1286
+ return;
1287
+ }
1288
+ this.transcriptToolProgressCallIds.add(params.id);
1289
+ const args = normalizeToolTranscriptArguments(params.arguments);
1290
+ const meta = inferToolMetaFromArgs(params.name, args, {
1291
+ detailMode: this.toolProgressDetailMode(),
1292
+ });
1293
+ if (
1294
+ !this.params.onToolResult ||
1295
+ !this.shouldEmitToolResult() ||
1296
+ this.toolResultSummaryItemIds.has(params.id) ||
1297
+ this.toolResultOutputStreamedItemIds.has(params.id)
1298
+ ) {
1299
+ return;
1300
+ }
1301
+ this.toolResultSummaryItemIds.add(params.id);
1302
+ this.emitToolResultMessage({
1303
+ itemId: params.id,
1304
+ text: formatToolSummary(params.name, meta),
1305
+ });
1306
+ }
1307
+
1308
+ private emitTranscriptToolResultProgress(params: ToolTranscriptResultInput): void {
1309
+ if (
1310
+ this.transcriptToolProgressSuppressedIds.has(params.id) ||
1311
+ !shouldEmitTranscriptToolProgress(
1312
+ params.name,
1313
+ this.toolTranscriptArgumentsById.get(params.id),
1314
+ )
1315
+ ) {
1316
+ return;
1317
+ }
1318
+ if (!this.transcriptToolProgressCallIds.has(params.id)) {
1319
+ this.emitTranscriptToolCallProgress({
1320
+ id: params.id,
1321
+ name: params.name,
1322
+ arguments: {},
1323
+ });
1324
+ }
1325
+ if (
1326
+ !this.params.onToolResult ||
1327
+ !this.shouldEmitToolOutput() ||
1328
+ this.toolResultOutputItemIds.has(params.id) ||
1329
+ this.toolResultOutputStreamedItemIds.has(params.id)
1330
+ ) {
1331
+ return;
1332
+ }
1333
+ const text = params.text?.trim();
1334
+ if (!text) {
1335
+ return;
1336
+ }
1337
+ this.emitToolResultMessage({
1338
+ itemId: params.id,
1339
+ text: formatToolOutput(params.name, undefined, text),
1340
+ finalOutput: true,
1341
+ isError: params.isError,
1342
+ });
1343
+ }
1344
+
1345
+ private formatCodexErrorMessage(params: JsonObject): string | undefined {
1346
+ const error = isJsonObject(params.error) ? params.error : undefined;
1347
+ return (
1348
+ formatCodexUsageLimitErrorMessage({
1349
+ message: error ? readString(error, "message") : undefined,
1350
+ codexErrorInfo: error?.codexErrorInfo,
1351
+ rateLimits: this.latestRateLimits ?? readRecentCodexRateLimits(),
1352
+ }) ?? readCodexErrorNotificationMessage(params)
1353
+ );
1354
+ }
1355
+
1356
+ private emitAgentEvent(
1357
+ event: Parameters<NonNullable<EmbeddedRunAttemptParams["onAgentEvent"]>>[0],
1358
+ ): void {
1359
+ try {
1360
+ emitGlobalAgentEvent({
1361
+ runId: this.params.runId,
1362
+ stream: event.stream,
1363
+ data: event.data,
1364
+ ...(this.params.sessionKey ? { sessionKey: this.params.sessionKey } : {}),
1365
+ });
1366
+ } catch (error) {
1367
+ embeddedAgentLog.debug("codex app-server global agent event emit failed", { error });
1368
+ }
1369
+ try {
1370
+ const maybePromise = this.params.onAgentEvent?.(event);
1371
+ void Promise.resolve(maybePromise).catch((error: unknown) => {
1372
+ embeddedAgentLog.debug("codex app-server agent event handler rejected", { error });
1373
+ });
1374
+ } catch (error) {
1375
+ // Downstream event consumers must not corrupt the canonical Codex turn projection.
1376
+ embeddedAgentLog.debug("codex app-server agent event handler threw", { error });
1377
+ }
1378
+ }
1379
+
1380
+ private collectAssistantTexts(): string[] {
1381
+ const finalText = this.resolveFinalAssistantText();
1382
+ return finalText ? [finalText] : [];
1383
+ }
1384
+
1385
+ private resolveFinalAssistantText(): string | undefined {
1386
+ for (let i = this.assistantItemOrder.length - 1; i >= 0; i -= 1) {
1387
+ const itemId = this.assistantItemOrder[i];
1388
+ if (!itemId) {
1389
+ continue;
1390
+ }
1391
+ const text = this.assistantTextByItem.get(itemId)?.trim();
1392
+ if (this.assistantPhaseByItem.get(itemId) === "commentary") {
1393
+ continue;
1394
+ }
1395
+ if (text && !this.toolProgressTexts.has(text)) {
1396
+ return text;
1397
+ }
1398
+ }
1399
+ return undefined;
1400
+ }
1401
+
1402
+ private rememberAssistantItem(itemId: string): void {
1403
+ if (!itemId || this.assistantItemOrder.includes(itemId)) {
1404
+ return;
1405
+ }
1406
+ this.assistantItemOrder.push(itemId);
1407
+ }
1408
+
1409
+ private async readMirroredSessionMessages(): Promise<AgentMessage[]> {
1410
+ return (await readCodexMirroredSessionHistoryMessages(this.params.sessionFile)) ?? [];
1411
+ }
1412
+
1413
+ private createAssistantMessage(text: string): AssistantMessage {
1414
+ const attribution = resolveCodexLocalRuntimeAttribution(this.params);
1415
+ const usage: Usage = this.tokenUsage
1416
+ ? {
1417
+ input: this.tokenUsage.input ?? 0,
1418
+ output: this.tokenUsage.output ?? 0,
1419
+ cacheRead: this.tokenUsage.cacheRead ?? 0,
1420
+ cacheWrite: this.tokenUsage.cacheWrite ?? 0,
1421
+ totalTokens:
1422
+ this.tokenUsage.total ??
1423
+ (this.tokenUsage.input ?? 0) +
1424
+ (this.tokenUsage.output ?? 0) +
1425
+ (this.tokenUsage.cacheRead ?? 0) +
1426
+ (this.tokenUsage.cacheWrite ?? 0),
1427
+ cost: ZERO_USAGE.cost,
1428
+ }
1429
+ : ZERO_USAGE;
1430
+ return {
1431
+ role: "assistant",
1432
+ content: [{ type: "text", text }],
1433
+ api: attribution.api ?? "openai-codex-responses",
1434
+ provider: attribution.provider,
1435
+ model: this.params.modelId,
1436
+ usage,
1437
+ stopReason: this.aborted ? "aborted" : this.promptError ? "error" : "stop",
1438
+ errorMessage: this.promptError ? formatErrorMessage(this.promptError) : undefined,
1439
+ timestamp: Date.now(),
1440
+ };
1441
+ }
1442
+
1443
+ private createAssistantMirrorMessage(title: string, text: string): AssistantMessage {
1444
+ const attribution = resolveCodexLocalRuntimeAttribution(this.params);
1445
+ return {
1446
+ role: "assistant",
1447
+ content: [{ type: "text", text: `${title}:\n${text}` }],
1448
+ api: attribution.api ?? "openai-codex-responses",
1449
+ provider: attribution.provider,
1450
+ model: this.params.modelId,
1451
+ usage: ZERO_USAGE,
1452
+ stopReason: "stop",
1453
+ timestamp: Date.now(),
1454
+ };
1455
+ }
1456
+
1457
+ private createToolCallMessage(params: ToolTranscriptCallInput): AgentMessage {
1458
+ const args = normalizeToolTranscriptArguments(params.arguments);
1459
+ const attribution = resolveCodexLocalRuntimeAttribution(this.params);
1460
+ return {
1461
+ role: "assistant",
1462
+ content: [
1463
+ {
1464
+ type: "toolCall",
1465
+ id: params.id,
1466
+ name: params.name,
1467
+ arguments: args,
1468
+ input: args,
1469
+ },
1470
+ ],
1471
+ api: attribution.api ?? "openai-codex-responses",
1472
+ provider: attribution.provider,
1473
+ model: this.params.modelId,
1474
+ usage: ZERO_USAGE,
1475
+ stopReason: "toolUse",
1476
+ timestamp: Date.now(),
1477
+ } as unknown as AgentMessage;
1478
+ }
1479
+
1480
+ private createToolResultMessage(params: ToolTranscriptResultInput): AgentMessage {
1481
+ const text = truncateToolTranscriptText(params.text?.trim() || toolResultStatusText(params));
1482
+ return {
1483
+ role: "toolResult",
1484
+ toolCallId: params.id,
1485
+ toolName: params.name,
1486
+ isError: params.isError,
1487
+ content: [
1488
+ {
1489
+ type: "toolResult",
1490
+ id: params.id,
1491
+ name: params.name,
1492
+ toolName: params.name,
1493
+ toolCallId: params.id,
1494
+ toolUseId: params.id,
1495
+ tool_use_id: params.id,
1496
+ content: text,
1497
+ text,
1498
+ },
1499
+ ],
1500
+ timestamp: Date.now(),
1501
+ } as unknown as AgentMessage;
1502
+ }
1503
+
1504
+ private isNotificationForTurn(params: JsonObject): boolean {
1505
+ const threadId = readString(params, "threadId");
1506
+ const turnId = readNotificationTurnId(params);
1507
+ return threadId === this.threadId && turnId === this.turnId;
1508
+ }
1509
+
1510
+ private isHookNotificationForCurrentThread(params: JsonObject): boolean {
1511
+ const threadId = readString(params, "threadId");
1512
+ const turnId = params.turnId;
1513
+ return threadId === this.threadId && (turnId === this.turnId || turnId === null);
1514
+ }
1515
+ }
1516
+
1517
+ function isHookNotificationMethod(method: string): method is "hook/started" | "hook/completed" {
1518
+ return method === "hook/started" || method === "hook/completed";
1519
+ }
1520
+
1521
+ function readNotificationTurnId(record: JsonObject): string | undefined {
1522
+ return readString(record, "turnId") ?? readNestedTurnId(record);
1523
+ }
1524
+
1525
+ function readNestedTurnId(record: JsonObject): string | undefined {
1526
+ const turn = record.turn;
1527
+ return isJsonObject(turn) ? readString(turn, "id") : undefined;
1528
+ }
1529
+
1530
+ function readString(record: JsonObject, key: string): string | undefined {
1531
+ const value = record[key];
1532
+ return typeof value === "string" ? value : undefined;
1533
+ }
1534
+
1535
+ function readNullableString(record: JsonObject, key: string): string | null | undefined {
1536
+ const value = record[key];
1537
+ if (value === null) {
1538
+ return null;
1539
+ }
1540
+ return typeof value === "string" ? value : undefined;
1541
+ }
1542
+
1543
+ function readNumber(record: JsonObject, key: string): number | undefined {
1544
+ const value = record[key];
1545
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1546
+ }
1547
+
1548
+ function readBoolean(record: JsonObject, key: string): boolean | undefined {
1549
+ const value = record[key];
1550
+ return typeof value === "boolean" ? value : undefined;
1551
+ }
1552
+
1553
+ function readBooleanAlias(record: JsonObject, keys: readonly string[]): boolean | undefined {
1554
+ for (const key of keys) {
1555
+ const value = readBoolean(record, key);
1556
+ if (value !== undefined) {
1557
+ return value;
1558
+ }
1559
+ }
1560
+ return undefined;
1561
+ }
1562
+
1563
+ function readCodexErrorNotificationMessage(record: JsonObject): string | undefined {
1564
+ const error = record.error;
1565
+ if (isJsonObject(error)) {
1566
+ return readString(error, "message") ?? readString(error, "error");
1567
+ }
1568
+ return readString(record, "message");
1569
+ }
1570
+
1571
+ function readHookOutputEntries(
1572
+ value: JsonValue | undefined,
1573
+ ): Array<{ kind?: string; text: string }> {
1574
+ if (!Array.isArray(value)) {
1575
+ return [];
1576
+ }
1577
+ return value.flatMap((entry) => {
1578
+ if (!isJsonObject(entry)) {
1579
+ return [];
1580
+ }
1581
+ const text = readString(entry, "text");
1582
+ if (!text) {
1583
+ return [];
1584
+ }
1585
+ const kind = readString(entry, "kind");
1586
+ return [{ ...(kind ? { kind } : {}), text }];
1587
+ });
1588
+ }
1589
+
1590
+ function readFirstJsonObject(record: JsonObject, keys: readonly string[]): JsonObject | undefined {
1591
+ for (const key of keys) {
1592
+ const value = record[key];
1593
+ if (isJsonObject(value)) {
1594
+ return value;
1595
+ }
1596
+ }
1597
+ return undefined;
1598
+ }
1599
+
1600
+ function readNumberAlias(record: JsonObject, keys: readonly string[]): number | undefined {
1601
+ for (const key of keys) {
1602
+ const value = readNumber(record, key);
1603
+ if (value !== undefined) {
1604
+ return value;
1605
+ }
1606
+ }
1607
+ return undefined;
1608
+ }
1609
+
1610
+ function normalizeCodexTokenUsage(record: JsonObject): ReturnType<typeof normalizeUsage> {
1611
+ const promptTotalInput = readNumberAlias(record, CODEX_PROMPT_TOTAL_INPUT_KEYS);
1612
+ const cacheRead = readNumberAlias(record, [
1613
+ "cachedInputTokens",
1614
+ "cached_input_tokens",
1615
+ "cacheRead",
1616
+ "cache_read",
1617
+ "cache_read_input_tokens",
1618
+ "cached_tokens",
1619
+ ]);
1620
+ const input =
1621
+ promptTotalInput !== undefined && cacheRead !== undefined
1622
+ ? Math.max(0, promptTotalInput - cacheRead)
1623
+ : (promptTotalInput ?? readNumber(record, "input"));
1624
+
1625
+ return normalizeUsage({
1626
+ input,
1627
+ output: readNumberAlias(record, ["outputTokens", "output_tokens", "output"]),
1628
+ cacheRead,
1629
+ cacheWrite: readNumberAlias(record, [
1630
+ "cacheWrite",
1631
+ "cache_write",
1632
+ "cacheCreationInputTokens",
1633
+ "cache_creation_input_tokens",
1634
+ ]),
1635
+ total: readNumberAlias(record, ["totalTokens", "total_tokens", "total"]),
1636
+ });
1637
+ }
1638
+
1639
+ function splitPlanText(text: string): string[] {
1640
+ return text
1641
+ .split(/\r?\n/)
1642
+ .map((line) => line.trim().replace(/^[-*]\s+/, ""))
1643
+ .filter((line) => line.length > 0);
1644
+ }
1645
+
1646
+ function collectTextValues(map: Map<string, string>): string[] {
1647
+ return [...map.values()].filter((text) => text.trim().length > 0);
1648
+ }
1649
+
1650
+ function extractRawAssistantText(item: JsonObject): string | undefined {
1651
+ const content = Array.isArray(item.content) ? item.content : [];
1652
+ const text = content
1653
+ .flatMap((entry) => {
1654
+ if (!isJsonObject(entry)) {
1655
+ return [];
1656
+ }
1657
+ const type = readString(entry, "type");
1658
+ if (type !== "output_text" && type !== "text") {
1659
+ return [];
1660
+ }
1661
+ const value = readString(entry, "text");
1662
+ return value ? [value] : [];
1663
+ })
1664
+ .join("");
1665
+ return text.trim() || undefined;
1666
+ }
1667
+
1668
+ function itemKind(
1669
+ item: CodexThreadItem,
1670
+ ): "tool" | "command" | "patch" | "search" | "analysis" | undefined {
1671
+ switch (item.type) {
1672
+ case "dynamicToolCall":
1673
+ case "mcpToolCall":
1674
+ return "tool";
1675
+ case "commandExecution":
1676
+ return "command";
1677
+ case "fileChange":
1678
+ return "patch";
1679
+ case "webSearch":
1680
+ return "search";
1681
+ case "reasoning":
1682
+ case "contextCompaction":
1683
+ return "analysis";
1684
+ default:
1685
+ return undefined;
1686
+ }
1687
+ }
1688
+
1689
+ function itemTitle(item: CodexThreadItem): string {
1690
+ switch (item.type) {
1691
+ case "commandExecution":
1692
+ return "Command";
1693
+ case "fileChange":
1694
+ return "File change";
1695
+ case "mcpToolCall":
1696
+ return "MCP tool";
1697
+ case "dynamicToolCall":
1698
+ return "Tool";
1699
+ case "webSearch":
1700
+ return "Web search";
1701
+ case "contextCompaction":
1702
+ return "Context compaction";
1703
+ case "reasoning":
1704
+ return "Reasoning";
1705
+ default:
1706
+ return item.type;
1707
+ }
1708
+ }
1709
+
1710
+ function itemStatus(item: CodexThreadItem): "completed" | "failed" | "running" | "blocked" {
1711
+ const status = readItemString(item, "status");
1712
+ if (status === "failed") {
1713
+ return "failed";
1714
+ }
1715
+ if (status === "declined") {
1716
+ return "blocked";
1717
+ }
1718
+ if (status === "inProgress" || status === "running") {
1719
+ return "running";
1720
+ }
1721
+ return "completed";
1722
+ }
1723
+
1724
+ function isNonSuccessItemStatus(status: ReturnType<typeof itemStatus>): boolean {
1725
+ return status === "failed" || status === "blocked";
1726
+ }
1727
+
1728
+ function itemName(item: CodexThreadItem): string | undefined {
1729
+ if (item.type === "dynamicToolCall" && typeof item.tool === "string") {
1730
+ return item.tool;
1731
+ }
1732
+ if (item.type === "mcpToolCall" && typeof item.tool === "string") {
1733
+ const server = typeof item.server === "string" ? item.server : undefined;
1734
+ return server ? `${server}.${item.tool}` : item.tool;
1735
+ }
1736
+ if (item.type === "commandExecution") {
1737
+ return "bash";
1738
+ }
1739
+ if (item.type === "fileChange") {
1740
+ return "apply_patch";
1741
+ }
1742
+ if (item.type === "webSearch") {
1743
+ return "web_search";
1744
+ }
1745
+ return undefined;
1746
+ }
1747
+
1748
+ function shouldSynthesizeToolProgressForItem(item: CodexThreadItem): boolean {
1749
+ switch (item.type) {
1750
+ case "commandExecution":
1751
+ case "fileChange":
1752
+ case "webSearch":
1753
+ case "mcpToolCall":
1754
+ return true;
1755
+ default:
1756
+ return false;
1757
+ }
1758
+ }
1759
+
1760
+ function isMutatingNativeToolItem(item: CodexThreadItem): boolean {
1761
+ return item.type === "commandExecution" || item.type === "fileChange";
1762
+ }
1763
+
1764
+ function nativeToolActionFingerprint(item: CodexThreadItem): string | undefined {
1765
+ if (item.type === "commandExecution" && typeof item.command === "string") {
1766
+ return JSON.stringify({
1767
+ type: item.type,
1768
+ command: item.command,
1769
+ cwd: typeof item.cwd === "string" ? item.cwd : "",
1770
+ });
1771
+ }
1772
+ if (item.type === "fileChange") {
1773
+ return JSON.stringify({
1774
+ type: item.type,
1775
+ changes: itemFileChanges(item),
1776
+ });
1777
+ }
1778
+ return undefined;
1779
+ }
1780
+
1781
+ function isNativePostToolUseRelayItem(item: CodexThreadItem): boolean {
1782
+ switch (item.type) {
1783
+ case "commandExecution":
1784
+ case "fileChange":
1785
+ case "mcpToolCall":
1786
+ return true;
1787
+ default:
1788
+ return false;
1789
+ }
1790
+ }
1791
+
1792
+ function shouldSuppressChannelProgressForItem(item: CodexThreadItem): boolean {
1793
+ if (shouldSynthesizeToolProgressForItem(item)) {
1794
+ return true;
1795
+ }
1796
+ // Dynamic AutoBot tool requests are emitted at the item/tool/call request
1797
+ // boundary in run-attempt.ts. Re-emitting item notifications to channels can
1798
+ // duplicate start/result progress when the app-server sends both signals.
1799
+ return item.type === "dynamicToolCall";
1800
+ }
1801
+
1802
+ function itemToolArgs(item: CodexThreadItem): Record<string, unknown> | undefined {
1803
+ if (item.type === "commandExecution") {
1804
+ return sanitizeCodexAgentEventRecord({
1805
+ command: item.command,
1806
+ ...(typeof item.cwd === "string" ? { cwd: item.cwd } : {}),
1807
+ });
1808
+ }
1809
+ if (item.type === "fileChange") {
1810
+ return sanitizeCodexAgentEventRecord({
1811
+ changes: itemFileChanges(item),
1812
+ });
1813
+ }
1814
+ if (item.type === "webSearch" && typeof item.query === "string") {
1815
+ return sanitizeCodexAgentEventRecord({ query: item.query });
1816
+ }
1817
+ if (item.type === "mcpToolCall") {
1818
+ return sanitizeCodexToolArguments(item.arguments);
1819
+ }
1820
+ return undefined;
1821
+ }
1822
+
1823
+ function itemToolResult(item: CodexThreadItem): { result?: Record<string, unknown> } {
1824
+ if (item.type === "commandExecution") {
1825
+ return {
1826
+ result: sanitizeCodexAgentEventRecord({
1827
+ status: item.status,
1828
+ exitCode: item.exitCode,
1829
+ durationMs: item.durationMs,
1830
+ }),
1831
+ };
1832
+ }
1833
+ if (item.type === "fileChange") {
1834
+ return {
1835
+ result: sanitizeCodexAgentEventRecord({
1836
+ status: item.status,
1837
+ changes: itemFileChanges(item),
1838
+ }),
1839
+ };
1840
+ }
1841
+ if (item.type === "mcpToolCall") {
1842
+ return {
1843
+ result: sanitizeCodexAgentEventRecord({
1844
+ status: item.status,
1845
+ durationMs: item.durationMs,
1846
+ ...(item.error ? { error: item.error } : {}),
1847
+ ...(item.result ? { result: item.result } : {}),
1848
+ }),
1849
+ };
1850
+ }
1851
+ if (item.type === "webSearch") {
1852
+ return { result: sanitizeCodexAgentEventRecord({ status: "completed" }) };
1853
+ }
1854
+ return {};
1855
+ }
1856
+
1857
+ function itemFileChanges(item: CodexThreadItem): Array<{ path: string; kind: string }> {
1858
+ return Array.isArray(item.changes)
1859
+ ? item.changes.map((change) => ({ path: change.path, kind: change.kind }))
1860
+ : [];
1861
+ }
1862
+
1863
+ function itemToolError(
1864
+ item: CodexThreadItem,
1865
+ status: ReturnType<typeof itemStatus>,
1866
+ outputTextByItem?: ReadonlyMap<string, string>,
1867
+ ): string | undefined {
1868
+ if (status === "blocked") {
1869
+ return "codex native tool blocked";
1870
+ }
1871
+ if (status !== "failed") {
1872
+ return undefined;
1873
+ }
1874
+ return itemOutputText(item, outputTextByItem) ?? "codex native tool failed";
1875
+ }
1876
+
1877
+ function itemMeta(
1878
+ item: CodexThreadItem,
1879
+ detailMode: ToolProgressDetailMode = "explain",
1880
+ ): string | undefined {
1881
+ if (item.type === "commandExecution" && typeof item.command === "string") {
1882
+ return inferToolMetaFromArgs(
1883
+ "exec",
1884
+ {
1885
+ command: item.command,
1886
+ cwd: typeof item.cwd === "string" ? item.cwd : undefined,
1887
+ },
1888
+ { detailMode },
1889
+ );
1890
+ }
1891
+ if (item.type === "webSearch" && typeof item.query === "string") {
1892
+ return item.query;
1893
+ }
1894
+ const toolName = itemName(item);
1895
+ if ((item.type === "dynamicToolCall" || item.type === "mcpToolCall") && toolName) {
1896
+ return inferToolMetaFromArgs(toolName, item.arguments, { detailMode });
1897
+ }
1898
+ return undefined;
1899
+ }
1900
+
1901
+ function itemOutputText(
1902
+ item: CodexThreadItem,
1903
+ outputTextByItem?: ReadonlyMap<string, string>,
1904
+ ): string | undefined {
1905
+ if (item.type === "commandExecution") {
1906
+ return item.aggregatedOutput?.trim() || outputTextByItem?.get(item.id)?.trim() || undefined;
1907
+ }
1908
+ if (item.type === "dynamicToolCall") {
1909
+ return collectDynamicToolContentText(item.contentItems).trim() || undefined;
1910
+ }
1911
+ if (item.type === "mcpToolCall") {
1912
+ if (item.error) {
1913
+ return stringifyJsonValue(item.error);
1914
+ }
1915
+ return item.result ? stringifyJsonValue(item.result) : undefined;
1916
+ }
1917
+ return undefined;
1918
+ }
1919
+
1920
+ function itemTranscriptResultText(
1921
+ item: CodexThreadItem,
1922
+ outputTextByItem?: ReadonlyMap<string, string>,
1923
+ ): string | undefined {
1924
+ const output = itemOutputText(item, outputTextByItem);
1925
+ if (output) {
1926
+ return output;
1927
+ }
1928
+ const result = itemToolResult(item).result;
1929
+ return result ? stringifyJsonValue(result) : itemStatus(item);
1930
+ }
1931
+
1932
+ function appendToolOutputDeltaText(
1933
+ outputTextByItem: Map<string, string>,
1934
+ itemId: string,
1935
+ delta: string,
1936
+ ): void {
1937
+ const current = outputTextByItem.get(itemId) ?? "";
1938
+ if (current.length >= TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS) {
1939
+ return;
1940
+ }
1941
+ const remaining = TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS - current.length;
1942
+ const next = current + (delta.length > remaining ? delta.slice(0, remaining) : delta);
1943
+ outputTextByItem.set(itemId, next);
1944
+ }
1945
+
1946
+ function normalizeToolTranscriptArguments(value: unknown): Record<string, unknown> {
1947
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1948
+ return {};
1949
+ }
1950
+ return value as Record<string, unknown>;
1951
+ }
1952
+
1953
+ function isActivityLogCommandProgress(toolName: string, args: unknown): boolean {
1954
+ if (toolName !== "bash" && toolName !== "exec" && toolName !== "shell") {
1955
+ return false;
1956
+ }
1957
+ const command = readToolCommandText(args);
1958
+ return Boolean(command && command.includes("log_activity.sh"));
1959
+ }
1960
+
1961
+ function readToolCommandText(value: unknown): string | undefined {
1962
+ if (typeof value === "string") {
1963
+ return value;
1964
+ }
1965
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1966
+ return undefined;
1967
+ }
1968
+ const record = value as Record<string, unknown>;
1969
+ for (const key of ["command", "cmd", "shellCommand", "script"]) {
1970
+ const text = record[key];
1971
+ if (typeof text === "string" && text) {
1972
+ return text;
1973
+ }
1974
+ }
1975
+ return undefined;
1976
+ }
1977
+
1978
+ function collectDynamicToolContentText(contentItems: CodexThreadItem["contentItems"]): string {
1979
+ if (!Array.isArray(contentItems)) {
1980
+ return "";
1981
+ }
1982
+ return contentItems
1983
+ .flatMap((entry) => {
1984
+ if (!isJsonObject(entry)) {
1985
+ return [];
1986
+ }
1987
+ const text = readString(entry, "text");
1988
+ return text ? [text] : [];
1989
+ })
1990
+ .join("\n");
1991
+ }
1992
+
1993
+ function truncateToolTranscriptText(text: string): string {
1994
+ if (text.length <= TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS) {
1995
+ return text;
1996
+ }
1997
+ return `${text.slice(0, TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS)}\n...(truncated)...`;
1998
+ }
1999
+
2000
+ function toolResultStatusText(params: ToolTranscriptResultInput): string {
2001
+ return params.isError ? `${params.name} failed` : `${params.name} completed`;
2002
+ }
2003
+
2004
+ function stringifyJsonValue(value: unknown): string | undefined {
2005
+ try {
2006
+ return JSON.stringify(value, null, 2);
2007
+ } catch {
2008
+ return undefined;
2009
+ }
2010
+ }
2011
+
2012
+ function formatToolSummary(toolName: string, meta?: string): string {
2013
+ const trimmedMeta = meta?.trim();
2014
+ return formatToolAggregate(toolName, trimmedMeta ? [trimmedMeta] : undefined, {
2015
+ markdown: true,
2016
+ });
2017
+ }
2018
+
2019
+ function formatToolOutput(toolName: string, meta: string | undefined, output: string): string {
2020
+ const formattedOutput = formatToolProgressOutput(output);
2021
+ if (!formattedOutput) {
2022
+ return formatToolSummary(toolName, meta);
2023
+ }
2024
+ const fence = markdownFenceForText(formattedOutput);
2025
+ return `${formatToolSummary(toolName, meta)}\n${fence}txt\n${formattedOutput}\n${fence}`;
2026
+ }
2027
+
2028
+ function markdownFenceForText(text: string): string {
2029
+ return "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
2030
+ }
2031
+
2032
+ function longestBacktickRun(value: string): number {
2033
+ let longest = 0;
2034
+ let current = 0;
2035
+ for (const char of value) {
2036
+ if (char === "`") {
2037
+ current += 1;
2038
+ longest = Math.max(longest, current);
2039
+ continue;
2040
+ }
2041
+ current = 0;
2042
+ }
2043
+ return longest;
2044
+ }
2045
+
2046
+ function readItemString(item: CodexThreadItem, key: string): string | undefined {
2047
+ const value = (item as Record<string, unknown>)[key];
2048
+ return typeof value === "string" ? value : undefined;
2049
+ }
2050
+
2051
+ function readItem(value: JsonValue | undefined): CodexThreadItem | undefined {
2052
+ if (!isJsonObject(value)) {
2053
+ return undefined;
2054
+ }
2055
+ const type = typeof value.type === "string" ? value.type : undefined;
2056
+ const id = typeof value.id === "string" ? value.id : undefined;
2057
+ if (!type || !id) {
2058
+ return undefined;
2059
+ }
2060
+ return value as CodexThreadItem;
2061
+ }
2062
+
2063
+ function readTurn(value: JsonValue | undefined): CodexTurn | undefined {
2064
+ return readCodexTurn(value);
2065
+ }