@axhub/genie 0.2.6 → 0.2.8

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 (102) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CTKZtqB1.js +460 -0
  3. package/dist/assets/{ReviewApp-BEicSBzW.js → ReviewApp-DM6BNAzR.js} +1 -1
  4. package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-CqJbRZ9y.js} +1 -1
  5. package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-BS8YH8jO.js} +1 -1
  6. package/dist/assets/{arc-CEsS3MdK.js → arc-BBmKEN-S.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BubZ7T3U.js → architectureDiagram-2XIMDMQ5-N5lcb82R.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-jhjtOQ12.js → c4Diagram-IC4MRINW-BTKlkXI9.js} +1 -1
  10. package/dist/assets/channel-1oJBvF-0.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-DyujyOvx.js → chunk-NQ4KR5QH-CM50ygWP.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-CQHHFLvx.js → chunk-WL4C6EOR-Cm9nQrsr.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
  21. package/dist/assets/clone-CinxIlEu.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-qykDd54p.js → cose-bilkent-S5V4N54A-Ccp_p0JZ.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BKtx468K.js → diagram-E7M64L7V-CeNVmFUp.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-DT9YzdNw.js → erDiagram-INFDFZHY-B8R5vwhd.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK--IgwcUhI.js → ganttDiagram-A5KZAMGK-DOu3hSNa.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
  31. package/dist/assets/{graph-Cw1rYoD9.js → graph-D11wiwHo.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-BCxJHuqY.js → highlighted-body-TPN3WLV5-Babpthg-.js} +1 -1
  33. package/dist/assets/index-DFxzgWoO.js +2 -0
  34. package/dist/assets/index-YCFGDVKw.css +1 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-Cl8yrezU.js → ishikawaDiagram-PHBUUO56-BEquZd3E.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-ddP0AMU9.js → journeyDiagram-4ABVD52K-BfemGz7f.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-CWja3mln.js} +1 -1
  39. package/dist/assets/{layout-W_tRx4UV.js → layout-BLUNf-PJ.js} +1 -1
  40. package/dist/assets/{linear-CcMb2ay-.js → linear-DukIV_Xv.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-BBJqt8pT.js → mermaid-O7DHMXV3-SgtM28qI.js} +265 -215
  42. package/dist/assets/{mindmap-definition-YRQLILUH-BGhZa7Na.js → mindmap-definition-YRQLILUH-4UjqXITU.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-BSYuqf0Q.js → quadrantDiagram-337W2JSQ-D60m8V8r.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-Cfi9YX9H.js → requirementDiagram-Z7DCOOCP-zqh9jBVf.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-CYTTG38e.js → sequenceDiagram-2WXFIKYE-7BReFd0L.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-B1sdb5mK.js → timeline-definition-YZTLITO2-CTVllFgr.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-BtyxboJZ.js} +1 -1
  52. package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +1 -1
  53. package/dist/assets/{xychartDiagram-JWTSCODW-C8QKSyRR.js → xychartDiagram-JWTSCODW-eRk-39YO.js} +1 -1
  54. package/dist/index.html +2 -2
  55. package/package.json +35 -33
  56. package/server/_legacy-providers/README.md +30 -0
  57. package/server/_legacy-providers/claude-sdk.js +956 -0
  58. package/server/_legacy-providers/gemini-cli.js +368 -0
  59. package/server/_legacy-providers/openai-codex.js +705 -0
  60. package/server/_legacy-providers/opencode-cli.js +674 -0
  61. package/server/acp-runtime/client.js +1872 -0
  62. package/server/acp-runtime/index.js +408 -0
  63. package/server/acp-runtime/registry.js +45 -0
  64. package/server/acp-runtime/session-store.js +254 -0
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +22 -80
  66. package/server/claude-sdk.js +24 -946
  67. package/server/cli.js +140 -2
  68. package/server/external-agent/service.js +52 -63
  69. package/server/gemini-cli.js +21 -360
  70. package/server/index.js +133 -58
  71. package/server/openai-codex.js +19 -695
  72. package/server/opencode-cli.js +68 -640
  73. package/server/projects.js +128 -85
  74. package/server/routes/agent.js +2 -0
  75. package/server/routes/cc-connect.js +1131 -0
  76. package/server/routes/cli-auth.js +1 -73
  77. package/server/routes/commands.js +4 -9
  78. package/server/routes/git.js +3 -20
  79. package/server/routes/projects.js +45 -24
  80. package/server/routes/session-core.js +44 -10
  81. package/server/session-core/abortSession.js +2 -18
  82. package/server/session-core/eventStore.js +5 -1
  83. package/server/session-core/providerAdapters.js +98 -10
  84. package/server/session-core/providerDiscovery.js +8 -3
  85. package/server/session-core/runtimeState.js +16 -17
  86. package/server/session-core/runtimeWriter.js +19 -12
  87. package/server/utils/ccConnectManager.js +390 -0
  88. package/server/utils/ccConnectState.js +575 -0
  89. package/server/utils/resolveCommandPath.js +71 -0
  90. package/server/utils/workspaceRoots.js +154 -0
  91. package/shared/conversationEvents.js +347 -10
  92. package/dist/assets/App-CYTE30Cf.js +0 -484
  93. package/dist/assets/channel-RmqTALN0.js +0 -1
  94. package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +0 -1
  95. package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +0 -1
  96. package/dist/assets/clone-oT5aWXpf.js +0 -1
  97. package/dist/assets/index-CBuAXA5S.js +0 -2
  98. package/dist/assets/index-CyLWKyxy.css +0 -1
  99. package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
  100. package/server/cli.test.js +0 -76
  101. package/server/external-agent/service.test.js +0 -53
  102. package/server/external-agent/ws.test.js +0 -289
