@agentex/agent 0.0.31 → 0.0.33
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 +63 -0
- package/README.md +30 -5
- 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/history.d.ts.map +1 -1
- package/dist/providers/codex/history.js +3 -1
- package/dist/providers/codex/history.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 +37 -5
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +56 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +400 -18
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.js +10 -1
- package/dist/providers/codex/transcript-normalize.js.map +1 -1
- package/dist/types.d.ts +35 -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/history.ts +3 -1
- package/src/providers/codex/index.ts +1 -0
- package/src/providers/codex/parse.ts +38 -5
- package/src/providers/codex/session.ts +431 -18
- package/src/providers/codex/transcript-normalize.ts +11 -1
- package/src/types.ts +48 -1
|
@@ -244,9 +244,16 @@ export class CodexSessionImpl {
|
|
|
244
244
|
model;
|
|
245
245
|
instructions;
|
|
246
246
|
_state = "idle";
|
|
247
|
+
/**
|
|
248
|
+
* The root thread represented by this AgentSession. Codex app-server also
|
|
249
|
+
* reports child-agent threads on the same stdout connection, so this id is
|
|
250
|
+
* pinned once discovered and must never be promoted to a child thread.
|
|
251
|
+
*/
|
|
247
252
|
_threadId = null;
|
|
248
253
|
/** Thread id to resume (from ctx.sessionParams); null starts a fresh thread. */
|
|
249
254
|
_resumeThreadId;
|
|
255
|
+
/** Expected root during handshake, cleared when resume falls back to fresh. */
|
|
256
|
+
_expectedThreadId;
|
|
250
257
|
_lineBuffer = "";
|
|
251
258
|
_nextId = 1;
|
|
252
259
|
// Pending outgoing RPC responses (keyed by request id)
|
|
@@ -264,6 +271,18 @@ export class CodexSessionImpl {
|
|
|
264
271
|
_draining = false;
|
|
265
272
|
/** Shared promise so concurrent / repeated `drain()` calls coalesce. */
|
|
266
273
|
_drainPromise = null;
|
|
274
|
+
/**
|
|
275
|
+
* Root turn targeted by `interrupt()`. The id is learned asynchronously from
|
|
276
|
+
* the leader `turn/start` response or the root `turn/started` notification.
|
|
277
|
+
* Concurrent sends reuse this latch so a queued response cannot replace the
|
|
278
|
+
* actual active turn.
|
|
279
|
+
*/
|
|
280
|
+
_activeTurnId = null;
|
|
281
|
+
_activeTurnReady = null;
|
|
282
|
+
/** Successful repeated interrupts coalesce until the terminal notification. */
|
|
283
|
+
_interruptPromise = null;
|
|
284
|
+
/** Prevents a late interrupt request after the terminal frame was observed. */
|
|
285
|
+
_turnTerminalObserved = false;
|
|
267
286
|
/** Stamps `tool_result.toolName` by correlating with prior `tool_call`s. */
|
|
268
287
|
_trackToolName = createToolNameTracker();
|
|
269
288
|
// Per-turn accumulators. Cleared after each result delivery so a subsequent
|
|
@@ -272,8 +291,12 @@ export class CodexSessionImpl {
|
|
|
272
291
|
_turnUsage = null;
|
|
273
292
|
_turnModel = null;
|
|
274
293
|
_turnIsError = false;
|
|
294
|
+
_turnWasInterrupted = false;
|
|
275
295
|
_turnErrorMessage = null;
|
|
276
296
|
_turnStartedAt = null;
|
|
297
|
+
/** Child-agent lifecycle is informational and never participates in root turn settlement. */
|
|
298
|
+
_backgroundTasks = new Map();
|
|
299
|
+
_backgroundTaskIdsByPath = new Map();
|
|
277
300
|
/**
|
|
278
301
|
* Serial dispatch chain for `onEvent`. Each dispatched event appends a
|
|
279
302
|
* handler invocation; the chain enforces in-order delivery and lets
|
|
@@ -290,6 +313,7 @@ export class CodexSessionImpl {
|
|
|
290
313
|
this.model = model;
|
|
291
314
|
this.instructions = instructions;
|
|
292
315
|
this._resumeThreadId = readCodexResumeId(ctx.sessionParams);
|
|
316
|
+
this._expectedThreadId = this._resumeThreadId;
|
|
293
317
|
this._goals = new GoalController({
|
|
294
318
|
providerType: "codex",
|
|
295
319
|
capability: codexGoalCapability,
|
|
@@ -364,9 +388,75 @@ export class CodexSessionImpl {
|
|
|
364
388
|
for (const [, p] of this._pendingRpc)
|
|
365
389
|
p.reject(err);
|
|
366
390
|
this._pendingRpc.clear();
|
|
391
|
+
this.clearActiveTurn(err);
|
|
367
392
|
}
|
|
368
393
|
get sessionId() { return this._threadId; }
|
|
369
394
|
get state() { return this._state; }
|
|
395
|
+
/** Start the identity latch before writing the leader `turn/start` request. */
|
|
396
|
+
beginActiveTurn() {
|
|
397
|
+
let resolveFn;
|
|
398
|
+
let rejectFn;
|
|
399
|
+
const promise = new Promise((resolve, reject) => {
|
|
400
|
+
resolveFn = resolve;
|
|
401
|
+
rejectFn = reject;
|
|
402
|
+
});
|
|
403
|
+
// A turn can finish without anyone calling interrupt(). Keep a later
|
|
404
|
+
// process-exit rejection from becoming an unhandled promise rejection.
|
|
405
|
+
void promise.catch(() => { });
|
|
406
|
+
const ready = {
|
|
407
|
+
promise,
|
|
408
|
+
resolve: resolveFn,
|
|
409
|
+
reject: rejectFn,
|
|
410
|
+
settled: false,
|
|
411
|
+
};
|
|
412
|
+
this._activeTurnId = null;
|
|
413
|
+
this._activeTurnReady = ready;
|
|
414
|
+
this._interruptPromise = null;
|
|
415
|
+
this._turnTerminalObserved = false;
|
|
416
|
+
this._turnWasInterrupted = false;
|
|
417
|
+
return ready;
|
|
418
|
+
}
|
|
419
|
+
/** First root turn id wins for the current latch. */
|
|
420
|
+
captureActiveTurnId(turnId, expected) {
|
|
421
|
+
const ready = this._activeTurnReady;
|
|
422
|
+
if (!turnId || !ready || ready.settled)
|
|
423
|
+
return;
|
|
424
|
+
if (expected && ready !== expected)
|
|
425
|
+
return;
|
|
426
|
+
this._activeTurnId = turnId;
|
|
427
|
+
ready.settled = true;
|
|
428
|
+
ready.resolve(turnId);
|
|
429
|
+
}
|
|
430
|
+
rejectActiveTurnReady(err, expected) {
|
|
431
|
+
if (this._activeTurnReady !== expected || expected.settled)
|
|
432
|
+
return;
|
|
433
|
+
expected.settled = true;
|
|
434
|
+
expected.reject(err);
|
|
435
|
+
}
|
|
436
|
+
/** Clear the current turn and release an interrupt waiting for its id. */
|
|
437
|
+
clearActiveTurn(err) {
|
|
438
|
+
const ready = this._activeTurnReady;
|
|
439
|
+
if (ready && !ready.settled) {
|
|
440
|
+
ready.settled = true;
|
|
441
|
+
if (err)
|
|
442
|
+
ready.reject(err);
|
|
443
|
+
else
|
|
444
|
+
ready.resolve(null);
|
|
445
|
+
}
|
|
446
|
+
this._activeTurnId = null;
|
|
447
|
+
this._activeTurnReady = null;
|
|
448
|
+
this._interruptPromise = null;
|
|
449
|
+
this._turnTerminalObserved = false;
|
|
450
|
+
}
|
|
451
|
+
/** Mark root turn termination and release an interrupt still awaiting its id. */
|
|
452
|
+
markTurnTerminalObserved() {
|
|
453
|
+
this._turnTerminalObserved = true;
|
|
454
|
+
const ready = this._activeTurnReady;
|
|
455
|
+
if (ready && !ready.settled) {
|
|
456
|
+
ready.settled = true;
|
|
457
|
+
ready.resolve(null);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
370
460
|
/**
|
|
371
461
|
* Durable identity for persistence + later `attachSession`. Null until Codex
|
|
372
462
|
* has assigned a thread id; serializes `{sessionId, cwd}` through the codec so
|
|
@@ -448,6 +538,7 @@ export class CodexSessionImpl {
|
|
|
448
538
|
// thread/resume may echo the thread back or return {}; fall back to the
|
|
449
539
|
// id we resumed with so `sessionId` is always populated.
|
|
450
540
|
this._threadId = str(thread, "id") || str(thread, "sessionId") || this._resumeThreadId;
|
|
541
|
+
this._expectedThreadId = this._threadId;
|
|
451
542
|
// Rehydrate a durable Codex goal so getGoal() reflects it immediately
|
|
452
543
|
// (goals live in SQLite, not the transcript, so a resumed thread would
|
|
453
544
|
// otherwise report null until the next goal notification).
|
|
@@ -455,6 +546,12 @@ export class CodexSessionImpl {
|
|
|
455
546
|
return;
|
|
456
547
|
}
|
|
457
548
|
catch (err) {
|
|
549
|
+
// A failed resume can emit thread/started before its error response.
|
|
550
|
+
// Clear that provisional identity so the fresh thread's init event is
|
|
551
|
+
// accepted instead of being mistaken for a foreign child thread.
|
|
552
|
+
if (this._threadId === this._expectedThreadId)
|
|
553
|
+
this._threadId = null;
|
|
554
|
+
this._expectedThreadId = null;
|
|
458
555
|
// The thread is unknown to this codex install (different machine, pruned
|
|
459
556
|
// history). Don't fail the whole session — fall back to a fresh thread
|
|
460
557
|
// and surface the downgrade on stderr. The new id flows back out via
|
|
@@ -502,6 +599,7 @@ export class CodexSessionImpl {
|
|
|
502
599
|
// codex-cli 0.130.0+ shape: { thread: { id, sessionId, ... }, model, ... }
|
|
503
600
|
const thread = asObj(res, "thread");
|
|
504
601
|
this._threadId = str(thread, "id") || str(thread, "sessionId") || null;
|
|
602
|
+
this._expectedThreadId = this._threadId;
|
|
505
603
|
}
|
|
506
604
|
// -------------------------------------------------------------------------
|
|
507
605
|
// Public API
|
|
@@ -518,6 +616,9 @@ export class CodexSessionImpl {
|
|
|
518
616
|
// and pass through. If the second `turn/start` lands during the first
|
|
519
617
|
// turn, the per-turn accumulators continue collecting until the result
|
|
520
618
|
// event fires; the result then drains all pending resolvers.
|
|
619
|
+
const existingTurnReady = this._activeTurnReady;
|
|
620
|
+
const isTurnLeader = existingTurnReady === null;
|
|
621
|
+
const turnReady = existingTurnReady ?? this.beginActiveTurn();
|
|
521
622
|
if (this._state === "idle") {
|
|
522
623
|
this._state = "thinking";
|
|
523
624
|
this._turnStartedAt = new Date();
|
|
@@ -555,14 +656,30 @@ export class CodexSessionImpl {
|
|
|
555
656
|
// Per-send timeout / abort, falling back to the session-level
|
|
556
657
|
// ProviderConfig.timeoutSec default when no per-call timeout is given.
|
|
557
658
|
this.armSendDeadline(entry, options);
|
|
558
|
-
this.rpcRequest("turn/start", turnParams)
|
|
559
|
-
|
|
560
|
-
|
|
659
|
+
const turnStart = this.rpcRequest("turn/start", turnParams);
|
|
660
|
+
if (isTurnLeader) {
|
|
661
|
+
void turnStart.then((response) => {
|
|
662
|
+
const turnId = str(asObj(response, "turn"), "id");
|
|
663
|
+
if (turnId) {
|
|
664
|
+
this.captureActiveTurnId(turnId, turnReady);
|
|
665
|
+
}
|
|
666
|
+
// Some app-server versions may omit the id from the response and send
|
|
667
|
+
// it only in turn/started. Keep the latch open for that notification.
|
|
668
|
+
}).catch((err) => {
|
|
669
|
+
this.rejectActiveTurnReady(err instanceof Error ? err : new Error(String(err)), turnReady);
|
|
670
|
+
// Turn-level failures may also arrive via turn/failed notifications.
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
void turnStart.catch(() => {
|
|
675
|
+
// Turn-level failures arrive via turn/failed notifications.
|
|
676
|
+
});
|
|
677
|
+
}
|
|
561
678
|
return { uuid, result };
|
|
562
679
|
}
|
|
563
680
|
/**
|
|
564
681
|
* Wire up this send's timeout and/or abort signal. On fire, the active turn
|
|
565
|
-
* is
|
|
682
|
+
* is interrupted (`turn/interrupt`) and the send settles with `timeout` /
|
|
566
683
|
* `aborted`. No-op when neither a timeout nor a signal applies.
|
|
567
684
|
*/
|
|
568
685
|
armSendDeadline(entry, options) {
|
|
@@ -605,7 +722,7 @@ export class CodexSessionImpl {
|
|
|
605
722
|
this._pendingResults.splice(idx, 1);
|
|
606
723
|
// Best-effort cancel of the active turn. With concurrent sends this ends
|
|
607
724
|
// the single shared turn for all of them — see SendOptions JSDoc.
|
|
608
|
-
void this.interrupt();
|
|
725
|
+
void this.interrupt().catch(() => { });
|
|
609
726
|
entry.resolve({
|
|
610
727
|
summary: null,
|
|
611
728
|
usage: undefined,
|
|
@@ -619,7 +736,7 @@ export class CodexSessionImpl {
|
|
|
619
736
|
}
|
|
620
737
|
async cancel(_uuid) {
|
|
621
738
|
// Codex's JSON-RPC protocol exposes no per-message cancel — only
|
|
622
|
-
// turn-wide `turn/
|
|
739
|
+
// turn-wide `turn/interrupt` (which is what `interrupt()` calls).
|
|
623
740
|
// capabilities.cancelQueuedMessage is false; this is a documented no-op.
|
|
624
741
|
return { cancelled: false };
|
|
625
742
|
}
|
|
@@ -673,13 +790,39 @@ export class CodexSessionImpl {
|
|
|
673
790
|
}
|
|
674
791
|
}
|
|
675
792
|
async interrupt() {
|
|
676
|
-
if (this._state === "
|
|
793
|
+
if (this._state === "closed")
|
|
794
|
+
return;
|
|
795
|
+
const ready = this._activeTurnReady;
|
|
796
|
+
if (!ready || this._turnTerminalObserved)
|
|
677
797
|
return;
|
|
798
|
+
if (this._interruptPromise)
|
|
799
|
+
return this._interruptPromise;
|
|
800
|
+
const threadId = this._threadId;
|
|
801
|
+
if (!threadId) {
|
|
802
|
+
throw new Error("Cannot interrupt Codex turn before the root thread id is known");
|
|
803
|
+
}
|
|
678
804
|
this._goals.notifyInterrupted(); // don't let an emulated goal auto-continue
|
|
805
|
+
const interruptPromise = (async () => {
|
|
806
|
+
const turnId = this._activeTurnId ?? await ready.promise;
|
|
807
|
+
// The turn may have completed while interrupt() was waiting for the
|
|
808
|
+
// leader turn/start response. In that race, completion is the success.
|
|
809
|
+
if (!turnId || this._activeTurnReady !== ready || this._turnTerminalObserved)
|
|
810
|
+
return;
|
|
811
|
+
await this.rpcRequest("turn/interrupt", { threadId, turnId });
|
|
812
|
+
})();
|
|
813
|
+
this._interruptPromise = interruptPromise;
|
|
679
814
|
try {
|
|
680
|
-
await
|
|
815
|
+
await interruptPromise;
|
|
816
|
+
}
|
|
817
|
+
catch (err) {
|
|
818
|
+
// A rejected control request must reach the host instead of becoming a
|
|
819
|
+
// false successful Stop. Clear only this turn's failed attempt so a
|
|
820
|
+
// subsequent click can retry.
|
|
821
|
+
if (this._activeTurnReady === ready && this._interruptPromise === interruptPromise) {
|
|
822
|
+
this._interruptPromise = null;
|
|
823
|
+
}
|
|
824
|
+
throw err;
|
|
681
825
|
}
|
|
682
|
-
catch { /* best effort */ }
|
|
683
826
|
}
|
|
684
827
|
async drain() {
|
|
685
828
|
if (this._state === "closed")
|
|
@@ -700,6 +843,7 @@ export class CodexSessionImpl {
|
|
|
700
843
|
if (this._state === "closed")
|
|
701
844
|
return;
|
|
702
845
|
this._state = "closed";
|
|
846
|
+
this.rejectAllPending(new Error("Codex session closed"));
|
|
703
847
|
this.proc.stdin.end();
|
|
704
848
|
// Grace window before SIGKILL is configurable via ProviderConfig.graceSec
|
|
705
849
|
// for sessions running long tools.
|
|
@@ -863,6 +1007,207 @@ export class CodexSessionImpl {
|
|
|
863
1007
|
// -------------------------------------------------------------------------
|
|
864
1008
|
// Notification handling (v2 format)
|
|
865
1009
|
// -------------------------------------------------------------------------
|
|
1010
|
+
/** Extract the thread scope carried by a v2 app-server notification. */
|
|
1011
|
+
notificationThreadId(params) {
|
|
1012
|
+
const thread = asObj(params, "thread");
|
|
1013
|
+
return str(params, "threadId") || str(thread, "id") || str(thread, "sessionId") || null;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Whether an explicitly-scoped event belongs to another app-server thread.
|
|
1017
|
+
* `_expectedThreadId` protects the resume handshake window before `_threadId`
|
|
1018
|
+
* has been populated and is cleared if resume falls back to a fresh thread.
|
|
1019
|
+
* Unscoped global notifications remain eligible.
|
|
1020
|
+
*/
|
|
1021
|
+
isForeignThread(threadId) {
|
|
1022
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1023
|
+
return !!threadId && !!rootThreadId && threadId !== rootThreadId;
|
|
1024
|
+
}
|
|
1025
|
+
backgroundTaskParentIdForPath(agentPath) {
|
|
1026
|
+
if (!agentPath)
|
|
1027
|
+
return null;
|
|
1028
|
+
const separator = agentPath.lastIndexOf("/");
|
|
1029
|
+
if (separator <= 0)
|
|
1030
|
+
return null;
|
|
1031
|
+
return this._backgroundTaskIdsByPath.get(agentPath.slice(0, separator)) ?? null;
|
|
1032
|
+
}
|
|
1033
|
+
agentMessageText(item) {
|
|
1034
|
+
const direct = str(item, "text");
|
|
1035
|
+
if (direct)
|
|
1036
|
+
return direct;
|
|
1037
|
+
const content = Array.isArray(item["content"]) ? item["content"] : [];
|
|
1038
|
+
for (const entry of content) {
|
|
1039
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
1040
|
+
continue;
|
|
1041
|
+
const block = entry;
|
|
1042
|
+
const text = str(block, "text");
|
|
1043
|
+
if (text && (str(block, "type") === "output_text" || str(block, "type") === "text")) {
|
|
1044
|
+
return text;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Maintain just enough child metadata to turn a later foreign-thread
|
|
1051
|
+
* terminal notification into one provider-neutral task event. This reducer
|
|
1052
|
+
* is deliberately separate from every root turn accumulator.
|
|
1053
|
+
*/
|
|
1054
|
+
observeBackgroundTask(event) {
|
|
1055
|
+
const previous = this._backgroundTasks.get(event.taskId);
|
|
1056
|
+
// Codex reports `subAgentActivity:interacted` after it forwards a child's
|
|
1057
|
+
// final answer to the parent. The authoritative child turn/completed can
|
|
1058
|
+
// arrive first, so suppress that late progress edge instead of resurrecting
|
|
1059
|
+
// a task that already reached a terminal state. A later child turn/started
|
|
1060
|
+
// explicitly reactivates the record below in handleForeignNotification.
|
|
1061
|
+
if (previous?.terminal)
|
|
1062
|
+
return false;
|
|
1063
|
+
const description = event.description ?? previous?.description ?? null;
|
|
1064
|
+
const summary = event.summary ?? previous?.summary ?? null;
|
|
1065
|
+
const parentTaskId = event.parentTaskId
|
|
1066
|
+
?? previous?.parentTaskId
|
|
1067
|
+
?? this.backgroundTaskParentIdForPath(description);
|
|
1068
|
+
event.description = description;
|
|
1069
|
+
event.summary = summary;
|
|
1070
|
+
event.parentTaskId = parentTaskId;
|
|
1071
|
+
this._backgroundTasks.set(event.taskId, {
|
|
1072
|
+
taskId: event.taskId,
|
|
1073
|
+
description,
|
|
1074
|
+
summary,
|
|
1075
|
+
parentTaskId,
|
|
1076
|
+
terminal: event.phase === "completed",
|
|
1077
|
+
});
|
|
1078
|
+
if (description)
|
|
1079
|
+
this._backgroundTaskIdsByPath.set(description, event.taskId);
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* A Codex app-server connection also publishes child thread notifications.
|
|
1084
|
+
* They are useful only as background-task metadata. They must never flow
|
|
1085
|
+
* through root state, summary, usage, or `resolveTurn()`.
|
|
1086
|
+
*/
|
|
1087
|
+
handleForeignNotification(method, params, rawLine, childThreadId) {
|
|
1088
|
+
if (method === "thread/started") {
|
|
1089
|
+
const thread = asObj(params, "thread");
|
|
1090
|
+
const parentThreadId = str(thread, "parentThreadId") || str(thread, "parent_thread_id");
|
|
1091
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1092
|
+
const parentTask = this._backgroundTasks.get(parentThreadId);
|
|
1093
|
+
// App-server can publish child thread/started before the corresponding
|
|
1094
|
+
// root subAgentActivity item. Register only descendants of this session,
|
|
1095
|
+
// not unrelated foreign threads multiplexed by a future server version.
|
|
1096
|
+
if (parentThreadId !== rootThreadId && !parentTask)
|
|
1097
|
+
return;
|
|
1098
|
+
const source = asObj(thread, "source");
|
|
1099
|
+
const subAgent = Object.keys(asObj(source, "subAgent")).length > 0
|
|
1100
|
+
? asObj(source, "subAgent")
|
|
1101
|
+
: asObj(source, "subagent");
|
|
1102
|
+
const spawnSource = Object.keys(asObj(subAgent, "threadSpawn")).length > 0
|
|
1103
|
+
? asObj(subAgent, "threadSpawn")
|
|
1104
|
+
: asObj(subAgent, "thread_spawn");
|
|
1105
|
+
const description = str(spawnSource, "agentPath")
|
|
1106
|
+
|| str(spawnSource, "agent_path")
|
|
1107
|
+
|| str(thread, "name")
|
|
1108
|
+
|| str(thread, "agentNickname")
|
|
1109
|
+
|| str(thread, "agentRole")
|
|
1110
|
+
|| null;
|
|
1111
|
+
this.dispatchEvent({
|
|
1112
|
+
type: "background_task",
|
|
1113
|
+
taskId: childThreadId,
|
|
1114
|
+
taskType: "subagent",
|
|
1115
|
+
phase: "started",
|
|
1116
|
+
status: "running",
|
|
1117
|
+
description,
|
|
1118
|
+
summary: null,
|
|
1119
|
+
parentTaskId: parentThreadId === rootThreadId ? null : parentThreadId,
|
|
1120
|
+
timestamp: new Date().toISOString(),
|
|
1121
|
+
providerType: "codex",
|
|
1122
|
+
sessionId: rootThreadId,
|
|
1123
|
+
messageId: null,
|
|
1124
|
+
eventId: rootThreadId
|
|
1125
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:started`
|
|
1126
|
+
: null,
|
|
1127
|
+
turnId: null,
|
|
1128
|
+
parentToolCallId: null,
|
|
1129
|
+
raw: parseJson(rawLine) ?? params,
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const task = this._backgroundTasks.get(childThreadId);
|
|
1134
|
+
if (!task)
|
|
1135
|
+
return;
|
|
1136
|
+
if (method === "turn/started") {
|
|
1137
|
+
if (!task.terminal)
|
|
1138
|
+
return;
|
|
1139
|
+
task.terminal = false;
|
|
1140
|
+
task.summary = null;
|
|
1141
|
+
const turn = asObj(params, "turn");
|
|
1142
|
+
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1143
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1144
|
+
this.dispatchEvent({
|
|
1145
|
+
type: "background_task",
|
|
1146
|
+
taskId: childThreadId,
|
|
1147
|
+
taskType: "subagent",
|
|
1148
|
+
phase: "progress",
|
|
1149
|
+
status: "running",
|
|
1150
|
+
description: task.description,
|
|
1151
|
+
summary: null,
|
|
1152
|
+
parentTaskId: task.parentTaskId,
|
|
1153
|
+
timestamp: new Date().toISOString(),
|
|
1154
|
+
providerType: "codex",
|
|
1155
|
+
sessionId: rootThreadId,
|
|
1156
|
+
messageId: null,
|
|
1157
|
+
eventId: rootThreadId && turnId
|
|
1158
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:${turnId}:progress`
|
|
1159
|
+
: null,
|
|
1160
|
+
turnId,
|
|
1161
|
+
parentToolCallId: null,
|
|
1162
|
+
raw: parseJson(rawLine) ?? params,
|
|
1163
|
+
});
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (method === "item/completed") {
|
|
1167
|
+
const item = asObj(params, "item");
|
|
1168
|
+
const itemType = str(item, "type");
|
|
1169
|
+
if ((itemType === "agentMessage" || itemType === "agent_message") && str(item, "phase") !== "commentary") {
|
|
1170
|
+
const summary = this.agentMessageText(item);
|
|
1171
|
+
if (summary)
|
|
1172
|
+
task.summary = summary;
|
|
1173
|
+
}
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (method !== "turn/completed" && method !== "turn/failed")
|
|
1177
|
+
return;
|
|
1178
|
+
const turn = asObj(params, "turn");
|
|
1179
|
+
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1180
|
+
const nativeStatus = method === "turn/failed" ? "failed" : str(turn, "status");
|
|
1181
|
+
const status = nativeStatus === "failed"
|
|
1182
|
+
? "failed"
|
|
1183
|
+
: nativeStatus === "interrupted" || nativeStatus === "cancelled"
|
|
1184
|
+
? "stopped"
|
|
1185
|
+
: "completed";
|
|
1186
|
+
const errorMessage = str(asObj(turn, "error"), "message")
|
|
1187
|
+
|| str(params, "message")
|
|
1188
|
+
|| str(params, "error");
|
|
1189
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1190
|
+
this.dispatchEvent({
|
|
1191
|
+
type: "background_task",
|
|
1192
|
+
taskId: childThreadId,
|
|
1193
|
+
taskType: "subagent",
|
|
1194
|
+
phase: "completed",
|
|
1195
|
+
status,
|
|
1196
|
+
description: task.description,
|
|
1197
|
+
summary: task.summary ?? (errorMessage || null),
|
|
1198
|
+
parentTaskId: task.parentTaskId,
|
|
1199
|
+
timestamp: new Date().toISOString(),
|
|
1200
|
+
providerType: "codex",
|
|
1201
|
+
sessionId: rootThreadId,
|
|
1202
|
+
messageId: null,
|
|
1203
|
+
eventId: rootThreadId && turnId
|
|
1204
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:${turnId}:completed`
|
|
1205
|
+
: null,
|
|
1206
|
+
turnId,
|
|
1207
|
+
parentToolCallId: null,
|
|
1208
|
+
raw: parseJson(rawLine) ?? params,
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
866
1211
|
handleNotification(method, params, rawLine) {
|
|
867
1212
|
// codex/event — legacy wrapper
|
|
868
1213
|
if (method === "codex/event") {
|
|
@@ -875,11 +1220,26 @@ export class CodexSessionImpl {
|
|
|
875
1220
|
}
|
|
876
1221
|
return;
|
|
877
1222
|
}
|
|
1223
|
+
// One Codex app-server connection multiplexes notifications for the root
|
|
1224
|
+
// thread and any child agents it spawns. An AgentSession represents only
|
|
1225
|
+
// its root thread, so foreign items must not change root state/summary and,
|
|
1226
|
+
// most importantly, a child turn/completed must not resolve the root send.
|
|
1227
|
+
const notificationThreadId = this.notificationThreadId(params);
|
|
1228
|
+
if (this.isForeignThread(notificationThreadId)) {
|
|
1229
|
+
this.handleForeignNotification(method, params, rawLine, notificationThreadId);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
878
1232
|
// Map v2 notification methods to processing
|
|
879
1233
|
if (method === "thread/started") {
|
|
880
1234
|
// codex-cli 0.130.0+ shape: { thread: { id, sessionId, ... } }
|
|
881
|
-
|
|
882
|
-
|
|
1235
|
+
if (!this._threadId)
|
|
1236
|
+
this._threadId = notificationThreadId;
|
|
1237
|
+
this.emitStreamEvent(rawLine);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (method === "turn/started") {
|
|
1241
|
+
this.captureActiveTurnId(str(asObj(params, "turn"), "id") || str(params, "turnId"));
|
|
1242
|
+
// The parser intentionally suppresses this lifecycle-only event.
|
|
883
1243
|
this.emitStreamEvent(rawLine);
|
|
884
1244
|
return;
|
|
885
1245
|
}
|
|
@@ -899,6 +1259,7 @@ export class CodexSessionImpl {
|
|
|
899
1259
|
return;
|
|
900
1260
|
}
|
|
901
1261
|
if (method === "turn/failed") {
|
|
1262
|
+
this.markTurnTerminalObserved();
|
|
902
1263
|
this._turnIsError = true;
|
|
903
1264
|
this._turnErrorMessage = str(params, "message") || str(params, "error") || "Turn failed";
|
|
904
1265
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -926,8 +1287,14 @@ export class CodexSessionImpl {
|
|
|
926
1287
|
// -------------------------------------------------------------------------
|
|
927
1288
|
handleLegacyEvent(event, rawLine) {
|
|
928
1289
|
const type = str(event, "type");
|
|
1290
|
+
const eventThreadId = str(event, "thread_id") || str(event, "threadId") || str(event, "session_id") || null;
|
|
1291
|
+
// Older NDJSON-shaped events can also carry explicit thread scope. Keep
|
|
1292
|
+
// the same root-only invariant when that scope is available.
|
|
1293
|
+
if (this.isForeignThread(eventThreadId))
|
|
1294
|
+
return;
|
|
929
1295
|
if (type === "thread.started") {
|
|
930
|
-
|
|
1296
|
+
if (!this._threadId)
|
|
1297
|
+
this._threadId = eventThreadId;
|
|
931
1298
|
this.emitStreamEvent(rawLine);
|
|
932
1299
|
return;
|
|
933
1300
|
}
|
|
@@ -947,6 +1314,7 @@ export class CodexSessionImpl {
|
|
|
947
1314
|
return;
|
|
948
1315
|
}
|
|
949
1316
|
if (type === "turn.failed" || type === "error") {
|
|
1317
|
+
this.markTurnTerminalObserved();
|
|
950
1318
|
this._turnIsError = true;
|
|
951
1319
|
this._turnErrorMessage = str(event, "message") || str(event, "error") || "Turn failed";
|
|
952
1320
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -969,6 +1337,11 @@ export class CodexSessionImpl {
|
|
|
969
1337
|
const itemType = str(item, "type");
|
|
970
1338
|
if (itemType !== "agent_message" && itemType !== "agentMessage")
|
|
971
1339
|
return;
|
|
1340
|
+
// Commentary is progress, not the terminal answer. Keep phase-absent
|
|
1341
|
+
// legacy events as a compatibility fallback, while known final_answer
|
|
1342
|
+
// items remain eligible for TurnResult.summary.
|
|
1343
|
+
if (str(item, "phase") === "commentary")
|
|
1344
|
+
return;
|
|
972
1345
|
// Direct text (Codex 0.30+)
|
|
973
1346
|
const directText = str(item, "text");
|
|
974
1347
|
if (directText) {
|
|
@@ -989,6 +1362,7 @@ export class CodexSessionImpl {
|
|
|
989
1362
|
}
|
|
990
1363
|
}
|
|
991
1364
|
handleTurnCompleted(params) {
|
|
1365
|
+
this.markTurnTerminalObserved();
|
|
992
1366
|
const usage = typeof params["usage"] === "object" && params["usage"] !== null
|
|
993
1367
|
? params["usage"]
|
|
994
1368
|
: null;
|
|
@@ -1008,7 +1382,10 @@ export class CodexSessionImpl {
|
|
|
1008
1382
|
// the error instead of a false "completed".
|
|
1009
1383
|
const turn = asObj(params, "turn");
|
|
1010
1384
|
const turnStatus = str(turn, "status");
|
|
1011
|
-
if (turnStatus === "
|
|
1385
|
+
if (turnStatus === "interrupted" || turnStatus === "cancelled") {
|
|
1386
|
+
this._turnWasInterrupted = true;
|
|
1387
|
+
}
|
|
1388
|
+
else if (turnStatus === "failed") {
|
|
1012
1389
|
this._turnIsError = true;
|
|
1013
1390
|
const msg = str(asObj(turn, "error"), "message");
|
|
1014
1391
|
this._turnErrorMessage = msg || this._turnErrorMessage || `Turn ${turnStatus}`;
|
|
@@ -1074,9 +1451,9 @@ export class CodexSessionImpl {
|
|
|
1074
1451
|
summary: this._turnSummary,
|
|
1075
1452
|
usage,
|
|
1076
1453
|
costUsd: null,
|
|
1077
|
-
status: this._turnIsError ? "failed" : "completed",
|
|
1078
|
-
errorCode: this._turnIsError ? "execution_error" : null,
|
|
1079
|
-
errorMessage: this._turnErrorMessage,
|
|
1454
|
+
status: this._turnWasInterrupted ? "aborted" : this._turnIsError ? "failed" : "completed",
|
|
1455
|
+
errorCode: this._turnWasInterrupted ? "aborted" : this._turnIsError ? "execution_error" : null,
|
|
1456
|
+
errorMessage: this._turnWasInterrupted ? "Turn was interrupted" : this._turnErrorMessage,
|
|
1080
1457
|
};
|
|
1081
1458
|
// Drain pending onEvent handlers so callers awaiting send() see a settled
|
|
1082
1459
|
// DB / log / UI state by the time TurnResult resolves. The chain snapshot
|
|
@@ -1100,8 +1477,10 @@ export class CodexSessionImpl {
|
|
|
1100
1477
|
this._turnUsage = null;
|
|
1101
1478
|
this._turnModel = null;
|
|
1102
1479
|
this._turnIsError = false;
|
|
1480
|
+
this._turnWasInterrupted = false;
|
|
1103
1481
|
this._turnErrorMessage = null;
|
|
1104
1482
|
this._turnStartedAt = null;
|
|
1483
|
+
this.clearActiveTurn();
|
|
1105
1484
|
for (const p of pending) {
|
|
1106
1485
|
// Skip sends already settled early by timeout / abort.
|
|
1107
1486
|
if (p.settled)
|
|
@@ -1132,6 +1511,8 @@ export class CodexSessionImpl {
|
|
|
1132
1511
|
* throwing handler does not break delivery of subsequent events.
|
|
1133
1512
|
*/
|
|
1134
1513
|
dispatchEvent(event) {
|
|
1514
|
+
if (event.type === "background_task" && !this.observeBackgroundTask(event))
|
|
1515
|
+
return;
|
|
1135
1516
|
// Track native goal_status transitions (keeps getGoal() accurate).
|
|
1136
1517
|
this._goals.observe(event);
|
|
1137
1518
|
const cb = this.ctx.onEvent;
|
|
@@ -1146,8 +1527,9 @@ export class CodexSessionImpl {
|
|
|
1146
1527
|
// share an id; the last write wins. It also does NOT match the transcript
|
|
1147
1528
|
// reader's `codex:<sessionId>:<offset>` scheme (different wire vocabulary
|
|
1148
1529
|
// on disk) — cross-shape dedup remains a host concern.
|
|
1149
|
-
|
|
1150
|
-
|
|
1530
|
+
const eventThreadId = event.sessionId ?? this._threadId;
|
|
1531
|
+
if (!event.eventId && eventThreadId && event.turnId && event.messageId) {
|
|
1532
|
+
event.eventId = `codex:${eventThreadId}:${event.turnId}:${event.messageId}:${event.type}`;
|
|
1151
1533
|
}
|
|
1152
1534
|
// Enrich synchronously (in stream order) so tool_result events carry the
|
|
1153
1535
|
// name of the tool_call they answer.
|