@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.
- package/dist/claude/native-bridge.d.ts +2 -0
- package/dist/claude/native-bridge.js +23 -0
- package/dist/claude/native-hook-main.js +62 -0
- package/dist/claude/native-integration.d.ts +8 -3
- package/dist/claude/native-integration.js +38 -8
- package/dist/claude/transcript.js +27 -17
- package/dist/codex-app-server/client.js +11 -1
- package/dist/codex-app-server/forwarder.d.ts +25 -1
- package/dist/codex-app-server/forwarder.js +216 -55
- package/dist/codex-app-server/mapping.d.ts +0 -6
- package/dist/codex-app-server/mapping.js +70 -5
- package/dist/codex-app-server/protocol.d.ts +35 -2
- package/dist/host.d.ts +15 -7
- package/dist/host.js +517 -50
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/runner/child.d.ts +13 -13
- package/dist/runner/child.js +183 -13
- package/dist/runner/manager.d.ts +12 -4
- package/dist/runner/manager.js +204 -20
- package/dist/runner/protocol.d.ts +47 -1
- package/dist/runner/protocol.js +5 -0
- package/dist/terminal/registry.js +1 -1
- package/dist/terminal/tmux.d.ts +5 -5
- package/dist/terminal/tmux.js +6 -6
- 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;
|
|
@@ -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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
this.
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
? {
|
|
304
|
-
|
|
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
|
-
|
|
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 (
|
|
316
|
-
if (
|
|
317
|
-
|
|
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
|
-
//
|
|
334
|
-
// completed item establishes that the held message
|
|
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
|
|
686
|
+
const deferred = this.deferredAssistantMessage;
|
|
535
687
|
this.deferredAssistantMessage = null;
|
|
536
|
-
if (
|
|
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: "
|
|
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) {
|
|
@@ -208,7 +258,11 @@ export function mapCodexItem(method, item) {
|
|
|
208
258
|
return { events };
|
|
209
259
|
}
|
|
210
260
|
case "plan": {
|
|
211
|
-
|
|
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
|
|
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?:
|
|
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;
|