@rynx-ai/runtime 0.1.11-beta.24 → 0.1.11-beta.26

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.
@@ -1,4 +1,4 @@
1
- import { codexUserContent } from "../input-resources.js";
1
+ import { codexUserEchoContent } from "../input-resources.js";
2
2
  import { codexResumeTerminalStatus, mapCodexItem, mapCodexNotification, } from "./mapping.js";
3
3
  const MCP_STARTUP_STATUS_METHOD = "mcpServer/startupStatus/updated";
4
4
  const MCP_TERMINAL_STATES = new Set(["ready", "failed", "cancelled"]);
@@ -13,14 +13,7 @@ function turnIdFrom(params) {
13
13
  function userMessageContent(item) {
14
14
  if (item.type !== "userMessage")
15
15
  return undefined;
16
- const parts = codexUserContent(item.content);
17
- if (parts.length === 0)
18
- return undefined;
19
- if (parts.every((part) => part.type === "input_text")) {
20
- const text = parts.map((part) => part.text).join("").trim();
21
- return text || undefined;
22
- }
23
- return parts;
16
+ return codexUserEchoContent(item.content);
24
17
  }
25
18
  /** Whether a notification implies the thread is now active (its first turn has
26
19
  * begun, so the rollout exists). Mirrors reference implementation's `_event_indicates_thread_active`. */
@@ -59,6 +52,20 @@ function isTurnScopedMethod(method) {
59
52
  function canRecoverMissedTurnFrom(method) {
60
53
  return method === "item/agentMessage/delta" || method === "item/plan/delta";
61
54
  }
55
+ /** Omnigent forwards these live deltas only for the bridge's exact active
56
+ * Codex turn. Assistant/plan deltas may recover a missed `turn/started` when
57
+ * fully scoped; command output never opens or recovers a Turn. */
58
+ function requiresMatchingActiveTurn(method) {
59
+ return canRecoverMissedTurnFrom(method) || method === "item/commandExecution/outputDelta";
60
+ }
61
+ /** Content Omnigent forwards independently from the active-turn status edge. */
62
+ function isIndependentTurnContent(method) {
63
+ return method === "item/started" ||
64
+ method === "item/reasoning/textDelta" ||
65
+ method === "item/reasoning/summaryTextDelta" ||
66
+ method === "item/fileChange/patchUpdated" ||
67
+ method === "turn/plan/updated";
68
+ }
62
69
  export class CodexSessionForwarder {
63
70
  client;
64
71
  sink;
@@ -86,6 +93,11 @@ export class CodexSessionForwarder {
86
93
  /** Per-(thread,turn) position counter for items lacking a stable codex id
87
94
  * (peek-then-advance; advanced only on a successful claim). reference implementation anon path. */
88
95
  anonCounters = new Map();
96
+ pendingPlanImplementation = null;
97
+ /** Latest aggregate diff per Turn. Codex republishes the complete diff after
98
+ * every edit; only the terminal snapshot belongs in transcript history. */
99
+ turnDiffByTurn = new Map();
100
+ replayingBackfill = false;
89
101
  constructor(client, sink, options = {}) {
90
102
  this.client = client;
91
103
  this.sink = sink;
@@ -120,6 +132,7 @@ export class CodexSessionForwarder {
120
132
  else {
121
133
  this.currentTurnIdValue = null;
122
134
  }
135
+ this.turnDiffByTurn.clear();
123
136
  }
124
137
  /** True while the provider owns an active turn, including the short interval
125
138
  * between injection acceptance and observer confirmation. */
@@ -191,22 +204,37 @@ export class CodexSessionForwarder {
191
204
  * not doubled.
192
205
  */
193
206
  replayBackfill(turns) {
194
- for (const turn of turns) {
195
- const turnId = turn.id ?? turn.turnId;
196
- if (turnId)
197
- this.currentTurnIdValue = turnId;
198
- this.ensureTurn();
199
- for (const item of turn.items ?? [])
200
- this.processCompletedItem(item);
201
- if (codexResumeTerminalStatus(turn) === undefined)
202
- continue;
203
- const mapped = mapCodexNotification("turn/completed", { turn });
204
- this.turnOpen = false;
205
- this.currentTurnIdValue = null;
206
- if (mapped.fatalError)
207
- this.sink.onTurnError(mapped.fatalError);
208
- else
209
- this.sink.onTurnEnd(mapped.usage);
207
+ this.replayingBackfill = true;
208
+ try {
209
+ for (const turn of turns) {
210
+ const turnId = turn.id ?? turn.turnId;
211
+ if (turnId)
212
+ this.currentTurnIdValue = turnId;
213
+ this.ensureTurn();
214
+ for (const item of turn.items ?? [])
215
+ this.processCompletedItem(item);
216
+ if (codexResumeTerminalStatus(turn) === undefined)
217
+ continue;
218
+ const mapped = mapCodexNotification("turn/completed", { turn });
219
+ this.turnOpen = false;
220
+ this.currentTurnIdValue = null;
221
+ if (mapped.fatalError)
222
+ this.sink.onTurnError(mapped.fatalError);
223
+ else
224
+ this.sink.onTurnEnd(mapped.usage);
225
+ }
226
+ }
227
+ finally {
228
+ this.replayingBackfill = false;
229
+ }
230
+ }
231
+ /** Suppress our synthesized picker when a future app-server emits the native
232
+ * `plan_implementation` request itself. */
233
+ noteNativePlanImplementationPrompt(turnId) {
234
+ if (!this.pendingPlanImplementation)
235
+ return;
236
+ if (!turnId || this.pendingPlanImplementation.turnId === turnId) {
237
+ this.pendingPlanImplementation = null;
210
238
  }
211
239
  }
212
240
  /** Fail an open response exactly once when its observer or terminal exits. */
@@ -234,6 +262,7 @@ export class CodexSessionForwarder {
234
262
  }
235
263
  this.currentTurnIdValue = null;
236
264
  this.activeSignaled = false;
265
+ this.turnDiffByTurn.clear();
237
266
  this.currentThreadIdValue = tid;
238
267
  this.sink.onThreadStarted?.(tid, normalizedForkedFromId);
239
268
  }
@@ -248,6 +277,14 @@ export class CodexSessionForwarder {
248
277
  this.handleMcpStartupStatus(params);
249
278
  return;
250
279
  }
280
+ if (method === "thread/settings/updated") {
281
+ const threadSettings = params?.threadSettings;
282
+ const mode = (threadSettings?.collaborationMode ?? threadSettings?.collaboration_mode)?.mode;
283
+ if (mode === "plan" || mode === "default") {
284
+ this.sink.onCollaborationModeChanged?.(mode);
285
+ }
286
+ return;
287
+ }
251
288
  if (isThreadIdle(method, params) || isModelOutput(method, params)) {
252
289
  this.settleMcpStartup();
253
290
  }
@@ -264,6 +301,21 @@ export class CodexSessionForwarder {
264
301
  if (method === "queue/status" && this.currentTurnIdValue === null)
265
302
  return;
266
303
  const carriedTurnId = turnIdFrom(params);
304
+ if (method === "turn/diff/updated") {
305
+ if (!carriedTurnId)
306
+ return;
307
+ const update = params;
308
+ const diff = typeof update?.diff === "string"
309
+ ? update.diff
310
+ : typeof update?.unifiedDiff === "string"
311
+ ? update.unifiedDiff
312
+ : "";
313
+ if (diff)
314
+ this.turnDiffByTurn.set(carriedTurnId, diff);
315
+ else
316
+ this.turnDiffByTurn.delete(carriedTurnId);
317
+ return;
318
+ }
267
319
  if (method === "turn/started") {
268
320
  this.beginTurn(carriedTurnId);
269
321
  this.sink.onTurnObserved?.(carriedTurnId);
@@ -271,12 +323,21 @@ export class CodexSessionForwarder {
271
323
  }
272
324
  if (isTerminalTurnMethod(method)) {
273
325
  if (!this.terminalBoundaryMatchesActiveTurn(carriedTurnId, notificationThreadId)) {
326
+ const staleDiff = this.consumeTurnDiff(carriedTurnId);
327
+ if (staleDiff && carriedTurnId) {
328
+ this.sink.onTurnContentEvent?.(carriedTurnId, staleDiff);
329
+ }
274
330
  return;
275
331
  }
276
332
  const hadActiveTurn = this.currentTurnIdValue !== null;
277
333
  const hadOpenResponse = this.turnOpen;
278
334
  const mapped = mapCodexNotification(method, params);
335
+ const terminalTurnId = carriedTurnId ?? this.currentTurnIdValue;
336
+ const turnDiff = this.consumeTurnDiff(terminalTurnId);
279
337
  if (!hadActiveTurn && !hadOpenResponse) {
338
+ if (turnDiff && terminalTurnId) {
339
+ this.sink.onTurnContentEvent?.(terminalTurnId, turnDiff);
340
+ }
280
341
  const turn = params?.turn;
281
342
  const recoveredStatus = mapped.fatalError ||
282
343
  method === "turn/failed" ||
@@ -298,10 +359,18 @@ export class CodexSessionForwarder {
298
359
  this.sink.onEvent(event);
299
360
  }
300
361
  this.scheduleCompletion(mapped.fatalError
301
- ? { kind: "error", error: mapped.fatalError }
362
+ ? { kind: "error", error: mapped.fatalError, ...(turnDiff ? { turnDiff } : {}) }
302
363
  : mapped.turnInterrupted
303
- ? { kind: "interrupted", ...(mapped.usage ? { usage: mapped.usage } : {}) }
304
- : { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
364
+ ? {
365
+ kind: "interrupted",
366
+ ...(mapped.usage ? { usage: mapped.usage } : {}),
367
+ ...(turnDiff ? { turnDiff } : {}),
368
+ }
369
+ : {
370
+ kind: "end",
371
+ ...(mapped.usage ? { usage: mapped.usage } : {}),
372
+ ...(turnDiff ? { turnDiff } : {}),
373
+ });
305
374
  return;
306
375
  }
307
376
  // A late event from an older turn must not replace the app-server's active
@@ -310,32 +379,66 @@ export class CodexSessionForwarder {
310
379
  this.currentTurnIdValue &&
311
380
  carriedTurnId !== this.currentTurnIdValue &&
312
381
  isTurnScopedMethod(method)) {
313
- return;
382
+ // Durable completed items and passive content belong to their carried
383
+ // response even if a newer Turn is active. Only active-turn deltas and
384
+ // lifecycle edges are stale with respect to the newer Turn.
385
+ if (method === "item/completed") {
386
+ // Continue into the completed-item dedup/order path below.
387
+ }
388
+ else if (isIndependentTurnContent(method) && this.sink.onTurnContentEvent) {
389
+ const mapped = mapCodexNotification(method, params);
390
+ for (const event of mapped.events) {
391
+ if (event.type !== "runtime_debug") {
392
+ this.sink.onTurnContentEvent(carriedTurnId, event);
393
+ }
394
+ }
395
+ return;
396
+ }
397
+ else {
398
+ return;
399
+ }
314
400
  }
315
- if (carriedTurnId && this.currentTurnIdValue === null) {
316
- if (canRecoverMissedTurnFrom(method)) {
317
- if (!this.notificationMatchesCurrentThread(notificationThreadId))
401
+ if (requiresMatchingActiveTurn(method)) {
402
+ if (this.currentTurnIdValue !== null) {
403
+ // An id-less delta is not attributable to the active Turn. Omnigent's
404
+ // `_is_active_turn_delta` applies the same exact-id requirement.
405
+ if (carriedTurnId !== this.currentTurnIdValue)
318
406
  return;
407
+ }
408
+ else if (carriedTurnId &&
409
+ canRecoverMissedTurnFrom(method) &&
410
+ this.notificationMatchesCurrentThread(notificationThreadId)) {
319
411
  this.currentTurnIdValue = carriedTurnId;
320
412
  this.ensureTurn();
321
413
  }
414
+ else {
415
+ return;
416
+ }
322
417
  }
323
418
  // Completed items (user echo, assistant, tool, reasoning) go through the
324
419
  // deduped path so a resume backfill and the live stream never double them.
325
420
  if (method === "item/completed") {
326
421
  const item = params?.item;
327
422
  if (item) {
423
+ if (carriedTurnId &&
424
+ this.currentTurnIdValue &&
425
+ carriedTurnId !== this.currentTurnIdValue) {
426
+ // This is durable content for an older response, not an ordering
427
+ // signal for the newer Turn's deferred assistant message.
428
+ this.processCompletedItem(item, carriedTurnId);
429
+ return;
430
+ }
328
431
  if (this.shouldDeferAssistantMessage(item)) {
329
- this.deferAssistantMessage(item);
432
+ this.deferAssistantMessage(item, carriedTurnId);
330
433
  this.refreshCompletionGrace();
331
434
  return;
332
435
  }
333
- // A late reasoning item belongs before the held final answer. Any other
334
- // completed item establishes that the held message was not final and
335
- // must retain its original position.
436
+ // Within one Turn, a late reasoning item belongs before the held final
437
+ // answer. Any other completed item establishes that the held message
438
+ // was not final and must retain its original position.
336
439
  if (item.type !== "reasoning")
337
440
  this.flushDeferredAssistantMessage();
338
- this.processCompletedItem(item);
441
+ this.processCompletedItem(item, carriedTurnId);
339
442
  this.refreshCompletionGrace();
340
443
  return;
341
444
  }
@@ -350,6 +453,19 @@ export class CodexSessionForwarder {
350
453
  if (canonicalEvents.some((event) => event.type !== "reasoning_delta" && event.type !== "reasoning_completed")) {
351
454
  this.flushDeferredAssistantMessage();
352
455
  }
456
+ // Omnigent's item/content channel is independent from its turn-status
457
+ // channel. A scoped item or reasoning delta that arrives after the terminal
458
+ // edge remains visible, but must not synthesize another running response.
459
+ if (!this.turnOpen &&
460
+ this.currentTurnIdValue === null &&
461
+ !mapped.fatalError &&
462
+ canonicalEvents.length > 0 &&
463
+ this.sink.onTurnContentEvent) {
464
+ for (const event of canonicalEvents) {
465
+ this.sink.onTurnContentEvent(carriedTurnId, event);
466
+ }
467
+ return;
468
+ }
353
469
  if (canonicalEvents.length || mapped.turnCompleted || mapped.fatalError) {
354
470
  this.ensureTurn();
355
471
  }
@@ -400,6 +516,18 @@ export class CodexSessionForwarder {
400
516
  }
401
517
  settle(completion) {
402
518
  this.flushDeferredAssistantMessage();
519
+ const completedTurnId = this.pendingCompletionTurnId ?? this.currentTurnIdValue;
520
+ const planPrompt = completion.kind === "end" &&
521
+ this.pendingPlanImplementation?.turnId === completedTurnId
522
+ ? this.pendingPlanImplementation
523
+ : null;
524
+ if (this.pendingPlanImplementation?.turnId === completedTurnId) {
525
+ this.pendingPlanImplementation = null;
526
+ }
527
+ if (completion.turnDiff)
528
+ this.sink.onEvent(completion.turnDiff);
529
+ if (completedTurnId)
530
+ this.turnDiffByTurn.delete(completedTurnId);
403
531
  this.turnOpen = false;
404
532
  this.currentTurnIdValue = null;
405
533
  this.pendingCompletionTurnId = null;
@@ -411,8 +539,11 @@ export class CodexSessionForwarder {
411
539
  else
412
540
  this.sink.onTurnEnd(completion.usage);
413
541
  }
414
- else
542
+ else {
415
543
  this.sink.onTurnEnd(completion.usage);
544
+ if (planPrompt)
545
+ this.sink.onPlanImplementationPrompt?.(planPrompt);
546
+ }
416
547
  }
417
548
  handleMcpStartupStatus(params) {
418
549
  const update = params;
@@ -483,31 +614,52 @@ export class CodexSessionForwarder {
483
614
  }
484
615
  /** Map + emit one completed codex item, deduped by a TOTAL key and routing the
485
616
  * user echo to {@link CodexForwarderSink.onUserMessage}. Shared by live + backfill. */
486
- processCompletedItem(item) {
487
- if (!this.claimCompletedItem(item))
617
+ processCompletedItem(item, completedTurnId) {
618
+ if (!this.claimCompletedItem(item, completedTurnId))
488
619
  return;
489
- this.emitCompletedItem(item);
620
+ this.emitCompletedItem(item, completedTurnId);
490
621
  }
491
- claimCompletedItem(item) {
492
- const { key, isAnon } = this.completedItemKey(item);
622
+ claimCompletedItem(item, completedTurnId) {
623
+ const { key, isAnon } = this.completedItemKey(item, completedTurnId);
493
624
  if (this.seenCompletedItems.has(key))
494
625
  return false;
495
626
  this.seenCompletedItems.add(key);
496
627
  if (isAnon)
497
- this.advanceAnonCounter();
628
+ this.advanceAnonCounter(completedTurnId);
498
629
  return true;
499
630
  }
500
- emitCompletedItem(item) {
631
+ emitCompletedItem(item, completedTurnId) {
632
+ const outsideTurnLifecycle = !this.turnOpen || Boolean(completedTurnId &&
633
+ this.currentTurnIdValue &&
634
+ completedTurnId !== this.currentTurnIdValue);
501
635
  const userContent = userMessageContent(item);
502
636
  if (userContent !== undefined) {
637
+ if (outsideTurnLifecycle && this.sink.onTurnContentUserMessage) {
638
+ this.sink.onTurnContentUserMessage(completedTurnId, userContent);
639
+ return;
640
+ }
503
641
  this.ensureTurn();
504
642
  this.sink.onUserMessage?.(userContent);
505
643
  return;
506
644
  }
645
+ if (item.type === "plan" && !this.replayingBackfill) {
646
+ const text = item.text?.trim();
647
+ const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId;
648
+ const threadId = this.currentThreadIdValue;
649
+ if (text && turnId && threadId) {
650
+ this.pendingPlanImplementation = { threadId, turnId, text };
651
+ }
652
+ }
507
653
  const mapped = mapCodexItem("item/completed", item);
508
654
  const canonicalEvents = mapped.events.filter((event) => event.type !== "runtime_debug");
509
655
  if (canonicalEvents.length === 0)
510
656
  return;
657
+ if (outsideTurnLifecycle && this.sink.onTurnContentEvent) {
658
+ for (const event of canonicalEvents) {
659
+ this.sink.onTurnContentEvent(completedTurnId, event);
660
+ }
661
+ return;
662
+ }
511
663
  this.ensureTurn();
512
664
  for (const event of canonicalEvents)
513
665
  this.sink.onEvent(event);
@@ -518,11 +670,11 @@ export class CodexSessionForwarder {
518
670
  }
519
671
  return Boolean(item.text?.trim());
520
672
  }
521
- deferAssistantMessage(item) {
522
- if (!this.claimCompletedItem(item))
673
+ deferAssistantMessage(item, turnId) {
674
+ if (!this.claimCompletedItem(item, turnId))
523
675
  return;
524
676
  this.flushDeferredAssistantMessage();
525
- this.deferredAssistantMessage = item;
677
+ this.deferredAssistantMessage = { item, ...(turnId ? { turnId } : {}) };
526
678
  const graceMs = this.options.assistantMessageGraceMs ?? 0;
527
679
  this.assistantMessageTimer = setTimeout(() => this.flushDeferredAssistantMessage(), graceMs);
528
680
  this.assistantMessageTimer.unref?.();
@@ -531,26 +683,26 @@ export class CodexSessionForwarder {
531
683
  if (this.assistantMessageTimer)
532
684
  clearTimeout(this.assistantMessageTimer);
533
685
  this.assistantMessageTimer = null;
534
- const item = this.deferredAssistantMessage;
686
+ const deferred = this.deferredAssistantMessage;
535
687
  this.deferredAssistantMessage = null;
536
- if (item)
537
- this.emitCompletedItem(item);
688
+ if (deferred)
689
+ this.emitCompletedItem(deferred.item, deferred.turnId);
538
690
  }
539
691
  /** Build a total dedup key for one completed item. Stable-id items use
540
692
  * `threadId:turnId:item.id` — identical across replay + live, so the second
541
693
  * delivery is dropped. Items without a codex id fall back to a per-(thread,turn)
542
694
  * position counter (peeked here; advanced only on a successful claim, via
543
695
  * {@link advanceAnonCounter}). Mirrors reference implementation `_completed_item_key`. */
544
- completedItemKey(item) {
696
+ completedItemKey(item, completedTurnId) {
545
697
  const threadId = this.currentThreadIdValue ?? "thread";
546
- const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
698
+ const turnId = completedTurnId ?? this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
547
699
  if (item.id)
548
700
  return { key: `${threadId}:${turnId}:${item.id}`, isAnon: false };
549
701
  const n = this.anonCounters.get(`${threadId}:${turnId}`) ?? 0;
550
702
  return { key: `${threadId}:${turnId}:anon:${n}`, isAnon: true };
551
703
  }
552
- advanceAnonCounter() {
553
- const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
704
+ advanceAnonCounter(completedTurnId) {
705
+ const turnId = completedTurnId ?? this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
554
706
  const k = `${this.currentThreadIdValue ?? "thread"}:${turnId}`;
555
707
  this.anonCounters.set(k, (this.anonCounters.get(k) ?? 0) + 1);
556
708
  }
@@ -560,6 +712,15 @@ export class CodexSessionForwarder {
560
712
  this.sink.onTurnStart(this.currentTurnIdValue ?? undefined);
561
713
  }
562
714
  }
715
+ consumeTurnDiff(turnId) {
716
+ if (!turnId)
717
+ return undefined;
718
+ const diff = this.turnDiffByTurn.get(turnId);
719
+ this.turnDiffByTurn.delete(turnId);
720
+ return diff
721
+ ? { type: "turn_diff", callId: `codex_turn_diff_${turnId}`, diff }
722
+ : undefined;
723
+ }
563
724
  /** Start (or confirm) the app-server's authoritative active turn. A newer
564
725
  * start supersedes an older response whose terminal edge arrived late; a
565
726
  * pending Traex completion is flushed first so its final item grace remains
@@ -1,9 +1,3 @@
1
- /**
2
- * Pure codex app-server notification → {@link AgentEvent} mapping. Extracted from
3
- * the executor so BOTH the per-turn executor and the persistent session
4
- * forwarder (which mirrors every thread turn — web- AND TUI-initiated — into the
5
- * canonical log, reference implementation's codex-native model) share one mapping.
6
- */
7
1
  import type { AgentEvent } from "@rynx-ai/core";
8
2
  import type { ThreadItem } from "./protocol.js";
9
3
  export interface CodexMapResult {
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Pure codex app-server notification → {@link AgentEvent} mapping. Extracted from
3
+ * the executor so BOTH the per-turn executor and the persistent session
4
+ * forwarder (which mirrors every thread turn — web- AND TUI-initiated — into the
5
+ * canonical log, reference implementation's codex-native model) share one mapping.
6
+ */
7
+ import { extname, isAbsolute } from "node:path";
1
8
  function isRecord(value) {
2
9
  return value !== null && typeof value === "object" && !Array.isArray(value);
3
10
  }
@@ -115,6 +122,23 @@ function webSearchInput(item) {
115
122
  }
116
123
  return { id: item.id, query: item.query };
117
124
  }
125
+ function fileChangeSummary(changes) {
126
+ const lines = [];
127
+ for (const change of changes) {
128
+ if (!isRecord(change))
129
+ continue;
130
+ const kind = isRecord(change.kind) && typeof change.kind.type === "string" && change.kind.type
131
+ ? change.kind.type
132
+ : "change";
133
+ lines.push(`${kind} ${String(change.path)}`);
134
+ }
135
+ return lines.join("\n");
136
+ }
137
+ function isSupportedAbsoluteImagePath(path) {
138
+ if (!isAbsolute(path))
139
+ return false;
140
+ return [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"].includes(extname(path).toLowerCase());
141
+ }
118
142
  /** Map one thread item (`item/started` | `item/completed`) to events. */
119
143
  export function mapCodexItem(method, item) {
120
144
  const events = [];
@@ -144,16 +168,18 @@ export function mapCodexItem(method, item) {
144
168
  return { events };
145
169
  }
146
170
  case "fileChange": {
171
+ const fileChange = item;
172
+ const changes = Array.isArray(fileChange.changes) ? fileChange.changes : [];
147
173
  events.push({
148
174
  type: "tool",
149
175
  event: isStart ? "on_tool_start" : "on_tool_end",
150
- name: "file_change",
151
- input: isStart ? { id: item.id } : undefined,
176
+ name: "apply_patch",
177
+ input: isStart || isEnd ? { id: item.id, ...(changes.length ? { changes } : {}) } : undefined,
152
178
  output: isEnd
153
179
  ? {
154
180
  id: item.id,
155
- changes: item.changes,
156
- status: item.status,
181
+ status: fileChange.status,
182
+ aggregatedOutput: fileChangeSummary(changes),
157
183
  }
158
184
  : undefined,
159
185
  data: { method, item },
@@ -199,6 +225,30 @@ export function mapCodexItem(method, item) {
199
225
  });
200
226
  return { events };
201
227
  }
228
+ case "imageGeneration": {
229
+ const image = item;
230
+ events.push({
231
+ type: "tool",
232
+ event: isStart ? "on_tool_start" : "on_tool_end",
233
+ name: "image_generation",
234
+ input: isStart ? { id: image.id } : undefined,
235
+ output: isEnd
236
+ ? {
237
+ id: image.id,
238
+ status: image.status,
239
+ generatedImage: {
240
+ ...(!image.savedPath || !isSupportedAbsoluteImagePath(image.savedPath)
241
+ ? { result: image.result }
242
+ : {}),
243
+ revisedPrompt: image.revisedPrompt,
244
+ ...(image.savedPath ? { savedPath: image.savedPath } : {}),
245
+ },
246
+ }
247
+ : undefined,
248
+ data: { method, item: { ...image, result: image.result ? "[image data omitted]" : "" } },
249
+ });
250
+ return { events };
251
+ }
202
252
  case "agentMessage": {
203
253
  const text = item.text?.trim() ?? "";
204
254
  if (isEnd && text) {
@@ -208,7 +258,11 @@ export function mapCodexItem(method, item) {
208
258
  return { events };
209
259
  }
210
260
  case "plan": {
211
- events.push({ type: "runtime_debug", channel: "codexEvent", data: { method, item } });
261
+ const text = item.text?.trim() ?? "";
262
+ if (isEnd && text) {
263
+ events.push({ type: "message_completed", itemId: item.id, text });
264
+ return { events, finalText: text };
265
+ }
212
266
  return { events };
213
267
  }
214
268
  case "reasoning": {
@@ -276,6 +330,17 @@ export function mapCodexNotification(method, params) {
276
330
  }
277
331
  return { events };
278
332
  }
333
+ case "item/plan/delta": {
334
+ const delta = typed.params.delta ?? "";
335
+ if (delta) {
336
+ events.push({
337
+ type: "token",
338
+ text: delta,
339
+ metadata: { source: "app_server", itemId: typed.params.itemId },
340
+ });
341
+ }
342
+ return { events };
343
+ }
279
344
  case "item/reasoning/summaryTextDelta":
280
345
  case "item/reasoning/textDelta": {
281
346
  const p = typed.params;
@@ -87,6 +87,16 @@ export interface PermissionProfileModificationParams {
87
87
  export type PermissionSelection = string | PermissionProfileSelectionParams;
88
88
  /** Open since Codex App Server 0.144; values are advertised by `model/list`. */
89
89
  export type ReasoningEffort = string;
90
+ export type CollaborationModeKind = "plan" | "default";
91
+ /** Full Codex-lineage mode snapshot required by the App Server wire protocol. */
92
+ export interface CollaborationMode {
93
+ mode: CollaborationModeKind;
94
+ settings: {
95
+ model: string;
96
+ reasoning_effort: ReasoningEffort | null;
97
+ developer_instructions: string | null;
98
+ };
99
+ }
90
100
  export interface ThreadStartParams {
91
101
  model?: string | null;
92
102
  modelProvider?: string | null;
@@ -102,6 +112,8 @@ export interface ThreadStartParams {
102
112
  baseInstructions?: string | null;
103
113
  developerInstructions?: string | null;
104
114
  ephemeral?: boolean | null;
115
+ /** Native UI intent for a context-clearing fresh thread. */
116
+ sessionStartSource?: "clear" | string;
105
117
  }
106
118
  export interface ThreadResumeParams extends ThreadStartParams {
107
119
  threadId: string;
@@ -157,6 +169,7 @@ export interface TurnStartParams {
157
169
  permissions?: PermissionSelection | null;
158
170
  model?: string | null;
159
171
  effort?: ReasoningEffort | null;
172
+ collaborationMode?: CollaborationMode | null;
160
173
  }
161
174
  export interface TurnInterruptParams {
162
175
  threadId: string;
@@ -254,7 +267,16 @@ export interface WebSearchItem extends ThreadItemBase {
254
267
  type: "other";
255
268
  } | null;
256
269
  }
257
- export type ThreadItem = UserMessageItem | AgentMessageItem | ReasoningItem | PlanItem | CommandExecutionItem | FileChangeItem | McpToolCallItem | DynamicToolCallItem | WebSearchItem | (ThreadItemBase & Record<string, unknown>);
270
+ export interface ImageGenerationItem extends ThreadItemBase {
271
+ type: "imageGeneration";
272
+ status: string;
273
+ revisedPrompt: string | null;
274
+ /** Base64 image bytes supplied when no durable saved path is available. */
275
+ result: string;
276
+ /** Codex App Server guarantees this is absolute when present. */
277
+ savedPath?: string;
278
+ }
279
+ export type ThreadItem = UserMessageItem | AgentMessageItem | ReasoningItem | PlanItem | CommandExecutionItem | FileChangeItem | McpToolCallItem | DynamicToolCallItem | WebSearchItem | ImageGenerationItem | (ThreadItemBase & Record<string, unknown>);
258
280
  export interface ThreadSummary {
259
281
  id?: string;
260
282
  threadId?: string;
@@ -304,7 +326,7 @@ export interface ThreadSettingsUpdateParams {
304
326
  serviceTier?: string | null;
305
327
  effort?: ReasoningEffort | null;
306
328
  summary?: string | null;
307
- collaborationMode?: Record<string, unknown> | null;
329
+ collaborationMode?: CollaborationMode | null;
308
330
  personality?: string | null;
309
331
  }
310
332
  export interface ModelListParams {
@@ -478,6 +500,14 @@ export interface QueueStatusNotificationParams {
478
500
  export type ServerNotification = {
479
501
  method: "thread/started";
480
502
  params: ThreadStartedNotificationParams;
503
+ } | {
504
+ method: "thread/settings/updated";
505
+ params: {
506
+ threadId: string;
507
+ threadSettings: {
508
+ collaborationMode?: CollaborationMode | null;
509
+ } & Record<string, unknown>;
510
+ };
481
511
  } | {
482
512
  method: "turn/started";
483
513
  params: TurnStartedNotificationParams;
@@ -499,6 +529,9 @@ export type ServerNotification = {
499
529
  } | {
500
530
  method: "item/agentMessage/delta";
501
531
  params: AgentMessageDeltaNotificationParams;
532
+ } | {
533
+ method: "item/plan/delta";
534
+ params: AgentMessageDeltaNotificationParams;
502
535
  } | {
503
536
  method: "item/reasoning/summaryTextDelta";
504
537
  params: ReasoningSummaryTextDeltaNotificationParams;