@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
|
@@ -132,6 +132,33 @@ interface PendingResult {
|
|
|
132
132
|
cleanup?: () => void;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
interface TrackedBackgroundTask {
|
|
136
|
+
taskId: string;
|
|
137
|
+
description: string | null;
|
|
138
|
+
summary: string | null;
|
|
139
|
+
parentTaskId: string | null;
|
|
140
|
+
terminal: boolean;
|
|
141
|
+
generation: number;
|
|
142
|
+
activeTurnId: string | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface CodexBackgroundTaskState {
|
|
146
|
+
terminal: boolean;
|
|
147
|
+
status: "running" | "completed" | "failed" | "stopped";
|
|
148
|
+
summary: string | null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const BACKGROUND_TASK_POLL_INTERVAL_MS = 2_000;
|
|
152
|
+
const BACKGROUND_TASK_READ_TIMEOUT_MS = 5_000;
|
|
153
|
+
|
|
154
|
+
/** Identity latch for the root turn currently represented by this session. */
|
|
155
|
+
interface ActiveTurnReady {
|
|
156
|
+
promise: Promise<string | null>;
|
|
157
|
+
resolve: (turnId: string | null) => void;
|
|
158
|
+
reject: (err: Error) => void;
|
|
159
|
+
settled: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
135
162
|
// ---------------------------------------------------------------------------
|
|
136
163
|
// JSON-RPC 2.0 helpers
|
|
137
164
|
// ---------------------------------------------------------------------------
|
|
@@ -163,6 +190,32 @@ function asObj(parent: Record<string, unknown>, key: string): Record<string, unk
|
|
|
163
190
|
: {};
|
|
164
191
|
}
|
|
165
192
|
|
|
193
|
+
function obj(value: unknown): Record<string, unknown> {
|
|
194
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
195
|
+
? (value as Record<string, unknown>)
|
|
196
|
+
: {};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function stringArray(value: unknown): string[] {
|
|
200
|
+
return Array.isArray(value)
|
|
201
|
+
? value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
|
|
202
|
+
: [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function codexAgentState(value: unknown): CodexBackgroundTaskState {
|
|
206
|
+
const state = obj(value);
|
|
207
|
+
const nativeStatus = str(state, "status");
|
|
208
|
+
const summary = str(state, "message") || null;
|
|
209
|
+
if (nativeStatus === "completed") return { terminal: true, status: "completed", summary };
|
|
210
|
+
if (nativeStatus === "errored" || nativeStatus === "notFound") {
|
|
211
|
+
return { terminal: true, status: "failed", summary };
|
|
212
|
+
}
|
|
213
|
+
if (nativeStatus === "interrupted" || nativeStatus === "shutdown") {
|
|
214
|
+
return { terminal: true, status: "stopped", summary };
|
|
215
|
+
}
|
|
216
|
+
return { terminal: false, status: "running", summary };
|
|
217
|
+
}
|
|
218
|
+
|
|
166
219
|
/** Discriminated incoming message from the Codex CLI. */
|
|
167
220
|
type IncomingMessage =
|
|
168
221
|
| { kind: "response"; id: number; result?: Record<string, unknown>; error?: { code: number; message: string } }
|
|
@@ -342,6 +395,19 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
342
395
|
/** Shared promise so concurrent / repeated `drain()` calls coalesce. */
|
|
343
396
|
private _drainPromise: Promise<void> | null = null;
|
|
344
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Root turn targeted by `interrupt()`. The id is learned asynchronously from
|
|
400
|
+
* the leader `turn/start` response or the root `turn/started` notification.
|
|
401
|
+
* Concurrent sends reuse this latch so a queued response cannot replace the
|
|
402
|
+
* actual active turn.
|
|
403
|
+
*/
|
|
404
|
+
private _activeTurnId: string | null = null;
|
|
405
|
+
private _activeTurnReady: ActiveTurnReady | null = null;
|
|
406
|
+
/** Successful repeated interrupts coalesce until the terminal notification. */
|
|
407
|
+
private _interruptPromise: Promise<void> | null = null;
|
|
408
|
+
/** Prevents a late interrupt request after the terminal frame was observed. */
|
|
409
|
+
private _turnTerminalObserved = false;
|
|
410
|
+
|
|
345
411
|
/** Stamps `tool_result.toolName` by correlating with prior `tool_call`s. */
|
|
346
412
|
private readonly _trackToolName = createToolNameTracker();
|
|
347
413
|
|
|
@@ -351,9 +417,24 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
351
417
|
private _turnUsage: { inputTokens: number; outputTokens: number } | null = null;
|
|
352
418
|
private _turnModel: string | null = null;
|
|
353
419
|
private _turnIsError = false;
|
|
420
|
+
private _turnWasInterrupted = false;
|
|
354
421
|
private _turnErrorMessage: string | null = null;
|
|
355
422
|
private _turnStartedAt: Date | null = null;
|
|
356
423
|
|
|
424
|
+
/** Child-agent lifecycle is informational and never participates in root turn settlement. */
|
|
425
|
+
private readonly _backgroundTasks = new Map<string, TrackedBackgroundTask>();
|
|
426
|
+
private readonly _backgroundTaskIdsByPath = new Map<string, string>();
|
|
427
|
+
/** One protocol-native thread/read poller per child when Codex does not multiplex its notifications. */
|
|
428
|
+
private readonly _backgroundTaskPollers = new Map<string, {
|
|
429
|
+
generation: number;
|
|
430
|
+
promise: Promise<void>;
|
|
431
|
+
}>();
|
|
432
|
+
/** Cancellable retry delay for each poller. Cleared when the session closes. */
|
|
433
|
+
private readonly _backgroundTaskPollTimers = new Map<string, {
|
|
434
|
+
timer: ReturnType<typeof setTimeout>;
|
|
435
|
+
resolve: () => void;
|
|
436
|
+
}>();
|
|
437
|
+
|
|
357
438
|
/**
|
|
358
439
|
* Serial dispatch chain for `onEvent`. Each dispatched event appends a
|
|
359
440
|
* handler invocation; the chain enforces in-order delivery and lets
|
|
@@ -422,7 +503,14 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
422
503
|
proc.on("exit", (code, signal) => {
|
|
423
504
|
if (this._state !== "closed") {
|
|
424
505
|
this._state = "closed";
|
|
425
|
-
const
|
|
506
|
+
const message = `Codex process exited unexpectedly (code=${code}, signal=${signal})`;
|
|
507
|
+
this.finalizeActiveBackgroundTasks("failed", message, {
|
|
508
|
+
method: "process/exit",
|
|
509
|
+
code,
|
|
510
|
+
signal,
|
|
511
|
+
});
|
|
512
|
+
this.cancelBackgroundTaskPollTimers();
|
|
513
|
+
const err = new Error(message);
|
|
426
514
|
this.rejectAllPending(err);
|
|
427
515
|
}
|
|
428
516
|
});
|
|
@@ -430,11 +518,43 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
430
518
|
proc.on("error", (err) => {
|
|
431
519
|
if (this._state !== "closed") {
|
|
432
520
|
this._state = "closed";
|
|
521
|
+
this.finalizeActiveBackgroundTasks("failed", err.message, {
|
|
522
|
+
method: "process/error",
|
|
523
|
+
message: err.message,
|
|
524
|
+
});
|
|
525
|
+
this.cancelBackgroundTaskPollTimers();
|
|
433
526
|
this.rejectAllPending(err);
|
|
434
527
|
}
|
|
435
528
|
});
|
|
436
529
|
}
|
|
437
530
|
|
|
531
|
+
private cancelBackgroundTaskPollTimers(): void {
|
|
532
|
+
for (const pending of this._backgroundTaskPollTimers.values()) {
|
|
533
|
+
clearTimeout(pending.timer);
|
|
534
|
+
pending.resolve();
|
|
535
|
+
}
|
|
536
|
+
this._backgroundTaskPollTimers.clear();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private finalizeActiveBackgroundTasks(
|
|
540
|
+
status: "failed" | "stopped",
|
|
541
|
+
summary: string,
|
|
542
|
+
raw: Record<string, unknown>,
|
|
543
|
+
): void {
|
|
544
|
+
for (const task of this._backgroundTasks.values()) {
|
|
545
|
+
if (task.terminal) continue;
|
|
546
|
+
this.dispatchEvent(this.backgroundTaskEvent(task.taskId, "completed", status, {
|
|
547
|
+
description: task.description,
|
|
548
|
+
summary,
|
|
549
|
+
parentTaskId: task.parentTaskId,
|
|
550
|
+
eventId: this._threadId
|
|
551
|
+
? `codex:${this._threadId}:background-task:${task.taskId}:session-${status}:completed`
|
|
552
|
+
: null,
|
|
553
|
+
raw,
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
438
558
|
/** Reject every pending send() Promise and outgoing JSON-RPC call. */
|
|
439
559
|
private rejectAllPending(err: Error): void {
|
|
440
560
|
const pending = this._pendingResults.splice(0);
|
|
@@ -446,11 +566,78 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
446
566
|
}
|
|
447
567
|
for (const [, p] of this._pendingRpc) p.reject(err);
|
|
448
568
|
this._pendingRpc.clear();
|
|
569
|
+
this.clearActiveTurn(err);
|
|
449
570
|
}
|
|
450
571
|
|
|
451
572
|
get sessionId(): string | null { return this._threadId; }
|
|
452
573
|
get state(): SessionState { return this._state; }
|
|
453
574
|
|
|
575
|
+
/** Start the identity latch before writing the leader `turn/start` request. */
|
|
576
|
+
private beginActiveTurn(): ActiveTurnReady {
|
|
577
|
+
let resolveFn!: (turnId: string | null) => void;
|
|
578
|
+
let rejectFn!: (err: Error) => void;
|
|
579
|
+
const promise = new Promise<string | null>((resolve, reject) => {
|
|
580
|
+
resolveFn = resolve;
|
|
581
|
+
rejectFn = reject;
|
|
582
|
+
});
|
|
583
|
+
// A turn can finish without anyone calling interrupt(). Keep a later
|
|
584
|
+
// process-exit rejection from becoming an unhandled promise rejection.
|
|
585
|
+
void promise.catch(() => {});
|
|
586
|
+
|
|
587
|
+
const ready: ActiveTurnReady = {
|
|
588
|
+
promise,
|
|
589
|
+
resolve: resolveFn,
|
|
590
|
+
reject: rejectFn,
|
|
591
|
+
settled: false,
|
|
592
|
+
};
|
|
593
|
+
this._activeTurnId = null;
|
|
594
|
+
this._activeTurnReady = ready;
|
|
595
|
+
this._interruptPromise = null;
|
|
596
|
+
this._turnTerminalObserved = false;
|
|
597
|
+
this._turnWasInterrupted = false;
|
|
598
|
+
return ready;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** First root turn id wins for the current latch. */
|
|
602
|
+
private captureActiveTurnId(turnId: string, expected?: ActiveTurnReady): void {
|
|
603
|
+
const ready = this._activeTurnReady;
|
|
604
|
+
if (!turnId || !ready || ready.settled) return;
|
|
605
|
+
if (expected && ready !== expected) return;
|
|
606
|
+
this._activeTurnId = turnId;
|
|
607
|
+
ready.settled = true;
|
|
608
|
+
ready.resolve(turnId);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private rejectActiveTurnReady(err: Error, expected: ActiveTurnReady): void {
|
|
612
|
+
if (this._activeTurnReady !== expected || expected.settled) return;
|
|
613
|
+
expected.settled = true;
|
|
614
|
+
expected.reject(err);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Clear the current turn and release an interrupt waiting for its id. */
|
|
618
|
+
private clearActiveTurn(err?: Error): void {
|
|
619
|
+
const ready = this._activeTurnReady;
|
|
620
|
+
if (ready && !ready.settled) {
|
|
621
|
+
ready.settled = true;
|
|
622
|
+
if (err) ready.reject(err);
|
|
623
|
+
else ready.resolve(null);
|
|
624
|
+
}
|
|
625
|
+
this._activeTurnId = null;
|
|
626
|
+
this._activeTurnReady = null;
|
|
627
|
+
this._interruptPromise = null;
|
|
628
|
+
this._turnTerminalObserved = false;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** Mark root turn termination and release an interrupt still awaiting its id. */
|
|
632
|
+
private markTurnTerminalObserved(): void {
|
|
633
|
+
this._turnTerminalObserved = true;
|
|
634
|
+
const ready = this._activeTurnReady;
|
|
635
|
+
if (ready && !ready.settled) {
|
|
636
|
+
ready.settled = true;
|
|
637
|
+
ready.resolve(null);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
454
641
|
/**
|
|
455
642
|
* Durable identity for persistence + later `attachSession`. Null until Codex
|
|
456
643
|
* has assigned a thread id; serializes `{sessionId, cwd}` through the codec so
|
|
@@ -483,6 +670,35 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
483
670
|
});
|
|
484
671
|
}
|
|
485
672
|
|
|
673
|
+
/** A bounded RPC whose pending-map entry is removed if Codex never replies. */
|
|
674
|
+
private boundedRpcRequest(
|
|
675
|
+
method: string,
|
|
676
|
+
params: Record<string, unknown>,
|
|
677
|
+
timeoutMs = BACKGROUND_TASK_READ_TIMEOUT_MS,
|
|
678
|
+
): Promise<Record<string, unknown>> {
|
|
679
|
+
const id = this._nextId++;
|
|
680
|
+
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
681
|
+
|
|
682
|
+
return new Promise((resolve, reject) => {
|
|
683
|
+
const timer = setTimeout(() => {
|
|
684
|
+
if (!this._pendingRpc.delete(id)) return;
|
|
685
|
+
reject(new Error(`codex ${method} timed out`));
|
|
686
|
+
}, timeoutMs);
|
|
687
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
688
|
+
|
|
689
|
+
this._pendingRpc.set(id, {
|
|
690
|
+
resolve: (result) => {
|
|
691
|
+
clearTimeout(timer);
|
|
692
|
+
resolve(result);
|
|
693
|
+
},
|
|
694
|
+
reject: (err) => {
|
|
695
|
+
clearTimeout(timer);
|
|
696
|
+
reject(err);
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
486
702
|
/**
|
|
487
703
|
* Bounded RPC for experimental, best-effort methods (the `thread/goal/*`
|
|
488
704
|
* family). An app-server build that doesn't recognize the method may never
|
|
@@ -618,6 +834,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
618
834
|
// and pass through. If the second `turn/start` lands during the first
|
|
619
835
|
// turn, the per-turn accumulators continue collecting until the result
|
|
620
836
|
// event fires; the result then drains all pending resolvers.
|
|
837
|
+
const existingTurnReady = this._activeTurnReady;
|
|
838
|
+
const isTurnLeader = existingTurnReady === null;
|
|
839
|
+
const turnReady = existingTurnReady ?? this.beginActiveTurn();
|
|
840
|
+
|
|
621
841
|
if (this._state === "idle") {
|
|
622
842
|
this._state = "thinking";
|
|
623
843
|
this._turnStartedAt = new Date();
|
|
@@ -659,16 +879,34 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
659
879
|
// ProviderConfig.timeoutSec default when no per-call timeout is given.
|
|
660
880
|
this.armSendDeadline(entry, options);
|
|
661
881
|
|
|
662
|
-
this.rpcRequest("turn/start", turnParams)
|
|
663
|
-
|
|
664
|
-
|
|
882
|
+
const turnStart = this.rpcRequest("turn/start", turnParams);
|
|
883
|
+
if (isTurnLeader) {
|
|
884
|
+
void turnStart.then((response) => {
|
|
885
|
+
const turnId = str(asObj(response, "turn"), "id");
|
|
886
|
+
if (turnId) {
|
|
887
|
+
this.captureActiveTurnId(turnId, turnReady);
|
|
888
|
+
}
|
|
889
|
+
// Some app-server versions may omit the id from the response and send
|
|
890
|
+
// it only in turn/started. Keep the latch open for that notification.
|
|
891
|
+
}).catch((err: unknown) => {
|
|
892
|
+
this.rejectActiveTurnReady(
|
|
893
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
894
|
+
turnReady,
|
|
895
|
+
);
|
|
896
|
+
// Turn-level failures may also arrive via turn/failed notifications.
|
|
897
|
+
});
|
|
898
|
+
} else {
|
|
899
|
+
void turnStart.catch(() => {
|
|
900
|
+
// Turn-level failures arrive via turn/failed notifications.
|
|
901
|
+
});
|
|
902
|
+
}
|
|
665
903
|
|
|
666
904
|
return { uuid, result };
|
|
667
905
|
}
|
|
668
906
|
|
|
669
907
|
/**
|
|
670
908
|
* Wire up this send's timeout and/or abort signal. On fire, the active turn
|
|
671
|
-
* is
|
|
909
|
+
* is interrupted (`turn/interrupt`) and the send settles with `timeout` /
|
|
672
910
|
* `aborted`. No-op when neither a timeout nor a signal applies.
|
|
673
911
|
*/
|
|
674
912
|
private armSendDeadline(entry: PendingResult, options?: SendOptions): void {
|
|
@@ -711,7 +949,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
711
949
|
|
|
712
950
|
// Best-effort cancel of the active turn. With concurrent sends this ends
|
|
713
951
|
// the single shared turn for all of them — see SendOptions JSDoc.
|
|
714
|
-
void this.interrupt();
|
|
952
|
+
void this.interrupt().catch(() => {});
|
|
715
953
|
|
|
716
954
|
entry.resolve({
|
|
717
955
|
summary: null,
|
|
@@ -727,7 +965,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
727
965
|
|
|
728
966
|
async cancel(_uuid: string): Promise<CancelResult> {
|
|
729
967
|
// Codex's JSON-RPC protocol exposes no per-message cancel — only
|
|
730
|
-
// turn-wide `turn/
|
|
968
|
+
// turn-wide `turn/interrupt` (which is what `interrupt()` calls).
|
|
731
969
|
// capabilities.cancelQueuedMessage is false; this is a documented no-op.
|
|
732
970
|
return { cancelled: false };
|
|
733
971
|
}
|
|
@@ -780,11 +1018,37 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
780
1018
|
}
|
|
781
1019
|
|
|
782
1020
|
async interrupt(): Promise<void> {
|
|
783
|
-
if (this._state === "
|
|
1021
|
+
if (this._state === "closed") return;
|
|
1022
|
+
const ready = this._activeTurnReady;
|
|
1023
|
+
if (!ready || this._turnTerminalObserved) return;
|
|
1024
|
+
if (this._interruptPromise) return this._interruptPromise;
|
|
1025
|
+
|
|
1026
|
+
const threadId = this._threadId;
|
|
1027
|
+
if (!threadId) {
|
|
1028
|
+
throw new Error("Cannot interrupt Codex turn before the root thread id is known");
|
|
1029
|
+
}
|
|
784
1030
|
this._goals.notifyInterrupted(); // don't let an emulated goal auto-continue
|
|
1031
|
+
|
|
1032
|
+
const interruptPromise = (async () => {
|
|
1033
|
+
const turnId = this._activeTurnId ?? await ready.promise;
|
|
1034
|
+
// The turn may have completed while interrupt() was waiting for the
|
|
1035
|
+
// leader turn/start response. In that race, completion is the success.
|
|
1036
|
+
if (!turnId || this._activeTurnReady !== ready || this._turnTerminalObserved) return;
|
|
1037
|
+
await this.rpcRequest("turn/interrupt", { threadId, turnId });
|
|
1038
|
+
})();
|
|
1039
|
+
this._interruptPromise = interruptPromise;
|
|
1040
|
+
|
|
785
1041
|
try {
|
|
786
|
-
await
|
|
787
|
-
} catch {
|
|
1042
|
+
await interruptPromise;
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
// A rejected control request must reach the host instead of becoming a
|
|
1045
|
+
// false successful Stop. Clear only this turn's failed attempt so a
|
|
1046
|
+
// subsequent click can retry.
|
|
1047
|
+
if (this._activeTurnReady === ready && this._interruptPromise === interruptPromise) {
|
|
1048
|
+
this._interruptPromise = null;
|
|
1049
|
+
}
|
|
1050
|
+
throw err;
|
|
1051
|
+
}
|
|
788
1052
|
}
|
|
789
1053
|
|
|
790
1054
|
async drain(): Promise<void> {
|
|
@@ -804,6 +1068,11 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
804
1068
|
async close(): Promise<void> {
|
|
805
1069
|
if (this._state === "closed") return;
|
|
806
1070
|
this._state = "closed";
|
|
1071
|
+
this.finalizeActiveBackgroundTasks("stopped", "Codex session closed", {
|
|
1072
|
+
method: "session/closed",
|
|
1073
|
+
});
|
|
1074
|
+
this.cancelBackgroundTaskPollTimers();
|
|
1075
|
+
this.rejectAllPending(new Error("Codex session closed"));
|
|
807
1076
|
|
|
808
1077
|
this.proc.stdin!.end();
|
|
809
1078
|
|
|
@@ -1003,7 +1272,494 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1003
1272
|
return !!threadId && !!rootThreadId && threadId !== rootThreadId;
|
|
1004
1273
|
}
|
|
1005
1274
|
|
|
1275
|
+
private backgroundTaskParentIdForPath(agentPath: string | null): string | null {
|
|
1276
|
+
if (!agentPath) return null;
|
|
1277
|
+
const separator = agentPath.lastIndexOf("/");
|
|
1278
|
+
if (separator <= 0) return null;
|
|
1279
|
+
return this._backgroundTaskIdsByPath.get(agentPath.slice(0, separator)) ?? null;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
private agentMessageText(item: Record<string, unknown>): string | null {
|
|
1283
|
+
const direct = str(item, "text");
|
|
1284
|
+
if (direct) return direct;
|
|
1285
|
+
const content = Array.isArray(item["content"]) ? item["content"] : [];
|
|
1286
|
+
for (const entry of content) {
|
|
1287
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
1288
|
+
const block = entry as Record<string, unknown>;
|
|
1289
|
+
const text = str(block, "text");
|
|
1290
|
+
if (text && (str(block, "type") === "output_text" || str(block, "type") === "text")) {
|
|
1291
|
+
return text;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private backgroundTaskEvent(
|
|
1298
|
+
taskId: string,
|
|
1299
|
+
phase: "started" | "progress" | "completed",
|
|
1300
|
+
status: "running" | "completed" | "failed" | "stopped",
|
|
1301
|
+
options: {
|
|
1302
|
+
description?: string | null;
|
|
1303
|
+
summary?: string | null;
|
|
1304
|
+
parentTaskId?: string | null;
|
|
1305
|
+
turnId?: string | null;
|
|
1306
|
+
eventId?: string | null;
|
|
1307
|
+
raw: Record<string, unknown>;
|
|
1308
|
+
},
|
|
1309
|
+
): Extract<StreamEvent, { type: "background_task" }> {
|
|
1310
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1311
|
+
return {
|
|
1312
|
+
type: "background_task",
|
|
1313
|
+
taskId,
|
|
1314
|
+
taskType: "subagent",
|
|
1315
|
+
phase,
|
|
1316
|
+
status,
|
|
1317
|
+
description: options.description ?? null,
|
|
1318
|
+
summary: options.summary ?? null,
|
|
1319
|
+
parentTaskId: options.parentTaskId ?? null,
|
|
1320
|
+
timestamp: new Date().toISOString(),
|
|
1321
|
+
providerType: "codex",
|
|
1322
|
+
sessionId: rootThreadId,
|
|
1323
|
+
messageId: null,
|
|
1324
|
+
eventId: options.eventId ?? null,
|
|
1325
|
+
turnId: options.turnId ?? null,
|
|
1326
|
+
parentToolCallId: null,
|
|
1327
|
+
raw: options.raw,
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
private reconciledBackgroundTaskRaw(
|
|
1332
|
+
threadId: string,
|
|
1333
|
+
turn: Record<string, unknown> | null,
|
|
1334
|
+
): Record<string, unknown> {
|
|
1335
|
+
const error = turn ? str(asObj(turn, "error"), "message") || null : null;
|
|
1336
|
+
return {
|
|
1337
|
+
method: "thread/read",
|
|
1338
|
+
reconciled: true,
|
|
1339
|
+
threadId,
|
|
1340
|
+
turnId: turn ? str(turn, "id") || null : null,
|
|
1341
|
+
status: turn ? str(turn, "status") || null : null,
|
|
1342
|
+
error,
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Codex 0.144+ represents collaboration as a root collabAgentToolCall item.
|
|
1348
|
+
* A completed spawn call only means the child was created. Register every
|
|
1349
|
+
* receiver and then follow the child thread itself for the real terminus.
|
|
1350
|
+
*/
|
|
1351
|
+
private handleCollabAgentToolCall(
|
|
1352
|
+
item: Record<string, unknown>,
|
|
1353
|
+
raw: Record<string, unknown>,
|
|
1354
|
+
parentTaskId: string | null,
|
|
1355
|
+
): void {
|
|
1356
|
+
if (this._state === "closed" || !this.ctx.onEvent) return;
|
|
1357
|
+
|
|
1358
|
+
const tool = str(item, "tool");
|
|
1359
|
+
const states = asObj(item, "agentsStates");
|
|
1360
|
+
const receiverIds = stringArray(item["receiverThreadIds"]);
|
|
1361
|
+
const taskIds = [...new Set([...receiverIds, ...Object.keys(states)])];
|
|
1362
|
+
const canStart = tool === "spawnAgent";
|
|
1363
|
+
const metadataOnly = tool === "resumeAgent" || tool === "sendInput";
|
|
1364
|
+
const canStopWithoutState = tool === "closeAgent" && str(item, "status") === "completed";
|
|
1365
|
+
|
|
1366
|
+
for (const taskId of taskIds) {
|
|
1367
|
+
let previous = this._backgroundTasks.get(taskId);
|
|
1368
|
+
const description = str(item, "prompt") || previous?.description || null;
|
|
1369
|
+
const resolvedParentTaskId = previous?.parentTaskId ?? parentTaskId;
|
|
1370
|
+
|
|
1371
|
+
// The child thread's own turn/started notification is the only
|
|
1372
|
+
// authoritative reactivation edge. Root resume/send calls can arrive
|
|
1373
|
+
// before or after that child turn, so they only enrich an active task.
|
|
1374
|
+
if (metadataOnly) {
|
|
1375
|
+
if (!previous || previous.terminal) continue;
|
|
1376
|
+
if (description && description !== previous.description) {
|
|
1377
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "progress", "running", {
|
|
1378
|
+
description,
|
|
1379
|
+
summary: previous.summary,
|
|
1380
|
+
parentTaskId: resolvedParentTaskId,
|
|
1381
|
+
eventId: this._threadId
|
|
1382
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "metadata"}:progress`
|
|
1383
|
+
: null,
|
|
1384
|
+
raw,
|
|
1385
|
+
}));
|
|
1386
|
+
}
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (!previous && !canStart) continue;
|
|
1391
|
+
if (!canStart && !canStopWithoutState && !(taskId in states)) continue;
|
|
1392
|
+
|
|
1393
|
+
const hasState = taskId in states;
|
|
1394
|
+
const state = !hasState && tool === "closeAgent" && str(item, "status") === "completed"
|
|
1395
|
+
? { terminal: true, status: "stopped" as const, summary: null }
|
|
1396
|
+
: codexAgentState(states[taskId]);
|
|
1397
|
+
|
|
1398
|
+
if (!previous) {
|
|
1399
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "started", "running", {
|
|
1400
|
+
description,
|
|
1401
|
+
summary: null,
|
|
1402
|
+
parentTaskId: resolvedParentTaskId,
|
|
1403
|
+
eventId: this._threadId
|
|
1404
|
+
? `codex:${this._threadId}:background-task:${taskId}:started`
|
|
1405
|
+
: null,
|
|
1406
|
+
raw,
|
|
1407
|
+
}));
|
|
1408
|
+
previous = this._backgroundTasks.get(taskId);
|
|
1409
|
+
} else if (previous.terminal) {
|
|
1410
|
+
continue;
|
|
1411
|
+
} else if (description && description !== previous.description) {
|
|
1412
|
+
// thread/started can beat the richer root collab item. Preserve one
|
|
1413
|
+
// start edge, then publish the prompt as ordinary progress metadata.
|
|
1414
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "progress", "running", {
|
|
1415
|
+
description,
|
|
1416
|
+
summary: previous.summary,
|
|
1417
|
+
parentTaskId: resolvedParentTaskId,
|
|
1418
|
+
eventId: this._threadId
|
|
1419
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "metadata"}:progress`
|
|
1420
|
+
: null,
|
|
1421
|
+
raw,
|
|
1422
|
+
}));
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (state.terminal) {
|
|
1426
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", state.status, {
|
|
1427
|
+
description,
|
|
1428
|
+
summary: state.summary,
|
|
1429
|
+
parentTaskId: resolvedParentTaskId,
|
|
1430
|
+
eventId: this._threadId
|
|
1431
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "state"}:completed`
|
|
1432
|
+
: null,
|
|
1433
|
+
raw,
|
|
1434
|
+
}));
|
|
1435
|
+
} else {
|
|
1436
|
+
this.startBackgroundTaskPoller(taskId);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
private waitForBackgroundTaskPoll(pollKey: string): Promise<void> {
|
|
1442
|
+
if (this._state === "closed") return Promise.resolve();
|
|
1443
|
+
return new Promise((resolve) => {
|
|
1444
|
+
const timer = setTimeout(() => {
|
|
1445
|
+
this._backgroundTaskPollTimers.delete(pollKey);
|
|
1446
|
+
resolve();
|
|
1447
|
+
}, BACKGROUND_TASK_POLL_INTERVAL_MS);
|
|
1448
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1449
|
+
this._backgroundTaskPollTimers.set(pollKey, { timer, resolve });
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
private startBackgroundTaskPoller(taskId: string): void {
|
|
1454
|
+
if (this._state === "closed") return;
|
|
1455
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1456
|
+
if (!task || task.terminal) return;
|
|
1457
|
+
const existing = this._backgroundTaskPollers.get(taskId);
|
|
1458
|
+
if (existing?.generation === task.generation) return;
|
|
1459
|
+
|
|
1460
|
+
const generation = task.generation;
|
|
1461
|
+
const pollKey = `${taskId}:${generation}`;
|
|
1462
|
+
let poller!: Promise<void>;
|
|
1463
|
+
poller = this.pollBackgroundTask(taskId, generation, pollKey).finally(() => {
|
|
1464
|
+
if (this._backgroundTaskPollers.get(taskId)?.promise === poller) {
|
|
1465
|
+
this._backgroundTaskPollers.delete(taskId);
|
|
1466
|
+
}
|
|
1467
|
+
const pending = this._backgroundTaskPollTimers.get(pollKey);
|
|
1468
|
+
if (pending) clearTimeout(pending.timer);
|
|
1469
|
+
this._backgroundTaskPollTimers.delete(pollKey);
|
|
1470
|
+
});
|
|
1471
|
+
this._backgroundTaskPollers.set(taskId, { generation, promise: poller });
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
private async pollBackgroundTask(
|
|
1475
|
+
taskId: string,
|
|
1476
|
+
generation: number,
|
|
1477
|
+
pollKey: string,
|
|
1478
|
+
): Promise<void> {
|
|
1479
|
+
while (this._state !== "closed") {
|
|
1480
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1481
|
+
if (!task || task.generation !== generation || task.terminal) return;
|
|
1482
|
+
|
|
1483
|
+
try {
|
|
1484
|
+
const response = await this.boundedRpcRequest("thread/read", {
|
|
1485
|
+
threadId: taskId,
|
|
1486
|
+
includeTurns: true,
|
|
1487
|
+
});
|
|
1488
|
+
if (this.observeBackgroundTaskThread(response, taskId, generation)) return;
|
|
1489
|
+
} catch {
|
|
1490
|
+
// A child can briefly be pendingInit before thread/read can load it.
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const current = this._backgroundTasks.get(taskId);
|
|
1494
|
+
if (!current || current.generation !== generation || current.terminal) return;
|
|
1495
|
+
await this.waitForBackgroundTaskPoll(pollKey);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/** Return true once thread/read proves that the child's latest turn ended. */
|
|
1500
|
+
private observeBackgroundTaskThread(
|
|
1501
|
+
response: Record<string, unknown>,
|
|
1502
|
+
taskId: string,
|
|
1503
|
+
generation: number,
|
|
1504
|
+
): boolean {
|
|
1505
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1506
|
+
if (!task || task.generation !== generation || task.terminal) return true;
|
|
1507
|
+
|
|
1508
|
+
const thread = asObj(response, "thread");
|
|
1509
|
+
const turns = Array.isArray(thread["turns"])
|
|
1510
|
+
? thread["turns"].map(obj)
|
|
1511
|
+
: [];
|
|
1512
|
+
|
|
1513
|
+
const latestTurn = turns[turns.length - 1];
|
|
1514
|
+
if (!latestTurn) {
|
|
1515
|
+
const threadStatus = str(asObj(thread, "status"), "type");
|
|
1516
|
+
if (threadStatus !== "systemError") return false;
|
|
1517
|
+
const failureTurn = { status: "systemError", error: { message: "Subagent failed to initialize" } };
|
|
1518
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", "failed", {
|
|
1519
|
+
description: task.description,
|
|
1520
|
+
summary: "Subagent failed to initialize",
|
|
1521
|
+
parentTaskId: task.parentTaskId,
|
|
1522
|
+
eventId: this._threadId
|
|
1523
|
+
? `codex:${this._threadId}:background-task:${taskId}:system-error:completed`
|
|
1524
|
+
: null,
|
|
1525
|
+
raw: this.reconciledBackgroundTaskRaw(taskId, failureTurn),
|
|
1526
|
+
}));
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const nativeStatus = str(latestTurn, "status");
|
|
1531
|
+
if (nativeStatus === "inProgress" || !nativeStatus) return false;
|
|
1532
|
+
|
|
1533
|
+
const turnId = str(latestTurn, "id") || null;
|
|
1534
|
+
// The child turn/started notification is authoritative for a reactivated
|
|
1535
|
+
// generation. thread/read can briefly lag and still end at an older turn.
|
|
1536
|
+
if (task.activeTurnId && turnId !== task.activeTurnId) return false;
|
|
1537
|
+
|
|
1538
|
+
const items = Array.isArray(latestTurn["items"])
|
|
1539
|
+
? latestTurn["items"].map(obj)
|
|
1540
|
+
: [];
|
|
1541
|
+
let summary: string | null = null;
|
|
1542
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
1543
|
+
const item = items[index]!;
|
|
1544
|
+
if (str(item, "type") !== "agentMessage") continue;
|
|
1545
|
+
summary = this.agentMessageText(item);
|
|
1546
|
+
if (summary) break;
|
|
1547
|
+
}
|
|
1548
|
+
const errorMessage = str(asObj(latestTurn, "error"), "message") || null;
|
|
1549
|
+
const status = nativeStatus === "failed"
|
|
1550
|
+
? "failed"
|
|
1551
|
+
: nativeStatus === "interrupted" || nativeStatus === "cancelled"
|
|
1552
|
+
? "stopped"
|
|
1553
|
+
: "completed";
|
|
1554
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", status, {
|
|
1555
|
+
description: task.description,
|
|
1556
|
+
summary: summary ?? errorMessage,
|
|
1557
|
+
parentTaskId: task.parentTaskId,
|
|
1558
|
+
turnId,
|
|
1559
|
+
eventId: this._threadId && turnId
|
|
1560
|
+
? `codex:${this._threadId}:background-task:${taskId}:${turnId}:completed`
|
|
1561
|
+
: null,
|
|
1562
|
+
raw: this.reconciledBackgroundTaskRaw(taskId, latestTurn),
|
|
1563
|
+
}));
|
|
1564
|
+
return true;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* Maintain just enough child metadata to turn a later foreign-thread
|
|
1569
|
+
* terminal notification into one provider-neutral task event. This reducer
|
|
1570
|
+
* is deliberately separate from every root turn accumulator.
|
|
1571
|
+
*/
|
|
1572
|
+
private observeBackgroundTask(event: Extract<StreamEvent, { type: "background_task" }>): boolean {
|
|
1573
|
+
const previous = this._backgroundTasks.get(event.taskId);
|
|
1574
|
+
// Codex reports `subAgentActivity:interacted` after it forwards a child's
|
|
1575
|
+
// final answer to the parent. The authoritative child turn/completed can
|
|
1576
|
+
// arrive first, so suppress that late progress edge instead of resurrecting
|
|
1577
|
+
// a task that already reached a terminal state. A later child turn/started
|
|
1578
|
+
// explicitly reactivates the record below in handleForeignNotification.
|
|
1579
|
+
if (previous?.terminal) return false;
|
|
1580
|
+
// collabAgentToolCall and child thread/started can both announce the same
|
|
1581
|
+
// spawn. The first edge is sufficient and gives hosts one durable start.
|
|
1582
|
+
if (previous && event.phase === "started") return false;
|
|
1583
|
+
|
|
1584
|
+
const description = event.description ?? previous?.description ?? null;
|
|
1585
|
+
const summary = event.summary ?? previous?.summary ?? null;
|
|
1586
|
+
const parentTaskId = event.parentTaskId
|
|
1587
|
+
?? previous?.parentTaskId
|
|
1588
|
+
?? this.backgroundTaskParentIdForPath(description);
|
|
1589
|
+
|
|
1590
|
+
event.description = description;
|
|
1591
|
+
event.summary = summary;
|
|
1592
|
+
event.parentTaskId = parentTaskId;
|
|
1593
|
+
|
|
1594
|
+
this._backgroundTasks.set(event.taskId, {
|
|
1595
|
+
taskId: event.taskId,
|
|
1596
|
+
description,
|
|
1597
|
+
summary,
|
|
1598
|
+
parentTaskId,
|
|
1599
|
+
terminal: event.phase === "completed",
|
|
1600
|
+
generation: previous?.generation ?? 0,
|
|
1601
|
+
activeTurnId: event.phase === "completed"
|
|
1602
|
+
? null
|
|
1603
|
+
: event.turnId ?? previous?.activeTurnId ?? null,
|
|
1604
|
+
});
|
|
1605
|
+
if (description) this._backgroundTaskIdsByPath.set(description, event.taskId);
|
|
1606
|
+
return true;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* A Codex app-server connection also publishes child thread notifications.
|
|
1611
|
+
* They are useful only as background-task metadata. They must never flow
|
|
1612
|
+
* through root state, summary, usage, or `resolveTurn()`.
|
|
1613
|
+
*/
|
|
1614
|
+
private handleForeignNotification(
|
|
1615
|
+
method: string,
|
|
1616
|
+
params: Record<string, unknown>,
|
|
1617
|
+
rawLine: string,
|
|
1618
|
+
childThreadId: string,
|
|
1619
|
+
): void {
|
|
1620
|
+
if (method === "thread/started") {
|
|
1621
|
+
const thread = asObj(params, "thread");
|
|
1622
|
+
const parentThreadId = str(thread, "parentThreadId") || str(thread, "parent_thread_id");
|
|
1623
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1624
|
+
const parentTask = this._backgroundTasks.get(parentThreadId);
|
|
1625
|
+
// App-server can publish child thread/started before the corresponding
|
|
1626
|
+
// root subAgentActivity item. Register only descendants of this session,
|
|
1627
|
+
// not unrelated foreign threads multiplexed by a future server version.
|
|
1628
|
+
if (parentThreadId !== rootThreadId && !parentTask) return;
|
|
1629
|
+
|
|
1630
|
+
const source = asObj(thread, "source");
|
|
1631
|
+
const subAgent = Object.keys(asObj(source, "subAgent")).length > 0
|
|
1632
|
+
? asObj(source, "subAgent")
|
|
1633
|
+
: asObj(source, "subagent");
|
|
1634
|
+
const spawnSource = Object.keys(asObj(subAgent, "threadSpawn")).length > 0
|
|
1635
|
+
? asObj(subAgent, "threadSpawn")
|
|
1636
|
+
: asObj(subAgent, "thread_spawn");
|
|
1637
|
+
const description = str(spawnSource, "agentPath")
|
|
1638
|
+
|| str(spawnSource, "agent_path")
|
|
1639
|
+
|| str(thread, "name")
|
|
1640
|
+
|| str(thread, "agentNickname")
|
|
1641
|
+
|| str(thread, "agentRole")
|
|
1642
|
+
|| null;
|
|
1643
|
+
|
|
1644
|
+
this.dispatchEvent({
|
|
1645
|
+
type: "background_task",
|
|
1646
|
+
taskId: childThreadId,
|
|
1647
|
+
taskType: "subagent",
|
|
1648
|
+
phase: "started",
|
|
1649
|
+
status: "running",
|
|
1650
|
+
description,
|
|
1651
|
+
summary: null,
|
|
1652
|
+
parentTaskId: parentThreadId === rootThreadId ? null : parentThreadId,
|
|
1653
|
+
timestamp: new Date().toISOString(),
|
|
1654
|
+
providerType: "codex",
|
|
1655
|
+
sessionId: rootThreadId,
|
|
1656
|
+
messageId: null,
|
|
1657
|
+
eventId: rootThreadId
|
|
1658
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:started`
|
|
1659
|
+
: null,
|
|
1660
|
+
turnId: null,
|
|
1661
|
+
parentToolCallId: null,
|
|
1662
|
+
raw: parseJson(rawLine) ?? params,
|
|
1663
|
+
});
|
|
1664
|
+
this.startBackgroundTaskPoller(childThreadId);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const task = this._backgroundTasks.get(childThreadId);
|
|
1669
|
+
if (!task) return;
|
|
1670
|
+
|
|
1671
|
+
if (method === "turn/started") {
|
|
1672
|
+
if (!task.terminal) return;
|
|
1673
|
+
task.terminal = false;
|
|
1674
|
+
task.summary = null;
|
|
1675
|
+
task.generation += 1;
|
|
1676
|
+
const turn = asObj(params, "turn");
|
|
1677
|
+
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1678
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1679
|
+
this.dispatchEvent({
|
|
1680
|
+
type: "background_task",
|
|
1681
|
+
taskId: childThreadId,
|
|
1682
|
+
taskType: "subagent",
|
|
1683
|
+
phase: "progress",
|
|
1684
|
+
status: "running",
|
|
1685
|
+
description: task.description,
|
|
1686
|
+
summary: null,
|
|
1687
|
+
parentTaskId: task.parentTaskId,
|
|
1688
|
+
timestamp: new Date().toISOString(),
|
|
1689
|
+
providerType: "codex",
|
|
1690
|
+
sessionId: rootThreadId,
|
|
1691
|
+
messageId: null,
|
|
1692
|
+
eventId: rootThreadId && turnId
|
|
1693
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:${turnId}:progress`
|
|
1694
|
+
: null,
|
|
1695
|
+
turnId,
|
|
1696
|
+
parentToolCallId: null,
|
|
1697
|
+
raw: parseJson(rawLine) ?? params,
|
|
1698
|
+
});
|
|
1699
|
+
this.startBackgroundTaskPoller(childThreadId);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
if (method === "item/completed") {
|
|
1704
|
+
const item = asObj(params, "item");
|
|
1705
|
+
const itemType = str(item, "type");
|
|
1706
|
+
if (itemType === "collabAgentToolCall") {
|
|
1707
|
+
this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, childThreadId);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
if ((itemType === "agentMessage" || itemType === "agent_message") && str(item, "phase") !== "commentary") {
|
|
1711
|
+
const summary = this.agentMessageText(item);
|
|
1712
|
+
if (summary) task.summary = summary;
|
|
1713
|
+
}
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if (method !== "turn/completed" && method !== "turn/failed") return;
|
|
1718
|
+
|
|
1719
|
+
const turn = asObj(params, "turn");
|
|
1720
|
+
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1721
|
+
const nativeStatus = method === "turn/failed" ? "failed" : str(turn, "status");
|
|
1722
|
+
const status = nativeStatus === "failed"
|
|
1723
|
+
? "failed"
|
|
1724
|
+
: nativeStatus === "interrupted" || nativeStatus === "cancelled"
|
|
1725
|
+
? "stopped"
|
|
1726
|
+
: "completed";
|
|
1727
|
+
const errorMessage = str(asObj(turn, "error"), "message")
|
|
1728
|
+
|| str(params, "message")
|
|
1729
|
+
|| str(params, "error");
|
|
1730
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1731
|
+
|
|
1732
|
+
this.dispatchEvent({
|
|
1733
|
+
type: "background_task",
|
|
1734
|
+
taskId: childThreadId,
|
|
1735
|
+
taskType: "subagent",
|
|
1736
|
+
phase: "completed",
|
|
1737
|
+
status,
|
|
1738
|
+
description: task.description,
|
|
1739
|
+
summary: task.summary ?? (errorMessage || null),
|
|
1740
|
+
parentTaskId: task.parentTaskId,
|
|
1741
|
+
timestamp: new Date().toISOString(),
|
|
1742
|
+
providerType: "codex",
|
|
1743
|
+
sessionId: rootThreadId,
|
|
1744
|
+
messageId: null,
|
|
1745
|
+
eventId: rootThreadId && turnId
|
|
1746
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:${turnId}:completed`
|
|
1747
|
+
: null,
|
|
1748
|
+
turnId,
|
|
1749
|
+
parentToolCallId: null,
|
|
1750
|
+
raw: {
|
|
1751
|
+
method,
|
|
1752
|
+
reconciled: false,
|
|
1753
|
+
threadId: childThreadId,
|
|
1754
|
+
turnId,
|
|
1755
|
+
status: nativeStatus || null,
|
|
1756
|
+
error: errorMessage || null,
|
|
1757
|
+
},
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1006
1761
|
private handleNotification(method: string, params: Record<string, unknown>, rawLine: string): void {
|
|
1762
|
+
if (this._state === "closed") return;
|
|
1007
1763
|
// codex/event — legacy wrapper
|
|
1008
1764
|
if (method === "codex/event") {
|
|
1009
1765
|
const innerMsg = str(params, "msg");
|
|
@@ -1020,7 +1776,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1020
1776
|
// its root thread, so foreign items must not change root state/summary and,
|
|
1021
1777
|
// most importantly, a child turn/completed must not resolve the root send.
|
|
1022
1778
|
const notificationThreadId = this.notificationThreadId(params);
|
|
1023
|
-
if (this.isForeignThread(notificationThreadId))
|
|
1779
|
+
if (this.isForeignThread(notificationThreadId)) {
|
|
1780
|
+
this.handleForeignNotification(method, params, rawLine, notificationThreadId!);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1024
1783
|
|
|
1025
1784
|
// Map v2 notification methods to processing
|
|
1026
1785
|
if (method === "thread/started") {
|
|
@@ -1030,6 +1789,13 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1030
1789
|
return;
|
|
1031
1790
|
}
|
|
1032
1791
|
|
|
1792
|
+
if (method === "turn/started") {
|
|
1793
|
+
this.captureActiveTurnId(str(asObj(params, "turn"), "id") || str(params, "turnId"));
|
|
1794
|
+
// The parser intentionally suppresses this lifecycle-only event.
|
|
1795
|
+
this.emitStreamEvent(rawLine);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1033
1799
|
if (method === "item/started") {
|
|
1034
1800
|
this._state = "tool_executing";
|
|
1035
1801
|
this.emitStreamEvent(rawLine);
|
|
@@ -1039,6 +1805,11 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1039
1805
|
if (method === "item/completed") {
|
|
1040
1806
|
this._state = "thinking";
|
|
1041
1807
|
this.extractSummaryFromItem(params);
|
|
1808
|
+
const item = asObj(params, "item");
|
|
1809
|
+
if (str(item, "type") === "collabAgentToolCall") {
|
|
1810
|
+
this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, null);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1042
1813
|
this.emitStreamEvent(rawLine);
|
|
1043
1814
|
return;
|
|
1044
1815
|
}
|
|
@@ -1049,6 +1820,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1049
1820
|
}
|
|
1050
1821
|
|
|
1051
1822
|
if (method === "turn/failed") {
|
|
1823
|
+
this.markTurnTerminalObserved();
|
|
1052
1824
|
this._turnIsError = true;
|
|
1053
1825
|
this._turnErrorMessage = str(params, "message") || str(params, "error") || "Turn failed";
|
|
1054
1826
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -1078,6 +1850,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1078
1850
|
// -------------------------------------------------------------------------
|
|
1079
1851
|
|
|
1080
1852
|
private handleLegacyEvent(event: Record<string, unknown>, rawLine: string): void {
|
|
1853
|
+
if (this._state === "closed") return;
|
|
1081
1854
|
const type = str(event, "type");
|
|
1082
1855
|
const eventThreadId =
|
|
1083
1856
|
str(event, "thread_id") || str(event, "threadId") || str(event, "session_id") || null;
|
|
@@ -1111,6 +1884,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1111
1884
|
}
|
|
1112
1885
|
|
|
1113
1886
|
if (type === "turn.failed" || type === "error") {
|
|
1887
|
+
this.markTurnTerminalObserved();
|
|
1114
1888
|
this._turnIsError = true;
|
|
1115
1889
|
this._turnErrorMessage = str(event, "message") || str(event, "error") || "Turn failed";
|
|
1116
1890
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -1162,6 +1936,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1162
1936
|
}
|
|
1163
1937
|
|
|
1164
1938
|
private handleTurnCompleted(params: Record<string, unknown>): void {
|
|
1939
|
+
this.markTurnTerminalObserved();
|
|
1165
1940
|
const usage = typeof params["usage"] === "object" && params["usage"] !== null
|
|
1166
1941
|
? (params["usage"] as Record<string, unknown>)
|
|
1167
1942
|
: null;
|
|
@@ -1181,7 +1956,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1181
1956
|
// the error instead of a false "completed".
|
|
1182
1957
|
const turn = asObj(params, "turn");
|
|
1183
1958
|
const turnStatus = str(turn, "status");
|
|
1184
|
-
if (turnStatus === "
|
|
1959
|
+
if (turnStatus === "interrupted" || turnStatus === "cancelled") {
|
|
1960
|
+
this._turnWasInterrupted = true;
|
|
1961
|
+
} else if (turnStatus === "failed") {
|
|
1185
1962
|
this._turnIsError = true;
|
|
1186
1963
|
const msg = str(asObj(turn, "error"), "message");
|
|
1187
1964
|
this._turnErrorMessage = msg || this._turnErrorMessage || `Turn ${turnStatus}`;
|
|
@@ -1253,9 +2030,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1253
2030
|
summary: this._turnSummary,
|
|
1254
2031
|
usage,
|
|
1255
2032
|
costUsd: null,
|
|
1256
|
-
status: this._turnIsError ? "failed" : "completed",
|
|
1257
|
-
errorCode: this._turnIsError ? "execution_error" : null,
|
|
1258
|
-
errorMessage: this._turnErrorMessage,
|
|
2033
|
+
status: this._turnWasInterrupted ? "aborted" : this._turnIsError ? "failed" : "completed",
|
|
2034
|
+
errorCode: this._turnWasInterrupted ? "aborted" : this._turnIsError ? "execution_error" : null,
|
|
2035
|
+
errorMessage: this._turnWasInterrupted ? "Turn was interrupted" : this._turnErrorMessage,
|
|
1259
2036
|
};
|
|
1260
2037
|
|
|
1261
2038
|
// Drain pending onEvent handlers so callers awaiting send() see a settled
|
|
@@ -1283,8 +2060,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1283
2060
|
this._turnUsage = null;
|
|
1284
2061
|
this._turnModel = null;
|
|
1285
2062
|
this._turnIsError = false;
|
|
2063
|
+
this._turnWasInterrupted = false;
|
|
1286
2064
|
this._turnErrorMessage = null;
|
|
1287
2065
|
this._turnStartedAt = null;
|
|
2066
|
+
this.clearActiveTurn();
|
|
1288
2067
|
|
|
1289
2068
|
for (const p of pending) {
|
|
1290
2069
|
// Skip sends already settled early by timeout / abort.
|
|
@@ -1316,6 +2095,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1316
2095
|
* throwing handler does not break delivery of subsequent events.
|
|
1317
2096
|
*/
|
|
1318
2097
|
private dispatchEvent(event: StreamEvent): void {
|
|
2098
|
+
if (event.type === "background_task" && !this.observeBackgroundTask(event)) return;
|
|
1319
2099
|
// Track native goal_status transitions (keeps getGoal() accurate).
|
|
1320
2100
|
this._goals.observe(event);
|
|
1321
2101
|
const cb = this.ctx.onEvent;
|