@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.
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-CTKZtqB1.js +460 -0
- package/dist/assets/{ReviewApp-BEicSBzW.js → ReviewApp-DM6BNAzR.js} +1 -1
- package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-CqJbRZ9y.js} +1 -1
- package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-BS8YH8jO.js} +1 -1
- package/dist/assets/{arc-CEsS3MdK.js → arc-BBmKEN-S.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-BubZ7T3U.js → architectureDiagram-2XIMDMQ5-N5lcb82R.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-jhjtOQ12.js → c4Diagram-IC4MRINW-BTKlkXI9.js} +1 -1
- package/dist/assets/channel-1oJBvF-0.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-DyujyOvx.js → chunk-NQ4KR5QH-CM50ygWP.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-CQHHFLvx.js → chunk-WL4C6EOR-Cm9nQrsr.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
- package/dist/assets/clone-CinxIlEu.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-qykDd54p.js → cose-bilkent-S5V4N54A-Ccp_p0JZ.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-BKtx468K.js → diagram-E7M64L7V-CeNVmFUp.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-DT9YzdNw.js → erDiagram-INFDFZHY-B8R5vwhd.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK--IgwcUhI.js → ganttDiagram-A5KZAMGK-DOu3hSNa.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
- package/dist/assets/{graph-Cw1rYoD9.js → graph-D11wiwHo.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-BCxJHuqY.js → highlighted-body-TPN3WLV5-Babpthg-.js} +1 -1
- package/dist/assets/index-DFxzgWoO.js +2 -0
- package/dist/assets/index-YCFGDVKw.css +1 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-Cl8yrezU.js → ishikawaDiagram-PHBUUO56-BEquZd3E.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-ddP0AMU9.js → journeyDiagram-4ABVD52K-BfemGz7f.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-CWja3mln.js} +1 -1
- package/dist/assets/{layout-W_tRx4UV.js → layout-BLUNf-PJ.js} +1 -1
- package/dist/assets/{linear-CcMb2ay-.js → linear-DukIV_Xv.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-BBJqt8pT.js → mermaid-O7DHMXV3-SgtM28qI.js} +265 -215
- package/dist/assets/{mindmap-definition-YRQLILUH-BGhZa7Na.js → mindmap-definition-YRQLILUH-4UjqXITU.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-BSYuqf0Q.js → quadrantDiagram-337W2JSQ-D60m8V8r.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-Cfi9YX9H.js → requirementDiagram-Z7DCOOCP-zqh9jBVf.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-CYTTG38e.js → sequenceDiagram-2WXFIKYE-7BReFd0L.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-B1sdb5mK.js → timeline-definition-YZTLITO2-CTVllFgr.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-BtyxboJZ.js} +1 -1
- package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-C8QKSyRR.js → xychartDiagram-JWTSCODW-eRk-39YO.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +35 -33
- package/server/_legacy-providers/README.md +30 -0
- package/server/_legacy-providers/claude-sdk.js +956 -0
- package/server/_legacy-providers/gemini-cli.js +368 -0
- package/server/_legacy-providers/openai-codex.js +705 -0
- package/server/_legacy-providers/opencode-cli.js +674 -0
- package/server/acp-runtime/client.js +1872 -0
- package/server/acp-runtime/index.js +408 -0
- package/server/acp-runtime/registry.js +45 -0
- package/server/acp-runtime/session-store.js +254 -0
- package/server/channels/runtime/AgentRuntimeAdapter.js +22 -80
- package/server/claude-sdk.js +24 -946
- package/server/cli.js +140 -2
- package/server/external-agent/service.js +52 -63
- package/server/gemini-cli.js +21 -360
- package/server/index.js +133 -58
- package/server/openai-codex.js +19 -695
- package/server/opencode-cli.js +68 -640
- package/server/projects.js +128 -85
- package/server/routes/agent.js +2 -0
- package/server/routes/cc-connect.js +1131 -0
- package/server/routes/cli-auth.js +1 -73
- package/server/routes/commands.js +4 -9
- package/server/routes/git.js +3 -20
- package/server/routes/projects.js +45 -24
- package/server/routes/session-core.js +44 -10
- package/server/session-core/abortSession.js +2 -18
- package/server/session-core/eventStore.js +5 -1
- package/server/session-core/providerAdapters.js +98 -10
- package/server/session-core/providerDiscovery.js +8 -3
- package/server/session-core/runtimeState.js +16 -17
- package/server/session-core/runtimeWriter.js +19 -12
- package/server/utils/ccConnectManager.js +390 -0
- package/server/utils/ccConnectState.js +575 -0
- package/server/utils/resolveCommandPath.js +71 -0
- package/server/utils/workspaceRoots.js +154 -0
- package/shared/conversationEvents.js +347 -10
- package/dist/assets/App-CYTE30Cf.js +0 -484
- package/dist/assets/channel-RmqTALN0.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +0 -1
- package/dist/assets/clone-oT5aWXpf.js +0 -1
- package/dist/assets/index-CBuAXA5S.js +0 -2
- package/dist/assets/index-CyLWKyxy.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
- package/server/cli.test.js +0 -76
- package/server/external-agent/service.test.js +0 -53
- 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
|