@agentex/agent 0.0.32 → 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 +38 -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 +22 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +40 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +346 -14
- 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 +22 -0
- package/src/providers/codex/session.ts +377 -14
- package/src/types.ts +42 -0
|
@@ -271,6 +271,18 @@ export class CodexSessionImpl {
|
|
|
271
271
|
_draining = false;
|
|
272
272
|
/** Shared promise so concurrent / repeated `drain()` calls coalesce. */
|
|
273
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;
|
|
274
286
|
/** Stamps `tool_result.toolName` by correlating with prior `tool_call`s. */
|
|
275
287
|
_trackToolName = createToolNameTracker();
|
|
276
288
|
// Per-turn accumulators. Cleared after each result delivery so a subsequent
|
|
@@ -279,8 +291,12 @@ export class CodexSessionImpl {
|
|
|
279
291
|
_turnUsage = null;
|
|
280
292
|
_turnModel = null;
|
|
281
293
|
_turnIsError = false;
|
|
294
|
+
_turnWasInterrupted = false;
|
|
282
295
|
_turnErrorMessage = null;
|
|
283
296
|
_turnStartedAt = null;
|
|
297
|
+
/** Child-agent lifecycle is informational and never participates in root turn settlement. */
|
|
298
|
+
_backgroundTasks = new Map();
|
|
299
|
+
_backgroundTaskIdsByPath = new Map();
|
|
284
300
|
/**
|
|
285
301
|
* Serial dispatch chain for `onEvent`. Each dispatched event appends a
|
|
286
302
|
* handler invocation; the chain enforces in-order delivery and lets
|
|
@@ -372,9 +388,75 @@ export class CodexSessionImpl {
|
|
|
372
388
|
for (const [, p] of this._pendingRpc)
|
|
373
389
|
p.reject(err);
|
|
374
390
|
this._pendingRpc.clear();
|
|
391
|
+
this.clearActiveTurn(err);
|
|
375
392
|
}
|
|
376
393
|
get sessionId() { return this._threadId; }
|
|
377
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
|
+
}
|
|
378
460
|
/**
|
|
379
461
|
* Durable identity for persistence + later `attachSession`. Null until Codex
|
|
380
462
|
* has assigned a thread id; serializes `{sessionId, cwd}` through the codec so
|
|
@@ -534,6 +616,9 @@ export class CodexSessionImpl {
|
|
|
534
616
|
// and pass through. If the second `turn/start` lands during the first
|
|
535
617
|
// turn, the per-turn accumulators continue collecting until the result
|
|
536
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();
|
|
537
622
|
if (this._state === "idle") {
|
|
538
623
|
this._state = "thinking";
|
|
539
624
|
this._turnStartedAt = new Date();
|
|
@@ -571,14 +656,30 @@ export class CodexSessionImpl {
|
|
|
571
656
|
// Per-send timeout / abort, falling back to the session-level
|
|
572
657
|
// ProviderConfig.timeoutSec default when no per-call timeout is given.
|
|
573
658
|
this.armSendDeadline(entry, options);
|
|
574
|
-
this.rpcRequest("turn/start", turnParams)
|
|
575
|
-
|
|
576
|
-
|
|
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
|
+
}
|
|
577
678
|
return { uuid, result };
|
|
578
679
|
}
|
|
579
680
|
/**
|
|
580
681
|
* Wire up this send's timeout and/or abort signal. On fire, the active turn
|
|
581
|
-
* is
|
|
682
|
+
* is interrupted (`turn/interrupt`) and the send settles with `timeout` /
|
|
582
683
|
* `aborted`. No-op when neither a timeout nor a signal applies.
|
|
583
684
|
*/
|
|
584
685
|
armSendDeadline(entry, options) {
|
|
@@ -621,7 +722,7 @@ export class CodexSessionImpl {
|
|
|
621
722
|
this._pendingResults.splice(idx, 1);
|
|
622
723
|
// Best-effort cancel of the active turn. With concurrent sends this ends
|
|
623
724
|
// the single shared turn for all of them — see SendOptions JSDoc.
|
|
624
|
-
void this.interrupt();
|
|
725
|
+
void this.interrupt().catch(() => { });
|
|
625
726
|
entry.resolve({
|
|
626
727
|
summary: null,
|
|
627
728
|
usage: undefined,
|
|
@@ -635,7 +736,7 @@ export class CodexSessionImpl {
|
|
|
635
736
|
}
|
|
636
737
|
async cancel(_uuid) {
|
|
637
738
|
// Codex's JSON-RPC protocol exposes no per-message cancel — only
|
|
638
|
-
// turn-wide `turn/
|
|
739
|
+
// turn-wide `turn/interrupt` (which is what `interrupt()` calls).
|
|
639
740
|
// capabilities.cancelQueuedMessage is false; this is a documented no-op.
|
|
640
741
|
return { cancelled: false };
|
|
641
742
|
}
|
|
@@ -689,13 +790,39 @@ export class CodexSessionImpl {
|
|
|
689
790
|
}
|
|
690
791
|
}
|
|
691
792
|
async interrupt() {
|
|
692
|
-
if (this._state === "
|
|
793
|
+
if (this._state === "closed")
|
|
794
|
+
return;
|
|
795
|
+
const ready = this._activeTurnReady;
|
|
796
|
+
if (!ready || this._turnTerminalObserved)
|
|
693
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
|
+
}
|
|
694
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;
|
|
695
814
|
try {
|
|
696
|
-
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;
|
|
697
825
|
}
|
|
698
|
-
catch { /* best effort */ }
|
|
699
826
|
}
|
|
700
827
|
async drain() {
|
|
701
828
|
if (this._state === "closed")
|
|
@@ -716,6 +843,7 @@ export class CodexSessionImpl {
|
|
|
716
843
|
if (this._state === "closed")
|
|
717
844
|
return;
|
|
718
845
|
this._state = "closed";
|
|
846
|
+
this.rejectAllPending(new Error("Codex session closed"));
|
|
719
847
|
this.proc.stdin.end();
|
|
720
848
|
// Grace window before SIGKILL is configurable via ProviderConfig.graceSec
|
|
721
849
|
// for sessions running long tools.
|
|
@@ -894,6 +1022,192 @@ export class CodexSessionImpl {
|
|
|
894
1022
|
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
895
1023
|
return !!threadId && !!rootThreadId && threadId !== rootThreadId;
|
|
896
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
|
+
}
|
|
897
1211
|
handleNotification(method, params, rawLine) {
|
|
898
1212
|
// codex/event — legacy wrapper
|
|
899
1213
|
if (method === "codex/event") {
|
|
@@ -911,8 +1225,10 @@ export class CodexSessionImpl {
|
|
|
911
1225
|
// its root thread, so foreign items must not change root state/summary and,
|
|
912
1226
|
// most importantly, a child turn/completed must not resolve the root send.
|
|
913
1227
|
const notificationThreadId = this.notificationThreadId(params);
|
|
914
|
-
if (this.isForeignThread(notificationThreadId))
|
|
1228
|
+
if (this.isForeignThread(notificationThreadId)) {
|
|
1229
|
+
this.handleForeignNotification(method, params, rawLine, notificationThreadId);
|
|
915
1230
|
return;
|
|
1231
|
+
}
|
|
916
1232
|
// Map v2 notification methods to processing
|
|
917
1233
|
if (method === "thread/started") {
|
|
918
1234
|
// codex-cli 0.130.0+ shape: { thread: { id, sessionId, ... } }
|
|
@@ -921,6 +1237,12 @@ export class CodexSessionImpl {
|
|
|
921
1237
|
this.emitStreamEvent(rawLine);
|
|
922
1238
|
return;
|
|
923
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.
|
|
1243
|
+
this.emitStreamEvent(rawLine);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
924
1246
|
if (method === "item/started") {
|
|
925
1247
|
this._state = "tool_executing";
|
|
926
1248
|
this.emitStreamEvent(rawLine);
|
|
@@ -937,6 +1259,7 @@ export class CodexSessionImpl {
|
|
|
937
1259
|
return;
|
|
938
1260
|
}
|
|
939
1261
|
if (method === "turn/failed") {
|
|
1262
|
+
this.markTurnTerminalObserved();
|
|
940
1263
|
this._turnIsError = true;
|
|
941
1264
|
this._turnErrorMessage = str(params, "message") || str(params, "error") || "Turn failed";
|
|
942
1265
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -991,6 +1314,7 @@ export class CodexSessionImpl {
|
|
|
991
1314
|
return;
|
|
992
1315
|
}
|
|
993
1316
|
if (type === "turn.failed" || type === "error") {
|
|
1317
|
+
this.markTurnTerminalObserved();
|
|
994
1318
|
this._turnIsError = true;
|
|
995
1319
|
this._turnErrorMessage = str(event, "message") || str(event, "error") || "Turn failed";
|
|
996
1320
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -1038,6 +1362,7 @@ export class CodexSessionImpl {
|
|
|
1038
1362
|
}
|
|
1039
1363
|
}
|
|
1040
1364
|
handleTurnCompleted(params) {
|
|
1365
|
+
this.markTurnTerminalObserved();
|
|
1041
1366
|
const usage = typeof params["usage"] === "object" && params["usage"] !== null
|
|
1042
1367
|
? params["usage"]
|
|
1043
1368
|
: null;
|
|
@@ -1057,7 +1382,10 @@ export class CodexSessionImpl {
|
|
|
1057
1382
|
// the error instead of a false "completed".
|
|
1058
1383
|
const turn = asObj(params, "turn");
|
|
1059
1384
|
const turnStatus = str(turn, "status");
|
|
1060
|
-
if (turnStatus === "
|
|
1385
|
+
if (turnStatus === "interrupted" || turnStatus === "cancelled") {
|
|
1386
|
+
this._turnWasInterrupted = true;
|
|
1387
|
+
}
|
|
1388
|
+
else if (turnStatus === "failed") {
|
|
1061
1389
|
this._turnIsError = true;
|
|
1062
1390
|
const msg = str(asObj(turn, "error"), "message");
|
|
1063
1391
|
this._turnErrorMessage = msg || this._turnErrorMessage || `Turn ${turnStatus}`;
|
|
@@ -1123,9 +1451,9 @@ export class CodexSessionImpl {
|
|
|
1123
1451
|
summary: this._turnSummary,
|
|
1124
1452
|
usage,
|
|
1125
1453
|
costUsd: null,
|
|
1126
|
-
status: this._turnIsError ? "failed" : "completed",
|
|
1127
|
-
errorCode: this._turnIsError ? "execution_error" : null,
|
|
1128
|
-
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,
|
|
1129
1457
|
};
|
|
1130
1458
|
// Drain pending onEvent handlers so callers awaiting send() see a settled
|
|
1131
1459
|
// DB / log / UI state by the time TurnResult resolves. The chain snapshot
|
|
@@ -1149,8 +1477,10 @@ export class CodexSessionImpl {
|
|
|
1149
1477
|
this._turnUsage = null;
|
|
1150
1478
|
this._turnModel = null;
|
|
1151
1479
|
this._turnIsError = false;
|
|
1480
|
+
this._turnWasInterrupted = false;
|
|
1152
1481
|
this._turnErrorMessage = null;
|
|
1153
1482
|
this._turnStartedAt = null;
|
|
1483
|
+
this.clearActiveTurn();
|
|
1154
1484
|
for (const p of pending) {
|
|
1155
1485
|
// Skip sends already settled early by timeout / abort.
|
|
1156
1486
|
if (p.settled)
|
|
@@ -1181,6 +1511,8 @@ export class CodexSessionImpl {
|
|
|
1181
1511
|
* throwing handler does not break delivery of subsequent events.
|
|
1182
1512
|
*/
|
|
1183
1513
|
dispatchEvent(event) {
|
|
1514
|
+
if (event.type === "background_task" && !this.observeBackgroundTask(event))
|
|
1515
|
+
return;
|
|
1184
1516
|
// Track native goal_status transitions (keeps getGoal() accurate).
|
|
1185
1517
|
this._goals.observe(event);
|
|
1186
1518
|
const cb = this.ctx.onEvent;
|