@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
|
@@ -89,6 +89,8 @@ function buildCodexUserInputAnswers(questions, resp) {
|
|
|
89
89
|
}
|
|
90
90
|
return out;
|
|
91
91
|
}
|
|
92
|
+
const BACKGROUND_TASK_POLL_INTERVAL_MS = 2_000;
|
|
93
|
+
const BACKGROUND_TASK_READ_TIMEOUT_MS = 5_000;
|
|
92
94
|
// ---------------------------------------------------------------------------
|
|
93
95
|
// JSON-RPC 2.0 helpers
|
|
94
96
|
// ---------------------------------------------------------------------------
|
|
@@ -116,6 +118,30 @@ function asObj(parent, key) {
|
|
|
116
118
|
? v
|
|
117
119
|
: {};
|
|
118
120
|
}
|
|
121
|
+
function obj(value) {
|
|
122
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
123
|
+
? value
|
|
124
|
+
: {};
|
|
125
|
+
}
|
|
126
|
+
function stringArray(value) {
|
|
127
|
+
return Array.isArray(value)
|
|
128
|
+
? value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
129
|
+
: [];
|
|
130
|
+
}
|
|
131
|
+
function codexAgentState(value) {
|
|
132
|
+
const state = obj(value);
|
|
133
|
+
const nativeStatus = str(state, "status");
|
|
134
|
+
const summary = str(state, "message") || null;
|
|
135
|
+
if (nativeStatus === "completed")
|
|
136
|
+
return { terminal: true, status: "completed", summary };
|
|
137
|
+
if (nativeStatus === "errored" || nativeStatus === "notFound") {
|
|
138
|
+
return { terminal: true, status: "failed", summary };
|
|
139
|
+
}
|
|
140
|
+
if (nativeStatus === "interrupted" || nativeStatus === "shutdown") {
|
|
141
|
+
return { terminal: true, status: "stopped", summary };
|
|
142
|
+
}
|
|
143
|
+
return { terminal: false, status: "running", summary };
|
|
144
|
+
}
|
|
119
145
|
function classifyMessage(msg) {
|
|
120
146
|
// Detect JSON-RPC by *structure*, not by the `jsonrpc:"2.0"` discriminator.
|
|
121
147
|
// codex-cli 0.130.0's `app-server` emits responses without the `jsonrpc`
|
|
@@ -297,6 +323,10 @@ export class CodexSessionImpl {
|
|
|
297
323
|
/** Child-agent lifecycle is informational and never participates in root turn settlement. */
|
|
298
324
|
_backgroundTasks = new Map();
|
|
299
325
|
_backgroundTaskIdsByPath = new Map();
|
|
326
|
+
/** One protocol-native thread/read poller per child when Codex does not multiplex its notifications. */
|
|
327
|
+
_backgroundTaskPollers = new Map();
|
|
328
|
+
/** Cancellable retry delay for each poller. Cleared when the session closes. */
|
|
329
|
+
_backgroundTaskPollTimers = new Map();
|
|
300
330
|
/**
|
|
301
331
|
* Serial dispatch chain for `onEvent`. Each dispatched event appends a
|
|
302
332
|
* handler invocation; the chain enforces in-order delivery and lets
|
|
@@ -364,17 +394,51 @@ export class CodexSessionImpl {
|
|
|
364
394
|
proc.on("exit", (code, signal) => {
|
|
365
395
|
if (this._state !== "closed") {
|
|
366
396
|
this._state = "closed";
|
|
367
|
-
const
|
|
397
|
+
const message = `Codex process exited unexpectedly (code=${code}, signal=${signal})`;
|
|
398
|
+
this.finalizeActiveBackgroundTasks("failed", message, {
|
|
399
|
+
method: "process/exit",
|
|
400
|
+
code,
|
|
401
|
+
signal,
|
|
402
|
+
});
|
|
403
|
+
this.cancelBackgroundTaskPollTimers();
|
|
404
|
+
const err = new Error(message);
|
|
368
405
|
this.rejectAllPending(err);
|
|
369
406
|
}
|
|
370
407
|
});
|
|
371
408
|
proc.on("error", (err) => {
|
|
372
409
|
if (this._state !== "closed") {
|
|
373
410
|
this._state = "closed";
|
|
411
|
+
this.finalizeActiveBackgroundTasks("failed", err.message, {
|
|
412
|
+
method: "process/error",
|
|
413
|
+
message: err.message,
|
|
414
|
+
});
|
|
415
|
+
this.cancelBackgroundTaskPollTimers();
|
|
374
416
|
this.rejectAllPending(err);
|
|
375
417
|
}
|
|
376
418
|
});
|
|
377
419
|
}
|
|
420
|
+
cancelBackgroundTaskPollTimers() {
|
|
421
|
+
for (const pending of this._backgroundTaskPollTimers.values()) {
|
|
422
|
+
clearTimeout(pending.timer);
|
|
423
|
+
pending.resolve();
|
|
424
|
+
}
|
|
425
|
+
this._backgroundTaskPollTimers.clear();
|
|
426
|
+
}
|
|
427
|
+
finalizeActiveBackgroundTasks(status, summary, raw) {
|
|
428
|
+
for (const task of this._backgroundTasks.values()) {
|
|
429
|
+
if (task.terminal)
|
|
430
|
+
continue;
|
|
431
|
+
this.dispatchEvent(this.backgroundTaskEvent(task.taskId, "completed", status, {
|
|
432
|
+
description: task.description,
|
|
433
|
+
summary,
|
|
434
|
+
parentTaskId: task.parentTaskId,
|
|
435
|
+
eventId: this._threadId
|
|
436
|
+
? `codex:${this._threadId}:background-task:${task.taskId}:session-${status}:completed`
|
|
437
|
+
: null,
|
|
438
|
+
raw,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
378
442
|
/** Reject every pending send() Promise and outgoing JSON-RPC call. */
|
|
379
443
|
rejectAllPending(err) {
|
|
380
444
|
const pending = this._pendingResults.splice(0);
|
|
@@ -488,6 +552,30 @@ export class CodexSessionImpl {
|
|
|
488
552
|
this._pendingRpc.set(id, { resolve, reject });
|
|
489
553
|
});
|
|
490
554
|
}
|
|
555
|
+
/** A bounded RPC whose pending-map entry is removed if Codex never replies. */
|
|
556
|
+
boundedRpcRequest(method, params, timeoutMs = BACKGROUND_TASK_READ_TIMEOUT_MS) {
|
|
557
|
+
const id = this._nextId++;
|
|
558
|
+
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
559
|
+
return new Promise((resolve, reject) => {
|
|
560
|
+
const timer = setTimeout(() => {
|
|
561
|
+
if (!this._pendingRpc.delete(id))
|
|
562
|
+
return;
|
|
563
|
+
reject(new Error(`codex ${method} timed out`));
|
|
564
|
+
}, timeoutMs);
|
|
565
|
+
if (typeof timer.unref === "function")
|
|
566
|
+
timer.unref();
|
|
567
|
+
this._pendingRpc.set(id, {
|
|
568
|
+
resolve: (result) => {
|
|
569
|
+
clearTimeout(timer);
|
|
570
|
+
resolve(result);
|
|
571
|
+
},
|
|
572
|
+
reject: (err) => {
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
reject(err);
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
491
579
|
/**
|
|
492
580
|
* Bounded RPC for experimental, best-effort methods (the `thread/goal/*`
|
|
493
581
|
* family). An app-server build that doesn't recognize the method may never
|
|
@@ -843,6 +931,10 @@ export class CodexSessionImpl {
|
|
|
843
931
|
if (this._state === "closed")
|
|
844
932
|
return;
|
|
845
933
|
this._state = "closed";
|
|
934
|
+
this.finalizeActiveBackgroundTasks("stopped", "Codex session closed", {
|
|
935
|
+
method: "session/closed",
|
|
936
|
+
});
|
|
937
|
+
this.cancelBackgroundTaskPollTimers();
|
|
846
938
|
this.rejectAllPending(new Error("Codex session closed"));
|
|
847
939
|
this.proc.stdin.end();
|
|
848
940
|
// Grace window before SIGKILL is configurable via ProviderConfig.graceSec
|
|
@@ -1046,6 +1138,250 @@ export class CodexSessionImpl {
|
|
|
1046
1138
|
}
|
|
1047
1139
|
return null;
|
|
1048
1140
|
}
|
|
1141
|
+
backgroundTaskEvent(taskId, phase, status, options) {
|
|
1142
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1143
|
+
return {
|
|
1144
|
+
type: "background_task",
|
|
1145
|
+
taskId,
|
|
1146
|
+
taskType: "subagent",
|
|
1147
|
+
phase,
|
|
1148
|
+
status,
|
|
1149
|
+
description: options.description ?? null,
|
|
1150
|
+
summary: options.summary ?? null,
|
|
1151
|
+
parentTaskId: options.parentTaskId ?? null,
|
|
1152
|
+
timestamp: new Date().toISOString(),
|
|
1153
|
+
providerType: "codex",
|
|
1154
|
+
sessionId: rootThreadId,
|
|
1155
|
+
messageId: null,
|
|
1156
|
+
eventId: options.eventId ?? null,
|
|
1157
|
+
turnId: options.turnId ?? null,
|
|
1158
|
+
parentToolCallId: null,
|
|
1159
|
+
raw: options.raw,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
reconciledBackgroundTaskRaw(threadId, turn) {
|
|
1163
|
+
const error = turn ? str(asObj(turn, "error"), "message") || null : null;
|
|
1164
|
+
return {
|
|
1165
|
+
method: "thread/read",
|
|
1166
|
+
reconciled: true,
|
|
1167
|
+
threadId,
|
|
1168
|
+
turnId: turn ? str(turn, "id") || null : null,
|
|
1169
|
+
status: turn ? str(turn, "status") || null : null,
|
|
1170
|
+
error,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Codex 0.144+ represents collaboration as a root collabAgentToolCall item.
|
|
1175
|
+
* A completed spawn call only means the child was created. Register every
|
|
1176
|
+
* receiver and then follow the child thread itself for the real terminus.
|
|
1177
|
+
*/
|
|
1178
|
+
handleCollabAgentToolCall(item, raw, parentTaskId) {
|
|
1179
|
+
if (this._state === "closed" || !this.ctx.onEvent)
|
|
1180
|
+
return;
|
|
1181
|
+
const tool = str(item, "tool");
|
|
1182
|
+
const states = asObj(item, "agentsStates");
|
|
1183
|
+
const receiverIds = stringArray(item["receiverThreadIds"]);
|
|
1184
|
+
const taskIds = [...new Set([...receiverIds, ...Object.keys(states)])];
|
|
1185
|
+
const canStart = tool === "spawnAgent";
|
|
1186
|
+
const metadataOnly = tool === "resumeAgent" || tool === "sendInput";
|
|
1187
|
+
const canStopWithoutState = tool === "closeAgent" && str(item, "status") === "completed";
|
|
1188
|
+
for (const taskId of taskIds) {
|
|
1189
|
+
let previous = this._backgroundTasks.get(taskId);
|
|
1190
|
+
const description = str(item, "prompt") || previous?.description || null;
|
|
1191
|
+
const resolvedParentTaskId = previous?.parentTaskId ?? parentTaskId;
|
|
1192
|
+
// The child thread's own turn/started notification is the only
|
|
1193
|
+
// authoritative reactivation edge. Root resume/send calls can arrive
|
|
1194
|
+
// before or after that child turn, so they only enrich an active task.
|
|
1195
|
+
if (metadataOnly) {
|
|
1196
|
+
if (!previous || previous.terminal)
|
|
1197
|
+
continue;
|
|
1198
|
+
if (description && description !== previous.description) {
|
|
1199
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "progress", "running", {
|
|
1200
|
+
description,
|
|
1201
|
+
summary: previous.summary,
|
|
1202
|
+
parentTaskId: resolvedParentTaskId,
|
|
1203
|
+
eventId: this._threadId
|
|
1204
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "metadata"}:progress`
|
|
1205
|
+
: null,
|
|
1206
|
+
raw,
|
|
1207
|
+
}));
|
|
1208
|
+
}
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
if (!previous && !canStart)
|
|
1212
|
+
continue;
|
|
1213
|
+
if (!canStart && !canStopWithoutState && !(taskId in states))
|
|
1214
|
+
continue;
|
|
1215
|
+
const hasState = taskId in states;
|
|
1216
|
+
const state = !hasState && tool === "closeAgent" && str(item, "status") === "completed"
|
|
1217
|
+
? { terminal: true, status: "stopped", summary: null }
|
|
1218
|
+
: codexAgentState(states[taskId]);
|
|
1219
|
+
if (!previous) {
|
|
1220
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "started", "running", {
|
|
1221
|
+
description,
|
|
1222
|
+
summary: null,
|
|
1223
|
+
parentTaskId: resolvedParentTaskId,
|
|
1224
|
+
eventId: this._threadId
|
|
1225
|
+
? `codex:${this._threadId}:background-task:${taskId}:started`
|
|
1226
|
+
: null,
|
|
1227
|
+
raw,
|
|
1228
|
+
}));
|
|
1229
|
+
previous = this._backgroundTasks.get(taskId);
|
|
1230
|
+
}
|
|
1231
|
+
else if (previous.terminal) {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
else if (description && description !== previous.description) {
|
|
1235
|
+
// thread/started can beat the richer root collab item. Preserve one
|
|
1236
|
+
// start edge, then publish the prompt as ordinary progress metadata.
|
|
1237
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "progress", "running", {
|
|
1238
|
+
description,
|
|
1239
|
+
summary: previous.summary,
|
|
1240
|
+
parentTaskId: resolvedParentTaskId,
|
|
1241
|
+
eventId: this._threadId
|
|
1242
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "metadata"}:progress`
|
|
1243
|
+
: null,
|
|
1244
|
+
raw,
|
|
1245
|
+
}));
|
|
1246
|
+
}
|
|
1247
|
+
if (state.terminal) {
|
|
1248
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", state.status, {
|
|
1249
|
+
description,
|
|
1250
|
+
summary: state.summary,
|
|
1251
|
+
parentTaskId: resolvedParentTaskId,
|
|
1252
|
+
eventId: this._threadId
|
|
1253
|
+
? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "state"}:completed`
|
|
1254
|
+
: null,
|
|
1255
|
+
raw,
|
|
1256
|
+
}));
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
this.startBackgroundTaskPoller(taskId);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
waitForBackgroundTaskPoll(pollKey) {
|
|
1264
|
+
if (this._state === "closed")
|
|
1265
|
+
return Promise.resolve();
|
|
1266
|
+
return new Promise((resolve) => {
|
|
1267
|
+
const timer = setTimeout(() => {
|
|
1268
|
+
this._backgroundTaskPollTimers.delete(pollKey);
|
|
1269
|
+
resolve();
|
|
1270
|
+
}, BACKGROUND_TASK_POLL_INTERVAL_MS);
|
|
1271
|
+
if (typeof timer.unref === "function")
|
|
1272
|
+
timer.unref();
|
|
1273
|
+
this._backgroundTaskPollTimers.set(pollKey, { timer, resolve });
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
startBackgroundTaskPoller(taskId) {
|
|
1277
|
+
if (this._state === "closed")
|
|
1278
|
+
return;
|
|
1279
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1280
|
+
if (!task || task.terminal)
|
|
1281
|
+
return;
|
|
1282
|
+
const existing = this._backgroundTaskPollers.get(taskId);
|
|
1283
|
+
if (existing?.generation === task.generation)
|
|
1284
|
+
return;
|
|
1285
|
+
const generation = task.generation;
|
|
1286
|
+
const pollKey = `${taskId}:${generation}`;
|
|
1287
|
+
let poller;
|
|
1288
|
+
poller = this.pollBackgroundTask(taskId, generation, pollKey).finally(() => {
|
|
1289
|
+
if (this._backgroundTaskPollers.get(taskId)?.promise === poller) {
|
|
1290
|
+
this._backgroundTaskPollers.delete(taskId);
|
|
1291
|
+
}
|
|
1292
|
+
const pending = this._backgroundTaskPollTimers.get(pollKey);
|
|
1293
|
+
if (pending)
|
|
1294
|
+
clearTimeout(pending.timer);
|
|
1295
|
+
this._backgroundTaskPollTimers.delete(pollKey);
|
|
1296
|
+
});
|
|
1297
|
+
this._backgroundTaskPollers.set(taskId, { generation, promise: poller });
|
|
1298
|
+
}
|
|
1299
|
+
async pollBackgroundTask(taskId, generation, pollKey) {
|
|
1300
|
+
while (this._state !== "closed") {
|
|
1301
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1302
|
+
if (!task || task.generation !== generation || task.terminal)
|
|
1303
|
+
return;
|
|
1304
|
+
try {
|
|
1305
|
+
const response = await this.boundedRpcRequest("thread/read", {
|
|
1306
|
+
threadId: taskId,
|
|
1307
|
+
includeTurns: true,
|
|
1308
|
+
});
|
|
1309
|
+
if (this.observeBackgroundTaskThread(response, taskId, generation))
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
catch {
|
|
1313
|
+
// A child can briefly be pendingInit before thread/read can load it.
|
|
1314
|
+
}
|
|
1315
|
+
const current = this._backgroundTasks.get(taskId);
|
|
1316
|
+
if (!current || current.generation !== generation || current.terminal)
|
|
1317
|
+
return;
|
|
1318
|
+
await this.waitForBackgroundTaskPoll(pollKey);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/** Return true once thread/read proves that the child's latest turn ended. */
|
|
1322
|
+
observeBackgroundTaskThread(response, taskId, generation) {
|
|
1323
|
+
const task = this._backgroundTasks.get(taskId);
|
|
1324
|
+
if (!task || task.generation !== generation || task.terminal)
|
|
1325
|
+
return true;
|
|
1326
|
+
const thread = asObj(response, "thread");
|
|
1327
|
+
const turns = Array.isArray(thread["turns"])
|
|
1328
|
+
? thread["turns"].map(obj)
|
|
1329
|
+
: [];
|
|
1330
|
+
const latestTurn = turns[turns.length - 1];
|
|
1331
|
+
if (!latestTurn) {
|
|
1332
|
+
const threadStatus = str(asObj(thread, "status"), "type");
|
|
1333
|
+
if (threadStatus !== "systemError")
|
|
1334
|
+
return false;
|
|
1335
|
+
const failureTurn = { status: "systemError", error: { message: "Subagent failed to initialize" } };
|
|
1336
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", "failed", {
|
|
1337
|
+
description: task.description,
|
|
1338
|
+
summary: "Subagent failed to initialize",
|
|
1339
|
+
parentTaskId: task.parentTaskId,
|
|
1340
|
+
eventId: this._threadId
|
|
1341
|
+
? `codex:${this._threadId}:background-task:${taskId}:system-error:completed`
|
|
1342
|
+
: null,
|
|
1343
|
+
raw: this.reconciledBackgroundTaskRaw(taskId, failureTurn),
|
|
1344
|
+
}));
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
const nativeStatus = str(latestTurn, "status");
|
|
1348
|
+
if (nativeStatus === "inProgress" || !nativeStatus)
|
|
1349
|
+
return false;
|
|
1350
|
+
const turnId = str(latestTurn, "id") || null;
|
|
1351
|
+
// The child turn/started notification is authoritative for a reactivated
|
|
1352
|
+
// generation. thread/read can briefly lag and still end at an older turn.
|
|
1353
|
+
if (task.activeTurnId && turnId !== task.activeTurnId)
|
|
1354
|
+
return false;
|
|
1355
|
+
const items = Array.isArray(latestTurn["items"])
|
|
1356
|
+
? latestTurn["items"].map(obj)
|
|
1357
|
+
: [];
|
|
1358
|
+
let summary = null;
|
|
1359
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
1360
|
+
const item = items[index];
|
|
1361
|
+
if (str(item, "type") !== "agentMessage")
|
|
1362
|
+
continue;
|
|
1363
|
+
summary = this.agentMessageText(item);
|
|
1364
|
+
if (summary)
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
const errorMessage = str(asObj(latestTurn, "error"), "message") || null;
|
|
1368
|
+
const status = nativeStatus === "failed"
|
|
1369
|
+
? "failed"
|
|
1370
|
+
: nativeStatus === "interrupted" || nativeStatus === "cancelled"
|
|
1371
|
+
? "stopped"
|
|
1372
|
+
: "completed";
|
|
1373
|
+
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", status, {
|
|
1374
|
+
description: task.description,
|
|
1375
|
+
summary: summary ?? errorMessage,
|
|
1376
|
+
parentTaskId: task.parentTaskId,
|
|
1377
|
+
turnId,
|
|
1378
|
+
eventId: this._threadId && turnId
|
|
1379
|
+
? `codex:${this._threadId}:background-task:${taskId}:${turnId}:completed`
|
|
1380
|
+
: null,
|
|
1381
|
+
raw: this.reconciledBackgroundTaskRaw(taskId, latestTurn),
|
|
1382
|
+
}));
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1049
1385
|
/**
|
|
1050
1386
|
* Maintain just enough child metadata to turn a later foreign-thread
|
|
1051
1387
|
* terminal notification into one provider-neutral task event. This reducer
|
|
@@ -1060,6 +1396,10 @@ export class CodexSessionImpl {
|
|
|
1060
1396
|
// explicitly reactivates the record below in handleForeignNotification.
|
|
1061
1397
|
if (previous?.terminal)
|
|
1062
1398
|
return false;
|
|
1399
|
+
// collabAgentToolCall and child thread/started can both announce the same
|
|
1400
|
+
// spawn. The first edge is sufficient and gives hosts one durable start.
|
|
1401
|
+
if (previous && event.phase === "started")
|
|
1402
|
+
return false;
|
|
1063
1403
|
const description = event.description ?? previous?.description ?? null;
|
|
1064
1404
|
const summary = event.summary ?? previous?.summary ?? null;
|
|
1065
1405
|
const parentTaskId = event.parentTaskId
|
|
@@ -1074,6 +1414,10 @@ export class CodexSessionImpl {
|
|
|
1074
1414
|
summary,
|
|
1075
1415
|
parentTaskId,
|
|
1076
1416
|
terminal: event.phase === "completed",
|
|
1417
|
+
generation: previous?.generation ?? 0,
|
|
1418
|
+
activeTurnId: event.phase === "completed"
|
|
1419
|
+
? null
|
|
1420
|
+
: event.turnId ?? previous?.activeTurnId ?? null,
|
|
1077
1421
|
});
|
|
1078
1422
|
if (description)
|
|
1079
1423
|
this._backgroundTaskIdsByPath.set(description, event.taskId);
|
|
@@ -1128,6 +1472,7 @@ export class CodexSessionImpl {
|
|
|
1128
1472
|
parentToolCallId: null,
|
|
1129
1473
|
raw: parseJson(rawLine) ?? params,
|
|
1130
1474
|
});
|
|
1475
|
+
this.startBackgroundTaskPoller(childThreadId);
|
|
1131
1476
|
return;
|
|
1132
1477
|
}
|
|
1133
1478
|
const task = this._backgroundTasks.get(childThreadId);
|
|
@@ -1138,6 +1483,7 @@ export class CodexSessionImpl {
|
|
|
1138
1483
|
return;
|
|
1139
1484
|
task.terminal = false;
|
|
1140
1485
|
task.summary = null;
|
|
1486
|
+
task.generation += 1;
|
|
1141
1487
|
const turn = asObj(params, "turn");
|
|
1142
1488
|
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1143
1489
|
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
@@ -1161,11 +1507,16 @@ export class CodexSessionImpl {
|
|
|
1161
1507
|
parentToolCallId: null,
|
|
1162
1508
|
raw: parseJson(rawLine) ?? params,
|
|
1163
1509
|
});
|
|
1510
|
+
this.startBackgroundTaskPoller(childThreadId);
|
|
1164
1511
|
return;
|
|
1165
1512
|
}
|
|
1166
1513
|
if (method === "item/completed") {
|
|
1167
1514
|
const item = asObj(params, "item");
|
|
1168
1515
|
const itemType = str(item, "type");
|
|
1516
|
+
if (itemType === "collabAgentToolCall") {
|
|
1517
|
+
this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, childThreadId);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1169
1520
|
if ((itemType === "agentMessage" || itemType === "agent_message") && str(item, "phase") !== "commentary") {
|
|
1170
1521
|
const summary = this.agentMessageText(item);
|
|
1171
1522
|
if (summary)
|
|
@@ -1205,10 +1556,19 @@ export class CodexSessionImpl {
|
|
|
1205
1556
|
: null,
|
|
1206
1557
|
turnId,
|
|
1207
1558
|
parentToolCallId: null,
|
|
1208
|
-
raw:
|
|
1559
|
+
raw: {
|
|
1560
|
+
method,
|
|
1561
|
+
reconciled: false,
|
|
1562
|
+
threadId: childThreadId,
|
|
1563
|
+
turnId,
|
|
1564
|
+
status: nativeStatus || null,
|
|
1565
|
+
error: errorMessage || null,
|
|
1566
|
+
},
|
|
1209
1567
|
});
|
|
1210
1568
|
}
|
|
1211
1569
|
handleNotification(method, params, rawLine) {
|
|
1570
|
+
if (this._state === "closed")
|
|
1571
|
+
return;
|
|
1212
1572
|
// codex/event — legacy wrapper
|
|
1213
1573
|
if (method === "codex/event") {
|
|
1214
1574
|
const innerMsg = str(params, "msg");
|
|
@@ -1251,6 +1611,11 @@ export class CodexSessionImpl {
|
|
|
1251
1611
|
if (method === "item/completed") {
|
|
1252
1612
|
this._state = "thinking";
|
|
1253
1613
|
this.extractSummaryFromItem(params);
|
|
1614
|
+
const item = asObj(params, "item");
|
|
1615
|
+
if (str(item, "type") === "collabAgentToolCall") {
|
|
1616
|
+
this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, null);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1254
1619
|
this.emitStreamEvent(rawLine);
|
|
1255
1620
|
return;
|
|
1256
1621
|
}
|
|
@@ -1286,6 +1651,8 @@ export class CodexSessionImpl {
|
|
|
1286
1651
|
// Legacy event handling (NDJSON events with `type` field)
|
|
1287
1652
|
// -------------------------------------------------------------------------
|
|
1288
1653
|
handleLegacyEvent(event, rawLine) {
|
|
1654
|
+
if (this._state === "closed")
|
|
1655
|
+
return;
|
|
1289
1656
|
const type = str(event, "type");
|
|
1290
1657
|
const eventThreadId = str(event, "thread_id") || str(event, "threadId") || str(event, "session_id") || null;
|
|
1291
1658
|
// Older NDJSON-shaped events can also carry explicit thread scope. Keep
|