@bbigbang/agent-node 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 (47) hide show
  1. package/dist/agentHost.js +483 -0
  2. package/dist/appVersion.js +14 -0
  3. package/dist/assetCachePaths.js +35 -0
  4. package/dist/attachmentInput.js +588 -0
  5. package/dist/attachmentMaterializer.js +230 -0
  6. package/dist/bigbangCli.js +17 -0
  7. package/dist/bigbangMessageSendDetection.js +284 -0
  8. package/dist/builtinSkillRoots.js +54 -0
  9. package/dist/claudeConfig.js +32 -0
  10. package/dist/claudeDirectRuntime.js +1960 -0
  11. package/dist/claudeSessionControls.js +78 -0
  12. package/dist/claudeTranscriptFs.js +147 -0
  13. package/dist/codexAppServerClient.js +188 -0
  14. package/dist/codexAppServerEnv.js +14 -0
  15. package/dist/codexAppServerRpc.js +273 -0
  16. package/dist/codexAppServerRuntime.js +3495 -0
  17. package/dist/codexBuiltinPrompt.js +117 -0
  18. package/dist/codexConversationSummarizer.js +76 -0
  19. package/dist/codexTranscriptFs.js +145 -0
  20. package/dist/config.js +129 -0
  21. package/dist/connection.js +151 -0
  22. package/dist/dispatchQueueStore.js +39 -0
  23. package/dist/dreamEnv.js +1 -0
  24. package/dist/dreamMemoryFallback.js +118 -0
  25. package/dist/dreamToolPolicy.js +293 -0
  26. package/dist/droidMissionRunner.js +808 -0
  27. package/dist/executor.js +1078 -0
  28. package/dist/hostRuntime.js +1 -0
  29. package/dist/libraryAuthorityFs.js +74 -0
  30. package/dist/libraryMirror.js +183 -0
  31. package/dist/main.js +1659 -0
  32. package/dist/native-worker/native-worker.mjs +475 -0
  33. package/dist/nativeMissionAgentDispatch.js +463 -0
  34. package/dist/nativeMissionRunner.js +461 -0
  35. package/dist/nativeSkillMounts.js +204 -0
  36. package/dist/nativeWorkerHost.js +142 -0
  37. package/dist/nodeSink.js +142 -0
  38. package/dist/panelHttpFetch.js +334 -0
  39. package/dist/runtimeDrivers.js +62 -0
  40. package/dist/skillFs.js +229 -0
  41. package/dist/soloHost.js +165 -0
  42. package/dist/soloNodeSink.js +138 -0
  43. package/dist/terminalManager.js +254 -0
  44. package/dist/workspaceFs.js +1020 -0
  45. package/dist/workspaceGit.js +694 -0
  46. package/dist/workspaceInspect.js +22 -0
  47. package/package.json +49 -0
