@axhub/genie 0.1.5 → 0.1.7

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.
@@ -140,7 +140,7 @@ function transformCodexEvent(event) {
140
140
  case 'thread.started':
141
141
  return {
142
142
  type: 'thread_started',
143
- threadId: event.id
143
+ threadId: event.thread_id || event.id
144
144
  };
145
145
 
146
146
  case 'error':
@@ -157,6 +157,42 @@ function transformCodexEvent(event) {
157
157
  }
158
158
  }
159
159
 
160
+ /**
161
+ * Extract thread id from thread.started events (SDK uses thread_id)
162
+ * @param {object} event
163
+ * @returns {string|null}
164
+ */
165
+ function getThreadIdFromEvent(event) {
166
+ const threadId = event?.thread_id || event?.id;
167
+ return typeof threadId === 'string' && threadId.trim() ? threadId.trim() : null;
168
+ }
169
+
170
+ /**
171
+ * Extract text-bearing item info from Codex item events.
172
+ * Only agent_message and reasoning currently stream textual deltas.
173
+ * @param {object} item
174
+ * @returns {{itemId: string, itemType: string, text: string, isReasoning: boolean}|null}
175
+ */
176
+ function getTextItemInfo(item) {
177
+ if (!item || (item.type !== 'agent_message' && item.type !== 'reasoning')) {
178
+ return null;
179
+ }
180
+
181
+ const itemId = typeof item.id === 'string' && item.id.trim()
182
+ ? item.id.trim()
183
+ : null;
184
+ if (!itemId) {
185
+ return null;
186
+ }
187
+
188
+ return {
189
+ itemId,
190
+ itemType: item.type,
191
+ text: typeof item.text === 'string' ? item.text : '',
192
+ isReasoning: item.type === 'reasoning'
193
+ };
194
+ }
195
+
160
196
  /**
161
197
  * Map permission mode to Codex SDK options
162
198
  * @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
@@ -195,7 +231,8 @@ export async function queryCodex(command, options = {}, ws) {
195
231
  cwd,
196
232
  projectPath,
197
233
  model,
198
- permissionMode = 'default'
234
+ permissionMode = 'default',
235
+ modelReasoningEffort
199
236
  } = options;
200
237
 
201
238
  const workingDirectory = cwd || projectPath || process.cwd();
@@ -205,6 +242,7 @@ export async function queryCodex(command, options = {}, ws) {
205
242
  let thread;
206
243
  let currentSessionId = sessionId;
207
244
  let resolvedSessionIdFromEvent = null;
245
+ const streamedTextByItemId = new Map();
208
246
 
209
247
  const syncResolvedSessionId = (broadcastUpdate = false) => {
210
248
  const resolvedThreadId = typeof thread?.id === 'string' && thread.id.trim()
@@ -252,7 +290,8 @@ export async function queryCodex(command, options = {}, ws) {
252
290
  skipGitRepoCheck: true,
253
291
  sandboxMode,
254
292
  approvalPolicy,
255
- model
293
+ model,
294
+ ...(modelReasoningEffort ? { modelReasoningEffort } : {})
256
295
  };
257
296
 
258
297
  // Start or resume thread
@@ -289,8 +328,11 @@ export async function queryCodex(command, options = {}, ws) {
289
328
  syncResolvedSessionId(true);
290
329
 
291
330
  for await (const event of streamedTurn.events) {
292
- if (event?.type === 'thread.started' && typeof event.id === 'string' && event.id.trim()) {
293
- resolvedSessionIdFromEvent = event.id.trim();
331
+ if (event?.type === 'thread.started') {
332
+ const eventThreadId = getThreadIdFromEvent(event);
333
+ if (eventThreadId) {
334
+ resolvedSessionIdFromEvent = eventThreadId;
335
+ }
294
336
  }
295
337
 
296
338
  syncResolvedSessionId(true);
@@ -301,8 +343,61 @@ export async function queryCodex(command, options = {}, ws) {
301
343
  break;
302
344
  }
303
345
 
304
- if (event.type === 'item.started' || event.type === 'item.updated') {
305
- continue;
346
+ const isItemEvent = event.type === 'item.started' || event.type === 'item.updated' || event.type === 'item.completed';
347
+ if (isItemEvent) {
348
+ const textItemInfo = getTextItemInfo(event.item);
349
+
350
+ if (textItemInfo) {
351
+ const previousText = streamedTextByItemId.get(textItemInfo.itemId) || '';
352
+ const nextText = textItemInfo.text || '';
353
+
354
+ let delta = '';
355
+ if (nextText.startsWith(previousText)) {
356
+ delta = nextText.slice(previousText.length);
357
+ } else if (nextText !== previousText) {
358
+ // Fallback when text is rewritten rather than appended
359
+ delta = nextText;
360
+ }
361
+
362
+ streamedTextByItemId.set(textItemInfo.itemId, nextText);
363
+
364
+ if (delta) {
365
+ sendMessage(ws, {
366
+ type: 'codex-response',
367
+ data: {
368
+ type: 'item_delta',
369
+ itemType: textItemInfo.itemType,
370
+ itemId: textItemInfo.itemId,
371
+ isReasoning: textItemInfo.isReasoning,
372
+ delta
373
+ },
374
+ sessionId: currentSessionId
375
+ });
376
+ }
377
+
378
+ if (event.type === 'item.completed') {
379
+ sendMessage(ws, {
380
+ type: 'codex-response',
381
+ data: {
382
+ type: 'item_done',
383
+ itemType: textItemInfo.itemType,
384
+ itemId: textItemInfo.itemId,
385
+ isReasoning: textItemInfo.isReasoning,
386
+ content: nextText
387
+ },
388
+ sessionId: currentSessionId
389
+ });
390
+ streamedTextByItemId.delete(textItemInfo.itemId);
391
+ }
392
+
393
+ continue;
394
+ }
395
+
396
+ // Non-text items can be noisy during started/updated phases.
397
+ // Keep existing behavior: emit them when completed.
398
+ if (event.type !== 'item.completed') {
399
+ continue;
400
+ }
306
401
  }
307
402
 
308
403
  const transformed = transformCodexEvent(event);