@rynx-ai/runtime 0.1.11-beta.25 → 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.
- package/dist/claude/native-bridge.d.ts +2 -0
- package/dist/claude/native-bridge.js +23 -0
- package/dist/claude/native-integration.d.ts +8 -3
- package/dist/claude/native-integration.js +37 -7
- package/dist/claude/transcript.js +27 -17
- package/dist/codex-app-server/client.js +11 -1
- package/dist/codex-app-server/forwarder.d.ts +9 -0
- package/dist/codex-app-server/forwarder.js +155 -38
- package/dist/codex-app-server/mapping.d.ts +0 -6
- package/dist/codex-app-server/mapping.js +54 -4
- package/dist/codex-app-server/protocol.d.ts +10 -1
- package/dist/host.js +87 -26
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/runner/child.d.ts +6 -9
- package/dist/runner/child.js +154 -10
- package/dist/runner/manager.d.ts +3 -1
- package/dist/runner/manager.js +153 -15
- package/dist/runner/protocol.d.ts +31 -0
- package/dist/runner/protocol.js +5 -0
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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;
|
|
@@ -87,6 +94,9 @@ export class CodexSessionForwarder {
|
|
|
87
94
|
* (peek-then-advance; advanced only on a successful claim). reference implementation anon path. */
|
|
88
95
|
anonCounters = new Map();
|
|
89
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();
|
|
90
100
|
replayingBackfill = false;
|
|
91
101
|
constructor(client, sink, options = {}) {
|
|
92
102
|
this.client = client;
|
|
@@ -122,6 +132,7 @@ export class CodexSessionForwarder {
|
|
|
122
132
|
else {
|
|
123
133
|
this.currentTurnIdValue = null;
|
|
124
134
|
}
|
|
135
|
+
this.turnDiffByTurn.clear();
|
|
125
136
|
}
|
|
126
137
|
/** True while the provider owns an active turn, including the short interval
|
|
127
138
|
* between injection acceptance and observer confirmation. */
|
|
@@ -251,6 +262,7 @@ export class CodexSessionForwarder {
|
|
|
251
262
|
}
|
|
252
263
|
this.currentTurnIdValue = null;
|
|
253
264
|
this.activeSignaled = false;
|
|
265
|
+
this.turnDiffByTurn.clear();
|
|
254
266
|
this.currentThreadIdValue = tid;
|
|
255
267
|
this.sink.onThreadStarted?.(tid, normalizedForkedFromId);
|
|
256
268
|
}
|
|
@@ -289,6 +301,21 @@ export class CodexSessionForwarder {
|
|
|
289
301
|
if (method === "queue/status" && this.currentTurnIdValue === null)
|
|
290
302
|
return;
|
|
291
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
|
+
}
|
|
292
319
|
if (method === "turn/started") {
|
|
293
320
|
this.beginTurn(carriedTurnId);
|
|
294
321
|
this.sink.onTurnObserved?.(carriedTurnId);
|
|
@@ -296,12 +323,21 @@ export class CodexSessionForwarder {
|
|
|
296
323
|
}
|
|
297
324
|
if (isTerminalTurnMethod(method)) {
|
|
298
325
|
if (!this.terminalBoundaryMatchesActiveTurn(carriedTurnId, notificationThreadId)) {
|
|
326
|
+
const staleDiff = this.consumeTurnDiff(carriedTurnId);
|
|
327
|
+
if (staleDiff && carriedTurnId) {
|
|
328
|
+
this.sink.onTurnContentEvent?.(carriedTurnId, staleDiff);
|
|
329
|
+
}
|
|
299
330
|
return;
|
|
300
331
|
}
|
|
301
332
|
const hadActiveTurn = this.currentTurnIdValue !== null;
|
|
302
333
|
const hadOpenResponse = this.turnOpen;
|
|
303
334
|
const mapped = mapCodexNotification(method, params);
|
|
335
|
+
const terminalTurnId = carriedTurnId ?? this.currentTurnIdValue;
|
|
336
|
+
const turnDiff = this.consumeTurnDiff(terminalTurnId);
|
|
304
337
|
if (!hadActiveTurn && !hadOpenResponse) {
|
|
338
|
+
if (turnDiff && terminalTurnId) {
|
|
339
|
+
this.sink.onTurnContentEvent?.(terminalTurnId, turnDiff);
|
|
340
|
+
}
|
|
305
341
|
const turn = params?.turn;
|
|
306
342
|
const recoveredStatus = mapped.fatalError ||
|
|
307
343
|
method === "turn/failed" ||
|
|
@@ -323,10 +359,18 @@ export class CodexSessionForwarder {
|
|
|
323
359
|
this.sink.onEvent(event);
|
|
324
360
|
}
|
|
325
361
|
this.scheduleCompletion(mapped.fatalError
|
|
326
|
-
? { kind: "error", error: mapped.fatalError }
|
|
362
|
+
? { kind: "error", error: mapped.fatalError, ...(turnDiff ? { turnDiff } : {}) }
|
|
327
363
|
: mapped.turnInterrupted
|
|
328
|
-
? {
|
|
329
|
-
|
|
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
|
+
});
|
|
330
374
|
return;
|
|
331
375
|
}
|
|
332
376
|
// A late event from an older turn must not replace the app-server's active
|
|
@@ -335,32 +379,66 @@ export class CodexSessionForwarder {
|
|
|
335
379
|
this.currentTurnIdValue &&
|
|
336
380
|
carriedTurnId !== this.currentTurnIdValue &&
|
|
337
381
|
isTurnScopedMethod(method)) {
|
|
338
|
-
|
|
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
|
+
}
|
|
339
400
|
}
|
|
340
|
-
if (
|
|
341
|
-
if (
|
|
342
|
-
|
|
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)
|
|
343
406
|
return;
|
|
407
|
+
}
|
|
408
|
+
else if (carriedTurnId &&
|
|
409
|
+
canRecoverMissedTurnFrom(method) &&
|
|
410
|
+
this.notificationMatchesCurrentThread(notificationThreadId)) {
|
|
344
411
|
this.currentTurnIdValue = carriedTurnId;
|
|
345
412
|
this.ensureTurn();
|
|
346
413
|
}
|
|
414
|
+
else {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
347
417
|
}
|
|
348
418
|
// Completed items (user echo, assistant, tool, reasoning) go through the
|
|
349
419
|
// deduped path so a resume backfill and the live stream never double them.
|
|
350
420
|
if (method === "item/completed") {
|
|
351
421
|
const item = params?.item;
|
|
352
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
|
+
}
|
|
353
431
|
if (this.shouldDeferAssistantMessage(item)) {
|
|
354
|
-
this.deferAssistantMessage(item);
|
|
432
|
+
this.deferAssistantMessage(item, carriedTurnId);
|
|
355
433
|
this.refreshCompletionGrace();
|
|
356
434
|
return;
|
|
357
435
|
}
|
|
358
|
-
//
|
|
359
|
-
// completed item establishes that the held message
|
|
360
|
-
// 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.
|
|
361
439
|
if (item.type !== "reasoning")
|
|
362
440
|
this.flushDeferredAssistantMessage();
|
|
363
|
-
this.processCompletedItem(item);
|
|
441
|
+
this.processCompletedItem(item, carriedTurnId);
|
|
364
442
|
this.refreshCompletionGrace();
|
|
365
443
|
return;
|
|
366
444
|
}
|
|
@@ -375,6 +453,19 @@ export class CodexSessionForwarder {
|
|
|
375
453
|
if (canonicalEvents.some((event) => event.type !== "reasoning_delta" && event.type !== "reasoning_completed")) {
|
|
376
454
|
this.flushDeferredAssistantMessage();
|
|
377
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
|
+
}
|
|
378
469
|
if (canonicalEvents.length || mapped.turnCompleted || mapped.fatalError) {
|
|
379
470
|
this.ensureTurn();
|
|
380
471
|
}
|
|
@@ -433,6 +524,10 @@ export class CodexSessionForwarder {
|
|
|
433
524
|
if (this.pendingPlanImplementation?.turnId === completedTurnId) {
|
|
434
525
|
this.pendingPlanImplementation = null;
|
|
435
526
|
}
|
|
527
|
+
if (completion.turnDiff)
|
|
528
|
+
this.sink.onEvent(completion.turnDiff);
|
|
529
|
+
if (completedTurnId)
|
|
530
|
+
this.turnDiffByTurn.delete(completedTurnId);
|
|
436
531
|
this.turnOpen = false;
|
|
437
532
|
this.currentTurnIdValue = null;
|
|
438
533
|
this.pendingCompletionTurnId = null;
|
|
@@ -519,23 +614,30 @@ export class CodexSessionForwarder {
|
|
|
519
614
|
}
|
|
520
615
|
/** Map + emit one completed codex item, deduped by a TOTAL key and routing the
|
|
521
616
|
* user echo to {@link CodexForwarderSink.onUserMessage}. Shared by live + backfill. */
|
|
522
|
-
processCompletedItem(item) {
|
|
523
|
-
if (!this.claimCompletedItem(item))
|
|
617
|
+
processCompletedItem(item, completedTurnId) {
|
|
618
|
+
if (!this.claimCompletedItem(item, completedTurnId))
|
|
524
619
|
return;
|
|
525
|
-
this.emitCompletedItem(item);
|
|
620
|
+
this.emitCompletedItem(item, completedTurnId);
|
|
526
621
|
}
|
|
527
|
-
claimCompletedItem(item) {
|
|
528
|
-
const { key, isAnon } = this.completedItemKey(item);
|
|
622
|
+
claimCompletedItem(item, completedTurnId) {
|
|
623
|
+
const { key, isAnon } = this.completedItemKey(item, completedTurnId);
|
|
529
624
|
if (this.seenCompletedItems.has(key))
|
|
530
625
|
return false;
|
|
531
626
|
this.seenCompletedItems.add(key);
|
|
532
627
|
if (isAnon)
|
|
533
|
-
this.advanceAnonCounter();
|
|
628
|
+
this.advanceAnonCounter(completedTurnId);
|
|
534
629
|
return true;
|
|
535
630
|
}
|
|
536
|
-
emitCompletedItem(item) {
|
|
631
|
+
emitCompletedItem(item, completedTurnId) {
|
|
632
|
+
const outsideTurnLifecycle = !this.turnOpen || Boolean(completedTurnId &&
|
|
633
|
+
this.currentTurnIdValue &&
|
|
634
|
+
completedTurnId !== this.currentTurnIdValue);
|
|
537
635
|
const userContent = userMessageContent(item);
|
|
538
636
|
if (userContent !== undefined) {
|
|
637
|
+
if (outsideTurnLifecycle && this.sink.onTurnContentUserMessage) {
|
|
638
|
+
this.sink.onTurnContentUserMessage(completedTurnId, userContent);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
539
641
|
this.ensureTurn();
|
|
540
642
|
this.sink.onUserMessage?.(userContent);
|
|
541
643
|
return;
|
|
@@ -552,6 +654,12 @@ export class CodexSessionForwarder {
|
|
|
552
654
|
const canonicalEvents = mapped.events.filter((event) => event.type !== "runtime_debug");
|
|
553
655
|
if (canonicalEvents.length === 0)
|
|
554
656
|
return;
|
|
657
|
+
if (outsideTurnLifecycle && this.sink.onTurnContentEvent) {
|
|
658
|
+
for (const event of canonicalEvents) {
|
|
659
|
+
this.sink.onTurnContentEvent(completedTurnId, event);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
555
663
|
this.ensureTurn();
|
|
556
664
|
for (const event of canonicalEvents)
|
|
557
665
|
this.sink.onEvent(event);
|
|
@@ -562,11 +670,11 @@ export class CodexSessionForwarder {
|
|
|
562
670
|
}
|
|
563
671
|
return Boolean(item.text?.trim());
|
|
564
672
|
}
|
|
565
|
-
deferAssistantMessage(item) {
|
|
566
|
-
if (!this.claimCompletedItem(item))
|
|
673
|
+
deferAssistantMessage(item, turnId) {
|
|
674
|
+
if (!this.claimCompletedItem(item, turnId))
|
|
567
675
|
return;
|
|
568
676
|
this.flushDeferredAssistantMessage();
|
|
569
|
-
this.deferredAssistantMessage = item;
|
|
677
|
+
this.deferredAssistantMessage = { item, ...(turnId ? { turnId } : {}) };
|
|
570
678
|
const graceMs = this.options.assistantMessageGraceMs ?? 0;
|
|
571
679
|
this.assistantMessageTimer = setTimeout(() => this.flushDeferredAssistantMessage(), graceMs);
|
|
572
680
|
this.assistantMessageTimer.unref?.();
|
|
@@ -575,26 +683,26 @@ export class CodexSessionForwarder {
|
|
|
575
683
|
if (this.assistantMessageTimer)
|
|
576
684
|
clearTimeout(this.assistantMessageTimer);
|
|
577
685
|
this.assistantMessageTimer = null;
|
|
578
|
-
const
|
|
686
|
+
const deferred = this.deferredAssistantMessage;
|
|
579
687
|
this.deferredAssistantMessage = null;
|
|
580
|
-
if (
|
|
581
|
-
this.emitCompletedItem(item);
|
|
688
|
+
if (deferred)
|
|
689
|
+
this.emitCompletedItem(deferred.item, deferred.turnId);
|
|
582
690
|
}
|
|
583
691
|
/** Build a total dedup key for one completed item. Stable-id items use
|
|
584
692
|
* `threadId:turnId:item.id` — identical across replay + live, so the second
|
|
585
693
|
* delivery is dropped. Items without a codex id fall back to a per-(thread,turn)
|
|
586
694
|
* position counter (peeked here; advanced only on a successful claim, via
|
|
587
695
|
* {@link advanceAnonCounter}). Mirrors reference implementation `_completed_item_key`. */
|
|
588
|
-
completedItemKey(item) {
|
|
696
|
+
completedItemKey(item, completedTurnId) {
|
|
589
697
|
const threadId = this.currentThreadIdValue ?? "thread";
|
|
590
|
-
const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
|
|
698
|
+
const turnId = completedTurnId ?? this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
|
|
591
699
|
if (item.id)
|
|
592
700
|
return { key: `${threadId}:${turnId}:${item.id}`, isAnon: false };
|
|
593
701
|
const n = this.anonCounters.get(`${threadId}:${turnId}`) ?? 0;
|
|
594
702
|
return { key: `${threadId}:${turnId}:anon:${n}`, isAnon: true };
|
|
595
703
|
}
|
|
596
|
-
advanceAnonCounter() {
|
|
597
|
-
const turnId = this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
|
|
704
|
+
advanceAnonCounter(completedTurnId) {
|
|
705
|
+
const turnId = completedTurnId ?? this.currentTurnIdValue ?? this.pendingCompletionTurnId ?? "turn";
|
|
598
706
|
const k = `${this.currentThreadIdValue ?? "thread"}:${turnId}`;
|
|
599
707
|
this.anonCounters.set(k, (this.anonCounters.get(k) ?? 0) + 1);
|
|
600
708
|
}
|
|
@@ -604,6 +712,15 @@ export class CodexSessionForwarder {
|
|
|
604
712
|
this.sink.onTurnStart(this.currentTurnIdValue ?? undefined);
|
|
605
713
|
}
|
|
606
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
|
+
}
|
|
607
724
|
/** Start (or confirm) the app-server's authoritative active turn. A newer
|
|
608
725
|
* start supersedes an older response whose terminal edge arrived late; a
|
|
609
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: "
|
|
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
|
-
|
|
156
|
-
|
|
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) {
|
|
@@ -267,7 +267,16 @@ export interface WebSearchItem extends ThreadItemBase {
|
|
|
267
267
|
type: "other";
|
|
268
268
|
} | null;
|
|
269
269
|
}
|
|
270
|
-
export
|
|
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>);
|
|
271
280
|
export interface ThreadSummary {
|
|
272
281
|
id?: string;
|
|
273
282
|
threadId?: string;
|
package/dist/host.js
CHANGED
|
@@ -21,7 +21,7 @@ import { buildCodexRemoteArgs } from "./terminal/codex-tui.js";
|
|
|
21
21
|
import { buildClaudeTuiArgs } from "./terminal/claude-tui.js";
|
|
22
22
|
import { providerAdditionalDirs, threadWorkspaceParams, turnWorkspaceParams, } from "./provider-workspace.js";
|
|
23
23
|
import { ensureProjectTrusted } from "./claude/trust.js";
|
|
24
|
-
import { claudeAttachmentToken, claudeInputText, runtimeUserContent, } from "./input-resources.js";
|
|
24
|
+
import { claudeAttachmentToken, claudeInputText, codexUserEchoContent, runtimeUserContent, } from "./input-resources.js";
|
|
25
25
|
import { claudeBridgeDir, prepareClaudeBridgeDir, removeManagedClaudeSettings, writeManagedClaudeSettings, } from "./claude/native-bridge.js";
|
|
26
26
|
import { ClaudeLiveSession, injectViaTerminal, } from "./claude/native-integration.js";
|
|
27
27
|
import { buildClaudeHookSettings } from "./claude/native-hooks.js";
|
|
@@ -847,6 +847,9 @@ export class LocalAgentHost {
|
|
|
847
847
|
};
|
|
848
848
|
let normalizer = null;
|
|
849
849
|
let currentResponseId = null;
|
|
850
|
+
/** State for content-only events, keyed by Provider response so consecutive
|
|
851
|
+
* late deltas retain one canonical item id without owning lifecycle. */
|
|
852
|
+
const contentNormalizers = new Map();
|
|
850
853
|
const startNormalizer = (turnId) => {
|
|
851
854
|
const responseId = turnId ? `resp_codex_${turnId}` : "resp_codex_native";
|
|
852
855
|
if (normalizer && currentResponseId === responseId)
|
|
@@ -892,6 +895,40 @@ export class LocalAgentHost {
|
|
|
892
895
|
return;
|
|
893
896
|
live.pendingInjectedInputs = live.pendingInjectedInputs.filter((entry) => entry.responseId !== responseId);
|
|
894
897
|
};
|
|
898
|
+
const turnContentEvents = (turnId, produce) => {
|
|
899
|
+
const responseId = turnId ? `resp_codex_${turnId}` : "resp_codex_native";
|
|
900
|
+
let contentNormalizer = contentNormalizers.get(responseId);
|
|
901
|
+
if (!contentNormalizer) {
|
|
902
|
+
contentNormalizer = new SessionNormalizer({
|
|
903
|
+
sessionId: currentSessionId,
|
|
904
|
+
responseId,
|
|
905
|
+
model: live.model || live.runtime,
|
|
906
|
+
});
|
|
907
|
+
contentNormalizers.set(responseId, contentNormalizer);
|
|
908
|
+
}
|
|
909
|
+
for (const event of produce(contentNormalizer)) {
|
|
910
|
+
// Omnigent posts item/transient content under the Turn's response id
|
|
911
|
+
// independently of lifecycle status. Reuse Rynx's canonical item
|
|
912
|
+
// normalization but suppress its synthetic lifecycle edges.
|
|
913
|
+
if (event.type !== "response.created" && event.type !== "session.status") {
|
|
914
|
+
emitCurrent(event);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
const observedUserContent = (content) => {
|
|
919
|
+
const normalizedContent = typeof content === "string"
|
|
920
|
+
? [{ type: "input_text", text: content }]
|
|
921
|
+
: content;
|
|
922
|
+
const signature = JSON.stringify(normalizedContent);
|
|
923
|
+
const pending = live.pendingInjectedInputs.find((entry) => entry.signature === signature || entry.providerEchoSignature === signature);
|
|
924
|
+
if (pending?.state === "optimistic") {
|
|
925
|
+
live.pendingInjectedInputs.splice(live.pendingInjectedInputs.indexOf(pending), 1);
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
if (pending)
|
|
929
|
+
pending.observed = true;
|
|
930
|
+
return normalizedContent;
|
|
931
|
+
};
|
|
895
932
|
const rememberSettledInteraction = (interactionId) => {
|
|
896
933
|
live.settledInteractions.add(interactionId);
|
|
897
934
|
if (live.settledInteractions.size <= MAX_CODEX_SETTLED_INTERACTIONS)
|
|
@@ -1186,26 +1223,27 @@ export class LocalAgentHost {
|
|
|
1186
1223
|
}
|
|
1187
1224
|
},
|
|
1188
1225
|
onUserMessage: (content) => {
|
|
1189
|
-
const normalizedContent =
|
|
1190
|
-
|
|
1191
|
-
: content;
|
|
1192
|
-
const signature = JSON.stringify(normalizedContent);
|
|
1193
|
-
const pending = live.pendingInjectedInputs.find((entry) => entry.signature === signature);
|
|
1194
|
-
if (pending?.state === "optimistic") {
|
|
1195
|
-
live.pendingInjectedInputs.splice(live.pendingInjectedInputs.indexOf(pending), 1);
|
|
1226
|
+
const normalizedContent = observedUserContent(content);
|
|
1227
|
+
if (!normalizedContent)
|
|
1196
1228
|
return;
|
|
1197
|
-
}
|
|
1198
|
-
if (pending)
|
|
1199
|
-
pending.observed = true;
|
|
1200
1229
|
const n = normalizer ?? startNormalizer();
|
|
1201
1230
|
for (const se of n.userInput(normalizedContent))
|
|
1202
1231
|
emitCurrent(se);
|
|
1203
1232
|
},
|
|
1233
|
+
onTurnContentUserMessage: (turnId, content) => {
|
|
1234
|
+
const normalizedContent = observedUserContent(content);
|
|
1235
|
+
if (!normalizedContent)
|
|
1236
|
+
return;
|
|
1237
|
+
turnContentEvents(turnId, (n) => n.userInput(normalizedContent));
|
|
1238
|
+
},
|
|
1204
1239
|
onEvent: (event) => {
|
|
1205
1240
|
const n = normalizer ?? startNormalizer();
|
|
1206
1241
|
for (const se of n.next(event))
|
|
1207
1242
|
emitCurrent(se);
|
|
1208
1243
|
},
|
|
1244
|
+
onTurnContentEvent: (turnId, event) => {
|
|
1245
|
+
turnContentEvents(turnId, (n) => n.next(event));
|
|
1246
|
+
},
|
|
1209
1247
|
onStatus: (note, statusKind) => {
|
|
1210
1248
|
const responseId = currentResponseId ??
|
|
1211
1249
|
live.pendingInjectedInputs.find((entry) => entry.responseId)?.responseId;
|
|
@@ -1286,6 +1324,7 @@ export class LocalAgentHost {
|
|
|
1286
1324
|
currentSessionId = newSessionId;
|
|
1287
1325
|
normalizer = null;
|
|
1288
1326
|
currentResponseId = null;
|
|
1327
|
+
contentNormalizers.clear();
|
|
1289
1328
|
live.pendingInjectedInputs = [];
|
|
1290
1329
|
pendingRotationEvents = [];
|
|
1291
1330
|
void this.sessionStore
|
|
@@ -1739,9 +1778,16 @@ export class LocalAgentHost {
|
|
|
1739
1778
|
return { outcome: "notReady" };
|
|
1740
1779
|
const nativeInput = buildRuntimeUserInput(runtimeInput);
|
|
1741
1780
|
const content = runtimeUserContent(runtimeInput);
|
|
1781
|
+
const providerEcho = codexUserEchoContent(nativeInput);
|
|
1782
|
+
const providerEchoContent = typeof providerEcho === "string"
|
|
1783
|
+
? [{ type: "input_text", text: providerEcho }]
|
|
1784
|
+
: providerEcho;
|
|
1742
1785
|
const pendingInput = {
|
|
1743
1786
|
content,
|
|
1744
1787
|
signature: JSON.stringify(content),
|
|
1788
|
+
...(providerEchoContent
|
|
1789
|
+
? { providerEchoSignature: JSON.stringify(providerEchoContent) }
|
|
1790
|
+
: {}),
|
|
1745
1791
|
state: "awaiting",
|
|
1746
1792
|
observed: false,
|
|
1747
1793
|
};
|
|
@@ -2315,7 +2361,7 @@ export class LocalAgentHost {
|
|
|
2315
2361
|
responseId,
|
|
2316
2362
|
});
|
|
2317
2363
|
};
|
|
2318
|
-
const settleClaudeTurn = (interrupted, usage) => {
|
|
2364
|
+
const settleClaudeTurn = (interrupted, usage, backgroundTaskCount) => {
|
|
2319
2365
|
if (!normalizer)
|
|
2320
2366
|
return;
|
|
2321
2367
|
const rid = currentResponseId;
|
|
@@ -2334,8 +2380,11 @@ export class LocalAgentHost {
|
|
|
2334
2380
|
for (const se of normalizer.next({ type: "turn_completed", usage }))
|
|
2335
2381
|
emitCurrent(se);
|
|
2336
2382
|
}
|
|
2337
|
-
for (const se of normalizer.next({ type: "done" }))
|
|
2338
|
-
emitCurrent(se
|
|
2383
|
+
for (const se of normalizer.next({ type: "done" })) {
|
|
2384
|
+
emitCurrent(se.type === "session.status" && backgroundTaskCount !== undefined
|
|
2385
|
+
? { ...se, backgroundTaskCount }
|
|
2386
|
+
: se);
|
|
2387
|
+
}
|
|
2339
2388
|
}
|
|
2340
2389
|
live.pendingInjectedInputs = live.pendingInjectedInputs.filter((entry) => entry.responseId !== rid);
|
|
2341
2390
|
normalizer = null;
|
|
@@ -2398,30 +2447,42 @@ export class LocalAgentHost {
|
|
|
2398
2447
|
...(blockedOn ? { note: blockedOn } : {}),
|
|
2399
2448
|
});
|
|
2400
2449
|
},
|
|
2401
|
-
onTurnEnd: (usage) => settleClaudeTurn(false, usage),
|
|
2450
|
+
onTurnEnd: (usage, backgroundTaskCount) => settleClaudeTurn(false, usage, backgroundTaskCount),
|
|
2402
2451
|
onTurnInterrupted: (usage) => settleClaudeTurn(true, usage),
|
|
2403
2452
|
onTurnInterruptRequested: () => {
|
|
2404
2453
|
const responseId = currentResponseId;
|
|
2405
2454
|
if (responseId)
|
|
2406
2455
|
live.publishInterrupted(responseId);
|
|
2407
2456
|
},
|
|
2408
|
-
onIdle: () => {
|
|
2409
|
-
// Surface
|
|
2410
|
-
//
|
|
2411
|
-
|
|
2457
|
+
onIdle: (backgroundTaskCount) => {
|
|
2458
|
+
// Surface Stop even after its Turn has already closed: a later
|
|
2459
|
+
// authoritative zero is what clears a sticky background-shell tally.
|
|
2460
|
+
// When the Turn is still open, retain its identity without finalizing it
|
|
2461
|
+
// so a late assistant record continues to join the same Response.
|
|
2462
|
+
emitCurrent({
|
|
2463
|
+
type: "session.status",
|
|
2464
|
+
sessionId: currentSessionId,
|
|
2465
|
+
...(currentResponseId ? { responseId: currentResponseId } : {}),
|
|
2466
|
+
status: "idle",
|
|
2467
|
+
...(backgroundTaskCount === undefined ? {} : { backgroundTaskCount }),
|
|
2468
|
+
});
|
|
2469
|
+
},
|
|
2470
|
+
onTurnError: (error) => {
|
|
2471
|
+
const message = error.message || "Agent turn failed";
|
|
2472
|
+
if (!normalizer) {
|
|
2473
|
+
// Native pane/forwarder death is Session-level even when Claude is
|
|
2474
|
+
// between Turns. Omnigent publishes the same bare failed edge; the
|
|
2475
|
+
// explicit zero also retires any sticky background-shell tally.
|
|
2412
2476
|
emitCurrent({
|
|
2413
2477
|
type: "session.status",
|
|
2414
2478
|
sessionId: currentSessionId,
|
|
2415
|
-
|
|
2416
|
-
|
|
2479
|
+
status: "failed",
|
|
2480
|
+
backgroundTaskCount: 0,
|
|
2481
|
+
note: message,
|
|
2417
2482
|
});
|
|
2418
|
-
}
|
|
2419
|
-
},
|
|
2420
|
-
onTurnError: (error) => {
|
|
2421
|
-
if (!normalizer)
|
|
2422
2483
|
return;
|
|
2484
|
+
}
|
|
2423
2485
|
const rid = currentResponseId;
|
|
2424
|
-
const message = error.message || "Agent turn failed";
|
|
2425
2486
|
for (const se of normalizer.fail({
|
|
2426
2487
|
code: "agent_error",
|
|
2427
2488
|
message,
|