@@ -0,0 +1,705 @@
1
+ /**
2
+ * OpenAI Codex SDK Integration
3
+ * =============================
4
+ *
5
+ * This module provides integration with the OpenAI Codex SDK for non-interactive
6
+ * chat sessions. It mirrors the pattern used in claude-sdk.js for consistency.
7
+ *
8
+ * ## Usage
9
+ *
10
+ * - queryCodex(command, options, ws) - Execute a prompt with streaming via WebSocket
11
+ * - abortCodexSession(sessionId) - Cancel an active session
12
+ * - isCodexSessionActive(sessionId) - Check if a session is running
13
+ * - getActiveCodexSessions() - List all active sessions
14
+ */
15
+
16
+ import { Codex } from '@openai/codex-sdk';
17
+ import { parseCodexTurnUsage } from './utils/codexTokenUsage.js';
18
+ import { getCodexPathOverride, getCodexProcessEnv } from './utils/codexPath.js';
19
+ import { cleanupMaterializedImages, materializeImagesToTempFiles } from './utils/agentImages.js';
20
+ import { resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
21
+
22
+ // Track active sessions
23
+ const activeCodexSessions = new Map();
24
+ const CODEX_STREAM_START_TIMEOUT_MS = parseInt(process.env.CODEX_STREAM_START_TIMEOUT_MS, 10) || 20000;
25
+ const CODEX_MEANINGFUL_EVENT_TIMEOUT_MS = parseInt(process.env.CODEX_MEANINGFUL_EVENT_TIMEOUT_MS, 10) || 60000;
26
+
27
+ function isCodexPlaceholderSessionId(sessionId) {
28
+ return typeof sessionId === 'string' && /^codex-\d+$/.test(sessionId.trim());
29
+ }
30
+
31
+ /**
32
+ * Transform Codex SDK event to WebSocket message format
33
+ * @param {object} event - SDK event
34
+ * @returns {object} - Transformed event for WebSocket
35
+ */
36
+ function transformCodexEvent(event) {
37
+ // Map SDK event types to a consistent format
38
+ switch (event.type) {
39
+ case 'item.started':
40
+ case 'item.updated':
41
+ case 'item.completed':
42
+ const item = event.item;
43
+ if (!item) {
44
+ return { type: event.type, item: null };
45
+ }
46
+
47
+ // Transform based on item type
48
+ switch (item.type) {
49
+ case 'agent_message':
50
+ return {
51
+ type: 'item',
52
+ itemType: 'agent_message',
53
+ message: {
54
+ role: 'assistant',
55
+ content: item.text
56
+ }
57
+ };
58
+
59
+ case 'reasoning':
60
+ return {
61
+ type: 'item',
62
+ itemType: 'reasoning',
63
+ message: {
64
+ role: 'assistant',
65
+ content: item.text,
66
+ isReasoning: true
67
+ }
68
+ };
69
+
70
+ case 'command_execution':
71
+ return {
72
+ type: 'item',
73
+ itemType: 'command_execution',
74
+ command: item.command,
75
+ output: item.aggregated_output,
76
+ exitCode: item.exit_code,
77
+ status: item.status
78
+ };
79
+
80
+ case 'file_change':
81
+ return {
82
+ type: 'item',
83
+ itemType: 'file_change',
84
+ changes: item.changes,
85
+ status: item.status
86
+ };
87
+
88
+ case 'mcp_tool_call':
89
+ return {
90
+ type: 'item',
91
+ itemType: 'mcp_tool_call',
92
+ server: item.server,
93
+ tool: item.tool,
94
+ arguments: item.arguments,
95
+ result: item.result,
96
+ error: item.error,
97
+ status: item.status
98
+ };
99
+
100
+ case 'web_search':
101
+ return {
102
+ type: 'item',
103
+ itemType: 'web_search',
104
+ query: item.query
105
+ };
106
+
107
+ case 'todo_list':
108
+ return {
109
+ type: 'item',
110
+ itemType: 'todo_list',
111
+ items: item.items
112
+ };
113
+
114
+ case 'error':
115
+ return {
116
+ type: 'item',
117
+ itemType: 'error',
118
+ message: {
119
+ role: 'error',
120
+ content: item.message
121
+ }
122
+ };
123
+
124
+ default:
125
+ return {
126
+ type: 'item',
127
+ itemType: item.type,
128
+ item: item
129
+ };
130
+ }
131
+
132
+ case 'turn.started':
133
+ return {
134
+ type: 'turn_started'
135
+ };
136
+
137
+ case 'turn.completed':
138
+ return {
139
+ type: 'turn_complete',
140
+ usage: event.usage
141
+ };
142
+
143
+ case 'turn.failed':
144
+ return {
145
+ type: 'turn_failed',
146
+ error: event.error
147
+ };
148
+
149
+ case 'thread.started':
150
+ return {
151
+ type: 'thread_started',
152
+ threadId: event.thread_id || event.id
153
+ };
154
+
155
+ case 'error':
156
+ return {
157
+ type: 'error',
158
+ message: event.message
159
+ };
160
+
161
+ default:
162
+ return {
163
+ type: event.type,
164
+ data: event
165
+ };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Extract thread id from thread.started events (SDK uses thread_id)
171
+ * @param {object} event
172
+ * @returns {string|null}
173
+ */
174
+ function getThreadIdFromEvent(event) {
175
+ const threadId = event?.thread_id || event?.id;
176
+ return typeof threadId === 'string' && threadId.trim() ? threadId.trim() : null;
177
+ }
178
+
179
+ /**
180
+ * Extract text-bearing item info from Codex item events.
181
+ * Only agent_message and reasoning currently stream textual deltas.
182
+ * @param {object} item
183
+ * @returns {{itemId: string, itemType: string, text: string, isReasoning: boolean}|null}
184
+ */
185
+ function getTextItemInfo(item) {
186
+ if (!item || (item.type !== 'agent_message' && item.type !== 'reasoning')) {
187
+ return null;
188
+ }
189
+
190
+ const itemId = typeof item.id === 'string' && item.id.trim()
191
+ ? item.id.trim()
192
+ : null;
193
+ if (!itemId) {
194
+ return null;
195
+ }
196
+
197
+ return {
198
+ itemId,
199
+ itemType: item.type,
200
+ text: typeof item.text === 'string' ? item.text : '',
201
+ isReasoning: item.type === 'reasoning'
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Map permission mode to Codex SDK options
207
+ * @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
208
+ * @returns {object} - { sandboxMode, approvalPolicy }
209
+ */
210
+ function mapPermissionModeToCodexOptions(permissionMode) {
211
+ switch (permissionMode) {
212
+ case 'acceptEdits':
213
+ return {
214
+ sandboxMode: 'workspace-write',
215
+ approvalPolicy: 'never'
216
+ };
217
+ case 'bypassPermissions':
218
+ return {
219
+ sandboxMode: 'danger-full-access',
220
+ approvalPolicy: 'never'
221
+ };
222
+ case 'default':
223
+ default:
224
+ return {
225
+ sandboxMode: 'workspace-write',
226
+ // Keep default mode writable and non-blocking for coding workflows.
227
+ approvalPolicy: 'never'
228
+ };
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Execute a Codex query with streaming
234
+ * @param {string} command - The prompt to send
235
+ * @param {object} options - Options including cwd, sessionId, model, permissionMode
236
+ * @param {WebSocket|object} ws - WebSocket connection or response writer
237
+ */
238
+ export async function queryCodex(command, options = {}, ws) {
239
+ const {
240
+ sessionId,
241
+ cwd,
242
+ projectPath,
243
+ model,
244
+ permissionMode = 'default',
245
+ modelReasoningEffort,
246
+ images
247
+ } = options;
248
+
249
+ const workingDirectory = resolveWorkingDirectory({ cwd, projectPath });
250
+ const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
251
+
252
+ let codex;
253
+ let thread;
254
+ let currentSessionId = sessionId;
255
+ let resolvedSessionIdFromEvent = null;
256
+ const streamedTextByItemId = new Map();
257
+ const streamAbortController = new AbortController();
258
+ let hasStreamEvent = false;
259
+ let startupTimeoutId = null;
260
+ let meaningfulEventTimeoutId = null;
261
+ let timeoutErrorMessage = null;
262
+ let terminalStatus = 'completed';
263
+ let shouldSendCompletionEvent = true;
264
+ let tempImagePaths = [];
265
+ let tempImageDir = null;
266
+
267
+ const clearStartupTimeout = () => {
268
+ if (startupTimeoutId) {
269
+ clearTimeout(startupTimeoutId);
270
+ startupTimeoutId = null;
271
+ }
272
+ };
273
+
274
+ const clearMeaningfulEventTimeout = () => {
275
+ if (meaningfulEventTimeoutId) {
276
+ clearTimeout(meaningfulEventTimeoutId);
277
+ meaningfulEventTimeoutId = null;
278
+ }
279
+ };
280
+
281
+ const armStartupTimeout = () => {
282
+ clearStartupTimeout();
283
+
284
+ if (!Number.isFinite(CODEX_STREAM_START_TIMEOUT_MS) || CODEX_STREAM_START_TIMEOUT_MS <= 0) {
285
+ return;
286
+ }
287
+
288
+ startupTimeoutId = setTimeout(() => {
289
+ if (hasStreamEvent) {
290
+ return;
291
+ }
292
+
293
+ timeoutErrorMessage =
294
+ `Codex did not produce any stream events within ${Math.round(CODEX_STREAM_START_TIMEOUT_MS / 1000)} seconds. ` +
295
+ 'This usually means the Codex client or upstream service is unavailable.';
296
+ streamAbortController.abort();
297
+ }, CODEX_STREAM_START_TIMEOUT_MS);
298
+ };
299
+
300
+ const armMeaningfulEventTimeout = () => {
301
+ clearMeaningfulEventTimeout();
302
+
303
+ if (!Number.isFinite(CODEX_MEANINGFUL_EVENT_TIMEOUT_MS) || CODEX_MEANINGFUL_EVENT_TIMEOUT_MS <= 0) {
304
+ return;
305
+ }
306
+
307
+ meaningfulEventTimeoutId = setTimeout(() => {
308
+ timeoutErrorMessage =
309
+ `Codex stopped emitting progress events for ${Math.round(CODEX_MEANINGFUL_EVENT_TIMEOUT_MS / 1000)} seconds. ` +
310
+ 'The upstream Codex service may be unavailable or stalled.';
311
+ streamAbortController.abort();
312
+ }, CODEX_MEANINGFUL_EVENT_TIMEOUT_MS);
313
+ };
314
+
315
+ const markStreamEventObserved = (options = {}) => {
316
+ const { keepProgressWatch = true } = options;
317
+ hasStreamEvent = true;
318
+ clearStartupTimeout();
319
+
320
+ if (keepProgressWatch) {
321
+ armMeaningfulEventTimeout();
322
+ } else {
323
+ clearMeaningfulEventTimeout();
324
+ }
325
+ };
326
+
327
+ const syncResolvedSessionId = (broadcastUpdate = false) => {
328
+ const resolvedThreadId = typeof thread?.id === 'string' && thread.id.trim()
329
+ ? thread.id.trim()
330
+ : (typeof resolvedSessionIdFromEvent === 'string' && resolvedSessionIdFromEvent.trim()
331
+ ? resolvedSessionIdFromEvent.trim()
332
+ : null);
333
+
334
+ if (!resolvedThreadId || resolvedThreadId === currentSessionId) {
335
+ return false;
336
+ }
337
+
338
+ const previousSessionId = currentSessionId;
339
+ const trackedSession = previousSessionId ? activeCodexSessions.get(previousSessionId) : null;
340
+
341
+ if (trackedSession && previousSessionId !== resolvedThreadId) {
342
+ activeCodexSessions.delete(previousSessionId);
343
+ activeCodexSessions.set(resolvedThreadId, trackedSession);
344
+ }
345
+
346
+ currentSessionId = resolvedThreadId;
347
+
348
+ if (ws?.setSessionId && typeof ws.setSessionId === 'function') {
349
+ ws.setSessionId(currentSessionId);
350
+ }
351
+
352
+ if (broadcastUpdate) {
353
+ sendMessage(ws, {
354
+ type: 'session-created',
355
+ sessionId: currentSessionId,
356
+ provider: 'codex'
357
+ });
358
+ }
359
+
360
+ return true;
361
+ };
362
+
363
+ try {
364
+ // Initialize Codex SDK
365
+ codex = new Codex({
366
+ codexPathOverride: getCodexPathOverride(),
367
+ env: getCodexProcessEnv()
368
+ });
369
+
370
+ // Thread options with sandbox and approval settings
371
+ const threadOptions = {
372
+ workingDirectory,
373
+ skipGitRepoCheck: true,
374
+ sandboxMode,
375
+ approvalPolicy,
376
+ model,
377
+ ...(modelReasoningEffort ? { modelReasoningEffort } : {})
378
+ };
379
+
380
+ // Start or resume thread
381
+ if (sessionId) {
382
+ thread = codex.resumeThread(sessionId, threadOptions);
383
+ } else {
384
+ thread = codex.startThread(threadOptions);
385
+ }
386
+
387
+ // Get the thread ID
388
+ currentSessionId = thread.id || sessionId || `codex-${Date.now()}`;
389
+ const shouldBroadcastInitialSessionId = !isCodexPlaceholderSessionId(currentSessionId);
390
+
391
+ if (ws?.setSessionId && typeof ws.setSessionId === 'function') {
392
+ ws.setSessionId(currentSessionId);
393
+ }
394
+
395
+ // Track the session
396
+ activeCodexSessions.set(currentSessionId, {
397
+ thread,
398
+ codex,
399
+ abortController: streamAbortController,
400
+ status: 'running',
401
+ startedAt: new Date().toISOString()
402
+ });
403
+
404
+ // Send session created event
405
+ if (shouldBroadcastInitialSessionId) {
406
+ sendMessage(ws, {
407
+ type: 'session-created',
408
+ sessionId: currentSessionId,
409
+ provider: 'codex'
410
+ });
411
+ }
412
+
413
+ // Execute with streaming
414
+ armStartupTimeout();
415
+
416
+ if (Array.isArray(images) && images.length > 0) {
417
+ const materializedImages = await materializeImagesToTempFiles(images, workingDirectory, 'codex-images');
418
+ tempImagePaths = materializedImages.tempImagePaths;
419
+ tempImageDir = materializedImages.tempDir;
420
+ }
421
+
422
+ const runInput = tempImagePaths.length > 0
423
+ ? [
424
+ { type: 'text', text: command },
425
+ ...tempImagePaths.map((imagePath) => ({ type: 'local_image', path: imagePath }))
426
+ ]
427
+ : command;
428
+
429
+ const streamedTurn = await thread.runStreamed(runInput, {
430
+ signal: streamAbortController.signal
431
+ });
432
+ syncResolvedSessionId(true);
433
+
434
+ for await (const event of streamedTurn.events) {
435
+ const isTerminalEvent = event?.type === 'turn.completed' || event?.type === 'turn.failed';
436
+ markStreamEventObserved({ keepProgressWatch: !isTerminalEvent });
437
+
438
+ if (event?.type === 'thread.started') {
439
+ const eventThreadId = getThreadIdFromEvent(event);
440
+ if (eventThreadId) {
441
+ resolvedSessionIdFromEvent = eventThreadId;
442
+ }
443
+ }
444
+
445
+ syncResolvedSessionId(true);
446
+
447
+ // Check if session was aborted
448
+ const session = activeCodexSessions.get(currentSessionId);
449
+ if (!session || session.status === 'aborted') {
450
+ terminalStatus = 'aborted';
451
+ shouldSendCompletionEvent = false;
452
+ break;
453
+ }
454
+
455
+ if (event.type === 'error') {
456
+ const errorMessage = typeof event.message === 'string' ? event.message.trim() : '';
457
+ if (errorMessage) {
458
+ sendMessage(ws, {
459
+ type: 'codex-response',
460
+ data: {
461
+ type: 'provider_notice',
462
+ message: errorMessage
463
+ },
464
+ sessionId: currentSessionId
465
+ });
466
+ }
467
+ continue;
468
+ }
469
+
470
+ if (event.type === 'turn.failed') {
471
+ terminalStatus = 'errored';
472
+ shouldSendCompletionEvent = false;
473
+ clearStartupTimeout();
474
+ clearMeaningfulEventTimeout();
475
+
476
+ const transformed = transformCodexEvent(event);
477
+ sendMessage(ws, {
478
+ type: 'codex-response',
479
+ data: transformed,
480
+ sessionId: currentSessionId
481
+ });
482
+ break;
483
+ }
484
+
485
+ const isItemEvent = event.type === 'item.started' || event.type === 'item.updated' || event.type === 'item.completed';
486
+ if (isItemEvent) {
487
+ const textItemInfo = getTextItemInfo(event.item);
488
+
489
+ if (textItemInfo) {
490
+ const previousText = streamedTextByItemId.get(textItemInfo.itemId) || '';
491
+ const nextText = textItemInfo.text || '';
492
+
493
+ let delta = '';
494
+ if (nextText.startsWith(previousText)) {
495
+ delta = nextText.slice(previousText.length);
496
+ } else if (nextText !== previousText) {
497
+ // Fallback when text is rewritten rather than appended
498
+ delta = nextText;
499
+ }
500
+
501
+ streamedTextByItemId.set(textItemInfo.itemId, nextText);
502
+
503
+ if (delta) {
504
+ sendMessage(ws, {
505
+ type: 'codex-response',
506
+ data: {
507
+ type: 'item_delta',
508
+ itemType: textItemInfo.itemType,
509
+ itemId: textItemInfo.itemId,
510
+ isReasoning: textItemInfo.isReasoning,
511
+ delta
512
+ },
513
+ sessionId: currentSessionId
514
+ });
515
+ }
516
+
517
+ if (event.type === 'item.completed') {
518
+ sendMessage(ws, {
519
+ type: 'codex-response',
520
+ data: {
521
+ type: 'item_done',
522
+ itemType: textItemInfo.itemType,
523
+ itemId: textItemInfo.itemId,
524
+ isReasoning: textItemInfo.isReasoning,
525
+ content: nextText
526
+ },
527
+ sessionId: currentSessionId
528
+ });
529
+ streamedTextByItemId.delete(textItemInfo.itemId);
530
+ }
531
+
532
+ continue;
533
+ }
534
+
535
+ // Non-text items can be noisy during started/updated phases.
536
+ // Keep existing behavior: emit them when completed.
537
+ if (event.type !== 'item.completed') {
538
+ continue;
539
+ }
540
+ }
541
+
542
+ const transformed = transformCodexEvent(event);
543
+
544
+ sendMessage(ws, {
545
+ type: 'codex-response',
546
+ data: transformed,
547
+ sessionId: currentSessionId
548
+ });
549
+
550
+ // Extract and send token usage if available (normalized to match Claude format)
551
+ if (event.type === 'turn.completed' && event.usage) {
552
+ const tokenBudget = parseCodexTurnUsage(event.usage);
553
+ sendMessage(ws, {
554
+ type: 'token-budget',
555
+ data: tokenBudget,
556
+ sessionId: currentSessionId
557
+ });
558
+ }
559
+ }
560
+
561
+ syncResolvedSessionId(true);
562
+
563
+ // Send completion event
564
+ if (!shouldSendCompletionEvent) {
565
+ return;
566
+ }
567
+
568
+ const finalSessionId = (typeof thread?.id === 'string' && thread.id.trim())
569
+ ? thread.id.trim()
570
+ : currentSessionId;
571
+
572
+ currentSessionId = finalSessionId;
573
+ if (ws?.setSessionId && typeof ws.setSessionId === 'function') {
574
+ ws.setSessionId(currentSessionId);
575
+ }
576
+
577
+ sendMessage(ws, {
578
+ type: 'codex-complete',
579
+ sessionId: currentSessionId,
580
+ actualSessionId: finalSessionId
581
+ });
582
+
583
+ } catch (error) {
584
+ console.error('[Codex] Error:', error);
585
+
586
+ const trackedSession = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
587
+ if (trackedSession?.status === 'aborted') {
588
+ terminalStatus = 'aborted';
589
+ return;
590
+ }
591
+
592
+ terminalStatus = 'errored';
593
+ sendMessage(ws, {
594
+ type: 'codex-error',
595
+ error: timeoutErrorMessage || error.message,
596
+ sessionId: currentSessionId
597
+ });
598
+
599
+ } finally {
600
+ clearStartupTimeout();
601
+ clearMeaningfulEventTimeout();
602
+ await cleanupMaterializedImages(tempImagePaths, tempImageDir);
603
+ // Update session status
604
+ if (currentSessionId) {
605
+ const session = activeCodexSessions.get(currentSessionId);
606
+ if (session) {
607
+ session.status = terminalStatus;
608
+ }
609
+ }
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Abort an active Codex session
615
+ * @param {string} sessionId - Session ID to abort
616
+ * @returns {boolean} - Whether abort was successful
617
+ */
618
+ export function abortCodexSession(sessionId) {
619
+ const session = activeCodexSessions.get(sessionId);
620
+
621
+ if (!session) {
622
+ return false;
623
+ }
624
+
625
+ session.status = 'aborted';
626
+ if (session.abortController && !session.abortController.signal.aborted) {
627
+ session.abortController.abort();
628
+ }
629
+
630
+ // The SDK doesn't have a direct abort method, but marking status
631
+ // will cause the streaming loop to exit
632
+
633
+ return true;
634
+ }
635
+
636
+ /**
637
+ * Check if a session is active
638
+ * @param {string} sessionId - Session ID to check
639
+ * @returns {boolean} - Whether session is active
640
+ */
641
+ export function isCodexSessionActive(sessionId) {
642
+ const session = activeCodexSessions.get(sessionId);
643
+ return session?.status === 'running';
644
+ }
645
+
646
+ /**
647
+ * Get all active sessions
648
+ * @returns {Array} - Array of active session info
649
+ */
650
+ export function getActiveCodexSessions() {
651
+ const sessions = [];
652
+
653
+ for (const [id, session] of activeCodexSessions.entries()) {
654
+ if (session.status === 'running') {
655
+ sessions.push({
656
+ id,
657
+ status: session.status,
658
+ startedAt: session.startedAt
659
+ });
660
+ }
661
+ }
662
+
663
+ return sessions;
664
+ }
665
+
666
+ /**
667
+ * Helper to send message via WebSocket or writer
668
+ * @param {WebSocket|object} ws - WebSocket or response writer
669
+ * @param {object} data - Data to send
670
+ */
671
+ function sendMessage(ws, data) {
672
+ try {
673
+ const isStructuredWriter = !!(
674
+ ws?.isSSEStreamWriter ||
675
+ ws?.isWebSocketWriter ||
676
+ typeof ws?.setSessionId === 'function' ||
677
+ typeof ws?.getSessionId === 'function'
678
+ );
679
+
680
+ if (isStructuredWriter) {
681
+ // Internal writers expect plain objects and handle serialization themselves.
682
+ ws.send(data);
683
+ } else if (typeof ws.send === 'function') {
684
+ // Raw WebSocket clients expect a serialized payload.
685
+ ws.send(JSON.stringify(data));
686
+ }
687
+ } catch (error) {
688
+ console.error('[Codex] Error sending message:', error);
689
+ }
690
+ }
691
+
692
+ // Clean up old completed sessions periodically
693
+ setInterval(() => {
694
+ const now = Date.now();
695
+ const maxAge = 30 * 60 * 1000; // 30 minutes
696
+
697
+ for (const [id, session] of activeCodexSessions.entries()) {
698
+ if (session.status !== 'running') {
699
+ const startedAt = new Date(session.startedAt).getTime();
700
+ if (now - startedAt > maxAge) {
701
+ activeCodexSessions.delete(id);
702
+ }
703
+ }
704
+ }
705
+ }, 5 * 60 * 1000); // Every 5 minutes