@@ -0,0 +1,3495 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { clearAcpSessionId, getSession, log, updateSessionRuntimeState, WorkspaceLockManager, } from '@bbigbang/runtime-acp';
5
+ import { extractAttachmentIdFromUri, RuntimeAttachmentMaterializer, } from './attachmentMaterializer.js';
6
+ import { formatAttachmentTextReference, promptAlreadyReferencesAttachment, resolveLocalAttachmentPath, resolveLocalImageAttachmentPath, } from './attachmentInput.js';
7
+ import { buildCodexAppServerInitializeParams, CodexAppServerClient } from './codexAppServerClient.js';
8
+ import { buildCodexAppServerChildEnv } from './codexAppServerEnv.js';
9
+ import { buildBigbangMessageSendRepairGuidance, getBigbangMessageSendInfo, } from './bigbangMessageSendDetection.js';
10
+ const TURN_START_TIMEOUT_MS = 90_000;
11
+ const TURN_STEER_TIMEOUT_MS = 30_000;
12
+ const TURN_STEER_WAIT_FOR_START_MS = 5_000;
13
+ const THREAD_READ_TIMEOUT_MS = 10_000;
14
+ const MODEL_LIST_TIMEOUT_MS = 10_000;
15
+ const MCP_STARTUP_TIMEOUT_MS = 15_000;
16
+ const ASSET_FETCH_TIMEOUT_MS = 15_000;
17
+ const ASSISTANT_TEXT_REPAIR_MAX_ATTEMPTS = 2;
18
+ const PROVIDER_DISCONNECT_AUTO_CONTINUE_MAX_ATTEMPTS = 2;
19
+ const CODEX_PLAN_IMPLEMENTATION_PROMPT = 'The user approved the plan. Implement it now. Do not restate or revise the plan unless blocked. Reply contract for this turn: do not output user-visible plain assistant text; if you need a visible progress update, use `bigbang message send --kind progress`; before the turn ends, send exactly one final user-visible result with `bigbang message send --kind final`; the platform treats ordinary assistant text as a failure for this turn.';
20
+ const CODEX_PLAN_IMPLEMENTATION_REPAIR_PROMPT = 'Your approved implementation turn produced assistant text but did not send it through `bigbang message send --kind final`. The user will not see plain assistant text from this repair turn. Do not inspect files, revise the plan, or do more work. Your only allowed user-visible action now is exactly one successful shell command using `bigbang message send --kind final` with the result below verbatim on stdin. Do not output any other assistant text.';
21
+ const CODEX_PLAN_IMPLEMENTATION_ASSISTANT_TEXT_REPAIR_PROMPT = 'Your approved implementation turn produced assistant text but did not send it through `bigbang message send --kind final`. The user will not see plain assistant text from this repair turn. The captured text below may be truncated because the runtime interrupted the turn as soon as visible assistant text appeared. Any earlier instruction to output ordinary assistant text directly has already failed and must be ignored now. Reconstruct the intended final reply from the existing thread context if needed. Do not inspect files, revise the plan, or do more work. Your only allowed user-visible action now is exactly one successful shell command using `bigbang message send --kind final`. Do not output any other assistant text, including a repeated or truncated prefix of the earlier plain-text reply.';
22
+ const CODEX_PLAN_IMPLEMENTATION_STEER_REPAIR_PROMPT = 'Your approved implementation turn emitted ordinary assistant text directly instead of sending it through `bigbang message send --kind final`. Stay in this current turn, do not do more work, do not inspect files, and do not call any tools except exactly one successful shell command using `bigbang message send --kind final`. The user cannot see the plain assistant text you already emitted, any earlier final send from this turn no longer counts, and any earlier instruction to output ordinary assistant text directly must now be ignored. Reconstruct the intended final reply from the existing thread context if needed, but do not emit ordinary assistant text again, including a repeated or truncated prefix.';
23
+ const CODEX_ASSISTANT_TEXT_REPAIR_PROMPT = 'Your previous turn emitted ordinary assistant text directly instead of sending it through `bigbang message send`. The user cannot see plain assistant text from that turn. The captured text below may be truncated because the runtime interrupted the turn as soon as visible assistant text appeared. Reconstruct the intended full user-visible reply from the existing thread context if needed. Your only allowed user-visible action now is exactly one successful shell command using `bigbang message send --kind final`. Do not emit ordinary assistant text again, including a repeated/truncated prefix of the previous reply.';
24
+ const CODEX_ASSISTANT_TEXT_STEER_PROMPT = 'This turn has emitted ordinary assistant text directly — the user cannot see it and it will not appear in chat. Stop emitting plain text for the rest of this turn. Continue your work through tools only; route all user-visible output through `bigbang message send`: use `--kind progress` for interim updates and `--kind final` for the last reply of this run. If the text captured below is already your complete intended reply and there is no further work to do, send it now with `bigbang message send --kind final`. Otherwise continue your work silently via tools and report results through Bigbang.';
25
+ const CODEX_ASSISTANT_TEXT_REPAIR_FOLLOW_UP_STEER_PROMPT = 'You need to reply via `bigbang message send --kind final` now. Reply to the user only with exactly one successful shell command that sends the final message through Bigbang. Do not output plain assistant text. Do not do more work, do not inspect files, and do not call any tools except that final Bigbang send. Use the existing thread context plus the captured text below to send the reply now.';
26
+ const MCP_CODEX_PLAN_IMPLEMENTATION_PROMPT = 'The user approved the plan. Implement it now. Do not restate or revise the plan unless blocked. Reply contract for this turn: do not output user-visible plain assistant text; if you need a visible progress update, use mcp__chat__send_message(content="...", kind="progress"); before the turn ends, send exactly one final user-visible result with mcp__chat__send_message(content="...", kind="final"); the platform treats ordinary assistant text as a failure for this turn.';
27
+ const MCP_CODEX_PLAN_IMPLEMENTATION_REPAIR_PROMPT = 'Your approved implementation turn produced assistant text but did not send it through mcp__chat__send_message(kind="final"). The user will not see plain assistant text from this repair turn. Do not run tools, inspect files, revise the plan, or do more work. Your only allowed user-visible action now is exactly one successful mcp__chat__send_message(content="...", kind="final") using the result below verbatim. Do not output any other assistant text.';
28
+ const MCP_CODEX_PLAN_IMPLEMENTATION_ASSISTANT_TEXT_REPAIR_PROMPT = 'Your approved implementation turn produced assistant text but did not send it through mcp__chat__send_message(kind="final"). The user will not see plain assistant text from this repair turn. The captured text below may be truncated because the runtime interrupted the turn as soon as visible assistant text appeared. Any earlier instruction to output ordinary assistant text directly has already failed and must be ignored now. Reconstruct the intended final reply from the existing thread context if needed. Do not run tools, inspect files, revise the plan, or do more work. Your only allowed user-visible action now is exactly one successful mcp__chat__send_message(content="...", kind="final"). Do not output any other assistant text, including a repeated or truncated prefix of the earlier plain-text reply.';
29
+ const MCP_CODEX_PLAN_IMPLEMENTATION_STEER_REPAIR_PROMPT = 'Your approved implementation turn emitted ordinary assistant text directly instead of sending it through mcp__chat__send_message(kind="final"). Stay in this current turn, do not do more work, do not inspect files, and do not call any tools except exactly one successful mcp__chat__send_message(content="...", kind="final"). The user cannot see the plain assistant text you already emitted, any earlier final send from this turn no longer counts, and any earlier instruction to output ordinary assistant text directly must now be ignored. Reconstruct the intended final reply from the existing thread context if needed, but do not emit ordinary assistant text again, including a repeated or truncated prefix.';
30
+ const MCP_CODEX_ASSISTANT_TEXT_REPAIR_PROMPT = 'Your previous turn emitted ordinary assistant text directly instead of sending it through mcp__chat__send_message(...). The user cannot see plain assistant text from that turn. The captured text below may be truncated because the runtime interrupted the turn as soon as visible assistant text appeared. Reconstruct the intended full user-visible reply from the existing thread context if needed. Your only allowed user-visible action now is exactly one successful mcp__chat__send_message(content="...", kind="final"). Do not emit ordinary assistant text again, including a repeated/truncated prefix of the previous reply.';
31
+ const MCP_CODEX_ASSISTANT_TEXT_STEER_PROMPT = 'This turn has emitted ordinary assistant text directly — the user cannot see it and it will not appear in chat. Stop emitting plain text for the rest of this turn. Continue your work through tool calls only; route all user-visible output through mcp__chat__send_message instead: use kind="progress" for interim updates, kind="final" for the last reply of this run. If the text captured below is already your complete intended reply and there is no further work to do, send it now via mcp__chat__send_message(content="...", kind="final"). Otherwise continue your work silently via tool calls and report results through send_message.';
32
+ const MCP_CODEX_ASSISTANT_TEXT_REPAIR_FOLLOW_UP_STEER_PROMPT = 'You need to reply via mcp__chat__send_message now. Reply to the user only with exactly one successful mcp__chat__send_message(content="...", kind="final"). Do not output plain assistant text. Do not do more work, do not inspect files, and do not call any tools except that final send_message. Use the existing thread context plus the captured text below to send the reply now.';
33
+ const CODEX_PROVIDER_DISCONNECT_AUTO_CONTINUE_PROMPT = '继续,接着刚才的工作';
34
+ const CODEX_PLAN_REVISION_FALLBACK = 'Please refine the plan further.';
35
+ const CODEX_PLAN_MODE_PROMPT = 'Plan mode is active for this turn. Do not execute tools, inspect files, run shell commands, call MCP tools, send chat messages, or make changes. Only propose a concise implementation/research plan for the user to approve. Use the runtime plan/proposed-plan channel when available; otherwise return only <proposed_plan>...</proposed_plan>.';
36
+ export class CodexAppServerRuntime {
37
+ db;
38
+ sessionKey;
39
+ bindingKey;
40
+ toolAuth;
41
+ workspaceRoot;
42
+ agentCommand;
43
+ agentArgs;
44
+ env;
45
+ model;
46
+ reasoningEffort;
47
+ defaultCodexMode;
48
+ serviceTier;
49
+ disabledToolKinds;
50
+ channelBridgeMcpEntry;
51
+ agentSurfaceMode;
52
+ assetAccessConfig;
53
+ workspaceLockManager;
54
+ spawnImpl;
55
+ assetFetchTimeoutMs;
56
+ attachmentMaterializer;
57
+ client = null;
58
+ child = null;
59
+ sessionId;
60
+ sessionSystemPromptText = null;
61
+ threadLoaded = false;
62
+ loadedThreadPolicySignature = null;
63
+ memoryModeDisabledThreadId = null;
64
+ persistExtendedHistorySupportByMethod = {
65
+ 'thread/start': 'unknown',
66
+ 'thread/resume': 'unknown',
67
+ };
68
+ activeRunId = null;
69
+ currentTurnId = null;
70
+ activeSink = null;
71
+ activeUiMode = 'summary';
72
+ activeCodexMode = null;
73
+ activeCompiledPolicy = null;
74
+ cancelRequested = false;
75
+ turnStartInFlight = false;
76
+ awaitingTurnStartedNotification = false;
77
+ activeTurnCompletion = null;
78
+ activeTurnWorkspaceLease = null;
79
+ activeTurnWorkspaceLeasePromise = null;
80
+ workspaceLockAbortController = null;
81
+ pendingUserInput = null;
82
+ pendingPlanApproval = null;
83
+ closeInProgress = false;
84
+ closePromise = null;
85
+ notificationChain = Promise.resolve();
86
+ pendingNotificationCount = 0;
87
+ activeBlockedKinds = null;
88
+ policyViolationError = null;
89
+ latestPlanText = null;
90
+ pendingPlanApprovalAfterTurn = false;
91
+ planImplementationActive = false;
92
+ planImplementationRepairAttempted = false;
93
+ planImplementationVisibleReplySent = false;
94
+ planImplementationDeltaBuffer = '';
95
+ planImplementationRepairSourceText = null;
96
+ assistantTextRepairActive = false;
97
+ assistantTextRepairSteerActive = false;
98
+ assistantTextRepairFallbackPending = false;
99
+ assistantTextRepairRequiresFinal = false;
100
+ assistantTextRepairSteerChatSendCount = 0;
101
+ assistantTextRepairSteerFinalChatSendCount = 0;
102
+ assistantTextRepairAttempts = 0;
103
+ assistantTextRepairSourceText = '';
104
+ assistantTextRepairStage = 'idle';
105
+ currentTurnAssistantTextBuffer = '';
106
+ currentTurnAssistantTextRepairTriggered = false;
107
+ currentTurnSuccessfulChatSend = false;
108
+ currentTurnSuccessfulChatSendCount = 0;
109
+ currentTurnSuccessfulFinalChatSendCount = 0;
110
+ currentTurnSuccessfulBigbangFinalChatSendCount = 0;
111
+ currentTurnCompletionSafeNonPostedChatSend = false;
112
+ currentTurnBigbangMessageSendFailures = [];
113
+ lastBigbangMessageSendFailureForRepair = null;
114
+ runSuccessfulChatSendCount = 0;
115
+ runSuccessfulFinalChatSendCount = 0;
116
+ autoContinueAttempts = 0;
117
+ deliveryAckSent = false;
118
+ providerOutputSeen = false;
119
+ shouldBackfillCompletedTurnTools = false;
120
+ sawNonChatToolThisRun = false;
121
+ cachedCollaborationModeModel = null;
122
+ agentMessageBuffers = new Map();
123
+ planItemBuffers = new Map();
124
+ reasoningBuffers = new Map();
125
+ commandOutputBuffers = new Map();
126
+ fileChangeOutputBuffers = new Map();
127
+ toolStartedIds = new Set();
128
+ toolCompletedIds = new Set();
129
+ toolSnapshots = new Map();
130
+ shellToolCallInputs = new Map();
131
+ compactEventKeys = new Set();
132
+ mcpServerStartupStates = new Map();
133
+ mcpStatusChangeWaiters = new Set();
134
+ soloMode;
135
+ constructor(params) {
136
+ this.db = params.db;
137
+ this.sessionKey = params.sessionKey;
138
+ this.bindingKey = normalizeOptionalString(params.bindingKey) ?? null;
139
+ this.toolAuth = params.toolAuth ?? null;
140
+ this.workspaceRoot = params.workspaceRoot;
141
+ this.agentCommand = params.agentCommand;
142
+ this.agentArgs = [...params.agentArgs];
143
+ this.env = { ...(params.env ?? {}) };
144
+ this.model = normalizeOptionalString(params.model);
145
+ this.reasoningEffort = normalizeCodexReasoningEffort(params.reasoningEffort);
146
+ this.defaultCodexMode = normalizeCodexMode(params.codexMode);
147
+ this.serviceTier = normalizeCodexServiceTier(params.serviceTier);
148
+ this.disabledToolKinds = [...(params.disabledToolKinds ?? [])];
149
+ this.channelBridgeMcpEntry = params.channelBridgeMcpEntry;
150
+ this.agentSurfaceMode = params.agentSurfaceMode ?? 'bigbang';
151
+ this.assetAccessConfig = params.assetAccessConfig;
152
+ this.workspaceLockManager = params.workspaceLockManager ?? new WorkspaceLockManager();
153
+ this.spawnImpl = params.spawnImpl ?? spawn;
154
+ this.assetFetchTimeoutMs = params.assetFetchTimeoutMs ?? ASSET_FETCH_TIMEOUT_MS;
155
+ this.soloMode = params.soloMode === true;
156
+ this.attachmentMaterializer = new RuntimeAttachmentMaterializer({
157
+ sessionKey: this.sessionKey,
158
+ runtimeLabel: 'codex-app-server',
159
+ assetAccessConfig: this.assetAccessConfig,
160
+ assetCacheRoot: params.assetCacheRoot,
161
+ fetchTimeoutMs: this.assetFetchTimeoutMs,
162
+ isCancelled: () => this.cancelRequested,
163
+ onMaterialized: params.onAssetMaterialized,
164
+ });
165
+ const existingSession = getSession(this.db, this.sessionKey);
166
+ this.sessionId = existingSession?.acpSessionId?.trim() || null;
167
+ this.sessionSystemPromptText = existingSession?.systemPromptText?.trim() || null;
168
+ }
169
+ hasPendingPermission() {
170
+ return Boolean(this.pendingUserInput || this.pendingPlanApproval);
171
+ }
172
+ updateConfig(config) {
173
+ const previousModel = this.model;
174
+ this.model = normalizeOptionalString(config.model);
175
+ this.reasoningEffort = normalizeCodexReasoningEffort(config.reasoningEffort);
176
+ this.serviceTier = normalizeCodexServiceTier(config.codexServiceTier);
177
+ if (this.model !== previousModel) {
178
+ this.cachedCollaborationModeModel = null;
179
+ }
180
+ }
181
+ async respondToPermission(requestId, decision, selectedActionId, responseText, answers) {
182
+ const pendingPlanApproval = this.pendingPlanApproval;
183
+ if (pendingPlanApproval?.requestId === requestId) {
184
+ this.pendingPlanApproval = null;
185
+ if (decision === 'deny' || selectedActionId === 'reject') {
186
+ this.activeCodexMode = 'default';
187
+ this.latestPlanText = null;
188
+ this.clearPlanImplementationState();
189
+ this.activeTurnCompletion?.resolve({ stopReason: 'approval_denied' });
190
+ return true;
191
+ }
192
+ if (selectedActionId === 'continue_planning') {
193
+ void this.startPlanRevisionTurn(pendingPlanApproval.planText, responseText).catch((error) => {
194
+ this.activeTurnCompletion?.resolve({
195
+ stopReason: 'failed',
196
+ error: String(error?.message ?? error),
197
+ });
198
+ });
199
+ return true;
200
+ }
201
+ try {
202
+ await this.emitPlanPhase('implementation');
203
+ }
204
+ catch (error) {
205
+ this.activeTurnCompletion?.resolve({
206
+ stopReason: 'failed',
207
+ error: String(error?.message ?? error),
208
+ });
209
+ return true;
210
+ }
211
+ void this.startPlanImplementationTurn(pendingPlanApproval.planText).catch((error) => {
212
+ this.activeTurnCompletion?.resolve({
213
+ stopReason: 'failed',
214
+ error: String(error?.message ?? error),
215
+ });
216
+ });
217
+ return true;
218
+ }
219
+ const pending = this.pendingUserInput;
220
+ if (!pending || pending.requestId !== requestId) {
221
+ return false;
222
+ }
223
+ this.pendingUserInput = null;
224
+ pending.resolve(decision === 'allow'
225
+ ? buildUserInputResponse(pending.params, answers)
226
+ : { answers: {} });
227
+ return true;
228
+ }
229
+ async emitPlanPhase(phase) {
230
+ await this.activeSink?.sendUi?.({
231
+ kind: 'plan_phase',
232
+ mode: this.activeUiMode,
233
+ phase,
234
+ });
235
+ }
236
+ async startPlanRevisionTurn(planText, feedbackText) {
237
+ if (!this.client || !this.sessionId) {
238
+ throw new Error('Codex app-server plan revision cannot continue without an active thread.');
239
+ }
240
+ if (!this.activeCompiledPolicy) {
241
+ throw new Error('Codex app-server plan revision lost the active tool policy.');
242
+ }
243
+ this.activeCodexMode = 'plan';
244
+ this.clearPlanImplementationState();
245
+ this.currentTurnId = null;
246
+ this.latestPlanText = null;
247
+ this.resetRunBuffers();
248
+ if (this.cancelRequested)
249
+ return;
250
+ await this.emitPlanPhase('planning');
251
+ const turnStartParams = await this.buildTurnStartParams({
252
+ threadId: this.sessionId,
253
+ compiledPolicy: this.activeCompiledPolicy,
254
+ promptText: buildCodexPlanRevisionPrompt(planText, feedbackText),
255
+ codexMode: 'plan',
256
+ });
257
+ if (this.cancelRequested)
258
+ return;
259
+ await this.requestTurnStart(this.client, turnStartParams);
260
+ if (this.cancelRequested && this.currentTurnId) {
261
+ await this.interruptActiveTurn();
262
+ }
263
+ }
264
+ async cancelCurrentRun(runId) {
265
+ if (this.activeRunId !== runId)
266
+ return false;
267
+ this.cancelRequested = true;
268
+ this.abortWorkspaceLockWait();
269
+ this.abortAssetFetch();
270
+ this.resolvePendingUserInputAsCancelled();
271
+ this.pendingPlanApproval = null;
272
+ this.clearDeferredPlanApproval();
273
+ if (!this.currentTurnId
274
+ && this.activeTurnCompletion
275
+ && !this.turnStartInFlight
276
+ && !this.awaitingTurnStartedNotification) {
277
+ this.activeTurnCompletion.resolve({ stopReason: 'cancelled' });
278
+ return true;
279
+ }
280
+ if (this.currentTurnId) {
281
+ await this.interruptActiveTurn();
282
+ }
283
+ return true;
284
+ }
285
+ async steerCurrentRun(runId, promptText, attachments) {
286
+ if (this.activeRunId !== runId)
287
+ return false;
288
+ if (!this.client || !this.sessionId)
289
+ return false;
290
+ if (this.assistantTextRepairSteerActive)
291
+ return false;
292
+ return this.requestTurnSteer(promptText, attachments);
293
+ }
294
+ async requestTurnSteer(promptText, attachments) {
295
+ if (!this.client || !this.sessionId)
296
+ return false;
297
+ const turnId = await this.waitForActiveTurnId(TURN_STEER_WAIT_FOR_START_MS);
298
+ if (!turnId || this.cancelRequested)
299
+ return false;
300
+ const materializedAttachments = await this.materializeAssetAttachments(attachments, this.activeRunId);
301
+ if (this.cancelRequested)
302
+ return false;
303
+ await this.client.request('turn/steer', {
304
+ threadId: this.sessionId,
305
+ expectedTurnId: turnId,
306
+ input: buildCodexTurnInput(promptText, materializedAttachments),
307
+ }, TURN_STEER_TIMEOUT_MS);
308
+ return true;
309
+ }
310
+ async startPlanImplementationTurn(planText) {
311
+ if (!this.client || !this.sessionId) {
312
+ throw new Error('Codex app-server plan approval cannot continue without an active thread.');
313
+ }
314
+ if (!this.activeCompiledPolicy) {
315
+ throw new Error('Codex app-server plan approval lost the active tool policy.');
316
+ }
317
+ this.activeCodexMode = 'default';
318
+ this.currentTurnId = null;
319
+ this.latestPlanText = null;
320
+ this.resetRunBuffers();
321
+ this.planImplementationActive = true;
322
+ this.planImplementationRepairAttempted = false;
323
+ this.planImplementationVisibleReplySent = false;
324
+ this.planImplementationDeltaBuffer = '';
325
+ this.planImplementationRepairSourceText = null;
326
+ if (this.cancelRequested)
327
+ return;
328
+ const turnStartParams = await this.buildTurnStartParams({
329
+ threadId: this.sessionId,
330
+ compiledPolicy: this.activeCompiledPolicy,
331
+ promptText: buildCodexPlanImplementationPrompt(this.agentSurfaceMode, planText),
332
+ codexMode: 'default',
333
+ });
334
+ if (this.cancelRequested)
335
+ return;
336
+ await this.requestTurnStart(this.client, turnStartParams);
337
+ if (this.cancelRequested && this.currentTurnId) {
338
+ await this.interruptActiveTurn();
339
+ }
340
+ }
341
+ async requestTurnStart(client, params) {
342
+ this.turnStartInFlight = true;
343
+ this.awaitingTurnStartedNotification = true;
344
+ try {
345
+ await client.request('turn/start', params, TURN_START_TIMEOUT_MS);
346
+ }
347
+ catch (error) {
348
+ this.awaitingTurnStartedNotification = false;
349
+ throw error;
350
+ }
351
+ finally {
352
+ this.turnStartInFlight = false;
353
+ }
354
+ }
355
+ close() {
356
+ if (this.closePromise)
357
+ return this.closePromise;
358
+ this.closeInProgress = true;
359
+ this.cancelRequested = true;
360
+ this.activeBlockedKinds = null;
361
+ this.policyViolationError = null;
362
+ this.abortWorkspaceLockWait();
363
+ this.abortAssetFetch();
364
+ this.resolvePendingUserInputAsCancelled();
365
+ this.pendingPlanApproval = null;
366
+ this.clearDeferredPlanApproval();
367
+ this.clearPlanImplementationState();
368
+ this.clearAssistantTextRepairState();
369
+ this.toolStartedIds.clear();
370
+ this.toolCompletedIds.clear();
371
+ this.toolSnapshots.clear();
372
+ this.shellToolCallInputs.clear();
373
+ this.compactEventKeys.clear();
374
+ this.agentMessageBuffers.clear();
375
+ this.reasoningBuffers.clear();
376
+ this.commandOutputBuffers.clear();
377
+ this.fileChangeOutputBuffers.clear();
378
+ this.resetLoadedThreadState();
379
+ this.sessionId = null;
380
+ this.mcpServerStartupStates.clear();
381
+ this.resolvePendingMcpStatusWaiters();
382
+ const client = this.client;
383
+ const activeTurnCompletion = this.activeTurnCompletion;
384
+ this.client = null;
385
+ this.child = null;
386
+ const finalizeClose = () => {
387
+ activeTurnCompletion?.reject(new Error('Codex app-server runtime closed.'));
388
+ if (!activeTurnCompletion) {
389
+ this.releaseActiveTurnWorkspaceLock();
390
+ }
391
+ this.activeTurnCompletion = null;
392
+ this.activeRunId = null;
393
+ this.currentTurnId = null;
394
+ this.activeSink = null;
395
+ this.activeUiMode = 'summary';
396
+ this.activeCodexMode = null;
397
+ this.activeCompiledPolicy = null;
398
+ this.turnStartInFlight = false;
399
+ this.awaitingTurnStartedNotification = false;
400
+ };
401
+ this.closePromise = (async () => {
402
+ if (!client) {
403
+ finalizeClose();
404
+ return;
405
+ }
406
+ try {
407
+ await client.dispose();
408
+ }
409
+ catch (error) {
410
+ log.warn('[codex-app-server] failed to dispose app-server during close', {
411
+ sessionKey: this.sessionKey,
412
+ error: String(error?.message ?? error),
413
+ });
414
+ }
415
+ finally {
416
+ finalizeClose();
417
+ }
418
+ })();
419
+ return this.closePromise;
420
+ }
421
+ async prompt(params) {
422
+ if (this.activeRunId) {
423
+ throw new Error('Codex app-server runtime already has an active run.');
424
+ }
425
+ this.activeRunId = params.runId;
426
+ this.activeSink = params.sink;
427
+ this.activeUiMode = params.uiMode;
428
+ this.activeCodexMode = normalizeCodexMode(params.runtimeOverrides?.codexMode) ?? this.defaultCodexMode;
429
+ this.cancelRequested = false;
430
+ this.currentTurnId = null;
431
+ this.runSuccessfulChatSendCount = 0;
432
+ this.runSuccessfulFinalChatSendCount = 0;
433
+ this.resetRunBuffers();
434
+ const compiledPolicy = this.buildCompiledToolPolicy(params.disabledToolKinds ?? []);
435
+ this.activeCompiledPolicy = compiledPolicy;
436
+ this.activeBlockedKinds = compiledPolicy.blockedKinds;
437
+ this.autoContinueAttempts = 0;
438
+ this.deliveryAckSent = false;
439
+ this.providerOutputSeen = false;
440
+ const effectiveSystemPromptText = normalizeOptionalString(params.systemPromptText) ?? undefined;
441
+ try {
442
+ const preparedSession = await this.ensureSession({
443
+ systemPromptText: effectiveSystemPromptText,
444
+ policy: compiledPolicy,
445
+ });
446
+ this.shouldBackfillCompletedTurnTools = !preparedSession.isFreshSession;
447
+ const baseContextText = preparedSession.isFreshSession
448
+ ? joinPromptContext(params.resumeContextText, params.recoveryContextText, params.contextText)
449
+ : joinPromptContext(params.contextText);
450
+ const effectiveContextText = joinPromptContext(compiledPolicy.promptPreamble, baseContextText);
451
+ updateSessionRuntimeState(this.db, {
452
+ sessionKey: this.sessionKey,
453
+ acpSessionId: preparedSession.sessionId,
454
+ systemPromptText: effectiveSystemPromptText ?? null,
455
+ });
456
+ this.sessionSystemPromptText = effectiveSystemPromptText ?? null;
457
+ const preparedResult = {
458
+ sessionId: preparedSession.sessionId,
459
+ isFreshSession: preparedSession.isFreshSession,
460
+ effectiveSystemPromptText,
461
+ effectiveContextText: effectiveContextText || undefined,
462
+ };
463
+ const buildPromptResult = (stopReason) => ({
464
+ stopReason,
465
+ isFreshSession: preparedResult.isFreshSession,
466
+ sessionId: preparedResult.sessionId,
467
+ effectiveSystemPromptText,
468
+ effectiveContextText: effectiveContextText || undefined,
469
+ });
470
+ await params.onPrepared?.(preparedResult);
471
+ if (this.cancelRequested) {
472
+ return buildPromptResult('cancelled');
473
+ }
474
+ let turnPromptText = joinPromptContext(effectiveContextText, params.promptText);
475
+ let turnAttachments = params.attachments ?? params.promptResources;
476
+ while (true) {
477
+ try {
478
+ const outcome = await this.runTurnAttempt({
479
+ sessionId: preparedSession.sessionId,
480
+ compiledPolicy,
481
+ promptText: turnPromptText,
482
+ attachments: turnAttachments,
483
+ onDelivered: params.onDelivered,
484
+ });
485
+ return buildPromptResult(outcome.stopReason);
486
+ }
487
+ catch (error) {
488
+ await this.notificationChain.catch(() => { });
489
+ this.releaseActiveTurnWorkspaceLock();
490
+ if (this.closeInProgress) {
491
+ throw new Error('Codex app-server runtime closed.');
492
+ }
493
+ if (this.cancelRequested) {
494
+ return buildPromptResult('cancelled');
495
+ }
496
+ if (!this.shouldAutoContinueAfterProviderDisconnect(error)) {
497
+ throw error;
498
+ }
499
+ const autoContinued = await this.startProviderDisconnectAutoContinue({
500
+ sessionId: preparedSession.sessionId,
501
+ systemPromptText: effectiveSystemPromptText,
502
+ compiledPolicy,
503
+ });
504
+ if (this.closeInProgress) {
505
+ throw new Error('Codex app-server runtime closed.');
506
+ }
507
+ if (!autoContinued || this.cancelRequested) {
508
+ return buildPromptResult('cancelled');
509
+ }
510
+ turnPromptText = CODEX_PROVIDER_DISCONNECT_AUTO_CONTINUE_PROMPT;
511
+ turnAttachments = undefined;
512
+ }
513
+ }
514
+ }
515
+ catch (error) {
516
+ const message = String(error?.message ?? error);
517
+ if (isLikelyMissingThreadError(message)) {
518
+ clearAcpSessionId(this.db, this.sessionKey);
519
+ this.sessionId = null;
520
+ this.resetLoadedThreadState();
521
+ }
522
+ throw error instanceof Error ? error : new Error(message);
523
+ }
524
+ finally {
525
+ await this.notificationChain.catch(() => { });
526
+ this.resolvePendingUserInputAsCancelled();
527
+ this.releaseActiveTurnWorkspaceLock();
528
+ this.cleanupAfterRun();
529
+ }
530
+ }
531
+ async runTurnAttempt(params) {
532
+ const client = await this.ensureClient();
533
+ if (this.cancelRequested) {
534
+ return { stopReason: 'cancelled' };
535
+ }
536
+ const turnStartParams = await this.buildTurnStartParams({
537
+ threadId: params.sessionId,
538
+ compiledPolicy: params.compiledPolicy,
539
+ promptText: params.promptText,
540
+ attachments: params.attachments,
541
+ codexMode: this.activeCodexMode,
542
+ });
543
+ if (this.cancelRequested) {
544
+ return { stopReason: 'cancelled' };
545
+ }
546
+ const completion = new Promise((resolve, reject) => {
547
+ this.activeTurnCompletion = { resolve, reject };
548
+ });
549
+ void completion.catch(() => { });
550
+ try {
551
+ await this.requestTurnStart(client, turnStartParams);
552
+ if (!this.deliveryAckSent) {
553
+ this.deliveryAckSent = true;
554
+ await params.onDelivered?.();
555
+ }
556
+ if (this.cancelRequested && this.currentTurnId) {
557
+ await this.interruptActiveTurn();
558
+ }
559
+ const outcome = await completion;
560
+ await this.notificationChain;
561
+ this.releaseActiveTurnWorkspaceLock();
562
+ if (outcome.error) {
563
+ throw new Error(outcome.error);
564
+ }
565
+ return outcome;
566
+ }
567
+ finally {
568
+ this.activeTurnCompletion = null;
569
+ this.currentTurnId = null;
570
+ }
571
+ }
572
+ shouldAutoContinueAfterProviderDisconnect(error) {
573
+ if (this.cancelRequested)
574
+ return false;
575
+ if (this.activeCodexMode === 'plan')
576
+ return false;
577
+ if (!this.deliveryAckSent)
578
+ return false;
579
+ if (this.providerOutputSeen)
580
+ return false;
581
+ if (this.autoContinueAttempts >= PROVIDER_DISCONNECT_AUTO_CONTINUE_MAX_ATTEMPTS)
582
+ return false;
583
+ if (this.pendingUserInput || this.pendingPlanApproval || this.pendingPlanApprovalAfterTurn)
584
+ return false;
585
+ const message = String(error?.message ?? error).toLowerCase();
586
+ return message.includes('stream disconnected before completion')
587
+ && message.includes('backend-api/codex/responses');
588
+ }
589
+ async startProviderDisconnectAutoContinue(params) {
590
+ if (this.cancelRequested)
591
+ return false;
592
+ this.autoContinueAttempts += 1;
593
+ await this.activeSink?.sendActivityText?.(`Provider disconnected before completion; auto-continuing this run (attempt ${this.autoContinueAttempts}/${PROVIDER_DISCONNECT_AUTO_CONTINUE_MAX_ATTEMPTS}).`);
594
+ if (this.cancelRequested)
595
+ return false;
596
+ await this.disposeDisconnectedClient();
597
+ if (this.cancelRequested)
598
+ return false;
599
+ this.activeCodexMode = 'default';
600
+ this.currentTurnId = null;
601
+ this.resetRunBuffers();
602
+ await this.ensureClient();
603
+ if (this.cancelRequested)
604
+ return false;
605
+ await this.resumeThread({
606
+ sessionId: params.sessionId,
607
+ systemPromptText: params.systemPromptText,
608
+ policy: params.compiledPolicy,
609
+ });
610
+ if (this.cancelRequested)
611
+ return false;
612
+ await this.ensureThreadMemoryModeDisabled(params.sessionId);
613
+ if (this.cancelRequested)
614
+ return false;
615
+ updateSessionRuntimeState(this.db, {
616
+ sessionKey: this.sessionKey,
617
+ acpSessionId: params.sessionId,
618
+ systemPromptText: params.systemPromptText ?? null,
619
+ });
620
+ this.sessionSystemPromptText = params.systemPromptText ?? null;
621
+ return true;
622
+ }
623
+ async disposeDisconnectedClient() {
624
+ const client = this.client;
625
+ this.client = null;
626
+ this.child = null;
627
+ this.activeTurnCompletion = null;
628
+ this.currentTurnId = null;
629
+ this.turnStartInFlight = false;
630
+ this.awaitingTurnStartedNotification = false;
631
+ this.resetLoadedThreadState();
632
+ this.releaseActiveTurnWorkspaceLock();
633
+ if (!client) {
634
+ return;
635
+ }
636
+ try {
637
+ await client.dispose();
638
+ }
639
+ catch (error) {
640
+ log.warn('[codex-app-server] failed to dispose disconnected app-server client', {
641
+ sessionKey: this.sessionKey,
642
+ error: String(error?.message ?? error),
643
+ });
644
+ }
645
+ }
646
+ cleanupAfterRun() {
647
+ this.activeTurnCompletion = null;
648
+ this.activeRunId = null;
649
+ this.currentTurnId = null;
650
+ this.activeSink = null;
651
+ this.activeUiMode = 'summary';
652
+ this.activeCodexMode = null;
653
+ this.activeCompiledPolicy = null;
654
+ this.pendingPlanApproval = null;
655
+ this.clearDeferredPlanApproval();
656
+ this.pendingNotificationCount = 0;
657
+ this.activeBlockedKinds = null;
658
+ this.clearPlanImplementationState();
659
+ this.clearAssistantTextRepairState();
660
+ this.shouldBackfillCompletedTurnTools = false;
661
+ this.sawNonChatToolThisRun = false;
662
+ this.runSuccessfulChatSendCount = 0;
663
+ this.runSuccessfulFinalChatSendCount = 0;
664
+ this.autoContinueAttempts = 0;
665
+ this.deliveryAckSent = false;
666
+ this.providerOutputSeen = false;
667
+ this.cancelRequested = false;
668
+ this.turnStartInFlight = false;
669
+ this.awaitingTurnStartedNotification = false;
670
+ this.resetRunBuffers();
671
+ }
672
+ resetRunBuffers() {
673
+ this.policyViolationError = null;
674
+ this.latestPlanText = null;
675
+ this.clearDeferredPlanApproval();
676
+ this.agentMessageBuffers.clear();
677
+ this.planItemBuffers.clear();
678
+ this.reasoningBuffers.clear();
679
+ this.commandOutputBuffers.clear();
680
+ this.fileChangeOutputBuffers.clear();
681
+ this.toolStartedIds.clear();
682
+ this.toolCompletedIds.clear();
683
+ this.toolSnapshots.clear();
684
+ this.shellToolCallInputs.clear();
685
+ this.currentTurnAssistantTextBuffer = '';
686
+ this.currentTurnAssistantTextRepairTriggered = false;
687
+ this.currentTurnSuccessfulChatSend = false;
688
+ this.currentTurnSuccessfulChatSendCount = 0;
689
+ this.currentTurnSuccessfulFinalChatSendCount = 0;
690
+ this.currentTurnSuccessfulBigbangFinalChatSendCount = 0;
691
+ this.currentTurnCompletionSafeNonPostedChatSend = false;
692
+ this.currentTurnBigbangMessageSendFailures = [];
693
+ }
694
+ buildCurrentTurnBigbangRepairGuidance() {
695
+ if (this.agentSurfaceMode !== 'bigbang') {
696
+ return null;
697
+ }
698
+ const currentFailures = this.currentTurnBigbangMessageSendFailures;
699
+ const latestFinalFailure = [...currentFailures].reverse().find((failure) => failure.messageKind === 'final');
700
+ const selectedFailure = latestFinalFailure ?? currentFailures.at(-1) ?? this.lastBigbangMessageSendFailureForRepair;
701
+ if (!selectedFailure)
702
+ return null;
703
+ this.lastBigbangMessageSendFailureForRepair = selectedFailure;
704
+ return buildBigbangMessageSendRepairGuidance(selectedFailure.failureKind);
705
+ }
706
+ markProviderOutputSeen() {
707
+ this.providerOutputSeen = true;
708
+ }
709
+ clearPlanImplementationState() {
710
+ this.planImplementationActive = false;
711
+ this.planImplementationRepairAttempted = false;
712
+ this.planImplementationVisibleReplySent = false;
713
+ this.planImplementationDeltaBuffer = '';
714
+ this.planImplementationRepairSourceText = null;
715
+ }
716
+ clearAssistantTextRepairState() {
717
+ this.assistantTextRepairActive = false;
718
+ this.assistantTextRepairSteerActive = false;
719
+ this.assistantTextRepairFallbackPending = false;
720
+ this.assistantTextRepairRequiresFinal = false;
721
+ this.assistantTextRepairSteerChatSendCount = 0;
722
+ this.assistantTextRepairSteerFinalChatSendCount = 0;
723
+ this.assistantTextRepairAttempts = 0;
724
+ this.assistantTextRepairSourceText = '';
725
+ this.assistantTextRepairStage = 'idle';
726
+ this.lastBigbangMessageSendFailureForRepair = null;
727
+ }
728
+ async ensureClient() {
729
+ if (this.client)
730
+ return this.client;
731
+ const child = this.spawnImpl(this.agentCommand, this.agentArgs, {
732
+ cwd: this.workspaceRoot,
733
+ env: buildCodexAppServerChildEnv(process.env, this.env),
734
+ detached: process.platform !== 'win32',
735
+ stdio: ['pipe', 'pipe', 'pipe'],
736
+ });
737
+ this.child = child;
738
+ const client = new CodexAppServerClient({ child });
739
+ client.setNotificationHandler((method, params) => {
740
+ this.queueNotification(async () => {
741
+ await this.handleNotification(method, params);
742
+ });
743
+ });
744
+ client.setExitHandler((error) => {
745
+ if (this.client !== client) {
746
+ return;
747
+ }
748
+ this.client = null;
749
+ this.child = null;
750
+ this.awaitingTurnStartedNotification = false;
751
+ this.turnStartInFlight = false;
752
+ if (this.closeInProgress) {
753
+ return;
754
+ }
755
+ this.activeTurnCompletion?.reject(error);
756
+ });
757
+ client.setRequestHandler('item/commandExecution/requestApproval', (params) => (this.handleCommandExecutionApprovalRequest(params)));
758
+ client.setRequestHandler('item/fileChange/requestApproval', (params) => (this.handleFileChangeApprovalRequest(params)));
759
+ client.setRequestHandler('item/permissions/requestApproval', (params) => (this.handlePermissionsApprovalRequest(params)));
760
+ client.setRequestHandler('item/tool/requestUserInput', (params) => this.handleUserInputRequest(params));
761
+ client.setRequestHandler('tool/requestUserInput', (params) => this.handleUserInputRequest(params));
762
+ await client.request('initialize', buildCodexAppServerInitializeParams());
763
+ client.notify('initialized', {});
764
+ this.client = client;
765
+ return client;
766
+ }
767
+ async ensureSession(params) {
768
+ if (!this.sessionId) {
769
+ const sessionId = await this.startThread(params);
770
+ await this.ensureThreadMemoryModeDisabled(sessionId);
771
+ return { sessionId, isFreshSession: true };
772
+ }
773
+ if (this.threadLoaded
774
+ && this.loadedThreadPolicySignature === params.policy.threadPolicySignature
775
+ && this.memoryModeDisabledThreadId === this.sessionId) {
776
+ return { sessionId: this.sessionId, isFreshSession: false };
777
+ }
778
+ const existingSessionId = this.sessionId;
779
+ try {
780
+ await this.resumeThread({
781
+ sessionId: existingSessionId,
782
+ systemPromptText: params.systemPromptText,
783
+ policy: params.policy,
784
+ });
785
+ }
786
+ catch (error) {
787
+ const message = String(error?.message ?? error);
788
+ if (!isLikelyMissingThreadError(message)) {
789
+ log.warn('[codex-app-server] failed to resume thread', {
790
+ sessionKey: this.sessionKey,
791
+ sessionId: existingSessionId,
792
+ error: message,
793
+ });
794
+ throw error instanceof Error ? error : new Error(message);
795
+ }
796
+ log.warn('[codex-app-server] saved thread missing; starting new thread', {
797
+ sessionKey: this.sessionKey,
798
+ sessionId: existingSessionId,
799
+ error: message,
800
+ });
801
+ clearAcpSessionId(this.db, this.sessionKey);
802
+ this.sessionId = null;
803
+ this.resetLoadedThreadState();
804
+ const freshSessionId = await this.startThread(params);
805
+ await this.ensureThreadMemoryModeDisabled(freshSessionId);
806
+ return { sessionId: freshSessionId, isFreshSession: true };
807
+ }
808
+ await this.ensureThreadMemoryModeDisabled(existingSessionId);
809
+ return { sessionId: existingSessionId, isFreshSession: false };
810
+ }
811
+ async startThread(params) {
812
+ this.resetConfiguredMcpServerStates();
813
+ const response = await this.requestThreadLifecycle({
814
+ method: 'thread/start',
815
+ sessionId: null,
816
+ buildParams: (persistExtendedHistory) => this.buildThreadStartParams(params, { persistExtendedHistory }),
817
+ });
818
+ const sessionId = normalizeOptionalString(response?.thread?.id);
819
+ if (!sessionId) {
820
+ throw new Error('Codex app-server did not return a thread id.');
821
+ }
822
+ await this.waitForConfiguredMcpServersReady();
823
+ this.sessionId = sessionId;
824
+ this.threadLoaded = true;
825
+ this.loadedThreadPolicySignature = params.policy.threadPolicySignature;
826
+ this.memoryModeDisabledThreadId = null;
827
+ return sessionId;
828
+ }
829
+ async resumeThread(params) {
830
+ this.resetConfiguredMcpServerStates();
831
+ await this.requestThreadLifecycle({
832
+ method: 'thread/resume',
833
+ sessionId: params.sessionId,
834
+ buildParams: (persistExtendedHistory) => this.buildThreadResumeParams(params, { persistExtendedHistory }),
835
+ });
836
+ await this.waitForConfiguredMcpServersReady();
837
+ this.threadLoaded = true;
838
+ this.loadedThreadPolicySignature = params.policy.threadPolicySignature;
839
+ }
840
+ async requestThreadLifecycle(params) {
841
+ const client = await this.ensureClient();
842
+ const shouldPersistExtendedHistory = this.persistExtendedHistorySupportByMethod[params.method] !== 'unsupported';
843
+ try {
844
+ const response = await client.request(params.method, params.buildParams(shouldPersistExtendedHistory));
845
+ if (shouldPersistExtendedHistory) {
846
+ this.persistExtendedHistorySupportByMethod[params.method] = 'enabled';
847
+ }
848
+ return response;
849
+ }
850
+ catch (error) {
851
+ const message = String(error?.message ?? error);
852
+ if (!shouldPersistExtendedHistory || !isLikelyUnsupportedPersistExtendedHistoryError(message)) {
853
+ throw error instanceof Error ? error : new Error(message);
854
+ }
855
+ log.warn('[codex-app-server] app-server rejected persistExtendedHistory; retrying without it', {
856
+ sessionKey: this.sessionKey,
857
+ sessionId: params.sessionId,
858
+ method: params.method,
859
+ error: message,
860
+ });
861
+ this.persistExtendedHistorySupportByMethod[params.method] = 'unsupported';
862
+ this.resetConfiguredMcpServerStates();
863
+ return await client.request(params.method, params.buildParams(false));
864
+ }
865
+ }
866
+ async ensureThreadMemoryModeDisabled(sessionId) {
867
+ if (this.memoryModeDisabledThreadId === sessionId) {
868
+ return;
869
+ }
870
+ const client = await this.ensureClient();
871
+ try {
872
+ await client.request('thread/memoryMode/set', {
873
+ threadId: sessionId,
874
+ mode: 'disabled',
875
+ });
876
+ this.memoryModeDisabledThreadId = sessionId;
877
+ }
878
+ catch (error) {
879
+ this.memoryModeDisabledThreadId = null;
880
+ const message = String(error?.message ?? error);
881
+ log.warn('[codex-app-server] failed to disable provider memory', {
882
+ sessionKey: this.sessionKey,
883
+ sessionId,
884
+ error: message,
885
+ });
886
+ throw new Error(`Failed to disable Codex provider memory for codex_app_server thread ${sessionId}: ${message}`);
887
+ }
888
+ }
889
+ resetLoadedThreadState() {
890
+ this.threadLoaded = false;
891
+ this.loadedThreadPolicySignature = null;
892
+ this.memoryModeDisabledThreadId = null;
893
+ }
894
+ async interruptActiveTurn() {
895
+ if (!this.client || !this.sessionId || !this.currentTurnId)
896
+ return;
897
+ try {
898
+ await this.client.request('turn/interrupt', {
899
+ threadId: this.sessionId,
900
+ turnId: this.currentTurnId,
901
+ });
902
+ }
903
+ catch (error) {
904
+ log.warn('[codex-app-server] failed to interrupt active turn', {
905
+ sessionKey: this.sessionKey,
906
+ sessionId: this.sessionId,
907
+ turnId: this.currentTurnId,
908
+ error: String(error?.message ?? error),
909
+ });
910
+ }
911
+ }
912
+ async ensureActiveTurnWorkspaceWriteLock() {
913
+ if (this.activeTurnWorkspaceLease) {
914
+ return;
915
+ }
916
+ if (this.activeTurnWorkspaceLeasePromise) {
917
+ await this.activeTurnWorkspaceLeasePromise;
918
+ return;
919
+ }
920
+ const abortController = new AbortController();
921
+ this.workspaceLockAbortController = abortController;
922
+ const acquirePromise = (async () => {
923
+ const lease = await this.workspaceLockManager.acquire(this.workspaceRoot, {
924
+ signal: abortController.signal,
925
+ onWaitStart: () => {
926
+ void this.activeSink?.sendUi?.({
927
+ kind: 'task',
928
+ mode: this.activeUiMode,
929
+ title: 'waiting for workspace lock',
930
+ detail: `Waiting for exclusive workspace access: ${this.workspaceRoot}`,
931
+ silent: true,
932
+ }).catch((error) => {
933
+ log.warn('[codex-app-server] failed to emit workspace lock wait event', {
934
+ sessionKey: this.sessionKey,
935
+ error: String(error?.message ?? error),
936
+ });
937
+ });
938
+ },
939
+ });
940
+ this.activeTurnWorkspaceLease = lease;
941
+ })();
942
+ this.activeTurnWorkspaceLeasePromise = acquirePromise;
943
+ try {
944
+ await acquirePromise;
945
+ }
946
+ finally {
947
+ if (this.workspaceLockAbortController === abortController) {
948
+ this.workspaceLockAbortController = null;
949
+ }
950
+ if (this.activeTurnWorkspaceLeasePromise === acquirePromise) {
951
+ this.activeTurnWorkspaceLeasePromise = null;
952
+ }
953
+ }
954
+ }
955
+ async waitForWorkspaceWritesBeforeRead() {
956
+ await this.ensureActiveTurnWorkspaceWriteLock();
957
+ }
958
+ abortWorkspaceLockWait() {
959
+ const controller = this.workspaceLockAbortController;
960
+ if (!controller)
961
+ return;
962
+ this.workspaceLockAbortController = null;
963
+ controller.abort();
964
+ }
965
+ abortAssetFetch() {
966
+ this.attachmentMaterializer.abort();
967
+ }
968
+ releaseActiveTurnWorkspaceLock() {
969
+ this.activeTurnWorkspaceLeasePromise = null;
970
+ const lease = this.activeTurnWorkspaceLease;
971
+ if (!lease)
972
+ return;
973
+ this.activeTurnWorkspaceLease = null;
974
+ lease.release();
975
+ }
976
+ resolvePendingUserInputAsCancelled() {
977
+ const pending = this.pendingUserInput;
978
+ if (!pending)
979
+ return;
980
+ this.pendingUserInput = null;
981
+ pending.resolve({ answers: {} });
982
+ }
983
+ queueNotification(task) {
984
+ this.pendingNotificationCount += 1;
985
+ this.notificationChain = this.notificationChain
986
+ .then(async () => {
987
+ try {
988
+ await task();
989
+ }
990
+ finally {
991
+ this.pendingNotificationCount = Math.max(0, this.pendingNotificationCount - 1);
992
+ if (this.pendingNotificationCount === 0) {
993
+ this.scheduleDeferredPlanApprovalFlush();
994
+ }
995
+ }
996
+ })
997
+ .catch((error) => {
998
+ const err = error instanceof Error ? error : new Error(String(error));
999
+ this.activeTurnCompletion?.reject(err);
1000
+ });
1001
+ }
1002
+ clearDeferredPlanApproval() {
1003
+ this.pendingPlanApprovalAfterTurn = false;
1004
+ }
1005
+ scheduleDeferredPlanApprovalFlush() {
1006
+ if (!this.pendingPlanApprovalAfterTurn || this.pendingNotificationCount > 0)
1007
+ return;
1008
+ void this.flushDeferredPlanApproval().catch((error) => {
1009
+ const err = error instanceof Error ? error : new Error(String(error));
1010
+ this.activeTurnCompletion?.resolve({
1011
+ stopReason: 'failed',
1012
+ error: err.message,
1013
+ });
1014
+ });
1015
+ }
1016
+ async flushDeferredPlanApproval() {
1017
+ if (!this.pendingPlanApprovalAfterTurn)
1018
+ return;
1019
+ this.pendingPlanApprovalAfterTurn = false;
1020
+ if (this.cancelRequested || this.closeInProgress)
1021
+ return;
1022
+ if (!this.activeRunId || !this.activeTurnCompletion)
1023
+ return;
1024
+ if (this.activeCodexMode !== 'plan')
1025
+ return;
1026
+ if (this.pendingPlanApproval)
1027
+ return;
1028
+ let planText;
1029
+ try {
1030
+ planText = await this.readLatestPlanTextFromThreadSnapshot();
1031
+ }
1032
+ catch (error) {
1033
+ if (this.cancelRequested || this.closeInProgress)
1034
+ return;
1035
+ if (!this.activeRunId || !this.activeTurnCompletion)
1036
+ return;
1037
+ throw error;
1038
+ }
1039
+ if (this.cancelRequested || this.closeInProgress)
1040
+ return;
1041
+ if (!this.activeRunId || !this.activeTurnCompletion)
1042
+ return;
1043
+ if (this.activeCodexMode !== 'plan')
1044
+ return;
1045
+ if (this.pendingPlanApproval)
1046
+ return;
1047
+ if (planText) {
1048
+ await this.emitPlanApprovalRequest(planText);
1049
+ return;
1050
+ }
1051
+ this.activeTurnCompletion?.resolve({ stopReason: 'completed' });
1052
+ }
1053
+ async readLatestPlanTextFromThreadSnapshot() {
1054
+ if (!this.client || !this.sessionId)
1055
+ return null;
1056
+ try {
1057
+ const response = await this.client.request('thread/read', {
1058
+ threadId: this.sessionId,
1059
+ includeTurns: true,
1060
+ }, THREAD_READ_TIMEOUT_MS);
1061
+ return extractLatestPlanTextFromThreadReadResponse(response);
1062
+ }
1063
+ catch (error) {
1064
+ log.warn('[codex-app-server] failed to read completed plan snapshot', {
1065
+ sessionId: this.sessionId,
1066
+ error: String(error?.message ?? error),
1067
+ });
1068
+ throw new Error(`Codex app-server failed to read completed plan snapshot: ${String(error?.message ?? error)}`);
1069
+ }
1070
+ }
1071
+ async backfillCompletedTurnToolItemsFromThreadSnapshot(turnId) {
1072
+ if (!this.client || !this.sessionId || !turnId)
1073
+ return null;
1074
+ try {
1075
+ const response = await this.client.request('thread/read', {
1076
+ threadId: this.sessionId,
1077
+ includeTurns: true,
1078
+ }, THREAD_READ_TIMEOUT_MS);
1079
+ const items = extractCompletedTurnToolItems(response, turnId);
1080
+ for (const item of items) {
1081
+ const itemId = normalizeOptionalString(asRecord(item)?.id);
1082
+ if (isWorkspaceSensitiveToolItem(item, this.workspaceRoot)
1083
+ && (!itemId || (!this.toolStartedIds.has(itemId) && !this.toolCompletedIds.has(itemId)))) {
1084
+ return 'Codex app-server completed a workspace-sensitive tool without live lockable tool notifications.';
1085
+ }
1086
+ await this.handleThreadItem(item, { stage: 'complete' });
1087
+ }
1088
+ return null;
1089
+ }
1090
+ catch (error) {
1091
+ log.warn('[codex-app-server] failed to backfill completed turn tool snapshot', {
1092
+ sessionId: this.sessionId,
1093
+ turnId,
1094
+ error: String(error?.message ?? error),
1095
+ });
1096
+ return null;
1097
+ }
1098
+ }
1099
+ async handleDefaultTurnCompleted() {
1100
+ const completedTurnId = this.currentTurnId;
1101
+ this.currentTurnId = null;
1102
+ if (this.shouldBackfillCompletedTurnTools && this.sawNonChatToolThisRun) {
1103
+ const backfillError = await this.backfillCompletedTurnToolItemsFromThreadSnapshot(completedTurnId);
1104
+ if (backfillError) {
1105
+ this.activeTurnCompletion?.resolve({
1106
+ stopReason: 'failed',
1107
+ error: backfillError,
1108
+ });
1109
+ return;
1110
+ }
1111
+ }
1112
+ if (this.soloMode) {
1113
+ this.activeTurnCompletion?.resolve({ stopReason: 'completed' });
1114
+ return;
1115
+ }
1116
+ if (await this.handleAssistantTextRepairTurnCompleted()) {
1117
+ return;
1118
+ }
1119
+ if (!this.planImplementationActive) {
1120
+ this.activeTurnCompletion?.resolve({ stopReason: 'completed' });
1121
+ return;
1122
+ }
1123
+ if (this.planImplementationVisibleReplySent) {
1124
+ this.activeTurnCompletion?.resolve({ stopReason: 'completed' });
1125
+ return;
1126
+ }
1127
+ const repairSourceText = normalizeOptionalString(this.planImplementationRepairSourceText);
1128
+ const draftText = repairSourceText ?? normalizeOptionalString(this.planImplementationDeltaBuffer);
1129
+ if (!draftText) {
1130
+ this.activeTurnCompletion?.resolve({
1131
+ stopReason: 'failed',
1132
+ error: 'Codex app-server implementation completed without sending a final chat message.',
1133
+ });
1134
+ return;
1135
+ }
1136
+ if (this.planImplementationRepairAttempted) {
1137
+ this.activeTurnCompletion?.resolve({
1138
+ stopReason: 'failed',
1139
+ error: 'Codex app-server implementation repair did not send a final chat message.',
1140
+ });
1141
+ return;
1142
+ }
1143
+ await this.startPlanImplementationRepairTurn(draftText, {
1144
+ allowReconstruction: Boolean(repairSourceText),
1145
+ });
1146
+ }
1147
+ async startPlanImplementationRepairTurn(finalAnswerDraft, options) {
1148
+ if (!this.client || !this.sessionId) {
1149
+ throw new Error('Codex app-server plan implementation repair cannot continue without an active thread.');
1150
+ }
1151
+ if (!this.activeCompiledPolicy) {
1152
+ throw new Error('Codex app-server plan implementation repair lost the active tool policy.');
1153
+ }
1154
+ const bigbangRepairGuidance = this.buildCurrentTurnBigbangRepairGuidance();
1155
+ this.activeCodexMode = 'default';
1156
+ this.currentTurnId = null;
1157
+ this.resetRunBuffers();
1158
+ this.planImplementationActive = true;
1159
+ this.planImplementationRepairAttempted = true;
1160
+ this.planImplementationVisibleReplySent = false;
1161
+ this.planImplementationDeltaBuffer = finalAnswerDraft;
1162
+ this.planImplementationRepairSourceText = null;
1163
+ if (this.cancelRequested)
1164
+ return;
1165
+ const turnStartParams = await this.buildTurnStartParams({
1166
+ threadId: this.sessionId,
1167
+ compiledPolicy: this.activeCompiledPolicy,
1168
+ promptText: buildCodexPlanImplementationRepairPrompt(this.agentSurfaceMode, finalAnswerDraft, {
1169
+ allowReconstruction: options?.allowReconstruction === true,
1170
+ bigbangRepairGuidance,
1171
+ }),
1172
+ codexMode: 'default',
1173
+ });
1174
+ if (this.cancelRequested)
1175
+ return;
1176
+ await this.requestTurnStart(this.client, turnStartParams);
1177
+ if (this.cancelRequested && this.currentTurnId) {
1178
+ await this.interruptActiveTurn();
1179
+ }
1180
+ }
1181
+ async handleAssistantTextRepairTurnCompleted() {
1182
+ if (this.soloMode)
1183
+ return false;
1184
+ if (this.activeCodexMode === 'plan')
1185
+ return false;
1186
+ const currentTurnAssistantText = normalizeOptionalString(this.currentTurnAssistantTextBuffer);
1187
+ if (currentTurnAssistantText
1188
+ && this.agentSurfaceMode === 'bigbang'
1189
+ && this.currentTurnSuccessfulBigbangFinalChatSendCount > 0
1190
+ && !this.planImplementationActive
1191
+ && !this.assistantTextRepairRequiresFinal) {
1192
+ this.clearAssistantTextRepairState();
1193
+ return false;
1194
+ }
1195
+ if (currentTurnAssistantText) {
1196
+ this.assistantTextRepairSourceText = currentTurnAssistantText;
1197
+ if (this.planImplementationActive || this.assistantTextRepairRequiresFinal) {
1198
+ this.planImplementationRepairSourceText = currentTurnAssistantText;
1199
+ }
1200
+ }
1201
+ else if (this.assistantTextRepairActive && this.currentTurnSuccessfulChatSend) {
1202
+ const repairTurnSatisfied = this.currentTurnSuccessfulFinalChatSendCount > 0;
1203
+ if (repairTurnSatisfied) {
1204
+ if (this.planImplementationActive) {
1205
+ this.planImplementationVisibleReplySent = true;
1206
+ }
1207
+ this.clearAssistantTextRepairState();
1208
+ return false;
1209
+ }
1210
+ }
1211
+ if (this.assistantTextRepairActive
1212
+ && this.currentTurnCompletionSafeNonPostedChatSend
1213
+ && this.runSuccessfulFinalChatSendCount > 0) {
1214
+ if (this.planImplementationActive) {
1215
+ this.planImplementationVisibleReplySent = true;
1216
+ }
1217
+ this.clearAssistantTextRepairState();
1218
+ return false;
1219
+ }
1220
+ if (this.assistantTextRepairSteerActive) {
1221
+ const sawPostSteerReply = (this.currentTurnSuccessfulFinalChatSendCount > this.assistantTextRepairSteerFinalChatSendCount);
1222
+ if (this.currentTurnCompletionSafeNonPostedChatSend
1223
+ && this.runSuccessfulFinalChatSendCount > 0) {
1224
+ if (this.planImplementationActive) {
1225
+ this.planImplementationVisibleReplySent = true;
1226
+ }
1227
+ this.clearAssistantTextRepairState();
1228
+ return false;
1229
+ }
1230
+ if (this.assistantTextRepairRequiresFinal
1231
+ && sawPostSteerReply
1232
+ && !this.assistantTextRepairFallbackPending) {
1233
+ this.planImplementationVisibleReplySent = true;
1234
+ this.clearAssistantTextRepairState();
1235
+ return false;
1236
+ }
1237
+ if (sawPostSteerReply && !this.assistantTextRepairFallbackPending) {
1238
+ this.clearAssistantTextRepairState();
1239
+ return false;
1240
+ }
1241
+ this.assistantTextRepairSteerActive = false;
1242
+ this.assistantTextRepairFallbackPending = false;
1243
+ if (this.assistantTextRepairStage === 'repair_turn_follow_up_steer' && !this.planImplementationActive) {
1244
+ this.activeTurnCompletion?.resolve({
1245
+ stopReason: 'failed',
1246
+ error: 'Codex app-server assistant text repair exceeded retry cap.',
1247
+ });
1248
+ return true;
1249
+ }
1250
+ if (this.assistantTextRepairAttempts >= ASSISTANT_TEXT_REPAIR_MAX_ATTEMPTS) {
1251
+ this.activeTurnCompletion?.resolve({
1252
+ stopReason: 'failed',
1253
+ error: 'Codex app-server assistant text repair exceeded retry cap.',
1254
+ });
1255
+ return true;
1256
+ }
1257
+ await this.startAssistantTextRepairTurn();
1258
+ return true;
1259
+ }
1260
+ if (!currentTurnAssistantText && !this.assistantTextRepairActive) {
1261
+ return false;
1262
+ }
1263
+ if (this.currentTurnSuccessfulChatSend && !this.assistantTextRepairActive) {
1264
+ await this.startAssistantTextRepairTurn();
1265
+ return true;
1266
+ }
1267
+ if (this.assistantTextRepairAttempts >= ASSISTANT_TEXT_REPAIR_MAX_ATTEMPTS) {
1268
+ this.activeTurnCompletion?.resolve({
1269
+ stopReason: 'failed',
1270
+ error: 'Codex app-server assistant text repair exceeded retry cap.',
1271
+ });
1272
+ return true;
1273
+ }
1274
+ await this.startAssistantTextRepairTurn();
1275
+ return true;
1276
+ }
1277
+ async startAssistantTextRepairSteer(stage = 'original_turn_steer') {
1278
+ if (!this.activeRunId || !this.currentTurnId) {
1279
+ return false;
1280
+ }
1281
+ const sourceText = normalizeOptionalString(this.currentTurnAssistantTextBuffer);
1282
+ if (!sourceText) {
1283
+ return false;
1284
+ }
1285
+ this.assistantTextRepairActive = true;
1286
+ this.assistantTextRepairSteerActive = true;
1287
+ this.assistantTextRepairFallbackPending = false;
1288
+ this.assistantTextRepairRequiresFinal = this.planImplementationActive;
1289
+ this.assistantTextRepairSteerChatSendCount = this.currentTurnSuccessfulChatSendCount;
1290
+ this.assistantTextRepairSteerFinalChatSendCount = this.currentTurnSuccessfulFinalChatSendCount;
1291
+ this.assistantTextRepairSourceText = sourceText;
1292
+ this.assistantTextRepairStage = stage;
1293
+ if (stage === 'original_turn_steer') {
1294
+ this.assistantTextRepairAttempts += 1;
1295
+ }
1296
+ if (this.assistantTextRepairRequiresFinal) {
1297
+ this.planImplementationVisibleReplySent = false;
1298
+ this.planImplementationRepairSourceText = sourceText;
1299
+ }
1300
+ const bigbangRepairGuidance = this.buildCurrentTurnBigbangRepairGuidance();
1301
+ try {
1302
+ return await this.requestTurnSteer(this.assistantTextRepairRequiresFinal
1303
+ ? buildCodexPlanImplementationSteerRepairPrompt(this.agentSurfaceMode, sourceText, bigbangRepairGuidance)
1304
+ : stage === 'repair_turn_follow_up_steer'
1305
+ ? buildCodexAssistantTextRepairFollowUpSteerPrompt(this.agentSurfaceMode, sourceText, bigbangRepairGuidance)
1306
+ : buildCodexAssistantTextSteerPrompt(this.agentSurfaceMode, sourceText, bigbangRepairGuidance));
1307
+ }
1308
+ catch (error) {
1309
+ log.warn('[codex-app-server] assistant-text repair steer failed', {
1310
+ sessionKey: this.sessionKey,
1311
+ runId: this.activeRunId,
1312
+ turnId: this.currentTurnId,
1313
+ error: String(error?.message ?? error),
1314
+ });
1315
+ return false;
1316
+ }
1317
+ }
1318
+ async startAssistantTextRepairTurn() {
1319
+ if (!this.client || !this.sessionId) {
1320
+ throw new Error('Codex app-server assistant text repair cannot continue without an active thread.');
1321
+ }
1322
+ if (!this.activeCompiledPolicy) {
1323
+ throw new Error('Codex app-server assistant text repair lost the active tool policy.');
1324
+ }
1325
+ const sourceText = normalizeOptionalString(this.assistantTextRepairSourceText);
1326
+ if (!sourceText) {
1327
+ throw new Error('Codex app-server assistant text repair has no buffered assistant text to repair.');
1328
+ }
1329
+ const bigbangRepairGuidance = this.buildCurrentTurnBigbangRepairGuidance();
1330
+ this.activeCodexMode = 'default';
1331
+ this.currentTurnId = null;
1332
+ this.resetRunBuffers();
1333
+ this.assistantTextRepairActive = true;
1334
+ this.assistantTextRepairSteerActive = false;
1335
+ this.assistantTextRepairFallbackPending = false;
1336
+ this.assistantTextRepairRequiresFinal = false;
1337
+ this.assistantTextRepairSteerChatSendCount = 0;
1338
+ this.assistantTextRepairSteerFinalChatSendCount = 0;
1339
+ this.assistantTextRepairAttempts += 1;
1340
+ this.assistantTextRepairStage = 'fresh_repair_turn';
1341
+ if (this.cancelRequested)
1342
+ return;
1343
+ const turnStartParams = await this.buildTurnStartParams({
1344
+ threadId: this.sessionId,
1345
+ compiledPolicy: this.activeCompiledPolicy,
1346
+ promptText: buildCodexAssistantTextRepairPrompt(this.agentSurfaceMode, sourceText, bigbangRepairGuidance),
1347
+ codexMode: 'default',
1348
+ });
1349
+ if (this.cancelRequested)
1350
+ return;
1351
+ await this.requestTurnStart(this.client, turnStartParams);
1352
+ if (this.cancelRequested && this.currentTurnId) {
1353
+ await this.interruptActiveTurn();
1354
+ }
1355
+ }
1356
+ async handleNotification(method, params) {
1357
+ if (method === 'mcpServer/startupStatus/updated') {
1358
+ const payload = params;
1359
+ const name = normalizeOptionalString(payload?.name);
1360
+ if (name) {
1361
+ this.mcpServerStartupStates.set(name, {
1362
+ status: normalizeOptionalString(payload?.status) ?? 'starting',
1363
+ error: normalizeOptionalString(payload?.error) ?? null,
1364
+ });
1365
+ this.resolvePendingMcpStatusWaiters();
1366
+ }
1367
+ return;
1368
+ }
1369
+ if (method === 'thread/started') {
1370
+ const sessionId = normalizeOptionalString(params?.thread?.id);
1371
+ if (sessionId) {
1372
+ if (this.sessionId !== sessionId) {
1373
+ this.memoryModeDisabledThreadId = null;
1374
+ }
1375
+ this.sessionId = sessionId;
1376
+ this.threadLoaded = true;
1377
+ }
1378
+ return;
1379
+ }
1380
+ if (method === 'turn/started') {
1381
+ const turnId = normalizeOptionalString(params?.turn?.id);
1382
+ if (turnId) {
1383
+ this.currentTurnId = turnId;
1384
+ this.awaitingTurnStartedNotification = false;
1385
+ if (this.cancelRequested) {
1386
+ await this.interruptActiveTurn();
1387
+ }
1388
+ }
1389
+ return;
1390
+ }
1391
+ if (method === 'item/agentMessage/delta') {
1392
+ const payload = params;
1393
+ if (payload?.itemId && typeof payload.delta === 'string' && payload.delta.length > 0) {
1394
+ this.markProviderOutputSeen();
1395
+ this.agentMessageBuffers.set(payload.itemId, `${this.agentMessageBuffers.get(payload.itemId) ?? ''}${payload.delta}`);
1396
+ if (this.soloMode || this.activeCodexMode === 'plan') {
1397
+ await this.sendAssistantText(payload.delta);
1398
+ }
1399
+ else {
1400
+ await this.captureAssistantTextViolation(payload.delta);
1401
+ }
1402
+ }
1403
+ return;
1404
+ }
1405
+ if (method === 'item/plan/delta') {
1406
+ const payload = params;
1407
+ if (payload?.itemId && typeof payload.delta === 'string' && payload.delta.length > 0) {
1408
+ this.markProviderOutputSeen();
1409
+ this.planItemBuffers.set(payload.itemId, `${this.planItemBuffers.get(payload.itemId) ?? ''}${payload.delta}`);
1410
+ }
1411
+ return;
1412
+ }
1413
+ if (method === 'item/reasoning/textDelta' || method === 'item/reasoning/summaryTextDelta') {
1414
+ const payload = params;
1415
+ if (payload?.itemId && typeof payload.delta === 'string' && payload.delta.length > 0) {
1416
+ this.markProviderOutputSeen();
1417
+ this.reasoningBuffers.set(payload.itemId, `${this.reasoningBuffers.get(payload.itemId) ?? ''}${payload.delta}`);
1418
+ await this.sendThinkingText(payload.delta);
1419
+ }
1420
+ return;
1421
+ }
1422
+ if (method === 'item/commandExecution/outputDelta' || method === 'command/exec/outputDelta') {
1423
+ const payload = params;
1424
+ const itemId = payload?.itemId ?? payload?.item_id;
1425
+ appendBufferedDelta(this.commandOutputBuffers, itemId, payload?.delta);
1426
+ await this.emitBufferedToolUpdate(itemId, this.commandOutputBuffers);
1427
+ return;
1428
+ }
1429
+ if (method === 'item/fileChange/outputDelta' || method === 'file_change/output_delta') {
1430
+ const payload = params;
1431
+ const itemId = payload?.itemId ?? payload?.item_id;
1432
+ appendBufferedDelta(this.fileChangeOutputBuffers, itemId, payload?.delta);
1433
+ await this.emitBufferedToolUpdate(itemId, this.fileChangeOutputBuffers);
1434
+ return;
1435
+ }
1436
+ if (method === 'turn/plan/updated') {
1437
+ const mapped = mapPlanUpdate(params);
1438
+ if (mapped) {
1439
+ this.markProviderOutputSeen();
1440
+ const detail = normalizeOptionalString(mapped.detail ?? mapped.title);
1441
+ if (this.activeCodexMode === 'plan' && detail) {
1442
+ this.latestPlanText = detail;
1443
+ }
1444
+ await this.activeSink?.sendUi?.({
1445
+ kind: 'plan',
1446
+ mode: this.activeUiMode,
1447
+ title: mapped.title,
1448
+ detail: mapped.detail,
1449
+ silent: true,
1450
+ });
1451
+ }
1452
+ return;
1453
+ }
1454
+ const usage = mapUsageNotification(method, params);
1455
+ if (usage) {
1456
+ await this.activeSink?.sendUi?.({
1457
+ kind: 'usage',
1458
+ mode: this.activeUiMode,
1459
+ ...usage,
1460
+ });
1461
+ return;
1462
+ }
1463
+ if (method === 'thread/compacted') {
1464
+ const payload = params;
1465
+ await this.emitCompactEvent({
1466
+ threadId: payload?.threadId,
1467
+ turnId: payload?.turnId,
1468
+ source: 'thread_compacted',
1469
+ });
1470
+ return;
1471
+ }
1472
+ if (method === 'rawResponseItem/completed') {
1473
+ const payload = params;
1474
+ const item = asRecord(payload?.item);
1475
+ if (item?.type === 'compaction') {
1476
+ await this.emitCompactEvent({
1477
+ threadId: payload?.threadId,
1478
+ turnId: payload?.turnId,
1479
+ itemId: normalizeOptionalString(item.id),
1480
+ source: 'raw_response_item',
1481
+ });
1482
+ }
1483
+ return;
1484
+ }
1485
+ const task = mapTaskNotification(method, params);
1486
+ if (task) {
1487
+ this.markProviderOutputSeen();
1488
+ await this.activeSink?.sendUi?.({
1489
+ kind: 'task',
1490
+ mode: this.activeUiMode,
1491
+ title: task.title,
1492
+ detail: task.detail,
1493
+ silent: true,
1494
+ });
1495
+ return;
1496
+ }
1497
+ if (method === 'item/started') {
1498
+ const item = params?.item;
1499
+ await this.handleThreadItem(item, { stage: 'start' });
1500
+ return;
1501
+ }
1502
+ if (method === 'item/updated') {
1503
+ const item = params?.item;
1504
+ await this.handleThreadItem(item, { stage: 'update' });
1505
+ return;
1506
+ }
1507
+ if (method === 'item/completed') {
1508
+ const item = params?.item;
1509
+ await this.handleThreadItem(item, { stage: 'complete' });
1510
+ return;
1511
+ }
1512
+ if (method === 'turn/completed') {
1513
+ this.markProviderOutputSeen();
1514
+ this.awaitingTurnStartedNotification = false;
1515
+ const payload = params;
1516
+ const turn = payload?.turn;
1517
+ await this.emitCompactEventsFromThreadItems(payload?.threadId, turn);
1518
+ const status = turn?.status ?? '';
1519
+ if (this.policyViolationError) {
1520
+ this.activeTurnCompletion?.resolve({
1521
+ stopReason: 'failed',
1522
+ error: this.policyViolationError,
1523
+ });
1524
+ }
1525
+ else if (status === 'failed') {
1526
+ this.activeTurnCompletion?.resolve({
1527
+ stopReason: 'failed',
1528
+ error: turn?.error?.message?.trim() || 'Codex app-server turn failed.',
1529
+ });
1530
+ }
1531
+ else if (status === 'interrupted' || this.cancelRequested) {
1532
+ if (!this.cancelRequested
1533
+ && (this.currentTurnAssistantTextRepairTriggered || this.assistantTextRepairSteerActive)) {
1534
+ await this.handleDefaultTurnCompleted();
1535
+ return;
1536
+ }
1537
+ this.activeTurnCompletion?.resolve({ stopReason: 'cancelled' });
1538
+ }
1539
+ else if (this.activeCodexMode === 'plan') {
1540
+ this.currentTurnId = null;
1541
+ this.pendingPlanApprovalAfterTurn = true;
1542
+ }
1543
+ else {
1544
+ await this.handleDefaultTurnCompleted();
1545
+ }
1546
+ return;
1547
+ }
1548
+ }
1549
+ async emitCompactEventsFromThreadItems(threadId, turn) {
1550
+ if (!Array.isArray(turn?.items))
1551
+ return;
1552
+ for (const item of turn.items) {
1553
+ const row = asRecord(item);
1554
+ if (row?.type !== 'contextCompaction')
1555
+ continue;
1556
+ await this.emitCompactEvent({
1557
+ threadId,
1558
+ turnId: normalizeOptionalString(turn.id) ?? this.currentTurnId,
1559
+ itemId: normalizeOptionalString(row.id),
1560
+ source: 'thread_item',
1561
+ });
1562
+ }
1563
+ }
1564
+ async emitCompactEvent(params) {
1565
+ const threadId = normalizeOptionalString(params.threadId) ?? this.sessionId;
1566
+ const turnId = normalizeOptionalString(params.turnId) ?? this.currentTurnId;
1567
+ if (!threadId || !turnId)
1568
+ return;
1569
+ const itemId = normalizeOptionalString(params.itemId);
1570
+ const eventKey = [threadId, turnId, itemId ?? '-'].join(':');
1571
+ if (this.compactEventKeys.has(eventKey))
1572
+ return;
1573
+ this.compactEventKeys.add(eventKey);
1574
+ this.markProviderOutputSeen();
1575
+ await this.activeSink?.sendUi?.({
1576
+ kind: 'compact',
1577
+ mode: this.activeUiMode,
1578
+ threadId,
1579
+ turnId,
1580
+ ...(itemId ? { itemId } : {}),
1581
+ source: params.source,
1582
+ eventKey,
1583
+ });
1584
+ }
1585
+ async handleThreadItem(item, options) {
1586
+ if (!item || typeof item !== 'object')
1587
+ return;
1588
+ const threadItem = item;
1589
+ const itemId = normalizeOptionalString(threadItem.id) ?? normalizeOptionalString(threadItem.toolCallId);
1590
+ if (!itemId)
1591
+ return;
1592
+ this.markProviderOutputSeen();
1593
+ this.captureShellToolCallInput(threadItem, itemId);
1594
+ const bigbangDetectionItem = this.buildShellToolResultBigbangDetectionItem(threadItem, itemId);
1595
+ if (isShellToolCallOrResultItem(threadItem)) {
1596
+ this.sawNonChatToolThisRun = true;
1597
+ }
1598
+ if (threadItem.type === 'contextCompaction') {
1599
+ await this.emitCompactEvent({
1600
+ threadId: this.sessionId,
1601
+ turnId: this.currentTurnId,
1602
+ itemId,
1603
+ source: 'thread_item',
1604
+ });
1605
+ return;
1606
+ }
1607
+ if (threadItem.type === 'agentMessage' && options.stage === 'complete') {
1608
+ const fullText = typeof threadItem.text === 'string' ? threadItem.text : '';
1609
+ const buffered = this.agentMessageBuffers.get(itemId) ?? '';
1610
+ const delta = fullText.startsWith(buffered) ? fullText.slice(buffered.length) : fullText;
1611
+ if (delta) {
1612
+ if (this.soloMode || this.activeCodexMode === 'plan') {
1613
+ await this.sendAssistantText(delta);
1614
+ }
1615
+ else {
1616
+ await this.captureAssistantTextViolation(delta);
1617
+ }
1618
+ }
1619
+ await this.captureProposedPlanFromText(fullText);
1620
+ this.agentMessageBuffers.delete(itemId);
1621
+ return;
1622
+ }
1623
+ if (threadItem.type === 'plan' && options.stage === 'complete') {
1624
+ const fullText = normalizeOptionalString(threadItem.text)
1625
+ ?? normalizeOptionalString(this.planItemBuffers.get(itemId))
1626
+ ?? '';
1627
+ await this.recordPlanText(fullText, 'plan updated');
1628
+ this.planItemBuffers.delete(itemId);
1629
+ return;
1630
+ }
1631
+ if (threadItem.type === 'reasoning' && options.stage === 'complete') {
1632
+ const fullText = [...(threadItem.summary ?? []), ...(threadItem.content ?? [])].join('\n').trim();
1633
+ const buffered = this.reasoningBuffers.get(itemId) ?? '';
1634
+ const delta = fullText.startsWith(buffered) ? fullText.slice(buffered.length) : fullText;
1635
+ if (delta) {
1636
+ await this.sendThinkingText(delta);
1637
+ }
1638
+ this.reasoningBuffers.delete(itemId);
1639
+ return;
1640
+ }
1641
+ const taskItem = mapTaskItem(threadItem, options.stage);
1642
+ if (taskItem) {
1643
+ await this.activeSink?.sendUi?.({
1644
+ kind: 'task',
1645
+ mode: this.activeUiMode,
1646
+ title: taskItem.title,
1647
+ detail: taskItem.detail,
1648
+ silent: true,
1649
+ });
1650
+ return;
1651
+ }
1652
+ const blockedKinds = this.activeBlockedKinds ?? new Map();
1653
+ const blockedKind = detectBlockedToolKind(threadItem, blockedKinds);
1654
+ if (blockedKind) {
1655
+ if (!this.policyViolationError) {
1656
+ this.policyViolationError = formatBlockedToolMessage(blockedKind, blockedKinds.get(blockedKind) ?? 'disabled');
1657
+ await this.interruptActiveTurn();
1658
+ }
1659
+ return;
1660
+ }
1661
+ const itemTouchesWorkspace = (threadItem.type === 'fileChange'
1662
+ && requestWritesWorkspace(threadItem, this.workspaceRoot)) || (threadItem.type === 'commandExecution'
1663
+ && commandExecutionTouchesWorkspace(threadItem, this.workspaceRoot));
1664
+ if (itemTouchesWorkspace) {
1665
+ try {
1666
+ await this.ensureActiveTurnWorkspaceWriteLock();
1667
+ }
1668
+ catch (error) {
1669
+ this.policyViolationError = `Workspace lock unavailable: ${String(error?.message ?? error)}`;
1670
+ await this.interruptActiveTurn();
1671
+ return;
1672
+ }
1673
+ }
1674
+ await this.captureProposedPlanFromToolItem(threadItem, options.stage);
1675
+ if (!this.soloMode) {
1676
+ this.capturePlanImplementationVisibleReplyFromToolItem(bigbangDetectionItem ?? threadItem, options.stage);
1677
+ this.captureSuccessfulChatSendForCurrentTurn(bigbangDetectionItem ?? threadItem, options.stage);
1678
+ }
1679
+ if (options.stage === 'complete' && threadItem.type === 'tool.result') {
1680
+ this.shellToolCallInputs.delete(itemId);
1681
+ }
1682
+ const mapped = mapToolItem(threadItem, {
1683
+ commandOutput: options.stage === 'complete'
1684
+ ? consumeBufferedOutput(this.commandOutputBuffers, itemId)
1685
+ : peekBufferedOutput(this.commandOutputBuffers, itemId),
1686
+ fileChangeOutput: options.stage === 'complete'
1687
+ ? consumeBufferedOutput(this.fileChangeOutputBuffers, itemId)
1688
+ : peekBufferedOutput(this.fileChangeOutputBuffers, itemId),
1689
+ });
1690
+ if (!mapped)
1691
+ return;
1692
+ if (this.toolCompletedIds.has(mapped.toolCallId)) {
1693
+ return;
1694
+ }
1695
+ if (!isChatMcpToolName(mapped.name)) {
1696
+ this.sawNonChatToolThisRun = true;
1697
+ }
1698
+ if (options.stage === 'start') {
1699
+ if (this.toolStartedIds.has(mapped.toolCallId))
1700
+ return;
1701
+ this.toolStartedIds.add(mapped.toolCallId);
1702
+ this.toolSnapshots.set(mapped.toolCallId, { name: mapped.name, input: mapped.input });
1703
+ await this.activeSink?.sendUi?.({
1704
+ kind: 'tool',
1705
+ mode: this.activeUiMode,
1706
+ title: mapped.name,
1707
+ toolCallId: mapped.toolCallId,
1708
+ input: mapped.input,
1709
+ stage: 'start',
1710
+ status: 'running',
1711
+ detail: mapped.detail,
1712
+ metadata: mapped.metadata,
1713
+ });
1714
+ return;
1715
+ }
1716
+ if (!this.toolStartedIds.has(mapped.toolCallId)) {
1717
+ this.toolStartedIds.add(mapped.toolCallId);
1718
+ this.toolSnapshots.set(mapped.toolCallId, { name: mapped.name, input: mapped.input });
1719
+ await this.activeSink?.sendUi?.({
1720
+ kind: 'tool',
1721
+ mode: this.activeUiMode,
1722
+ title: mapped.name,
1723
+ toolCallId: mapped.toolCallId,
1724
+ input: mapped.input,
1725
+ stage: 'start',
1726
+ status: 'running',
1727
+ detail: mapped.detail,
1728
+ metadata: mapped.metadata,
1729
+ });
1730
+ }
1731
+ if (options.stage === 'update' || mapped.inProgress) {
1732
+ this.toolSnapshots.set(mapped.toolCallId, { name: mapped.name, input: mapped.input });
1733
+ await this.activeSink?.sendUi?.({
1734
+ kind: 'tool',
1735
+ mode: this.activeUiMode,
1736
+ title: mapped.name,
1737
+ toolCallId: mapped.toolCallId,
1738
+ input: mapped.input,
1739
+ detail: mapped.detail ?? mapped.output,
1740
+ stage: 'update',
1741
+ status: 'running',
1742
+ metadata: mapped.metadata,
1743
+ });
1744
+ return;
1745
+ }
1746
+ await this.activeSink?.sendUi?.({
1747
+ kind: 'tool',
1748
+ mode: this.activeUiMode,
1749
+ title: mapped.name,
1750
+ toolCallId: mapped.toolCallId,
1751
+ input: mapped.input,
1752
+ output: mapped.output,
1753
+ stage: 'complete',
1754
+ status: mapped.status,
1755
+ detail: mapped.output,
1756
+ metadata: mapped.metadata,
1757
+ });
1758
+ this.toolCompletedIds.add(mapped.toolCallId);
1759
+ }
1760
+ captureShellToolCallInput(item, itemId) {
1761
+ if (item.type !== 'tool.call' || item.name !== 'shell')
1762
+ return;
1763
+ const input = asRecord(item.input);
1764
+ const command = normalizeOptionalString(input?.command) ?? normalizeOptionalString(item.command);
1765
+ if (!command)
1766
+ return;
1767
+ const cwd = normalizeOptionalString(input?.cwd) ?? normalizeOptionalString(item.cwd);
1768
+ this.shellToolCallInputs.set(itemId, { command, cwd });
1769
+ }
1770
+ buildShellToolResultBigbangDetectionItem(item, itemId) {
1771
+ if (item.type !== 'tool.result')
1772
+ return null;
1773
+ const recorded = this.shellToolCallInputs.get(itemId);
1774
+ const input = asRecord(item.input);
1775
+ const command = recorded?.command
1776
+ ?? normalizeOptionalString(input?.command)
1777
+ ?? normalizeOptionalString(item.command);
1778
+ if (!command)
1779
+ return null;
1780
+ return {
1781
+ type: 'shellToolResult',
1782
+ command,
1783
+ cwd: recorded?.cwd ?? normalizeOptionalString(input?.cwd) ?? normalizeOptionalString(item.cwd),
1784
+ status: item.status,
1785
+ success: item.success,
1786
+ output: item.output,
1787
+ result: item.result,
1788
+ error: item.error,
1789
+ };
1790
+ }
1791
+ async captureProposedPlanFromText(text) {
1792
+ const proposedPlan = extractProposedPlanText(text);
1793
+ if (!proposedPlan)
1794
+ return;
1795
+ await this.recordPlanText(proposedPlan, 'Plan');
1796
+ }
1797
+ async captureProposedPlanFromToolItem(item, stage) {
1798
+ if (stage !== 'complete')
1799
+ return;
1800
+ if (!isChatSendMessageTool(item))
1801
+ return;
1802
+ const args = asRecord(item.arguments);
1803
+ const content = normalizeOptionalString(args?.content);
1804
+ if (!content)
1805
+ return;
1806
+ await this.captureProposedPlanFromText(content);
1807
+ }
1808
+ capturePlanImplementationVisibleReplyFromToolItem(item, stage) {
1809
+ if (stage !== 'complete')
1810
+ return;
1811
+ const bigbangMessageSend = getBigbangMessageSendInfo(item);
1812
+ const mcpMessageSend = isChatSendMessageTool(item);
1813
+ if (!bigbangMessageSend && !mcpMessageSend)
1814
+ return;
1815
+ if (this.agentSurfaceMode === 'bigbang' ? !bigbangMessageSend : !mcpMessageSend)
1816
+ return;
1817
+ const args = asRecord(item.arguments);
1818
+ const content = normalizeOptionalString(args?.content) ?? (bigbangMessageSend ? '<bigbang-message-send>' : null);
1819
+ const kind = bigbangMessageSend?.kind ?? normalizeOptionalString(args?.kind)?.toLowerCase() ?? null;
1820
+ if (!content)
1821
+ return;
1822
+ if (kind !== 'final')
1823
+ return;
1824
+ const status = normalizeToolItemStatus(item.status, item.success);
1825
+ if (bigbangMessageSend?.success !== true && (status.status !== 'completed' || item.error || bigbangMessageSend?.success === false))
1826
+ return;
1827
+ if (isNonPostedSendMessageResult(item.result))
1828
+ return;
1829
+ if (this.planImplementationActive) {
1830
+ this.planImplementationVisibleReplySent = true;
1831
+ }
1832
+ this.currentTurnSuccessfulFinalChatSendCount += 1;
1833
+ this.runSuccessfulFinalChatSendCount += 1;
1834
+ if (bigbangMessageSend) {
1835
+ this.currentTurnSuccessfulBigbangFinalChatSendCount += 1;
1836
+ }
1837
+ }
1838
+ captureSuccessfulChatSendForCurrentTurn(item, stage) {
1839
+ if (stage !== 'complete')
1840
+ return;
1841
+ const bigbangMessageSend = getBigbangMessageSendInfo(item);
1842
+ const mcpMessageSend = isChatSendMessageTool(item);
1843
+ if (!bigbangMessageSend && !mcpMessageSend)
1844
+ return;
1845
+ if (this.agentSurfaceMode === 'bigbang' ? !bigbangMessageSend : !mcpMessageSend)
1846
+ return;
1847
+ if (bigbangMessageSend?.success === true) {
1848
+ this.lastBigbangMessageSendFailureForRepair = null;
1849
+ }
1850
+ else if (bigbangMessageSend?.failureKind) {
1851
+ this.currentTurnBigbangMessageSendFailures.push({
1852
+ messageKind: bigbangMessageSend.kind,
1853
+ failureKind: bigbangMessageSend.failureKind,
1854
+ });
1855
+ }
1856
+ if (isCompletionSafeNonPostedSendMessageResult({ result: item.result, error: item.error })) {
1857
+ this.currentTurnCompletionSafeNonPostedChatSend = true;
1858
+ return;
1859
+ }
1860
+ const status = normalizeToolItemStatus(item.status, item.success);
1861
+ if (bigbangMessageSend?.success !== true && (status.status !== 'completed' || item.error || bigbangMessageSend?.success === false))
1862
+ return;
1863
+ if (isNonPostedSendMessageResult(item.result))
1864
+ return;
1865
+ this.currentTurnSuccessfulChatSend = true;
1866
+ this.currentTurnSuccessfulChatSendCount += 1;
1867
+ this.runSuccessfulChatSendCount += 1;
1868
+ }
1869
+ async recordPlanText(planText, title = 'Plan') {
1870
+ if (this.activeCodexMode !== 'plan')
1871
+ return;
1872
+ const normalized = normalizeOptionalString(planText);
1873
+ if (!normalized)
1874
+ return;
1875
+ this.latestPlanText = normalized;
1876
+ await this.activeSink?.sendUi?.({
1877
+ kind: 'plan',
1878
+ mode: this.activeUiMode,
1879
+ title,
1880
+ detail: normalized,
1881
+ silent: true,
1882
+ });
1883
+ }
1884
+ async emitBufferedToolUpdate(itemId, store) {
1885
+ const normalizedItemId = normalizeOptionalString(itemId);
1886
+ if (!normalizedItemId || !this.toolStartedIds.has(normalizedItemId))
1887
+ return;
1888
+ const snapshot = this.toolSnapshots.get(normalizedItemId);
1889
+ if (!snapshot)
1890
+ return;
1891
+ const detail = peekBufferedOutput(store, normalizedItemId);
1892
+ if (!detail)
1893
+ return;
1894
+ await this.activeSink?.sendUi?.({
1895
+ kind: 'tool',
1896
+ mode: this.activeUiMode,
1897
+ title: snapshot.name,
1898
+ toolCallId: normalizedItemId,
1899
+ input: snapshot.input,
1900
+ detail,
1901
+ stage: 'update',
1902
+ status: 'running',
1903
+ });
1904
+ }
1905
+ async sendAssistantText(text) {
1906
+ if (!text)
1907
+ return;
1908
+ this.markProviderOutputSeen();
1909
+ if (this.planImplementationActive && this.activeCodexMode !== 'plan') {
1910
+ this.planImplementationDeltaBuffer += text;
1911
+ }
1912
+ if (this.activeSink?.sendAgentText) {
1913
+ await this.activeSink.sendAgentText(text);
1914
+ return;
1915
+ }
1916
+ await this.activeSink?.sendText(text);
1917
+ }
1918
+ async sendActivityAssistantText(text) {
1919
+ if (!text)
1920
+ return;
1921
+ this.markProviderOutputSeen();
1922
+ await this.activeSink?.sendActivityText?.(text);
1923
+ }
1924
+ async captureAssistantTextViolation(text) {
1925
+ if (!text)
1926
+ return;
1927
+ if (this.planImplementationActive) {
1928
+ this.planImplementationDeltaBuffer += text;
1929
+ }
1930
+ this.currentTurnAssistantTextBuffer += text;
1931
+ await this.sendActivityAssistantText(text);
1932
+ if (this.assistantTextRepairSteerActive) {
1933
+ this.assistantTextRepairFallbackPending = true;
1934
+ }
1935
+ else if (this.assistantTextRepairActive
1936
+ && !this.planImplementationActive
1937
+ && this.assistantTextRepairStage === 'fresh_repair_turn'
1938
+ && this.activeCodexMode !== 'plan'
1939
+ && this.currentTurnId
1940
+ && this.activeRunId) {
1941
+ const steered = await this.startAssistantTextRepairSteer('repair_turn_follow_up_steer');
1942
+ if (steered) {
1943
+ return;
1944
+ }
1945
+ this.assistantTextRepairSteerActive = false;
1946
+ this.assistantTextRepairFallbackPending = true;
1947
+ }
1948
+ else if (!this.assistantTextRepairActive
1949
+ && this.activeCodexMode !== 'plan'
1950
+ && this.currentTurnId
1951
+ && this.activeRunId) {
1952
+ const steered = await this.startAssistantTextRepairSteer();
1953
+ if (steered) {
1954
+ return;
1955
+ }
1956
+ this.assistantTextRepairSteerActive = false;
1957
+ this.assistantTextRepairFallbackPending = true;
1958
+ }
1959
+ if (!this.currentTurnAssistantTextRepairTriggered) {
1960
+ this.currentTurnAssistantTextRepairTriggered = true;
1961
+ if (this.currentTurnId && !this.cancelRequested) {
1962
+ await this.interruptActiveTurn();
1963
+ }
1964
+ }
1965
+ }
1966
+ async sendThinkingText(text) {
1967
+ if (!text)
1968
+ return;
1969
+ this.markProviderOutputSeen();
1970
+ await this.activeSink?.sendThinkingText?.(text);
1971
+ }
1972
+ async handleCommandExecutionApprovalRequest(params) {
1973
+ this.markProviderOutputSeen();
1974
+ const blockedKinds = this.getEffectiveBlockedKinds();
1975
+ const blockedKind = detectBlockedToolKindForCommandApproval(params, blockedKinds);
1976
+ if (blockedKind) {
1977
+ await this.emitBlockedApprovalEvent(blockedKind, params);
1978
+ return { decision: 'decline' };
1979
+ }
1980
+ if (commandExecutionTouchesWorkspace(params, this.workspaceRoot)) {
1981
+ await this.ensureActiveTurnWorkspaceWriteLock();
1982
+ }
1983
+ return { decision: 'accept' };
1984
+ }
1985
+ async handleFileChangeApprovalRequest(params) {
1986
+ this.markProviderOutputSeen();
1987
+ const blockedKinds = this.getEffectiveBlockedKinds();
1988
+ const blockedKind = detectBlockedToolKindForApproval('item/fileChange/requestApproval', params, blockedKinds);
1989
+ if (blockedKind) {
1990
+ await this.emitBlockedApprovalEvent(blockedKind, params);
1991
+ return { decision: 'decline' };
1992
+ }
1993
+ if (requestWritesWorkspace(params, this.workspaceRoot)) {
1994
+ await this.ensureActiveTurnWorkspaceWriteLock();
1995
+ }
1996
+ return { decision: 'accept' };
1997
+ }
1998
+ async handlePermissionsApprovalRequest(params) {
1999
+ this.markProviderOutputSeen();
2000
+ const blockedKinds = this.getEffectiveBlockedKinds();
2001
+ const blockedKind = detectBlockedToolKindForApproval('item/permissions/requestApproval', params, blockedKinds);
2002
+ if (blockedKind) {
2003
+ await this.emitBlockedApprovalEvent(blockedKind, params);
2004
+ }
2005
+ if (!blockedKinds.has('edit') && permissionsRequestWritesWorkspace(params, this.workspaceRoot)) {
2006
+ await this.ensureActiveTurnWorkspaceWriteLock();
2007
+ }
2008
+ else if (!blockedKinds.has('read') && permissionsRequestReadsWorkspace(params, this.workspaceRoot)) {
2009
+ await this.waitForWorkspaceWritesBeforeRead();
2010
+ }
2011
+ return buildPermissionsApprovalResponse(params, blockedKinds);
2012
+ }
2013
+ async handleUserInputRequest(params) {
2014
+ this.markProviderOutputSeen();
2015
+ if (!this.activeSink?.requestPermission) {
2016
+ return buildUserInputResponse(params);
2017
+ }
2018
+ if (this.pendingUserInput) {
2019
+ throw new Error('Another Codex app-server user input request is already pending.');
2020
+ }
2021
+ const requestId = buildUserInputRequestId(params, this.sessionKey);
2022
+ let resolvePending;
2023
+ const pending = new Promise((resolve) => {
2024
+ resolvePending = resolve;
2025
+ });
2026
+ this.pendingUserInput = {
2027
+ requestId,
2028
+ resolve: resolvePending,
2029
+ params,
2030
+ };
2031
+ try {
2032
+ await this.activeSink.requestPermission({
2033
+ uiMode: this.activeUiMode,
2034
+ sessionKey: this.sessionKey,
2035
+ requestId,
2036
+ toolTitle: 'Codex requested user input',
2037
+ toolKind: 'other',
2038
+ toolName: 'request_user_input',
2039
+ toolArgs: params,
2040
+ approvalKind: 'question',
2041
+ title: 'Question',
2042
+ input: { questions: normalizeCodexQuestionPrompts(params) },
2043
+ });
2044
+ }
2045
+ catch (error) {
2046
+ if (this.pendingUserInput?.requestId === requestId) {
2047
+ this.pendingUserInput = null;
2048
+ }
2049
+ throw error;
2050
+ }
2051
+ return await pending;
2052
+ }
2053
+ async emitPlanApprovalRequest(planText) {
2054
+ this.markProviderOutputSeen();
2055
+ if (!this.activeSink?.requestPermission) {
2056
+ this.activeTurnCompletion?.resolve({ stopReason: 'completed' });
2057
+ return;
2058
+ }
2059
+ const requestId = `codex-plan:${randomUUID()}`;
2060
+ this.pendingPlanApproval = {
2061
+ requestId,
2062
+ planText,
2063
+ };
2064
+ await this.activeSink.requestPermission({
2065
+ uiMode: this.activeUiMode,
2066
+ sessionKey: this.sessionKey,
2067
+ requestId,
2068
+ toolTitle: 'Codex plan approval',
2069
+ toolKind: 'other',
2070
+ toolName: 'CodexPlanApproval',
2071
+ toolArgs: { plan: planText },
2072
+ approvalKind: 'plan',
2073
+ title: 'Plan',
2074
+ description: 'Review the proposed plan before implementation starts.',
2075
+ input: { plan: planText },
2076
+ actions: buildPlanApprovalActions(),
2077
+ });
2078
+ }
2079
+ async emitBlockedApprovalEvent(toolKind, params) {
2080
+ const blockedKinds = this.getEffectiveBlockedKinds();
2081
+ const message = formatBlockedToolMessage(toolKind, blockedKinds.get(toolKind) ?? 'disabled');
2082
+ const toolCallId = extractApprovalItemId(params) ?? `approval-denied-${randomUUID()}`;
2083
+ await this.activeSink?.sendUi?.({
2084
+ kind: 'tool',
2085
+ mode: this.activeUiMode,
2086
+ title: 'permission_denied',
2087
+ toolCallId,
2088
+ input: params,
2089
+ stage: 'start',
2090
+ });
2091
+ await this.activeSink?.sendUi?.({
2092
+ kind: 'tool',
2093
+ mode: this.activeUiMode,
2094
+ title: 'permission_denied',
2095
+ toolCallId,
2096
+ input: params,
2097
+ output: message,
2098
+ detail: message,
2099
+ stage: 'complete',
2100
+ status: 'failed',
2101
+ });
2102
+ }
2103
+ getEffectiveBlockedKinds() {
2104
+ return this.activeBlockedKinds
2105
+ ? this.activeBlockedKinds
2106
+ : this.buildCompiledToolPolicy().blockedKinds;
2107
+ }
2108
+ buildThreadStartParams(params, options) {
2109
+ return {
2110
+ ...(this.model ? { model: this.model } : {}),
2111
+ ...(this.serviceTier ? { serviceTier: this.serviceTier } : {}),
2112
+ cwd: this.workspaceRoot,
2113
+ approvalPolicy: 'never',
2114
+ ...(params.policy.permissionProfile
2115
+ ? { permissionProfile: params.policy.permissionProfile }
2116
+ : { sandbox: 'danger-full-access' }),
2117
+ ...(params.systemPromptText ? { developerInstructions: params.systemPromptText } : {}),
2118
+ ...(params.policy.threadConfig ? { config: params.policy.threadConfig } : {}),
2119
+ experimentalRawEvents: false,
2120
+ ...(options.persistExtendedHistory ? { persistExtendedHistory: true } : {}),
2121
+ };
2122
+ }
2123
+ buildThreadResumeParams(params, options) {
2124
+ return {
2125
+ threadId: params.sessionId,
2126
+ ...(this.model ? { model: this.model } : {}),
2127
+ ...(this.serviceTier ? { serviceTier: this.serviceTier } : {}),
2128
+ cwd: this.workspaceRoot,
2129
+ approvalPolicy: 'never',
2130
+ ...(params.policy.permissionProfile
2131
+ ? { permissionProfile: params.policy.permissionProfile }
2132
+ : { sandbox: 'danger-full-access' }),
2133
+ ...(params.systemPromptText ? { developerInstructions: params.systemPromptText } : {}),
2134
+ ...(params.policy.threadConfig ? { config: params.policy.threadConfig } : {}),
2135
+ ...(options.persistExtendedHistory ? { persistExtendedHistory: true } : {}),
2136
+ };
2137
+ }
2138
+ async buildTurnStartParams(params) {
2139
+ const codexMode = params.codexMode ?? this.activeCodexMode;
2140
+ const collaborationMode = await this.buildCollaborationMode(codexMode);
2141
+ const promptText = codexMode === 'plan'
2142
+ ? joinPromptContext(CODEX_PLAN_MODE_PROMPT, params.promptText)
2143
+ : params.promptText;
2144
+ const materializedAttachments = await this.materializeAssetAttachments(params.attachments, this.activeRunId);
2145
+ return {
2146
+ threadId: params.threadId,
2147
+ input: buildCodexTurnInput(promptText, materializedAttachments),
2148
+ cwd: this.workspaceRoot,
2149
+ approvalPolicy: 'never',
2150
+ ...(params.compiledPolicy.permissionProfile
2151
+ ? { permissionProfile: params.compiledPolicy.permissionProfile }
2152
+ : { sandboxPolicy: { type: 'dangerFullAccess' } }),
2153
+ ...(this.model ? { model: this.model } : {}),
2154
+ ...(this.serviceTier ? { serviceTier: this.serviceTier } : {}),
2155
+ ...(this.reasoningEffort ? { effort: this.reasoningEffort } : {}),
2156
+ ...(collaborationMode ? { collaborationMode } : {}),
2157
+ };
2158
+ }
2159
+ async materializeAssetAttachments(attachments, runId) {
2160
+ return this.attachmentMaterializer.materialize(attachments, { runId });
2161
+ }
2162
+ async buildCollaborationMode(codexMode) {
2163
+ const mode = codexMode === 'plan' ? 'plan' : 'default';
2164
+ if (mode === 'default' && !this.model && !this.cachedCollaborationModeModel) {
2165
+ return null;
2166
+ }
2167
+ const model = await this.resolveCollaborationModeModel({ strict: mode === 'plan' });
2168
+ return {
2169
+ mode,
2170
+ settings: {
2171
+ model,
2172
+ reasoning_effort: this.reasoningEffort,
2173
+ developer_instructions: null,
2174
+ },
2175
+ };
2176
+ }
2177
+ async resolveCollaborationModeModel(options) {
2178
+ if (this.model)
2179
+ return this.model;
2180
+ if (this.cachedCollaborationModeModel)
2181
+ return this.cachedCollaborationModeModel;
2182
+ if (!options.strict)
2183
+ return 'default';
2184
+ const client = await this.ensureClient();
2185
+ let defaultModel = null;
2186
+ try {
2187
+ const response = await client.request('model/list', { limit: 100 }, MODEL_LIST_TIMEOUT_MS);
2188
+ defaultModel = extractDefaultCodexModel(response);
2189
+ }
2190
+ catch (error) {
2191
+ if (options.strict) {
2192
+ throw error;
2193
+ }
2194
+ log.warn('[codex-app-server] failed to resolve default collaboration mode model; using app-server default sentinel', {
2195
+ sessionKey: this.sessionKey,
2196
+ error: String(error?.message ?? error),
2197
+ });
2198
+ }
2199
+ if (!defaultModel) {
2200
+ if (!options.strict) {
2201
+ this.cachedCollaborationModeModel = 'default';
2202
+ return this.cachedCollaborationModeModel;
2203
+ }
2204
+ throw new Error('Codex plan mode requires a model, but model/list did not return a default model.');
2205
+ }
2206
+ this.cachedCollaborationModeModel = defaultModel;
2207
+ return defaultModel;
2208
+ }
2209
+ buildCompiledToolPolicy(overrideDisabledToolKinds = []) {
2210
+ const blockedKinds = new Map();
2211
+ for (const toolKind of overrideDisabledToolKinds) {
2212
+ blockedKinds.set(toolKind, 'disabled');
2213
+ }
2214
+ if (this.toolAuth && this.bindingKey) {
2215
+ for (const policy of this.toolAuth.listPersistentPolicies(this.bindingKey, 'reject')) {
2216
+ if (!blockedKinds.has(policy.toolKind)) {
2217
+ blockedKinds.set(policy.toolKind, 'policy_reject');
2218
+ }
2219
+ }
2220
+ }
2221
+ const permissionProfile = buildPermissionProfile({
2222
+ workspaceRoot: this.workspaceRoot,
2223
+ blockedKinds,
2224
+ });
2225
+ const threadConfig = this.buildThreadConfig({
2226
+ blockedKinds,
2227
+ permissionProfile,
2228
+ });
2229
+ return {
2230
+ blockedKinds,
2231
+ permissionProfile,
2232
+ threadConfig,
2233
+ promptPreamble: buildToolPolicyPromptPreamble(blockedKinds),
2234
+ threadPolicySignature: JSON.stringify({
2235
+ blockedKinds: [...blockedKinds.entries()].sort(([left], [right]) => left.localeCompare(right)),
2236
+ permissionProfile,
2237
+ threadConfig,
2238
+ }),
2239
+ };
2240
+ }
2241
+ buildThreadConfig(params) {
2242
+ const channelBridgeConfig = this.agentSurfaceMode === 'mcp'
2243
+ ? toCodexMcpServerConfig(this.channelBridgeMcpEntry, this.workspaceRoot)
2244
+ : null;
2245
+ const config = {};
2246
+ if (channelBridgeConfig) {
2247
+ config.mcp_servers = {
2248
+ [channelBridgeConfig.name]: channelBridgeConfig.config,
2249
+ };
2250
+ }
2251
+ if (params.blockedKinds.has('fetch') || params.permissionProfile?.network?.enabled === false) {
2252
+ config.web_search = 'disabled';
2253
+ }
2254
+ return Object.keys(config).length > 0 ? config : null;
2255
+ }
2256
+ resetConfiguredMcpServerStates() {
2257
+ for (const name of this.getConfiguredMcpServerNames()) {
2258
+ this.mcpServerStartupStates.delete(name);
2259
+ }
2260
+ this.resolvePendingMcpStatusWaiters();
2261
+ }
2262
+ getConfiguredMcpServerNames() {
2263
+ if (this.agentSurfaceMode !== 'mcp')
2264
+ return [];
2265
+ const entry = toCodexMcpServerConfig(this.channelBridgeMcpEntry, this.workspaceRoot);
2266
+ return entry ? [entry.name] : [];
2267
+ }
2268
+ async waitForConfiguredMcpServersReady() {
2269
+ const configuredServerNames = this.getConfiguredMcpServerNames();
2270
+ if (configuredServerNames.length === 0)
2271
+ return;
2272
+ const deadline = Date.now() + MCP_STARTUP_TIMEOUT_MS;
2273
+ for (const serverName of configuredServerNames) {
2274
+ for (;;) {
2275
+ const state = this.mcpServerStartupStates.get(serverName);
2276
+ if (state?.status === 'ready')
2277
+ break;
2278
+ if (state && state.status !== 'starting') {
2279
+ throw new Error(state.error ?? `Codex MCP server ${serverName} failed to start (${state.status}).`);
2280
+ }
2281
+ const remainingMs = deadline - Date.now();
2282
+ if (remainingMs <= 0) {
2283
+ throw new Error(`Timed out waiting for Codex MCP server ${serverName} to become ready.`);
2284
+ }
2285
+ await this.waitForMcpStatusChange(Math.min(remainingMs, 250));
2286
+ }
2287
+ }
2288
+ }
2289
+ waitForMcpStatusChange(timeoutMs) {
2290
+ return new Promise((resolve) => {
2291
+ let settled = false;
2292
+ const done = () => {
2293
+ if (settled)
2294
+ return;
2295
+ settled = true;
2296
+ clearTimeout(timer);
2297
+ this.mcpStatusChangeWaiters.delete(done);
2298
+ resolve();
2299
+ };
2300
+ const timer = setTimeout(done, timeoutMs);
2301
+ this.mcpStatusChangeWaiters.add(done);
2302
+ });
2303
+ }
2304
+ async waitForActiveTurnId(timeoutMs) {
2305
+ if (this.currentTurnId)
2306
+ return this.currentTurnId;
2307
+ const deadline = Date.now() + timeoutMs;
2308
+ while (!this.currentTurnId && this.activeRunId && !this.cancelRequested && Date.now() < deadline) {
2309
+ await delay(25);
2310
+ }
2311
+ return this.currentTurnId;
2312
+ }
2313
+ resolvePendingMcpStatusWaiters() {
2314
+ const waiters = [...this.mcpStatusChangeWaiters];
2315
+ this.mcpStatusChangeWaiters.clear();
2316
+ for (const waiter of waiters) {
2317
+ waiter();
2318
+ }
2319
+ }
2320
+ }
2321
+ function delay(ms) {
2322
+ return new Promise((resolve) => setTimeout(resolve, ms));
2323
+ }
2324
+ function normalizeOptionalString(value) {
2325
+ if (typeof value !== 'string')
2326
+ return null;
2327
+ const normalized = value.trim();
2328
+ return normalized ? normalized : null;
2329
+ }
2330
+ function normalizeCodexReasoningEffort(value) {
2331
+ const normalized = normalizeOptionalString(value);
2332
+ if (!normalized)
2333
+ return null;
2334
+ switch (normalized) {
2335
+ case 'none':
2336
+ case 'minimal':
2337
+ case 'low':
2338
+ case 'medium':
2339
+ case 'high':
2340
+ case 'xhigh':
2341
+ return normalized;
2342
+ default:
2343
+ return null;
2344
+ }
2345
+ }
2346
+ function normalizeCodexMode(value) {
2347
+ return value === 'default' || value === 'plan' ? value : null;
2348
+ }
2349
+ function normalizeCodexServiceTier(value) {
2350
+ return value === 'fast' || value === 'flex' ? value : null;
2351
+ }
2352
+ function extractDefaultCodexModel(value) {
2353
+ const data = Array.isArray(value?.data)
2354
+ ? value.data
2355
+ : [];
2356
+ const defaultModel = data.find((entry) => entry?.isDefault === true)
2357
+ ?? data[0];
2358
+ if (!defaultModel || typeof defaultModel !== 'object')
2359
+ return null;
2360
+ const model = defaultModel;
2361
+ return normalizeOptionalString(model.model) ?? normalizeOptionalString(model.id);
2362
+ }
2363
+ function toCodexMcpServerConfig(entry, workspaceRoot) {
2364
+ if (!entry)
2365
+ return null;
2366
+ if ('command' in entry) {
2367
+ return {
2368
+ name: entry.name,
2369
+ config: {
2370
+ command: entry.command,
2371
+ args: [...entry.args],
2372
+ cwd: workspaceRoot,
2373
+ env: Object.fromEntries((entry.env ?? []).map((item) => [item.name, item.value])),
2374
+ enabled: true,
2375
+ required: true,
2376
+ default_tools_approval_mode: 'approve',
2377
+ },
2378
+ };
2379
+ }
2380
+ if (entry.type === 'http' || entry.type === 'sse') {
2381
+ return {
2382
+ name: entry.name,
2383
+ config: {
2384
+ url: entry.url,
2385
+ http_headers: Object.fromEntries((entry.headers ?? []).map((item) => [item.name, item.value])),
2386
+ enabled: true,
2387
+ required: true,
2388
+ default_tools_approval_mode: 'approve',
2389
+ },
2390
+ };
2391
+ }
2392
+ return null;
2393
+ }
2394
+ function joinPromptContext(...parts) {
2395
+ return parts
2396
+ .map((part) => part?.trim())
2397
+ .filter((part) => Boolean(part))
2398
+ .join('\n\n');
2399
+ }
2400
+ function buildCodexTurnInput(promptText, attachments) {
2401
+ const blocks = [
2402
+ { type: 'text', text: promptText, text_elements: [] },
2403
+ ];
2404
+ for (const [index, attachment] of (attachments ?? []).entries()) {
2405
+ const imagePath = resolveLocalImageAttachmentPath(attachment);
2406
+ if (imagePath) {
2407
+ blocks.push({ type: 'localImage', path: imagePath });
2408
+ continue;
2409
+ }
2410
+ const localPath = normalizeOptionalString(attachment.preferredLocalPath) ?? resolveLocalAttachmentPath(attachment);
2411
+ const attachmentId = normalizeOptionalString(attachment.assetId) ?? extractAttachmentIdFromUri(attachment.uri);
2412
+ if (!localPath && attachmentId && promptAlreadyReferencesAttachment(promptText, attachmentId)) {
2413
+ continue;
2414
+ }
2415
+ blocks.push({
2416
+ type: 'text',
2417
+ text: formatAttachmentTextReference(attachment, index, localPath, attachmentId),
2418
+ text_elements: [],
2419
+ });
2420
+ }
2421
+ return blocks;
2422
+ }
2423
+ function appendBufferedDelta(store, itemId, delta) {
2424
+ if (!itemId || !delta)
2425
+ return;
2426
+ const existing = store.get(itemId) ?? [];
2427
+ existing.push(delta);
2428
+ store.set(itemId, existing);
2429
+ }
2430
+ function consumeBufferedOutput(store, itemId) {
2431
+ const chunks = store.get(itemId);
2432
+ if (!chunks || chunks.length === 0)
2433
+ return null;
2434
+ store.delete(itemId);
2435
+ return chunks.join('');
2436
+ }
2437
+ function peekBufferedOutput(store, itemId) {
2438
+ const chunks = store.get(itemId);
2439
+ if (!chunks || chunks.length === 0)
2440
+ return null;
2441
+ return chunks.join('');
2442
+ }
2443
+ function mergeAggregatedAndBufferedOutput(aggregated, buffered) {
2444
+ if (aggregated && buffered) {
2445
+ if (aggregated.includes(buffered))
2446
+ return aggregated;
2447
+ if (buffered.includes(aggregated))
2448
+ return buffered;
2449
+ return [aggregated, buffered].join('\n\n');
2450
+ }
2451
+ return aggregated ?? buffered ?? null;
2452
+ }
2453
+ function summarizeJson(value) {
2454
+ if (typeof value === 'string')
2455
+ return value;
2456
+ try {
2457
+ const json = JSON.stringify(value, null, 2);
2458
+ return typeof json === 'string' ? json : String(value);
2459
+ }
2460
+ catch {
2461
+ return String(value);
2462
+ }
2463
+ }
2464
+ function isHeldStaleSendMessageResult(value) {
2465
+ if (isHeldStaleResultShape(value))
2466
+ return true;
2467
+ const text = summarizeJson(value);
2468
+ return text.includes('Draft held; nothing was posted')
2469
+ || text.includes('Your draft was not sent because newer messages exist on this surface');
2470
+ }
2471
+ function isNonPostedSendMessageResult(value) {
2472
+ if (isHeldStaleSendMessageResult(value))
2473
+ return true;
2474
+ const text = summarizeJson(value);
2475
+ return text.includes('Nothing was confirmed posted')
2476
+ || text.includes('success without a messageId');
2477
+ }
2478
+ function isCompletionSafeNonPostedSendMessageResult(value) {
2479
+ if (isTaskThreadPostFinalDuplicateResultShape(value))
2480
+ return true;
2481
+ const record = asRecord(value);
2482
+ const errorTexts = [
2483
+ normalizeOptionalString(record?.error),
2484
+ normalizeOptionalString(asRecord(record?.error)?.message),
2485
+ normalizeOptionalString(asRecord(record?.result)?.error),
2486
+ normalizeOptionalString(asRecord(record?.result)?.message),
2487
+ ].filter((text) => Boolean(text));
2488
+ if (errorTexts.some(isCompletionSafePostFinalDuplicateText))
2489
+ return true;
2490
+ return isCompletionSafePostFinalDuplicateText(summarizeJson(value));
2491
+ }
2492
+ function isCompletionSafePostFinalDuplicateText(text) {
2493
+ return (text.includes('task_thread_post_final_duplicate')
2494
+ && text.includes('completionSafe')) || (text.includes('already sent its substantive final reply')
2495
+ && text.includes('another user-visible message'));
2496
+ }
2497
+ function isTaskThreadPostFinalDuplicateResultShape(value, depth = 0) {
2498
+ if (depth > 4 || value == null)
2499
+ return false;
2500
+ if (Array.isArray(value)) {
2501
+ return value.some((item) => isTaskThreadPostFinalDuplicateResultShape(item, depth + 1));
2502
+ }
2503
+ if (typeof value !== 'object')
2504
+ return false;
2505
+ const record = value;
2506
+ if (record.blockedReason === 'task_thread_post_final_duplicate'
2507
+ && record.nonPosted === true
2508
+ && record.completionSafe === true) {
2509
+ return true;
2510
+ }
2511
+ for (const key of ['structuredContent', 'result', 'data', 'content', 'text', 'output', 'error']) {
2512
+ if (isTaskThreadPostFinalDuplicateResultShape(record[key], depth + 1))
2513
+ return true;
2514
+ }
2515
+ return false;
2516
+ }
2517
+ function isHeldStaleResultShape(value, depth = 0) {
2518
+ if (depth > 4 || value == null)
2519
+ return false;
2520
+ if (Array.isArray(value)) {
2521
+ return value.some((item) => isHeldStaleResultShape(item, depth + 1));
2522
+ }
2523
+ if (typeof value !== 'object')
2524
+ return false;
2525
+ const record = value;
2526
+ if (record.held === true && record.stale === true)
2527
+ return true;
2528
+ for (const key of ['structuredContent', 'result', 'data', 'content', 'text', 'output']) {
2529
+ if (isHeldStaleResultShape(record[key], depth + 1))
2530
+ return true;
2531
+ }
2532
+ return false;
2533
+ }
2534
+ function summarizeFileChanges(changes) {
2535
+ if (!Array.isArray(changes) || changes.length === 0) {
2536
+ return 'Applied file changes.';
2537
+ }
2538
+ return changes
2539
+ .map((change) => `${change.kind ?? 'update'} ${change.path ?? '(unknown file)'}`)
2540
+ .join('\n');
2541
+ }
2542
+ function mapPlanUpdate(params) {
2543
+ const payload = asRecord(params);
2544
+ if (!payload)
2545
+ return { title: 'plan updated' };
2546
+ if (Array.isArray(payload.plan)) {
2547
+ const detailParts = [
2548
+ normalizeOptionalString(payload.explanation),
2549
+ formatPlanChecklist(payload.plan),
2550
+ ].filter((value) => Boolean(value));
2551
+ return {
2552
+ title: 'plan updated',
2553
+ ...(detailParts.length ? { detail: detailParts.join('\n\n') } : {}),
2554
+ };
2555
+ }
2556
+ const plan = asRecord(payload.plan) ?? payload;
2557
+ const title = normalizeOptionalString(plan.title)
2558
+ ?? normalizeOptionalString(plan.name)
2559
+ ?? 'plan updated';
2560
+ const detail = normalizeOptionalString(plan.markdown)
2561
+ ?? normalizeOptionalString(plan.detail)
2562
+ ?? formatPlanChecklist(plan.items ?? plan.steps ?? plan.todos ?? plan.tasks);
2563
+ return { title, ...(detail ? { detail } : {}) };
2564
+ }
2565
+ function formatPlanChecklist(value) {
2566
+ if (!Array.isArray(value) || value.length === 0)
2567
+ return undefined;
2568
+ const lines = value
2569
+ .map((item) => {
2570
+ const record = asRecord(item);
2571
+ const text = normalizeOptionalString(record?.text)
2572
+ ?? normalizeOptionalString(record?.step)
2573
+ ?? normalizeOptionalString(record?.title)
2574
+ ?? normalizeOptionalString(record?.description)
2575
+ ?? (typeof item === 'string' ? normalizeOptionalString(item) : null);
2576
+ if (!text)
2577
+ return null;
2578
+ const status = normalizeOptionalString(record?.status)?.toLowerCase() ?? '';
2579
+ const checked = ['done', 'completed', 'complete', 'success', 'succeeded'].includes(status);
2580
+ return `- [${checked ? 'x' : ' '}] ${text}`;
2581
+ })
2582
+ .filter((line) => Boolean(line));
2583
+ return lines.length > 0 ? lines.join('\n') : undefined;
2584
+ }
2585
+ function extractProposedPlanText(text) {
2586
+ const match = /<proposed_plan>\s*([\s\S]*?)\s*<\/proposed_plan>/i.exec(text);
2587
+ return normalizeOptionalString(match?.[1]);
2588
+ }
2589
+ function isChatSendMessageTool(item) {
2590
+ const toolName = item.server && item.tool
2591
+ ? `${item.server}/${item.tool}`
2592
+ : item.namespace && item.tool
2593
+ ? `${item.namespace}.${item.tool}`
2594
+ : item.tool ?? '';
2595
+ return toolName === 'chat/send_message'
2596
+ || toolName === 'chat.send_message'
2597
+ || toolName === 'send_message';
2598
+ }
2599
+ function isChatMcpToolName(name) {
2600
+ return name === 'send_message'
2601
+ || name.startsWith('chat/')
2602
+ || name.startsWith('chat.');
2603
+ }
2604
+ function extractLatestPlanTextFromThreadReadResponse(value) {
2605
+ const payload = asRecord(value);
2606
+ const thread = asRecord(payload?.thread) ?? payload;
2607
+ if (!thread)
2608
+ return null;
2609
+ const turns = Array.isArray(thread.turns) ? thread.turns : [];
2610
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
2611
+ const planText = extractPlanTextFromSnapshotValue(turns[index], 0);
2612
+ if (planText)
2613
+ return planText;
2614
+ }
2615
+ return extractPlanTextFromSnapshotValue(thread, 0);
2616
+ }
2617
+ function extractCompletedTurnToolItems(value, turnId) {
2618
+ if (!turnId)
2619
+ return [];
2620
+ const payload = asRecord(value);
2621
+ const thread = asRecord(payload?.thread) ?? payload;
2622
+ if (!thread)
2623
+ return [];
2624
+ const turns = Array.isArray(thread.turns) ? thread.turns : [];
2625
+ const selectedTurn = selectSnapshotTurn(turns, turnId);
2626
+ if (!selectedTurn)
2627
+ return [];
2628
+ return collectToolItemsFromSnapshotValue(selectedTurn, 0);
2629
+ }
2630
+ function selectSnapshotTurn(turns, turnId) {
2631
+ if (turns.length === 0 || !turnId)
2632
+ return null;
2633
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
2634
+ const turn = asRecord(turns[index]);
2635
+ const id = normalizeOptionalString(turn?.id)
2636
+ ?? normalizeOptionalString(turn?.turnId)
2637
+ ?? normalizeOptionalString(turn?.turn_id);
2638
+ if (id === turnId)
2639
+ return turns[index];
2640
+ }
2641
+ return null;
2642
+ }
2643
+ function collectToolItemsFromSnapshotValue(value, depth) {
2644
+ if (depth > 8 || value == null)
2645
+ return [];
2646
+ if (Array.isArray(value)) {
2647
+ return value.flatMap((item) => collectToolItemsFromSnapshotValue(item, depth + 1));
2648
+ }
2649
+ const record = asRecord(value);
2650
+ if (!record)
2651
+ return [];
2652
+ if (isToolSnapshotItem(record))
2653
+ return [record];
2654
+ const result = [];
2655
+ for (const key of ['items', 'entries', 'children']) {
2656
+ const child = record[key];
2657
+ if (Array.isArray(child)) {
2658
+ result.push(...collectToolItemsFromSnapshotValue(child, depth + 1));
2659
+ }
2660
+ }
2661
+ return result;
2662
+ }
2663
+ function isToolSnapshotItem(item) {
2664
+ const type = normalizeOptionalString(item.type)?.toLowerCase();
2665
+ return type === 'commandexecution'
2666
+ || type === 'filechange'
2667
+ || type === 'mcptoolcall'
2668
+ || type === 'dynamictoolcall'
2669
+ || type === 'tool.call'
2670
+ || type === 'tool.result'
2671
+ || type === 'websearch';
2672
+ }
2673
+ function isShellToolCallOrResultItem(item) {
2674
+ return (item.type === 'tool.call' || item.type === 'tool.result') && item.name === 'shell';
2675
+ }
2676
+ function isWorkspaceSensitiveToolItem(item, workspaceRoot) {
2677
+ const record = asRecord(item);
2678
+ if (!record)
2679
+ return false;
2680
+ const type = normalizeOptionalString(record.type)?.toLowerCase();
2681
+ if (type === 'filechange') {
2682
+ return requestWritesWorkspace(record, workspaceRoot);
2683
+ }
2684
+ if (type === 'commandexecution') {
2685
+ return commandExecutionTouchesWorkspace(record, workspaceRoot);
2686
+ }
2687
+ return false;
2688
+ }
2689
+ function extractPlanTextFromSnapshotValue(value, depth) {
2690
+ if (depth > 8 || value == null)
2691
+ return null;
2692
+ if (typeof value === 'string') {
2693
+ return extractProposedPlanText(value);
2694
+ }
2695
+ if (Array.isArray(value)) {
2696
+ for (let index = value.length - 1; index >= 0; index -= 1) {
2697
+ const planText = extractPlanTextFromSnapshotValue(value[index], depth + 1);
2698
+ if (planText)
2699
+ return planText;
2700
+ }
2701
+ return null;
2702
+ }
2703
+ const record = asRecord(value);
2704
+ if (!record)
2705
+ return null;
2706
+ for (const key of ['plan', 'proposedPlan', 'proposed_plan']) {
2707
+ const planText = extractPlanTextFromPlanLikeValue(record[key], depth + 1);
2708
+ if (planText)
2709
+ return planText;
2710
+ }
2711
+ const type = normalizeOptionalString(record.type)?.toLowerCase() ?? '';
2712
+ if (type.includes('plan')) {
2713
+ const planText = extractPlanTextFromPlanLikeValue(record, depth + 1);
2714
+ if (planText)
2715
+ return planText;
2716
+ }
2717
+ for (const key of ['content', 'text', 'message', 'markdown']) {
2718
+ const raw = normalizeOptionalString(record[key]);
2719
+ const planText = raw ? extractProposedPlanText(raw) : null;
2720
+ if (planText)
2721
+ return planText;
2722
+ }
2723
+ for (const key of [
2724
+ 'arguments',
2725
+ 'input',
2726
+ 'items',
2727
+ 'events',
2728
+ 'messages',
2729
+ 'output',
2730
+ 'outputs',
2731
+ 'response',
2732
+ 'result',
2733
+ 'turn',
2734
+ 'turns',
2735
+ ]) {
2736
+ const planText = extractPlanTextFromSnapshotValue(record[key], depth + 1);
2737
+ if (planText)
2738
+ return planText;
2739
+ }
2740
+ return null;
2741
+ }
2742
+ function extractPlanTextFromPlanLikeValue(value, depth) {
2743
+ if (depth > 8 || value == null)
2744
+ return null;
2745
+ if (typeof value === 'string') {
2746
+ return normalizeOptionalString(value);
2747
+ }
2748
+ if (Array.isArray(value)) {
2749
+ return formatPlanChecklist(value) ?? extractPlanTextFromSnapshotValue(value, depth + 1);
2750
+ }
2751
+ const record = asRecord(value);
2752
+ if (!record)
2753
+ return null;
2754
+ const direct = normalizeOptionalString(record.markdown)
2755
+ ?? normalizeOptionalString(record.detail)
2756
+ ?? normalizeOptionalString(record.text)
2757
+ ?? normalizeOptionalString(record.content);
2758
+ if (direct)
2759
+ return extractProposedPlanText(direct) ?? direct;
2760
+ return formatPlanChecklist(record.items ?? record.steps ?? record.todos ?? record.tasks)
2761
+ ?? extractPlanTextFromSnapshotValue(record, depth + 1);
2762
+ }
2763
+ function mapTaskNotification(method, params) {
2764
+ if (method !== 'codex/event/task_complete' && method !== 'codex/event')
2765
+ return null;
2766
+ const payload = asRecord(params);
2767
+ if (!payload)
2768
+ return null;
2769
+ const eventType = normalizeOptionalString(payload.type);
2770
+ if (method === 'codex/event' && eventType !== 'task_complete')
2771
+ return null;
2772
+ const task = asRecord(payload.task) ?? payload;
2773
+ const title = normalizeOptionalString(task.title)
2774
+ ?? normalizeOptionalString(task.name)
2775
+ ?? 'task complete';
2776
+ const detail = normalizeOptionalString(task.detail)
2777
+ ?? normalizeOptionalString(task.summary)
2778
+ ?? normalizeOptionalString(task.message);
2779
+ return { title, ...(detail ? { detail } : {}) };
2780
+ }
2781
+ function mapTaskItem(item, stage) {
2782
+ const type = normalizeOptionalString(item.type)?.toLowerCase();
2783
+ if (!type || (!type.includes('task') && !type.includes('todo')))
2784
+ return null;
2785
+ const title = normalizeOptionalString(item.title)
2786
+ ?? normalizeOptionalString(item.text)
2787
+ ?? normalizeOptionalString(item.description)
2788
+ ?? (stage === 'complete' ? 'task complete' : 'task updated');
2789
+ const status = normalizeOptionalString(item.status);
2790
+ const detailParts = [
2791
+ status ? `status: ${status}` : null,
2792
+ normalizeOptionalString(item.detail),
2793
+ normalizeOptionalString(item.description),
2794
+ ].filter((value) => Boolean(value));
2795
+ return {
2796
+ title,
2797
+ ...(detailParts.length ? { detail: detailParts.join('\n') } : {}),
2798
+ };
2799
+ }
2800
+ function mapUsageNotification(method, params) {
2801
+ const payload = asRecord(params);
2802
+ const payloadType = normalizeOptionalString(payload?.type);
2803
+ const looksLikeUsage = method.toLowerCase().includes('usage')
2804
+ || method.toLowerCase().includes('token')
2805
+ || payloadType === 'token_count'
2806
+ || payloadType === 'usage';
2807
+ if (!looksLikeUsage)
2808
+ return null;
2809
+ const usage = parseUsagePayload(payload ?? {});
2810
+ if (!usage)
2811
+ return null;
2812
+ return {
2813
+ ...usage,
2814
+ metadata: {
2815
+ method,
2816
+ ...(payloadType ? { eventType: payloadType } : {}),
2817
+ },
2818
+ };
2819
+ }
2820
+ function parseUsagePayload(payload) {
2821
+ const info = asRecord(payload.info)
2822
+ ?? asRecord(payload.usage)
2823
+ ?? asRecord(payload.tokenUsage)
2824
+ ?? payload;
2825
+ const total = asRecord(info.total_token_usage)
2826
+ ?? asRecord(info.totalTokenUsage)
2827
+ ?? asRecord(info.total)
2828
+ ?? {};
2829
+ const last = asRecord(info.last_token_usage)
2830
+ ?? asRecord(info.lastTokenUsage)
2831
+ ?? asRecord(info.last)
2832
+ ?? {};
2833
+ const result = {
2834
+ currentInputTokens: numberValue(info.currentInputTokens) ?? numberValue(info.current_input_tokens) ?? numberValue(last.input_tokens),
2835
+ currentCachedInputTokens: numberValue(info.currentCachedInputTokens) ?? numberValue(info.current_cached_input_tokens) ?? numberValue(last.cached_input_tokens),
2836
+ inputTokens: numberValue(info.inputTokens) ?? numberValue(info.input_tokens) ?? numberValue(total.input_tokens),
2837
+ cachedInputTokens: numberValue(info.cachedInputTokens) ?? numberValue(info.cached_input_tokens) ?? numberValue(total.cached_input_tokens),
2838
+ outputTokens: numberValue(info.outputTokens) ?? numberValue(info.output_tokens) ?? numberValue(total.output_tokens),
2839
+ reasoningOutputTokens: numberValue(info.reasoningOutputTokens) ?? numberValue(info.reasoning_output_tokens) ?? numberValue(total.reasoning_output_tokens),
2840
+ totalTokens: numberValue(info.totalTokens) ?? numberValue(info.total_tokens) ?? numberValue(total.total_tokens),
2841
+ modelContextWindow: numberValue(info.modelContextWindow) ?? numberValue(info.model_context_window) ?? numberValue(info.contextWindow) ?? numberValue(info.context_window),
2842
+ };
2843
+ const hasUsage = Object.values(result).some((value) => typeof value === 'number');
2844
+ return hasUsage ? result : null;
2845
+ }
2846
+ function asRecord(value) {
2847
+ return value && typeof value === 'object' && !Array.isArray(value)
2848
+ ? value
2849
+ : null;
2850
+ }
2851
+ function numberValue(value) {
2852
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
2853
+ }
2854
+ function buildPlanApprovalActions() {
2855
+ return [
2856
+ { id: 'reject', label: 'Reject', variant: 'danger' },
2857
+ {
2858
+ id: 'continue_planning',
2859
+ label: 'Continue planning',
2860
+ variant: 'secondary',
2861
+ requiresInput: true,
2862
+ inputPlaceholder: 'Tell the agent what to change or clarify in the plan...',
2863
+ },
2864
+ { id: 'implement', label: 'Implement', variant: 'primary' },
2865
+ ];
2866
+ }
2867
+ function codexPromptSet(agentSurfaceMode) {
2868
+ return agentSurfaceMode === 'mcp'
2869
+ ? {
2870
+ planImplementation: MCP_CODEX_PLAN_IMPLEMENTATION_PROMPT,
2871
+ planImplementationRepair: MCP_CODEX_PLAN_IMPLEMENTATION_REPAIR_PROMPT,
2872
+ planImplementationAssistantTextRepair: MCP_CODEX_PLAN_IMPLEMENTATION_ASSISTANT_TEXT_REPAIR_PROMPT,
2873
+ planImplementationSteerRepair: MCP_CODEX_PLAN_IMPLEMENTATION_STEER_REPAIR_PROMPT,
2874
+ assistantTextRepair: MCP_CODEX_ASSISTANT_TEXT_REPAIR_PROMPT,
2875
+ assistantTextSteer: MCP_CODEX_ASSISTANT_TEXT_STEER_PROMPT,
2876
+ assistantTextRepairFollowUpSteer: MCP_CODEX_ASSISTANT_TEXT_REPAIR_FOLLOW_UP_STEER_PROMPT,
2877
+ }
2878
+ : {
2879
+ planImplementation: CODEX_PLAN_IMPLEMENTATION_PROMPT,
2880
+ planImplementationRepair: CODEX_PLAN_IMPLEMENTATION_REPAIR_PROMPT,
2881
+ planImplementationAssistantTextRepair: CODEX_PLAN_IMPLEMENTATION_ASSISTANT_TEXT_REPAIR_PROMPT,
2882
+ planImplementationSteerRepair: CODEX_PLAN_IMPLEMENTATION_STEER_REPAIR_PROMPT,
2883
+ assistantTextRepair: CODEX_ASSISTANT_TEXT_REPAIR_PROMPT,
2884
+ assistantTextSteer: CODEX_ASSISTANT_TEXT_STEER_PROMPT,
2885
+ assistantTextRepairFollowUpSteer: CODEX_ASSISTANT_TEXT_REPAIR_FOLLOW_UP_STEER_PROMPT,
2886
+ };
2887
+ }
2888
+ function buildCodexPlanImplementationPrompt(agentSurfaceMode, planText) {
2889
+ const prompts = codexPromptSet(agentSurfaceMode);
2890
+ const normalizedPlanText = normalizeOptionalString(planText);
2891
+ return normalizedPlanText
2892
+ ? `${prompts.planImplementation}\n\nApproved plan:\n\n${normalizedPlanText}`
2893
+ : prompts.planImplementation;
2894
+ }
2895
+ function buildCodexPlanImplementationRepairPrompt(agentSurfaceMode, finalAnswerDraft, options) {
2896
+ const prompts = codexPromptSet(agentSurfaceMode);
2897
+ const prompt = options?.allowReconstruction
2898
+ ? `${prompts.planImplementationAssistantTextRepair}\n\nCaptured assistant text:\n\n${finalAnswerDraft}`
2899
+ : `${prompts.planImplementationRepair}\n\nSend this exact final answer content:\n\n${finalAnswerDraft}`;
2900
+ return appendBigbangRepairGuidance(prompt, options?.bigbangRepairGuidance);
2901
+ }
2902
+ function buildCodexPlanImplementationSteerRepairPrompt(agentSurfaceMode, finalAnswerDraft, bigbangRepairGuidance) {
2903
+ return appendBigbangRepairGuidance(`${codexPromptSet(agentSurfaceMode).planImplementationSteerRepair}\n\nSend this exact final answer content:\n\n${finalAnswerDraft}`, bigbangRepairGuidance);
2904
+ }
2905
+ function buildCodexAssistantTextRepairPrompt(agentSurfaceMode, assistantText, bigbangRepairGuidance) {
2906
+ return appendBigbangRepairGuidance(`${codexPromptSet(agentSurfaceMode).assistantTextRepair}\n\nPrevious plain assistant text:\n\n${assistantText}`, bigbangRepairGuidance);
2907
+ }
2908
+ function buildCodexAssistantTextSteerPrompt(agentSurfaceMode, assistantText, bigbangRepairGuidance) {
2909
+ return appendBigbangRepairGuidance(`${codexPromptSet(agentSurfaceMode).assistantTextSteer}\n\nCaptured text so far:\n\n${assistantText}`, bigbangRepairGuidance);
2910
+ }
2911
+ function buildCodexAssistantTextRepairFollowUpSteerPrompt(agentSurfaceMode, assistantText, bigbangRepairGuidance) {
2912
+ return appendBigbangRepairGuidance(`${codexPromptSet(agentSurfaceMode).assistantTextRepairFollowUpSteer}\n\nCaptured text so far:\n\n${assistantText}`, bigbangRepairGuidance);
2913
+ }
2914
+ function appendBigbangRepairGuidance(prompt, guidance) {
2915
+ return guidance ? `${prompt}\n\n${guidance}` : prompt;
2916
+ }
2917
+ function buildCodexPlanRevisionPrompt(planText, feedbackText) {
2918
+ const normalizedPlanText = normalizeOptionalString(planText);
2919
+ const normalizedFeedbackText = normalizeOptionalString(feedbackText) ?? CODEX_PLAN_REVISION_FALLBACK;
2920
+ return [
2921
+ 'The user has not approved the current plan yet.',
2922
+ 'Revise the plan using the feedback below. Do not implement the plan or change files.',
2923
+ normalizedPlanText ? `Current plan:\n\n${normalizedPlanText}` : null,
2924
+ `User feedback:\n\n${normalizedFeedbackText}`,
2925
+ ].filter(Boolean).join('\n\n');
2926
+ }
2927
+ function buildPermissionProfile(params) {
2928
+ const { blockedKinds, workspaceRoot } = params;
2929
+ const needsNativePermissionProfile = blockedKinds.has('fetch') || blockedKinds.has('read') || blockedKinds.has('edit');
2930
+ if (!needsNativePermissionProfile)
2931
+ return null;
2932
+ const network = blockedKinds.has('fetch')
2933
+ ? { enabled: false }
2934
+ : { enabled: true };
2935
+ const fileSystem = blockedKinds.has('read') || blockedKinds.has('edit')
2936
+ ? {
2937
+ type: 'restricted',
2938
+ entries: [
2939
+ {
2940
+ path: { type: 'path', path: workspaceRoot },
2941
+ access: blockedKinds.has('read') ? 'none' : 'read',
2942
+ },
2943
+ ],
2944
+ }
2945
+ : { type: 'unrestricted' };
2946
+ return {
2947
+ type: 'managed',
2948
+ network,
2949
+ fileSystem,
2950
+ };
2951
+ }
2952
+ function buildToolPolicyPromptPreamble(blockedKinds) {
2953
+ if (blockedKinds.size === 0)
2954
+ return null;
2955
+ const blocked = [...blockedKinds.keys()].sort().join(', ');
2956
+ return [
2957
+ '[Platform tool policy]',
2958
+ `The following tool kinds are blocked for this turn: ${blocked}.`,
2959
+ 'Do not call blocked tools. If the task would require one, explain the restriction instead.',
2960
+ ].join('\n');
2961
+ }
2962
+ function detectBlockedToolKindForApproval(method, params, blockedKinds) {
2963
+ if (method === 'item/fileChange/requestApproval') {
2964
+ return blockedKinds.has('edit') ? 'edit' : null;
2965
+ }
2966
+ return detectBlockedToolKindForPermissionsRequest(params, blockedKinds);
2967
+ }
2968
+ function detectBlockedToolKindForCommandApproval(params, blockedKinds) {
2969
+ if (blockedKinds.has('execute')) {
2970
+ return 'execute';
2971
+ }
2972
+ const payload = params;
2973
+ if (blockedKinds.has('fetch')
2974
+ && (payload?.networkApprovalContext != null
2975
+ || payload?.additionalPermissions?.network?.enabled === true
2976
+ || (Array.isArray(payload?.proposedNetworkPolicyAmendments) && payload.proposedNetworkPolicyAmendments.length > 0))) {
2977
+ return 'fetch';
2978
+ }
2979
+ for (const action of extractCommandActions(payload?.commandActions)) {
2980
+ if (action.type === 'read' && blockedKinds.has('read')) {
2981
+ return 'read';
2982
+ }
2983
+ if ((action.type === 'search' || action.type === 'listFiles') && blockedKinds.has('search')) {
2984
+ return 'search';
2985
+ }
2986
+ }
2987
+ return null;
2988
+ }
2989
+ function detectBlockedToolKindForPermissionsRequest(params, blockedKinds) {
2990
+ const permissions = params?.permissions;
2991
+ if (permissions?.network?.enabled && blockedKinds.has('fetch')) {
2992
+ return 'fetch';
2993
+ }
2994
+ if (Array.isArray(permissions?.fileSystem?.entries)) {
2995
+ if (permissions.fileSystem.entries.some((entry) => entry?.access === 'write')
2996
+ && blockedKinds.has('edit')) {
2997
+ return 'edit';
2998
+ }
2999
+ if (permissions.fileSystem.entries.some((entry) => entry?.access === 'read')
3000
+ && blockedKinds.has('read')) {
3001
+ return 'read';
3002
+ }
3003
+ }
3004
+ if ((permissions?.fileSystem?.write?.length ?? 0) > 0 && blockedKinds.has('edit')) {
3005
+ return 'edit';
3006
+ }
3007
+ if ((permissions?.fileSystem?.read?.length ?? 0) > 0 && blockedKinds.has('read')) {
3008
+ return 'read';
3009
+ }
3010
+ return null;
3011
+ }
3012
+ function requestWritesWorkspace(params, workspaceRoot) {
3013
+ const payload = params;
3014
+ const cwd = normalizeOptionalString(payload?.cwd) ?? workspaceRoot;
3015
+ const paths = [
3016
+ normalizeOptionalString(payload?.path),
3017
+ ...(payload?.changes ?? []).map((change) => normalizeOptionalString(change?.path)),
3018
+ ].filter((value) => Boolean(value));
3019
+ if (paths.length === 0) {
3020
+ return isPathInsideOrEqual(cwd, workspaceRoot);
3021
+ }
3022
+ return paths.some((value) => pathTargetsWorkspace(value, cwd, workspaceRoot));
3023
+ }
3024
+ function commandExecutionTouchesWorkspace(params, workspaceRoot) {
3025
+ const payload = params;
3026
+ const cwd = normalizeOptionalString(payload?.cwd) ?? workspaceRoot;
3027
+ if (isPathInsideOrEqual(cwd, workspaceRoot)) {
3028
+ return true;
3029
+ }
3030
+ for (const action of extractCommandActions(payload?.commandActions)) {
3031
+ for (const value of action.paths) {
3032
+ if (pathTargetsWorkspace(value, cwd, workspaceRoot)) {
3033
+ return true;
3034
+ }
3035
+ }
3036
+ }
3037
+ const command = normalizeOptionalString(payload?.command);
3038
+ return Boolean(command && commandMentionsWorkspacePath(command, workspaceRoot));
3039
+ }
3040
+ function permissionsRequestWritesWorkspace(params, workspaceRoot) {
3041
+ const payload = params;
3042
+ const fileSystem = payload?.permissions?.fileSystem;
3043
+ if (!fileSystem)
3044
+ return false;
3045
+ const cwd = normalizeOptionalString(payload?.cwd) ?? workspaceRoot;
3046
+ for (const value of fileSystem.write ?? []) {
3047
+ if (pathTargetsWorkspace(value, cwd, workspaceRoot)) {
3048
+ return true;
3049
+ }
3050
+ }
3051
+ for (const entry of fileSystem.entries ?? []) {
3052
+ if (normalizeOptionalString(entry?.access)?.toLowerCase() !== 'write') {
3053
+ continue;
3054
+ }
3055
+ const entryPath = entry?.path;
3056
+ const type = normalizeOptionalString(entryPath?.type)?.toLowerCase();
3057
+ if (type === 'path') {
3058
+ if (pathTargetsWorkspace(entryPath?.path, cwd, workspaceRoot)) {
3059
+ return true;
3060
+ }
3061
+ continue;
3062
+ }
3063
+ if (type === 'special') {
3064
+ const value = normalizeOptionalString(entryPath?.value)?.toLowerCase();
3065
+ if ((value === ':cwd' || value === 'cwd') && isPathInsideOrEqual(cwd, workspaceRoot)) {
3066
+ return true;
3067
+ }
3068
+ continue;
3069
+ }
3070
+ if (type === 'glob_pattern' && isPathInsideOrEqual(cwd, workspaceRoot)) {
3071
+ return true;
3072
+ }
3073
+ }
3074
+ return false;
3075
+ }
3076
+ function permissionsRequestReadsWorkspace(params, workspaceRoot) {
3077
+ const payload = params;
3078
+ const fileSystem = payload?.permissions?.fileSystem;
3079
+ if (!fileSystem)
3080
+ return false;
3081
+ const cwd = normalizeOptionalString(payload?.cwd) ?? workspaceRoot;
3082
+ for (const value of fileSystem.read ?? []) {
3083
+ if (pathTargetsWorkspace(value, cwd, workspaceRoot)) {
3084
+ return true;
3085
+ }
3086
+ }
3087
+ for (const entry of fileSystem.entries ?? []) {
3088
+ if (normalizeOptionalString(entry?.access)?.toLowerCase() !== 'read') {
3089
+ continue;
3090
+ }
3091
+ const entryPath = entry?.path;
3092
+ const type = normalizeOptionalString(entryPath?.type)?.toLowerCase();
3093
+ if (type === 'path') {
3094
+ if (pathTargetsWorkspace(entryPath?.path, cwd, workspaceRoot)) {
3095
+ return true;
3096
+ }
3097
+ continue;
3098
+ }
3099
+ if (type === 'special') {
3100
+ const value = normalizeOptionalString(entryPath?.value)?.toLowerCase();
3101
+ if ((value === ':cwd' || value === 'cwd') && isPathInsideOrEqual(cwd, workspaceRoot)) {
3102
+ return true;
3103
+ }
3104
+ continue;
3105
+ }
3106
+ if (type === 'glob_pattern' && isPathInsideOrEqual(cwd, workspaceRoot)) {
3107
+ return true;
3108
+ }
3109
+ }
3110
+ return false;
3111
+ }
3112
+ function pathTargetsWorkspace(value, cwd, workspaceRoot) {
3113
+ const normalized = normalizeOptionalString(value);
3114
+ if (!normalized)
3115
+ return false;
3116
+ const candidate = path.isAbsolute(normalized)
3117
+ ? normalized
3118
+ : path.resolve(cwd, normalized);
3119
+ return isPathInsideOrEqual(candidate, workspaceRoot);
3120
+ }
3121
+ function isPathInsideOrEqual(candidatePath, rootPath) {
3122
+ const candidate = path.resolve(candidatePath);
3123
+ const root = path.resolve(rootPath);
3124
+ const relative = path.relative(root, candidate);
3125
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
3126
+ }
3127
+ function buildPermissionsApprovalResponse(params, blockedKinds) {
3128
+ const requested = params?.permissions;
3129
+ const response = {
3130
+ permissions: {},
3131
+ scope: 'turn',
3132
+ };
3133
+ if (requested?.network?.enabled === true && !blockedKinds.has('fetch')) {
3134
+ response.permissions.network = { enabled: true };
3135
+ }
3136
+ const requestedFileSystem = requested?.fileSystem;
3137
+ if (requestedFileSystem && !blockedKinds.has('read')) {
3138
+ const read = dedupeStrings(requestedFileSystem.read);
3139
+ const write = blockedKinds.has('edit') ? [] : dedupeStrings(requestedFileSystem.write);
3140
+ const entries = (requestedFileSystem.entries ?? [])
3141
+ .map((entry) => normalizeGrantedFileSystemEntry(entry))
3142
+ .filter((entry) => Boolean(entry))
3143
+ .filter((entry) => {
3144
+ if (entry.access === 'read') {
3145
+ return !blockedKinds.has('read');
3146
+ }
3147
+ if (entry.access === 'write') {
3148
+ return !blockedKinds.has('edit');
3149
+ }
3150
+ return false;
3151
+ });
3152
+ if (read.length > 0 || write.length > 0 || entries.length > 0) {
3153
+ response.permissions.fileSystem = {
3154
+ read: read.length > 0 ? read : null,
3155
+ write: write.length > 0 ? write : null,
3156
+ ...(typeof requestedFileSystem.globScanMaxDepth === 'number'
3157
+ ? { globScanMaxDepth: requestedFileSystem.globScanMaxDepth }
3158
+ : {}),
3159
+ ...(entries.length > 0 ? { entries } : {}),
3160
+ };
3161
+ }
3162
+ }
3163
+ return response;
3164
+ }
3165
+ function normalizeGrantedFileSystemEntry(entry) {
3166
+ const access = normalizeOptionalString(entry?.access)?.toLowerCase();
3167
+ if (access !== 'read' && access !== 'write' && access !== 'none') {
3168
+ return null;
3169
+ }
3170
+ const path = entry?.path;
3171
+ const pathType = normalizeOptionalString(path?.type)?.toLowerCase();
3172
+ if (pathType === 'path') {
3173
+ const normalizedPath = normalizeOptionalString(path?.path);
3174
+ return normalizedPath ? { path: { type: 'path', path: normalizedPath }, access } : null;
3175
+ }
3176
+ if (pathType === 'glob_pattern') {
3177
+ const pattern = normalizeOptionalString(path?.pattern);
3178
+ return pattern ? { path: { type: 'glob_pattern', pattern }, access } : null;
3179
+ }
3180
+ if (pathType === 'special') {
3181
+ const value = normalizeOptionalString(path?.value);
3182
+ return value ? { path: { type: 'special', value }, access } : null;
3183
+ }
3184
+ return null;
3185
+ }
3186
+ function dedupeStrings(values) {
3187
+ const out = [];
3188
+ const seen = new Set();
3189
+ for (const value of values ?? []) {
3190
+ const normalized = normalizeOptionalString(value);
3191
+ if (!normalized || seen.has(normalized))
3192
+ continue;
3193
+ seen.add(normalized);
3194
+ out.push(normalized);
3195
+ }
3196
+ return out;
3197
+ }
3198
+ function detectBlockedToolKind(item, blockedKinds) {
3199
+ if (blockedKinds.size === 0)
3200
+ return null;
3201
+ switch (item.type) {
3202
+ case 'commandExecution': {
3203
+ if (blockedKinds.has('execute')) {
3204
+ return 'execute';
3205
+ }
3206
+ for (const action of extractCommandActions(item.commandActions)) {
3207
+ if (action.type === 'read' && blockedKinds.has('read')) {
3208
+ return 'read';
3209
+ }
3210
+ if ((action.type === 'search' || action.type === 'listFiles') && blockedKinds.has('search')) {
3211
+ return 'search';
3212
+ }
3213
+ }
3214
+ return null;
3215
+ }
3216
+ case 'fileChange': {
3217
+ const changes = item.changes ?? [];
3218
+ for (const change of changes) {
3219
+ const normalizedKind = normalizeFileChangeKind(change.kind);
3220
+ if (normalizedKind === 'delete' && blockedKinds.has('delete')) {
3221
+ return 'delete';
3222
+ }
3223
+ if (normalizedKind === 'move' && blockedKinds.has('move')) {
3224
+ return 'move';
3225
+ }
3226
+ }
3227
+ if (blockedKinds.has('edit')) {
3228
+ return 'edit';
3229
+ }
3230
+ return null;
3231
+ }
3232
+ case 'webSearch':
3233
+ return blockedKinds.has('fetch') ? 'fetch' : null;
3234
+ default:
3235
+ return null;
3236
+ }
3237
+ }
3238
+ function normalizeFileChangeKind(value) {
3239
+ const normalized = normalizeOptionalString(value)?.toLowerCase();
3240
+ if (normalized === 'delete' || normalized === 'remove') {
3241
+ return 'delete';
3242
+ }
3243
+ if (normalized === 'move' || normalized === 'rename') {
3244
+ return 'move';
3245
+ }
3246
+ return 'edit';
3247
+ }
3248
+ function extractCommandActions(value) {
3249
+ if (!Array.isArray(value))
3250
+ return [];
3251
+ return value
3252
+ .map((action) => {
3253
+ const record = asRecord(action);
3254
+ const type = normalizeOptionalString(record?.type);
3255
+ if (type === 'read' || type === 'listFiles' || type === 'search' || type === 'unknown') {
3256
+ return { type, paths: extractPathStringsFromUnknown(record) };
3257
+ }
3258
+ return null;
3259
+ })
3260
+ .filter(Boolean);
3261
+ }
3262
+ function extractPathStringsFromUnknown(value, depth = 0) {
3263
+ if (depth > 4 || value == null)
3264
+ return [];
3265
+ if (typeof value === 'string')
3266
+ return [value];
3267
+ if (Array.isArray(value)) {
3268
+ return value.flatMap((item) => extractPathStringsFromUnknown(item, depth + 1));
3269
+ }
3270
+ const record = asRecord(value);
3271
+ if (!record)
3272
+ return [];
3273
+ const result = [];
3274
+ for (const key of ['path', 'paths', 'file', 'files', 'file_path', 'filePath', 'cwd', 'root', 'directory', 'pattern']) {
3275
+ result.push(...extractPathStringsFromUnknown(record[key], depth + 1));
3276
+ }
3277
+ return result.filter((item) => item.length > 0);
3278
+ }
3279
+ function commandMentionsWorkspacePath(command, workspaceRoot) {
3280
+ const normalizedRoot = path.resolve(workspaceRoot);
3281
+ const normalizedCommand = command.replace(/\\ /g, ' ');
3282
+ return normalizedCommand.includes(normalizedRoot);
3283
+ }
3284
+ function formatBlockedToolMessage(toolKind, reason) {
3285
+ return reason === 'policy_reject'
3286
+ ? `Tool call denied by policy: ${toolKind}.`
3287
+ : `Tool call denied by agent settings: ${toolKind} disabled.`;
3288
+ }
3289
+ function mapToolItem(item, buffers) {
3290
+ const toolCallId = normalizeOptionalString(item.id);
3291
+ if (!toolCallId)
3292
+ return null;
3293
+ switch (item.type) {
3294
+ case 'commandExecution': {
3295
+ const normalized = normalizeToolItemStatus(item.status);
3296
+ const commandOutput = mergeAggregatedAndBufferedOutput(normalizeOptionalString(item.aggregatedOutput ?? undefined), normalizeOptionalString(buffers.commandOutput ?? undefined));
3297
+ const output = [
3298
+ commandOutput,
3299
+ typeof item.exitCode === 'number' ? `exit_code=${item.exitCode}` : null,
3300
+ ].filter((value) => Boolean(value)).join('\n\n') || (normalized.status === 'failed' ? 'Command failed.' : 'Command completed.');
3301
+ return {
3302
+ toolCallId,
3303
+ name: 'shell',
3304
+ input: {
3305
+ command: item.command ?? '',
3306
+ cwd: item.cwd ?? null,
3307
+ },
3308
+ output,
3309
+ detail: output,
3310
+ inProgress: normalized.inProgress,
3311
+ status: normalized.status,
3312
+ metadata: buildToolMetadata(item),
3313
+ };
3314
+ }
3315
+ case 'fileChange': {
3316
+ const normalized = normalizeToolItemStatus(item.status);
3317
+ const output = normalizeOptionalString(buffers.fileChangeOutput ?? undefined) ?? summarizeFileChanges(item.changes);
3318
+ return {
3319
+ toolCallId,
3320
+ name: 'apply_patch',
3321
+ input: item.changes ?? [],
3322
+ output,
3323
+ detail: output,
3324
+ inProgress: normalized.inProgress,
3325
+ status: normalized.status,
3326
+ metadata: buildToolMetadata(item),
3327
+ };
3328
+ }
3329
+ case 'mcpToolCall': {
3330
+ const normalized = normalizeToolItemStatus(item.status);
3331
+ const output = item.error?.message?.trim() || summarizeJson(item.result ?? '');
3332
+ return {
3333
+ toolCallId,
3334
+ name: item.server && item.tool ? `${item.server}/${item.tool}` : item.tool ?? 'mcp_tool',
3335
+ input: item.arguments ?? null,
3336
+ output,
3337
+ detail: output,
3338
+ inProgress: normalized.inProgress,
3339
+ status: normalized.status,
3340
+ metadata: buildToolMetadata(item),
3341
+ };
3342
+ }
3343
+ case 'dynamicToolCall': {
3344
+ const normalized = normalizeToolItemStatus(item.status, item.success);
3345
+ const output = summarizeJson(item.contentItems ?? '');
3346
+ return {
3347
+ toolCallId,
3348
+ name: item.namespace ? `${item.namespace}.${item.tool ?? 'tool'}` : item.tool ?? 'tool',
3349
+ input: item.arguments ?? null,
3350
+ output,
3351
+ detail: output,
3352
+ inProgress: normalized.inProgress,
3353
+ status: normalized.status,
3354
+ metadata: buildToolMetadata(item),
3355
+ };
3356
+ }
3357
+ case 'webSearch': {
3358
+ const normalized = normalizeToolItemStatus(item.status, item.success);
3359
+ const output = normalized.status === 'failed'
3360
+ ? item.error?.message?.trim() || 'Web search failed.'
3361
+ : normalized.status === 'cancelled'
3362
+ ? 'Web search cancelled.'
3363
+ : 'Web search completed.';
3364
+ return {
3365
+ toolCallId,
3366
+ name: 'web_search',
3367
+ input: {
3368
+ query: item.query ?? '',
3369
+ action: item.action ?? null,
3370
+ },
3371
+ output,
3372
+ detail: output,
3373
+ inProgress: normalized.inProgress,
3374
+ status: normalized.status,
3375
+ metadata: buildToolMetadata(item),
3376
+ };
3377
+ }
3378
+ default:
3379
+ return null;
3380
+ }
3381
+ }
3382
+ function normalizeToolItemStatus(status, success) {
3383
+ const normalized = normalizeOptionalString(status)?.toLowerCase();
3384
+ if (normalized === 'inprogress' || normalized === 'in_progress' || normalized === 'running') {
3385
+ return { status: 'completed', inProgress: true };
3386
+ }
3387
+ if (normalized === 'cancelled' || normalized === 'canceled') {
3388
+ return { status: 'cancelled', inProgress: false };
3389
+ }
3390
+ if (normalized === 'failed' || normalized === 'error' || normalized === 'declined' || success === false) {
3391
+ return { status: 'failed', inProgress: false };
3392
+ }
3393
+ return { status: 'completed', inProgress: false };
3394
+ }
3395
+ function buildToolMetadata(item) {
3396
+ return {
3397
+ source: 'codex_app_server',
3398
+ itemType: item.type ?? null,
3399
+ ...(item.status ? { appStatus: item.status } : {}),
3400
+ ...(typeof item.success === 'boolean' ? { success: item.success } : {}),
3401
+ ...(typeof item.exitCode === 'number' ? { exitCode: item.exitCode } : {}),
3402
+ };
3403
+ }
3404
+ function normalizeCodexQuestionPrompts(params) {
3405
+ const questions = Array.isArray(params?.questions)
3406
+ ? (params.questions)
3407
+ : [];
3408
+ const normalized = [];
3409
+ questions.forEach((question, index) => {
3410
+ const record = asRecord(question);
3411
+ if (!record)
3412
+ return;
3413
+ const id = normalizeOptionalString(record.id) ?? `question_${index + 1}`;
3414
+ const questionText = normalizeOptionalString(record.question);
3415
+ if (!questionText)
3416
+ return;
3417
+ const header = normalizeOptionalString(record.header) ?? id;
3418
+ const options = Array.isArray(record.options)
3419
+ ? record.options.flatMap((option) => {
3420
+ const optionRecord = asRecord(option);
3421
+ if (!optionRecord)
3422
+ return [];
3423
+ const label = normalizeOptionalString(optionRecord.label);
3424
+ if (!label)
3425
+ return [];
3426
+ const description = normalizeOptionalString(optionRecord.description);
3427
+ return [{ label, ...(description ? { description } : {}) }];
3428
+ })
3429
+ : [];
3430
+ normalized.push({
3431
+ id,
3432
+ header,
3433
+ question: questionText,
3434
+ options,
3435
+ ...(record.multiSelect === true ? { multiSelect: true } : {}),
3436
+ ...(record.isOther === true ? { isOther: true } : {}),
3437
+ ...(record.isSecret === true ? { isSecret: true } : {}),
3438
+ });
3439
+ });
3440
+ return normalized;
3441
+ }
3442
+ function buildUserInputResponse(params, submittedAnswers) {
3443
+ const questions = normalizeCodexQuestionPrompts(params);
3444
+ const answers = {};
3445
+ for (const question of questions) {
3446
+ const submitted = submittedAnswers?.[question.id]
3447
+ ?.map((answer) => answer.trim())
3448
+ .filter((answer) => answer.length > 0);
3449
+ if (submitted?.length) {
3450
+ answers[question.id] = { answers: submitted };
3451
+ continue;
3452
+ }
3453
+ const firstOption = question.options.find((option) => normalizeOptionalString(option.label));
3454
+ answers[question.id] = { answers: firstOption ? [firstOption.label] : [] };
3455
+ }
3456
+ return { answers };
3457
+ }
3458
+ function buildUserInputRequestId(params, sessionKey) {
3459
+ const payload = params;
3460
+ const stableId = normalizeOptionalString(payload?.requestId) ?? normalizeOptionalString(payload?.itemId);
3461
+ const scopedId = stableId ?? randomUUID();
3462
+ return `codex-app-input:${sessionKey}:${scopedId}`;
3463
+ }
3464
+ function extractApprovalItemId(params) {
3465
+ const payload = params;
3466
+ return normalizeOptionalString(payload?.itemId) ?? normalizeOptionalString(payload?.requestId);
3467
+ }
3468
+ function isLikelyMissingThreadError(message) {
3469
+ const normalized = message.toLowerCase();
3470
+ return normalized.includes('thread') && (normalized.includes('not found')
3471
+ || normalized.includes('missing')
3472
+ || normalized.includes('unknown'));
3473
+ }
3474
+ function isLikelyUnsupportedPersistExtendedHistoryError(message) {
3475
+ const normalized = message.toLowerCase();
3476
+ const referencesField = normalized.includes('persistextendedhistory')
3477
+ || normalized.includes('persist_extended_history')
3478
+ || normalized.includes('persist extended history');
3479
+ if (!referencesField) {
3480
+ return false;
3481
+ }
3482
+ return (normalized.includes('unknown field')
3483
+ || normalized.includes('unrecognized field')
3484
+ || normalized.includes('unexpected field')
3485
+ || normalized.includes('unexpected property')
3486
+ || normalized.includes('unknown property')
3487
+ || normalized.includes('unrecognized property')
3488
+ || normalized.includes('additional properties')
3489
+ || normalized.includes('unknown key')
3490
+ || normalized.includes('unrecognized key')
3491
+ || normalized.includes('unexpected key')
3492
+ || normalized.includes('unknown argument')
3493
+ || normalized.includes('unrecognized argument')
3494
+ || normalized.includes('unexpected argument'));
3495
+ }