@agentex/agent 0.0.32 → 0.0.34
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/CHANGELOG.md +57 -0
- package/README.md +29 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +1 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/parse.d.ts +4 -6
- package/dist/providers/claude/parse.d.ts.map +1 -1
- package/dist/providers/claude/parse.js +79 -25
- package/dist/providers/claude/parse.js.map +1 -1
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +1 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +69 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +61 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +714 -15
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/providers/claude/index.ts +1 -0
- package/src/providers/claude/parse.ts +81 -23
- package/src/providers/codex/index.ts +1 -0
- package/src/providers/codex/parse.ts +72 -0
- package/src/providers/codex/session.ts +795 -15
- package/src/types.ts +42 -0
|
@@ -89,6 +89,8 @@ function buildCodexUserInputAnswers(questions, resp) {
|
|
|
89
89
|
}
|
|
90
90
|
return out;
|
|
91
91
|
}
|
|
92
|
+
const BACKGROUND_TASK_POLL_INTERVAL_MS = 2_000;
|
|
93
|
+
const BACKGROUND_TASK_READ_TIMEOUT_MS = 5_000;
|
|
92
94
|
// ---------------------------------------------------------------------------
|
|
93
95
|
// JSON-RPC 2.0 helpers
|
|
94
96
|
// ---------------------------------------------------------------------------
|
|
@@ -116,6 +118,30 @@ function asObj(parent, key) {
|
|
|
116
118
|
? v
|
|
117
119
|
: {};
|
|
118
120
|
}
|
|
121
|
+
function obj(value) {
|
|
122
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
123
|
+
? value
|
|
124
|
+
: {};
|
|
125
|
+
}
|
|
126
|
+
function stringArray(value) {
|
|
127
|
+
return Array.isArray(value)
|
|
128
|
+
? value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
129
|
+
: [];
|
|
130
|
+
}
|
|
131
|
+
function codexAgentState(value) {
|
|
132
|
+
const state = obj(value);
|
|
133
|
+
const nativeStatus = str(state, "status");
|
|
134
|
+
const summary = str(state, "message") || null;
|
|
135
|
+
if (nativeStatus === "completed")
|
|
136
|
+
return { terminal: true, status: "completed", summary };
|
|
137
|
+
if (nativeStatus === "errored" || nativeStatus === "notFound") {
|
|
138
|
+
return { terminal: true, status: "failed", summary };
|
|
139
|
+
}
|
|
140
|
+
if (nativeStatus === "interrupted" || nativeStatus === "shutdown") {
|
|
141
|
+
return { terminal: true, status: "stopped", summary };
|
|
142
|
+
}
|
|
143
|
+
return { terminal: false, status: "running", summary };
|
|
144
|
+
}
|
|
119
145
|
function classifyMessage(msg) {
|
|
120
146
|
// Detect JSON-RPC by *structure*, not by the `jsonrpc:"2.0"` discriminator.
|
|
121
147
|
// codex-cli 0.130.0's `app-server` emits responses without the `jsonrpc`
|
|
@@ -271,6 +297,18 @@ export class CodexSessionImpl {
|
|
|
271
297
|
_draining = false;
|
|
272
298
|
/** Shared promise so concurrent / repeated `drain()` calls coalesce. */
|
|
273
299
|
_drainPromise = null;
|
|
300
|
+
/**
|
|
301
|
+
* Root turn targeted by `interrupt()`. The id is learned asynchronously from
|
|
302
|
+
* the leader `turn/start` response or the root `turn/started` notification.
|
|
303
|
+
* Concurrent sends reuse this latch so a queued response cannot replace the
|
|
304
|
+
* actual active turn.
|
|
305
|
+
*/
|
|
306
|
+
_activeTurnId = null;
|
|
307
|
+
_activeTurnReady = null;
|
|
308
|
+
/** Successful repeated interrupts coalesce until the terminal notification. */
|
|
309
|
+
_interruptPromise = null;
|
|
310
|
+
/** Prevents a late interrupt request after the terminal frame was observed. */
|
|
311
|
+
_turnTerminalObserved = false;
|
|
274
312
|
/** Stamps `tool_result.toolName` by correlating with prior `tool_call`s. */
|
|
275
313
|
_trackToolName = createToolNameTracker();
|
|
276
314
|
// Per-turn accumulators. Cleared after each result delivery so a subsequent
|
|
@@ -279,8 +317,16 @@ export class CodexSessionImpl {
|
|
|
279
317
|
_turnUsage = null;
|
|
280
318
|
_turnModel = null;
|
|
281
319
|
_turnIsError = false;
|
|
320
|
+
_turnWasInterrupted = false;
|
|
282
321
|
_turnErrorMessage = null;
|
|
283
322
|
_turnStartedAt = null;
|
|
323
|
+
/** Child-agent lifecycle is informational and never participates in root turn settlement. */
|
|
324
|
+
_backgroundTasks = new Map();
|
|
325
|
+
_backgroundTaskIdsByPath = new Map();
|
|
326
|
+
/** One protocol-native thread/read poller per child when Codex does not multiplex its notifications. */
|
|
327
|
+
_backgroundTaskPollers = new Map();
|
|
328
|
+
/** Cancellable retry delay for each poller. Cleared when the session closes. */
|
|
329
|
+
_backgroundTaskPollTimers = new Map();
|
|
284
330
|
/**
|
|
285
331
|
* Serial dispatch chain for `onEvent`. Each dispatched event appends a
|
|
286
332
|
* handler invocation; the chain enforces in-order delivery and lets
|
|
@@ -348,17 +394,51 @@ export class CodexSessionImpl {
|
|
|
348
394
|
proc.on("exit", (code, signal) => {
|
|
349
395
|
if (this._state !== "closed") {
|
|
350
396
|
this._state = "closed";
|
|
351
|
-
const
|
|
397
|
+
const message = `Codex process exited unexpectedly (code=${code}, signal=${signal})`;
|
|
398
|
+
this.finalizeActiveBackgroundTasks("failed", message, {
|
|
399
|
+
method: "process/exit",
|
|
400
|
+
code,
|
|
401
|
+
signal,
|
|
402
|
+
});
|
|
403
|
+
this.cancelBackgroundTaskPollTimers();
|
|
404
|
+
const err = new Error(message);
|
|
352
405
|
this.rejectAllPending(err);
|
|
353
406
|
}
|
|
354
407
|
});
|
|
355
408
|
proc.on("error", (err) => {
|
|
356
409
|
if (this._state !== "closed") {
|
|
357
410
|
this._state = "closed";
|
|
411
|
+
this.finalizeActiveBackgroundTasks("failed", err.message, {
|
|
412
|
+
method: "process/error",
|
|
413
|
+
message: err.message,
|
|
414
|
+
});
|
|
415
|
+
this.cancelBackgroundTaskPollTimers();
|
|
358
416
|
this.rejectAllPending(err);
|
|
359
417
|
}
|
|
360
418
|
});
|
|
361
419
|
}
|
|
420
|
+
cancelBackgroundTaskPollTimers() {
|
|
421
|
+
for (const pending of this._backgroundTaskPollTimers.values()) {
|
|
422
|
+
clearTimeout(pending.timer);
|
|
423
|
+
pending.resolve();
|
|
424
|
+
}
|
|
425
|
+
this._backgroundTaskPollTimers.clear();
|
|
426
|
+
}
|
|
427
|
+
finalizeActiveBackgroundTasks(status, summary, raw) {
|
|
428
|
+
for (const task of this._backgroundTasks.values()) {
|
|
429
|
+
if (task.terminal)
|
|
430
|
+
continue;
|
|
431
|
+
this.dispatchEvent(this.backgroundTaskEvent(task.taskId, "completed", status, {
|
|
432
|
+
description: task.description,
|
|
433
|
+
summary,
|
|
434
|
+
parentTaskId: task.parentTaskId,
|
|
435
|
+
eventId: this._threadId
|
|
436
|
+
? `codex:${this._threadId}:background-task:${task.taskId}:session-${status}:completed`
|
|
437
|
+
: null,
|
|
438
|
+
raw,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
362
442
|
/** Reject every pending send() Promise and outgoing JSON-RPC call. */
|
|
363
443
|
rejectAllPending(err) {
|
|
364
444
|
const pending = this._pendingResults.splice(0);
|
|
@@ -372,9 +452,75 @@ export class CodexSessionImpl {
|
|
|
372
452
|
for (const [, p] of this._pendingRpc)
|
|
373
453
|
p.reject(err);
|
|
374
454
|
this._pendingRpc.clear();
|
|
455
|
+
this.clearActiveTurn(err);
|
|
375
456
|
}
|
|
376
457
|
get sessionId() { return this._threadId; }
|
|
377
458
|
get state() { return this._state; }
|
|
459
|
+
/** Start the identity latch before writing the leader `turn/start` request. */
|
|
460
|
+
beginActiveTurn() {
|
|
461
|
+
let resolveFn;
|
|
462
|
+
let rejectFn;
|
|
463
|
+
const promise = new Promise((resolve, reject) => {
|
|
464
|
+
resolveFn = resolve;
|
|
465
|
+
rejectFn = reject;
|
|
466
|
+
});
|
|
467
|
+
// A turn can finish without anyone calling interrupt(). Keep a later
|
|
468
|
+
// process-exit rejection from becoming an unhandled promise rejection.
|
|
469
|
+
void promise.catch(() => { });
|
|
470
|
+
const ready = {
|
|
471
|
+
promise,
|
|
472
|
+
resolve: resolveFn,
|
|
473
|
+
reject: rejectFn,
|
|
474
|
+
settled: false,
|
|
475
|
+
};
|
|
476
|
+
this._activeTurnId = null;
|
|
477
|
+
this._activeTurnReady = ready;
|
|
478
|
+
this._interruptPromise = null;
|
|
479
|
+
this._turnTerminalObserved = false;
|
|
480
|
+
this._turnWasInterrupted = false;
|
|
481
|
+
return ready;
|
|
482
|
+
}
|
|
483
|
+
/** First root turn id wins for the current latch. */
|
|
484
|
+
captureActiveTurnId(turnId, expected) {
|
|
485
|
+
const ready = this._activeTurnReady;
|
|
486
|
+
if (!turnId || !ready || ready.settled)
|
|
487
|
+
return;
|
|
488
|
+
if (expected && ready !== expected)
|
|
489
|
+
return;
|
|
490
|
+
this._activeTurnId = turnId;
|
|
491
|
+
ready.settled = true;
|
|
492
|
+
ready.resolve(turnId);
|
|
493
|
+
}
|
|
494
|
+
rejectActiveTurnReady(err, expected) {
|
|
495
|
+
if (this._activeTurnReady !== expected || expected.settled)
|
|
496
|
+
return;
|
|
497
|
+
expected.settled = true;
|
|
498
|
+
expected.reject(err);
|
|
499
|
+
}
|
|
500
|
+
/** Clear the current turn and release an interrupt waiting for its id. */
|
|
501
|
+
clearActiveTurn(err) {
|
|
502
|
+
const ready = this._activeTurnReady;
|
|
503
|
+
if (ready && !ready.settled) {
|
|
504
|
+
ready.settled = true;
|
|
505
|
+
if (err)
|
|
506
|
+
ready.reject(err);
|
|
507
|
+
else
|
|
508
|
+
ready.resolve(null);
|
|
509
|
+
}
|
|
510
|
+
this._activeTurnId = null;
|
|
511
|
+
this._activeTurnReady = null;
|
|
512
|
+
this._interruptPromise = null;
|
|
513
|
+
this._turnTerminalObserved = false;
|
|
514
|
+
}
|
|
515
|
+
/** Mark root turn termination and release an interrupt still awaiting its id. */
|
|
516
|
+
markTurnTerminalObserved() {
|
|
517
|
+
this._turnTerminalObserved = true;
|
|
518
|
+
const ready = this._activeTurnReady;
|
|
519
|
+
if (ready && !ready.settled) {
|
|
520
|
+
ready.settled = true;
|
|
521
|
+
ready.resolve(null);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
378
524
|
/**
|
|
379
525
|
* Durable identity for persistence + later `attachSession`. Null until Codex
|
|
380
526
|
* has assigned a thread id; serializes `{sessionId, cwd}` through the codec so
|
|
@@ -406,6 +552,30 @@ export class CodexSessionImpl {
|
|
|
406
552
|
this._pendingRpc.set(id, { resolve, reject });
|
|
407
553
|
});
|
|
408
554
|
}
|
|
555
|
+
/** A bounded RPC whose pending-map entry is removed if Codex never replies. */
|
|
556
|
+
boundedRpcRequest(method, params, timeoutMs = BACKGROUND_TASK_READ_TIMEOUT_MS) {
|
|
557
|
+
const id = this._nextId++;
|
|
558
|
+
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
559
|
+
return new Promise((resolve, reject) => {
|
|
560
|
+
const timer = setTimeout(() => {
|
|
561
|
+
if (!this._pendingRpc.delete(id))
|
|
562
|
+
return;
|
|
563
|
+
reject(new Error(`codex ${method} timed out`));
|
|
564
|
+
}, timeoutMs);
|
|
565
|
+
if (typeof timer.unref === "function")
|
|
566
|
+
timer.unref();
|
|
567
|
+
this._pendingRpc.set(id, {
|
|
568
|
+
resolve: (result) => {
|
|
569
|
+
clearTimeout(timer);
|
|
570
|
+
resolve(result);
|
|
571
|
+
},
|
|
572
|
+
reject: (err) => {
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
reject(err);
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
409
579
|
/**
|
|
410
580
|
* Bounded RPC for experimental, best-effort methods (the `thread/goal/*`
|
|
411
581
|
* family). An app-server build that doesn't recognize the method may never
|
|
@@ -534,6 +704,9 @@ export class CodexSessionImpl {
|
|
|
534
704
|
// and pass through. If the second `turn/start` lands during the first
|
|
535
705
|
// turn, the per-turn accumulators continue collecting until the result
|
|
536
706
|
// event fires; the result then drains all pending resolvers.
|
|
707
|
+
const existingTurnReady = this._activeTurnReady;
|
|
708
|
+
const isTurnLeader = existingTurnReady === null;
|
|
709
|
+
const turnReady = existingTurnReady ?? this.beginActiveTurn();
|
|
537
710
|
if (this._state === "idle") {
|
|
538
711
|
this._state = "thinking";
|
|
539
712
|
this._turnStartedAt = new Date();
|
|
@@ -571,14 +744,30 @@ export class CodexSessionImpl {
|
|
|
571
744
|
// Per-send timeout / abort, falling back to the session-level
|
|
572
745
|
// ProviderConfig.timeoutSec default when no per-call timeout is given.
|
|
573
746
|
this.armSendDeadline(entry, options);
|
|
574
|
-
this.rpcRequest("turn/start", turnParams)
|
|
575
|
-
|
|
576
|
-
|
|
747
|
+
const turnStart = this.rpcRequest("turn/start", turnParams);
|
|
748
|
+
if (isTurnLeader) {
|
|
749
|
+
void turnStart.then((response) => {
|
|
750
|
+
const turnId = str(asObj(response, "turn"), "id");
|
|
751
|
+
if (turnId) {
|
|
752
|
+
this.captureActiveTurnId(turnId, turnReady);
|
|
753
|
+
}
|
|
754
|
+
// Some app-server versions may omit the id from the response and send
|
|
755
|
+
// it only in turn/started. Keep the latch open for that notification.
|
|
756
|
+
}).catch((err) => {
|
|
757
|
+
this.rejectActiveTurnReady(err instanceof Error ? err : new Error(String(err)), turnReady);
|
|
758
|
+
// Turn-level failures may also arrive via turn/failed notifications.
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
void turnStart.catch(() => {
|
|
763
|
+
// Turn-level failures arrive via turn/failed notifications.
|
|
764
|
+
});
|
|
765
|
+
}
|
|
577
766
|
return { uuid, result };
|
|
578
767
|
}
|
|
579
768
|
/**
|
|
580
769
|
* Wire up this send's timeout and/or abort signal. On fire, the active turn
|
|
581
|
-
* is
|
|
770
|
+
* is interrupted (`turn/interrupt`) and the send settles with `timeout` /
|
|
582
771
|
* `aborted`. No-op when neither a timeout nor a signal applies.
|
|
583
772
|
*/
|
|
584
773
|
armSendDeadline(entry, options) {
|
|
@@ -621,7 +810,7 @@ export class CodexSessionImpl {
|
|
|
621
810
|
this._pendingResults.splice(idx, 1);
|
|
622
811
|
// Best-effort cancel of the active turn. With concurrent sends this ends
|
|
623
812
|
// the single shared turn for all of them — see SendOptions JSDoc.
|
|
624
|
-
void this.interrupt();
|
|
813
|
+
void this.interrupt().catch(() => { });
|
|
625
814
|
entry.resolve({
|
|
626
815
|
summary: null,
|
|
627
816
|
usage: undefined,
|
|
@@ -635,7 +824,7 @@ export class CodexSessionImpl {
|
|
|
635
824
|
}
|
|
636
825
|
async cancel(_uuid) {
|
|
637
826
|
// Codex's JSON-RPC protocol exposes no per-message cancel — only
|
|
638
|
-
// turn-wide `turn/
|
|
827
|
+
// turn-wide `turn/interrupt` (which is what `interrupt()` calls).
|
|
639
828
|
// capabilities.cancelQueuedMessage is false; this is a documented no-op.
|
|
640
829
|
return { cancelled: false };
|
|
641
830
|
}
|
|
@@ -689,13 +878,39 @@ export class CodexSessionImpl {
|
|
|
689
878
|
}
|
|
690
879
|
}
|
|
691
880
|
async interrupt() {
|
|
692
|
-
if (this._state === "
|
|
881
|
+
if (this._state === "closed")
|
|
693
882
|
return;
|
|
883
|
+
const ready = this._activeTurnReady;
|
|
884
|
+
if (!ready || this._turnTerminalObserved)
|
|
885
|
+
return;
|
|
886
|
+
if (this._interruptPromise)
|
|
887
|
+
return this._interruptPromise;
|
|
888
|
+
const threadId = this._threadId;
|
|
889
|
+
if (!threadId) {
|
|
890
|
+
throw new Error("Cannot interrupt Codex turn before the root thread id is known");
|
|
891
|
+
}
|
|
694
892
|
this._goals.notifyInterrupted(); // don't let an emulated goal auto-continue
|
|
893
|
+
const interruptPromise = (async () => {
|
|
894
|
+
const turnId = this._activeTurnId ?? await ready.promise;
|
|
895
|
+
// The turn may have completed while interrupt() was waiting for the
|
|
896
|
+
// leader turn/start response. In that race, completion is the success.
|
|
897
|
+
if (!turnId || this._activeTurnReady !== ready || this._turnTerminalObserved)
|
|
898
|
+
return;
|
|
899
|
+
await this.rpcRequest("turn/interrupt", { threadId, turnId });
|
|
900
|
+
})();
|
|
901
|
+
this._interruptPromise = interruptPromise;
|
|
695
902
|
try {
|
|
696
|
-
await
|
|
903
|
+
await interruptPromise;
|
|
904
|
+
}
|
|
905
|
+
catch (err) {
|
|
906
|
+
// A rejected control request must reach the host instead of becoming a
|
|
907
|
+
// false successful Stop. Clear only this turn's failed attempt so a
|
|
908
|
+
// subsequent click can retry.
|
|
909
|
+
if (this._activeTurnReady === ready && this._interruptPromise === interruptPromise) {
|
|
910
|
+
this._interruptPromise = null;
|
|
911
|
+
}
|
|
912
|
+
throw err;
|
|
697
913
|
}
|
|
698
|
-
catch { /* best effort */ }
|
|
699
914
|
}
|
|
700
915
|
async drain() {
|
|
701
916
|
if (this._state === "closed")
|
|
@@ -716,6 +931,11 @@ export class CodexSessionImpl {
|
|
|
716
931
|
if (this._state === "closed")
|
|
717
932
|
return;
|
|
718
933
|
this._state = "closed";
|
|
934
|
+
this.finalizeActiveBackgroundTasks("stopped", "Codex session closed", {
|
|
935
|
+
method: "session/closed",
|
|
936
|
+
});
|
|
937
|
+
this.cancelBackgroundTaskPollTimers();
|
|
938
|
+
this.rejectAllPending(new Error("Codex session closed"));
|
|
719
939
|
this.proc.stdin.end();
|
|
720
940
|
// Grace window before SIGKILL is configurable via ProviderConfig.graceSec
|
|
721
941
|
// for sessions running long tools.
|
|
@@ -894,7 +1114,461 @@ export class CodexSessionImpl {
|
|
|
894
1114
|
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
895
1115
|
return !!threadId && !!rootThreadId && threadId !== rootThreadId;
|
|
896
1116
|
}
|
|
1117
|
+
backgroundTaskParentIdForPath(agentPath) {
|
|
1118
|
+
if (!agentPath)
|
|
1119
|
+
return null;
|
|
1120
|
+
const separator = agentPath.lastIndexOf("/");
|
|
1121
|
+
if (separator <= 0)
|
|
1122
|
+
return null;
|
|
1123
|
+
return this._backgroundTaskIdsByPath.get(agentPath.slice(0, separator)) ?? null;
|
|
1124
|
+
}
|
|
1125
|
+
agentMessageText(item) {
|
|
1126
|
+
const direct = str(item, "text");
|
|
1127
|
+
if (direct)
|
|
1128
|
+
return direct;
|
|
1129
|
+
const content = Array.isArray(item["content"]) ? item["content"] : [];
|
|
1130
|
+
for (const entry of content) {
|
|
1131
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
1132
|
+
continue;
|
|
1133
|
+
const block = entry;
|
|
1134
|
+
const text = str(block, "text");
|
|
1135
|
+
if (text && (str(block, "type") === "output_text" || str(block, "type") === "text")) {
|
|
1136
|
+
return text;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
backgroundTaskEvent(taskId, phase, status, options) {
|
|
1142
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1143
|
+
return {
|
|
1144
|
+
type: "background_task",
|
|
1145
|
+
taskId,
|
|
1146
|
+
taskType: "subagent",
|
|
1147
|
+
phase,
|
|
1148
|
+
status,
|
|
1149
|
+
description: options.description ?? null,
|
|
1150
|
+
summary: options.summary ?? null,
|
|
1151
|
+
parentTaskId: options.parentTaskId ?? null,
|
|
1152
|
+
timestamp: new Date().toISOString(),
|
|
1153
|
+
providerType: "codex",
|
|
1154
|
+
sessionId: rootThreadId,
|
|
1155
|
+
messageId: null,
|
|
1156
|
+
eventId: options.eventId ?? null,
|
|
1157
|
+
turnId: options.turnId ?? null,
|
|
1158
|
+
parentToolCallId: null,
|
|
1159
|
+
raw: options.raw,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
reconciledBackgroundTaskRaw(threadId, turn) {
|
|
1163
|
+
const error = turn ? str(asObj(turn, "error"), "message") || null : null;
|
|
1164
|
+
return {
|
|
1165
|
+
method: "thread/read",
|
|
1166
|
+
reconciled: true,
|
|
1167
|
+
threadId,
|
|
1168
|
+
turnId: turn ? str(turn, "id") || null : null,
|
|
1169
|
+
status: turn ? str(turn, "status") || null : null,
|
|
1170
|
+
error,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Codex 0.144+ represents collaboration as a root collabAgentToolCall item.
|
|
1175
|
+
* A completed spawn call only means the child was created. Register every
|
|
1176
|
+
* receiver and then follow the child thread itself for the real terminus.
|
|
1177
|
+
*/
|
|
1178
|
+
handleCollabAgentToolCall(item, raw, parentTaskId) {
|
|
1179
|
+
if (this._state === "closed" || !this.ctx.onEvent)
|
|
1180
|
+
return;
|
|
1181
|
+
const tool = str(item, "tool");
|
|
1182
|
+
const states = asObj(item, "agentsStates");
|
|
1183
|
+
const receiverIds = stringArray(item["receiverThreadIds"]);
|
|
1184
|
+
const taskIds = [...new Set([...receiverIds, ...Object.keys(states)])];
|
|
1185
|
+
const canStart = tool === "spawnAgent";
|
|
1186
|
+
const metadataOnly = tool === "resumeAgent" || tool === "sendInput";
|
|
1187
|
+
const canStopWithoutState = tool === "closeAgent" && str(item, "status") === "completed";
|
|
1188
|
+
for (const taskId of taskIds) {
|
|
1189
|
+
let previous = this._backgroundTasks.get(taskId);
|
|
1190
|
+
const description = str(item, "prompt") || previous?.description || null;
|
|
1191
|
+
const resolvedParentTaskId = previous?.parentTaskId ?? parentTaskId;
|
|
1192
|
+
// The child thread's own turn/started notification is the only
|
|
1193
|
+
// authoritative reactivation edge. Root resume/send calls can arrive
|
|
1194
|
+
// before or after that child turn, so they only enrich an active task.
|
|
1195
|
+
if (metadataOnly) {
|
|
1196
|
+
if (!previous || previous.terminal)
|
|
1197
|
+
continue;
|
|
1198
|
+
if (description && description !== previous.description) {
|
|
1199
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "progress", "running", {
|
|
1200
|
+
description,
|
|
1201
|
+
summary: previous.summary,
|
|
1202
|
+
parentTaskId: resolvedParentTaskId,
|
|
1203
|
+
eventId: this._threadId
|
|
1204
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "metadata"}:progress`
|
|
1205
|
+
: null,
|
|
1206
|
+
raw,
|
|
1207
|
+
}));
|
|
1208
|
+
}
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
if (!previous && !canStart)
|
|
1212
|
+
continue;
|
|
1213
|
+
if (!canStart && !canStopWithoutState && !(taskId in states))
|
|
1214
|
+
continue;
|
|
1215
|
+
const hasState = taskId in states;
|
|
1216
|
+
const state = !hasState && tool === "closeAgent" && str(item, "status") === "completed"
|
|
1217
|
+
? { terminal: true, status: "stopped", summary: null }
|
|
1218
|
+
: codexAgentState(states[taskId]);
|
|
1219
|
+
if (!previous) {
|
|
1220
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "started", "running", {
|
|
1221
|
+
description,
|
|
1222
|
+
summary: null,
|
|
1223
|
+
parentTaskId: resolvedParentTaskId,
|
|
1224
|
+
eventId: this._threadId
|
|
1225
|
+
? `codex:${this._threadId}:background-task:${taskId}:started`
|
|
1226
|
+
: null,
|
|
1227
|
+
raw,
|
|
1228
|
+
}));
|
|
1229
|
+
previous = this._backgroundTasks.get(taskId);
|
|
1230
|
+
}
|
|
1231
|
+
else if (previous.terminal) {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
else if (description && description !== previous.description) {
|
|
1235
|
+
// thread/started can beat the richer root collab item. Preserve one
|
|
1236
|
+
// start edge, then publish the prompt as ordinary progress metadata.
|
|
1237
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "progress", "running", {
|
|
1238
|
+
description,
|
|
1239
|
+
summary: previous.summary,
|
|
1240
|
+
parentTaskId: resolvedParentTaskId,
|
|
1241
|
+
eventId: this._threadId
|
|
1242
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "metadata"}:progress`
|
|
1243
|
+
: null,
|
|
1244
|
+
raw,
|
|
1245
|
+
}));
|
|
1246
|
+
}
|
|
1247
|
+
if (state.terminal) {
|
|
1248
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", state.status, {
|
|
1249
|
+
description,
|
|
1250
|
+
summary: state.summary,
|
|
1251
|
+
parentTaskId: resolvedParentTaskId,
|
|
1252
|
+
eventId: this._threadId
|
|
1253
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "state"}:completed`
|
|
1254
|
+
: null,
|
|
1255
|
+
raw,
|
|
1256
|
+
}));
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
this.startBackgroundTaskPoller(taskId);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
waitForBackgroundTaskPoll(pollKey) {
|
|
1264
|
+
if (this._state === "closed")
|
|
1265
|
+
return Promise.resolve();
|
|
1266
|
+
return new Promise((resolve) => {
|
|
1267
|
+
const timer = setTimeout(() => {
|
|
1268
|
+
this._backgroundTaskPollTimers.delete(pollKey);
|
|
1269
|
+
resolve();
|
|
1270
|
+
}, BACKGROUND_TASK_POLL_INTERVAL_MS);
|
|
1271
|
+
if (typeof timer.unref === "function")
|
|
1272
|
+
timer.unref();
|
|
1273
|
+
this._backgroundTaskPollTimers.set(pollKey, { timer, resolve });
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
startBackgroundTaskPoller(taskId) {
|
|
1277
|
+
if (this._state === "closed")
|
|
1278
|
+
return;
|
|
1279
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1280
|
+
if (!task || task.terminal)
|
|
1281
|
+
return;
|
|
1282
|
+
const existing = this._backgroundTaskPollers.get(taskId);
|
|
1283
|
+
if (existing?.generation === task.generation)
|
|
1284
|
+
return;
|
|
1285
|
+
const generation = task.generation;
|
|
1286
|
+
const pollKey = `${taskId}:${generation}`;
|
|
1287
|
+
let poller;
|
|
1288
|
+
poller = this.pollBackgroundTask(taskId, generation, pollKey).finally(() => {
|
|
1289
|
+
if (this._backgroundTaskPollers.get(taskId)?.promise === poller) {
|
|
1290
|
+
this._backgroundTaskPollers.delete(taskId);
|
|
1291
|
+
}
|
|
1292
|
+
const pending = this._backgroundTaskPollTimers.get(pollKey);
|
|
1293
|
+
if (pending)
|
|
1294
|
+
clearTimeout(pending.timer);
|
|
1295
|
+
this._backgroundTaskPollTimers.delete(pollKey);
|
|
1296
|
+
});
|
|
1297
|
+
this._backgroundTaskPollers.set(taskId, { generation, promise: poller });
|
|
1298
|
+
}
|
|
1299
|
+
async pollBackgroundTask(taskId, generation, pollKey) {
|
|
1300
|
+
while (this._state !== "closed") {
|
|
1301
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1302
|
+
if (!task || task.generation !== generation || task.terminal)
|
|
1303
|
+
return;
|
|
1304
|
+
try {
|
|
1305
|
+
const response = await this.boundedRpcRequest("thread/read", {
|
|
1306
|
+
threadId: taskId,
|
|
1307
|
+
includeTurns: true,
|
|
1308
|
+
});
|
|
1309
|
+
if (this.observeBackgroundTaskThread(response, taskId, generation))
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
catch {
|
|
1313
|
+
// A child can briefly be pendingInit before thread/read can load it.
|
|
1314
|
+
}
|
|
1315
|
+
const current = this._backgroundTasks.get(taskId);
|
|
1316
|
+
if (!current || current.generation !== generation || current.terminal)
|
|
1317
|
+
return;
|
|
1318
|
+
await this.waitForBackgroundTaskPoll(pollKey);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/** Return true once thread/read proves that the child's latest turn ended. */
|
|
1322
|
+
observeBackgroundTaskThread(response, taskId, generation) {
|
|
1323
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1324
|
+
if (!task || task.generation !== generation || task.terminal)
|
|
1325
|
+
return true;
|
|
1326
|
+
const thread = asObj(response, "thread");
|
|
1327
|
+
const turns = Array.isArray(thread["turns"])
|
|
1328
|
+
? thread["turns"].map(obj)
|
|
1329
|
+
: [];
|
|
1330
|
+
const latestTurn = turns[turns.length - 1];
|
|
1331
|
+
if (!latestTurn) {
|
|
1332
|
+
const threadStatus = str(asObj(thread, "status"), "type");
|
|
1333
|
+
if (threadStatus !== "systemError")
|
|
1334
|
+
return false;
|
|
1335
|
+
const failureTurn = { status: "systemError", error: { message: "Subagent failed to initialize" } };
|
|
1336
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", "failed", {
|
|
1337
|
+
description: task.description,
|
|
1338
|
+
summary: "Subagent failed to initialize",
|
|
1339
|
+
parentTaskId: task.parentTaskId,
|
|
1340
|
+
eventId: this._threadId
|
|
1341
|
+
? `codex:${this._threadId}:background-task:${taskId}:system-error:completed`
|
|
1342
|
+
: null,
|
|
1343
|
+
raw: this.reconciledBackgroundTaskRaw(taskId, failureTurn),
|
|
1344
|
+
}));
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
const nativeStatus = str(latestTurn, "status");
|
|
1348
|
+
if (nativeStatus === "inProgress" || !nativeStatus)
|
|
1349
|
+
return false;
|
|
1350
|
+
const turnId = str(latestTurn, "id") || null;
|
|
1351
|
+
// The child turn/started notification is authoritative for a reactivated
|
|
1352
|
+
// generation. thread/read can briefly lag and still end at an older turn.
|
|
1353
|
+
if (task.activeTurnId && turnId !== task.activeTurnId)
|
|
1354
|
+
return false;
|
|
1355
|
+
const items = Array.isArray(latestTurn["items"])
|
|
1356
|
+
? latestTurn["items"].map(obj)
|
|
1357
|
+
: [];
|
|
1358
|
+
let summary = null;
|
|
1359
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
1360
|
+
const item = items[index];
|
|
1361
|
+
if (str(item, "type") !== "agentMessage")
|
|
1362
|
+
continue;
|
|
1363
|
+
summary = this.agentMessageText(item);
|
|
1364
|
+
if (summary)
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
const errorMessage = str(asObj(latestTurn, "error"), "message") || null;
|
|
1368
|
+
const status = nativeStatus === "failed"
|
|
1369
|
+
? "failed"
|
|
1370
|
+
: nativeStatus === "interrupted" || nativeStatus === "cancelled"
|
|
1371
|
+
? "stopped"
|
|
1372
|
+
: "completed";
|
|
1373
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", status, {
|
|
1374
|
+
description: task.description,
|
|
1375
|
+
summary: summary ?? errorMessage,
|
|
1376
|
+
parentTaskId: task.parentTaskId,
|
|
1377
|
+
turnId,
|
|
1378
|
+
eventId: this._threadId && turnId
|
|
1379
|
+
? `codex:${this._threadId}:background-task:${taskId}:${turnId}:completed`
|
|
1380
|
+
: null,
|
|
1381
|
+
raw: this.reconciledBackgroundTaskRaw(taskId, latestTurn),
|
|
1382
|
+
}));
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Maintain just enough child metadata to turn a later foreign-thread
|
|
1387
|
+
* terminal notification into one provider-neutral task event. This reducer
|
|
1388
|
+
* is deliberately separate from every root turn accumulator.
|
|
1389
|
+
*/
|
|
1390
|
+
observeBackgroundTask(event) {
|
|
1391
|
+
const previous = this._backgroundTasks.get(event.taskId);
|
|
1392
|
+
// Codex reports `subAgentActivity:interacted` after it forwards a child's
|
|
1393
|
+
// final answer to the parent. The authoritative child turn/completed can
|
|
1394
|
+
// arrive first, so suppress that late progress edge instead of resurrecting
|
|
1395
|
+
// a task that already reached a terminal state. A later child turn/started
|
|
1396
|
+
// explicitly reactivates the record below in handleForeignNotification.
|
|
1397
|
+
if (previous?.terminal)
|
|
1398
|
+
return false;
|
|
1399
|
+
// collabAgentToolCall and child thread/started can both announce the same
|
|
1400
|
+
// spawn. The first edge is sufficient and gives hosts one durable start.
|
|
1401
|
+
if (previous && event.phase === "started")
|
|
1402
|
+
return false;
|
|
1403
|
+
const description = event.description ?? previous?.description ?? null;
|
|
1404
|
+
const summary = event.summary ?? previous?.summary ?? null;
|
|
1405
|
+
const parentTaskId = event.parentTaskId
|
|
1406
|
+
?? previous?.parentTaskId
|
|
1407
|
+
?? this.backgroundTaskParentIdForPath(description);
|
|
1408
|
+
event.description = description;
|
|
1409
|
+
event.summary = summary;
|
|
1410
|
+
event.parentTaskId = parentTaskId;
|
|
1411
|
+
this._backgroundTasks.set(event.taskId, {
|
|
1412
|
+
taskId: event.taskId,
|
|
1413
|
+
description,
|
|
1414
|
+
summary,
|
|
1415
|
+
parentTaskId,
|
|
1416
|
+
terminal: event.phase === "completed",
|
|
1417
|
+
generation: previous?.generation ?? 0,
|
|
1418
|
+
activeTurnId: event.phase === "completed"
|
|
1419
|
+
? null
|
|
1420
|
+
: event.turnId ?? previous?.activeTurnId ?? null,
|
|
1421
|
+
});
|
|
1422
|
+
if (description)
|
|
1423
|
+
this._backgroundTaskIdsByPath.set(description, event.taskId);
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* A Codex app-server connection also publishes child thread notifications.
|
|
1428
|
+
* They are useful only as background-task metadata. They must never flow
|
|
1429
|
+
* through root state, summary, usage, or `resolveTurn()`.
|
|
1430
|
+
*/
|
|
1431
|
+
handleForeignNotification(method, params, rawLine, childThreadId) {
|
|
1432
|
+
if (method === "thread/started") {
|
|
1433
|
+
const thread = asObj(params, "thread");
|
|
1434
|
+
const parentThreadId = str(thread, "parentThreadId") || str(thread, "parent_thread_id");
|
|
1435
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1436
|
+
const parentTask = this._backgroundTasks.get(parentThreadId);
|
|
1437
|
+
// App-server can publish child thread/started before the corresponding
|
|
1438
|
+
// root subAgentActivity item. Register only descendants of this session,
|
|
1439
|
+
// not unrelated foreign threads multiplexed by a future server version.
|
|
1440
|
+
if (parentThreadId !== rootThreadId && !parentTask)
|
|
1441
|
+
return;
|
|
1442
|
+
const source = asObj(thread, "source");
|
|
1443
|
+
const subAgent = Object.keys(asObj(source, "subAgent")).length > 0
|
|
1444
|
+
? asObj(source, "subAgent")
|
|
1445
|
+
: asObj(source, "subagent");
|
|
1446
|
+
const spawnSource = Object.keys(asObj(subAgent, "threadSpawn")).length > 0
|
|
1447
|
+
? asObj(subAgent, "threadSpawn")
|
|
1448
|
+
: asObj(subAgent, "thread_spawn");
|
|
1449
|
+
const description = str(spawnSource, "agentPath")
|
|
1450
|
+
|| str(spawnSource, "agent_path")
|
|
1451
|
+
|| str(thread, "name")
|
|
1452
|
+
|| str(thread, "agentNickname")
|
|
1453
|
+
|| str(thread, "agentRole")
|
|
1454
|
+
|| null;
|
|
1455
|
+
this.dispatchEvent({
|
|
1456
|
+
type: "background_task",
|
|
1457
|
+
taskId: childThreadId,
|
|
1458
|
+
taskType: "subagent",
|
|
1459
|
+
phase: "started",
|
|
1460
|
+
status: "running",
|
|
1461
|
+
description,
|
|
1462
|
+
summary: null,
|
|
1463
|
+
parentTaskId: parentThreadId === rootThreadId ? null : parentThreadId,
|
|
1464
|
+
timestamp: new Date().toISOString(),
|
|
1465
|
+
providerType: "codex",
|
|
1466
|
+
sessionId: rootThreadId,
|
|
1467
|
+
messageId: null,
|
|
1468
|
+
eventId: rootThreadId
|
|
1469
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:started`
|
|
1470
|
+
: null,
|
|
1471
|
+
turnId: null,
|
|
1472
|
+
parentToolCallId: null,
|
|
1473
|
+
raw: parseJson(rawLine) ?? params,
|
|
1474
|
+
});
|
|
1475
|
+
this.startBackgroundTaskPoller(childThreadId);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
const task = this._backgroundTasks.get(childThreadId);
|
|
1479
|
+
if (!task)
|
|
1480
|
+
return;
|
|
1481
|
+
if (method === "turn/started") {
|
|
1482
|
+
if (!task.terminal)
|
|
1483
|
+
return;
|
|
1484
|
+
task.terminal = false;
|
|
1485
|
+
task.summary = null;
|
|
1486
|
+
task.generation += 1;
|
|
1487
|
+
const turn = asObj(params, "turn");
|
|
1488
|
+
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1489
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1490
|
+
this.dispatchEvent({
|
|
1491
|
+
type: "background_task",
|
|
1492
|
+
taskId: childThreadId,
|
|
1493
|
+
taskType: "subagent",
|
|
1494
|
+
phase: "progress",
|
|
1495
|
+
status: "running",
|
|
1496
|
+
description: task.description,
|
|
1497
|
+
summary: null,
|
|
1498
|
+
parentTaskId: task.parentTaskId,
|
|
1499
|
+
timestamp: new Date().toISOString(),
|
|
1500
|
+
providerType: "codex",
|
|
1501
|
+
sessionId: rootThreadId,
|
|
1502
|
+
messageId: null,
|
|
1503
|
+
eventId: rootThreadId && turnId
|
|
1504
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:${turnId}:progress`
|
|
1505
|
+
: null,
|
|
1506
|
+
turnId,
|
|
1507
|
+
parentToolCallId: null,
|
|
1508
|
+
raw: parseJson(rawLine) ?? params,
|
|
1509
|
+
});
|
|
1510
|
+
this.startBackgroundTaskPoller(childThreadId);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (method === "item/completed") {
|
|
1514
|
+
const item = asObj(params, "item");
|
|
1515
|
+
const itemType = str(item, "type");
|
|
1516
|
+
if (itemType === "collabAgentToolCall") {
|
|
1517
|
+
this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, childThreadId);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if ((itemType === "agentMessage" || itemType === "agent_message") && str(item, "phase") !== "commentary") {
|
|
1521
|
+
const summary = this.agentMessageText(item);
|
|
1522
|
+
if (summary)
|
|
1523
|
+
task.summary = summary;
|
|
1524
|
+
}
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (method !== "turn/completed" && method !== "turn/failed")
|
|
1528
|
+
return;
|
|
1529
|
+
const turn = asObj(params, "turn");
|
|
1530
|
+
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1531
|
+
const nativeStatus = method === "turn/failed" ? "failed" : str(turn, "status");
|
|
1532
|
+
const status = nativeStatus === "failed"
|
|
1533
|
+
? "failed"
|
|
1534
|
+
: nativeStatus === "interrupted" || nativeStatus === "cancelled"
|
|
1535
|
+
? "stopped"
|
|
1536
|
+
: "completed";
|
|
1537
|
+
const errorMessage = str(asObj(turn, "error"), "message")
|
|
1538
|
+
|| str(params, "message")
|
|
1539
|
+
|| str(params, "error");
|
|
1540
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1541
|
+
this.dispatchEvent({
|
|
1542
|
+
type: "background_task",
|
|
1543
|
+
taskId: childThreadId,
|
|
1544
|
+
taskType: "subagent",
|
|
1545
|
+
phase: "completed",
|
|
1546
|
+
status,
|
|
1547
|
+
description: task.description,
|
|
1548
|
+
summary: task.summary ?? (errorMessage || null),
|
|
1549
|
+
parentTaskId: task.parentTaskId,
|
|
1550
|
+
timestamp: new Date().toISOString(),
|
|
1551
|
+
providerType: "codex",
|
|
1552
|
+
sessionId: rootThreadId,
|
|
1553
|
+
messageId: null,
|
|
1554
|
+
eventId: rootThreadId && turnId
|
|
1555
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:${turnId}:completed`
|
|
1556
|
+
: null,
|
|
1557
|
+
turnId,
|
|
1558
|
+
parentToolCallId: null,
|
|
1559
|
+
raw: {
|
|
1560
|
+
method,
|
|
1561
|
+
reconciled: false,
|
|
1562
|
+
threadId: childThreadId,
|
|
1563
|
+
turnId,
|
|
1564
|
+
status: nativeStatus || null,
|
|
1565
|
+
error: errorMessage || null,
|
|
1566
|
+
},
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
897
1569
|
handleNotification(method, params, rawLine) {
|
|
1570
|
+
if (this._state === "closed")
|
|
1571
|
+
return;
|
|
898
1572
|
// codex/event — legacy wrapper
|
|
899
1573
|
if (method === "codex/event") {
|
|
900
1574
|
const innerMsg = str(params, "msg");
|
|
@@ -911,8 +1585,10 @@ export class CodexSessionImpl {
|
|
|
911
1585
|
// its root thread, so foreign items must not change root state/summary and,
|
|
912
1586
|
// most importantly, a child turn/completed must not resolve the root send.
|
|
913
1587
|
const notificationThreadId = this.notificationThreadId(params);
|
|
914
|
-
if (this.isForeignThread(notificationThreadId))
|
|
1588
|
+
if (this.isForeignThread(notificationThreadId)) {
|
|
1589
|
+
this.handleForeignNotification(method, params, rawLine, notificationThreadId);
|
|
915
1590
|
return;
|
|
1591
|
+
}
|
|
916
1592
|
// Map v2 notification methods to processing
|
|
917
1593
|
if (method === "thread/started") {
|
|
918
1594
|
// codex-cli 0.130.0+ shape: { thread: { id, sessionId, ... } }
|
|
@@ -921,6 +1597,12 @@ export class CodexSessionImpl {
|
|
|
921
1597
|
this.emitStreamEvent(rawLine);
|
|
922
1598
|
return;
|
|
923
1599
|
}
|
|
1600
|
+
if (method === "turn/started") {
|
|
1601
|
+
this.captureActiveTurnId(str(asObj(params, "turn"), "id") || str(params, "turnId"));
|
|
1602
|
+
// The parser intentionally suppresses this lifecycle-only event.
|
|
1603
|
+
this.emitStreamEvent(rawLine);
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
924
1606
|
if (method === "item/started") {
|
|
925
1607
|
this._state = "tool_executing";
|
|
926
1608
|
this.emitStreamEvent(rawLine);
|
|
@@ -929,6 +1611,11 @@ export class CodexSessionImpl {
|
|
|
929
1611
|
if (method === "item/completed") {
|
|
930
1612
|
this._state = "thinking";
|
|
931
1613
|
this.extractSummaryFromItem(params);
|
|
1614
|
+
const item = asObj(params, "item");
|
|
1615
|
+
if (str(item, "type") === "collabAgentToolCall") {
|
|
1616
|
+
this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, null);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
932
1619
|
this.emitStreamEvent(rawLine);
|
|
933
1620
|
return;
|
|
934
1621
|
}
|
|
@@ -937,6 +1624,7 @@ export class CodexSessionImpl {
|
|
|
937
1624
|
return;
|
|
938
1625
|
}
|
|
939
1626
|
if (method === "turn/failed") {
|
|
1627
|
+
this.markTurnTerminalObserved();
|
|
940
1628
|
this._turnIsError = true;
|
|
941
1629
|
this._turnErrorMessage = str(params, "message") || str(params, "error") || "Turn failed";
|
|
942
1630
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -963,6 +1651,8 @@ export class CodexSessionImpl {
|
|
|
963
1651
|
// Legacy event handling (NDJSON events with `type` field)
|
|
964
1652
|
// -------------------------------------------------------------------------
|
|
965
1653
|
handleLegacyEvent(event, rawLine) {
|
|
1654
|
+
if (this._state === "closed")
|
|
1655
|
+
return;
|
|
966
1656
|
const type = str(event, "type");
|
|
967
1657
|
const eventThreadId = str(event, "thread_id") || str(event, "threadId") || str(event, "session_id") || null;
|
|
968
1658
|
// Older NDJSON-shaped events can also carry explicit thread scope. Keep
|
|
@@ -991,6 +1681,7 @@ export class CodexSessionImpl {
|
|
|
991
1681
|
return;
|
|
992
1682
|
}
|
|
993
1683
|
if (type === "turn.failed" || type === "error") {
|
|
1684
|
+
this.markTurnTerminalObserved();
|
|
994
1685
|
this._turnIsError = true;
|
|
995
1686
|
this._turnErrorMessage = str(event, "message") || str(event, "error") || "Turn failed";
|
|
996
1687
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -1038,6 +1729,7 @@ export class CodexSessionImpl {
|
|
|
1038
1729
|
}
|
|
1039
1730
|
}
|
|
1040
1731
|
handleTurnCompleted(params) {
|
|
1732
|
+
this.markTurnTerminalObserved();
|
|
1041
1733
|
const usage = typeof params["usage"] === "object" && params["usage"] !== null
|
|
1042
1734
|
? params["usage"]
|
|
1043
1735
|
: null;
|
|
@@ -1057,7 +1749,10 @@ export class CodexSessionImpl {
|
|
|
1057
1749
|
// the error instead of a false "completed".
|
|
1058
1750
|
const turn = asObj(params, "turn");
|
|
1059
1751
|
const turnStatus = str(turn, "status");
|
|
1060
|
-
if (turnStatus === "
|
|
1752
|
+
if (turnStatus === "interrupted" || turnStatus === "cancelled") {
|
|
1753
|
+
this._turnWasInterrupted = true;
|
|
1754
|
+
}
|
|
1755
|
+
else if (turnStatus === "failed") {
|
|
1061
1756
|
this._turnIsError = true;
|
|
1062
1757
|
const msg = str(asObj(turn, "error"), "message");
|
|
1063
1758
|
this._turnErrorMessage = msg || this._turnErrorMessage || `Turn ${turnStatus}`;
|
|
@@ -1123,9 +1818,9 @@ export class CodexSessionImpl {
|
|
|
1123
1818
|
summary: this._turnSummary,
|
|
1124
1819
|
usage,
|
|
1125
1820
|
costUsd: null,
|
|
1126
|
-
status: this._turnIsError ? "failed" : "completed",
|
|
1127
|
-
errorCode: this._turnIsError ? "execution_error" : null,
|
|
1128
|
-
errorMessage: this._turnErrorMessage,
|
|
1821
|
+
status: this._turnWasInterrupted ? "aborted" : this._turnIsError ? "failed" : "completed",
|
|
1822
|
+
errorCode: this._turnWasInterrupted ? "aborted" : this._turnIsError ? "execution_error" : null,
|
|
1823
|
+
errorMessage: this._turnWasInterrupted ? "Turn was interrupted" : this._turnErrorMessage,
|
|
1129
1824
|
};
|
|
1130
1825
|
// Drain pending onEvent handlers so callers awaiting send() see a settled
|
|
1131
1826
|
// DB / log / UI state by the time TurnResult resolves. The chain snapshot
|
|
@@ -1149,8 +1844,10 @@ export class CodexSessionImpl {
|
|
|
1149
1844
|
this._turnUsage = null;
|
|
1150
1845
|
this._turnModel = null;
|
|
1151
1846
|
this._turnIsError = false;
|
|
1847
|
+
this._turnWasInterrupted = false;
|
|
1152
1848
|
this._turnErrorMessage = null;
|
|
1153
1849
|
this._turnStartedAt = null;
|
|
1850
|
+
this.clearActiveTurn();
|
|
1154
1851
|
for (const p of pending) {
|
|
1155
1852
|
// Skip sends already settled early by timeout / abort.
|
|
1156
1853
|
if (p.settled)
|
|
@@ -1181,6 +1878,8 @@ export class CodexSessionImpl {
|
|
|
1181
1878
|
* throwing handler does not break delivery of subsequent events.
|
|
1182
1879
|
*/
|
|
1183
1880
|
dispatchEvent(event) {
|
|
1881
|
+
if (event.type === "background_task" && !this.observeBackgroundTask(event))
|
|
1882
|
+
return;
|
|
1184
1883
|
// Track native goal_status transitions (keeps getGoal() accurate).
|
|
1185
1884
|
this._goals.observe(event);
|
|
1186
1885
|
const cb = this.ctx.onEvent;
|