@agentex/agent 0.0.33 → 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 +19 -0
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +47 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +21 -0
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +369 -2
- package/dist/providers/codex/session.js.map +1 -1
- package/package.json +1 -1
- package/src/providers/codex/parse.ts +50 -0
- package/src/providers/codex/session.ts +419 -2
|
@@ -138,8 +138,19 @@ interface TrackedBackgroundTask {
|
|
|
138
138
|
summary: string | null;
|
|
139
139
|
parentTaskId: string | null;
|
|
140
140
|
terminal: boolean;
|
|
141
|
+
generation: number;
|
|
142
|
+
activeTurnId: string | null;
|
|
141
143
|
}
|
|
142
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
|
+
|
|
143
154
|
/** Identity latch for the root turn currently represented by this session. */
|
|
144
155
|
interface ActiveTurnReady {
|
|
145
156
|
promise: Promise<string | null>;
|
|
@@ -179,6 +190,32 @@ function asObj(parent: Record<string, unknown>, key: string): Record<string, unk
|
|
|
179
190
|
: {};
|
|
180
191
|
}
|
|
181
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
|
+
|
|
182
219
|
/** Discriminated incoming message from the Codex CLI. */
|
|
183
220
|
type IncomingMessage =
|
|
184
221
|
| { kind: "response"; id: number; result?: Record<string, unknown>; error?: { code: number; message: string } }
|
|
@@ -387,6 +424,16 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
387
424
|
/** Child-agent lifecycle is informational and never participates in root turn settlement. */
|
|
388
425
|
private readonly _backgroundTasks = new Map<string, TrackedBackgroundTask>();
|
|
389
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
|
+
}>();
|
|
390
437
|
|
|
391
438
|
/**
|
|
392
439
|
* Serial dispatch chain for `onEvent`. Each dispatched event appends a
|
|
@@ -456,7 +503,14 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
456
503
|
proc.on("exit", (code, signal) => {
|
|
457
504
|
if (this._state !== "closed") {
|
|
458
505
|
this._state = "closed";
|
|
459
|
-
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);
|
|
460
514
|
this.rejectAllPending(err);
|
|
461
515
|
}
|
|
462
516
|
});
|
|
@@ -464,11 +518,43 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
464
518
|
proc.on("error", (err) => {
|
|
465
519
|
if (this._state !== "closed") {
|
|
466
520
|
this._state = "closed";
|
|
521
|
+
this.finalizeActiveBackgroundTasks("failed", err.message, {
|
|
522
|
+
method: "process/error",
|
|
523
|
+
message: err.message,
|
|
524
|
+
});
|
|
525
|
+
this.cancelBackgroundTaskPollTimers();
|
|
467
526
|
this.rejectAllPending(err);
|
|
468
527
|
}
|
|
469
528
|
});
|
|
470
529
|
}
|
|
471
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
|
+
|
|
472
558
|
/** Reject every pending send() Promise and outgoing JSON-RPC call. */
|
|
473
559
|
private rejectAllPending(err: Error): void {
|
|
474
560
|
const pending = this._pendingResults.splice(0);
|
|
@@ -584,6 +670,35 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
584
670
|
});
|
|
585
671
|
}
|
|
586
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
|
+
|
|
587
702
|
/**
|
|
588
703
|
* Bounded RPC for experimental, best-effort methods (the `thread/goal/*`
|
|
589
704
|
* family). An app-server build that doesn't recognize the method may never
|
|
@@ -953,6 +1068,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
953
1068
|
async close(): Promise<void> {
|
|
954
1069
|
if (this._state === "closed") return;
|
|
955
1070
|
this._state = "closed";
|
|
1071
|
+
this.finalizeActiveBackgroundTasks("stopped", "Codex session closed", {
|
|
1072
|
+
method: "session/closed",
|
|
1073
|
+
});
|
|
1074
|
+
this.cancelBackgroundTaskPollTimers();
|
|
956
1075
|
this.rejectAllPending(new Error("Codex session closed"));
|
|
957
1076
|
|
|
958
1077
|
this.proc.stdin!.end();
|
|
@@ -1175,6 +1294,276 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1175
1294
|
return null;
|
|
1176
1295
|
}
|
|
1177
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
|
+
|
|
1178
1567
|
/**
|
|
1179
1568
|
* Maintain just enough child metadata to turn a later foreign-thread
|
|
1180
1569
|
* terminal notification into one provider-neutral task event. This reducer
|
|
@@ -1188,6 +1577,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1188
1577
|
// a task that already reached a terminal state. A later child turn/started
|
|
1189
1578
|
// explicitly reactivates the record below in handleForeignNotification.
|
|
1190
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;
|
|
1191
1583
|
|
|
1192
1584
|
const description = event.description ?? previous?.description ?? null;
|
|
1193
1585
|
const summary = event.summary ?? previous?.summary ?? null;
|
|
@@ -1205,6 +1597,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1205
1597
|
summary,
|
|
1206
1598
|
parentTaskId,
|
|
1207
1599
|
terminal: event.phase === "completed",
|
|
1600
|
+
generation: previous?.generation ?? 0,
|
|
1601
|
+
activeTurnId: event.phase === "completed"
|
|
1602
|
+
? null
|
|
1603
|
+
: event.turnId ?? previous?.activeTurnId ?? null,
|
|
1208
1604
|
});
|
|
1209
1605
|
if (description) this._backgroundTaskIdsByPath.set(description, event.taskId);
|
|
1210
1606
|
return true;
|
|
@@ -1265,6 +1661,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1265
1661
|
parentToolCallId: null,
|
|
1266
1662
|
raw: parseJson(rawLine) ?? params,
|
|
1267
1663
|
});
|
|
1664
|
+
this.startBackgroundTaskPoller(childThreadId);
|
|
1268
1665
|
return;
|
|
1269
1666
|
}
|
|
1270
1667
|
|
|
@@ -1275,6 +1672,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1275
1672
|
if (!task.terminal) return;
|
|
1276
1673
|
task.terminal = false;
|
|
1277
1674
|
task.summary = null;
|
|
1675
|
+
task.generation += 1;
|
|
1278
1676
|
const turn = asObj(params, "turn");
|
|
1279
1677
|
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1280
1678
|
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
@@ -1298,12 +1696,17 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1298
1696
|
parentToolCallId: null,
|
|
1299
1697
|
raw: parseJson(rawLine) ?? params,
|
|
1300
1698
|
});
|
|
1699
|
+
this.startBackgroundTaskPoller(childThreadId);
|
|
1301
1700
|
return;
|
|
1302
1701
|
}
|
|
1303
1702
|
|
|
1304
1703
|
if (method === "item/completed") {
|
|
1305
1704
|
const item = asObj(params, "item");
|
|
1306
1705
|
const itemType = str(item, "type");
|
|
1706
|
+
if (itemType === "collabAgentToolCall") {
|
|
1707
|
+
this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, childThreadId);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1307
1710
|
if ((itemType === "agentMessage" || itemType === "agent_message") && str(item, "phase") !== "commentary") {
|
|
1308
1711
|
const summary = this.agentMessageText(item);
|
|
1309
1712
|
if (summary) task.summary = summary;
|
|
@@ -1344,11 +1747,19 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1344
1747
|
: null,
|
|
1345
1748
|
turnId,
|
|
1346
1749
|
parentToolCallId: null,
|
|
1347
|
-
raw:
|
|
1750
|
+
raw: {
|
|
1751
|
+
method,
|
|
1752
|
+
reconciled: false,
|
|
1753
|
+
threadId: childThreadId,
|
|
1754
|
+
turnId,
|
|
1755
|
+
status: nativeStatus || null,
|
|
1756
|
+
error: errorMessage || null,
|
|
1757
|
+
},
|
|
1348
1758
|
});
|
|
1349
1759
|
}
|
|
1350
1760
|
|
|
1351
1761
|
private handleNotification(method: string, params: Record<string, unknown>, rawLine: string): void {
|
|
1762
|
+
if (this._state === "closed") return;
|
|
1352
1763
|
// codex/event — legacy wrapper
|
|
1353
1764
|
if (method === "codex/event") {
|
|
1354
1765
|
const innerMsg = str(params, "msg");
|
|
@@ -1394,6 +1805,11 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1394
1805
|
if (method === "item/completed") {
|
|
1395
1806
|
this._state = "thinking";
|
|
1396
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
|
+
}
|
|
1397
1813
|
this.emitStreamEvent(rawLine);
|
|
1398
1814
|
return;
|
|
1399
1815
|
}
|
|
@@ -1434,6 +1850,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1434
1850
|
// -------------------------------------------------------------------------
|
|
1435
1851
|
|
|
1436
1852
|
private handleLegacyEvent(event: Record<string, unknown>, rawLine: string): void {
|
|
1853
|
+
if (this._state === "closed") return;
|
|
1437
1854
|
const type = str(event, "type");
|
|
1438
1855
|
const eventThreadId =
|
|
1439
1856
|
str(event, "thread_id") || str(event, "threadId") || str(event, "session_id") || null;
|