@junctionpanel/server 0.1.61 → 0.1.63

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.
Files changed (57) hide show
  1. package/dist/server/client/daemon-client.d.ts +28 -1
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +236 -3
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-manager.d.ts +5 -1
  6. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-manager.js +18 -2
  8. package/dist/server/server/agent/agent-manager.js.map +1 -1
  9. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  10. package/dist/server/server/agent/agent-projections.js +3 -0
  11. package/dist/server/server/agent/agent-projections.js.map +1 -1
  12. package/dist/server/server/agent/agent-sdk-types.d.ts +13 -0
  13. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  14. package/dist/server/server/agent/agent-storage.d.ts +3 -0
  15. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  16. package/dist/server/server/agent/agent-storage.js +1 -0
  17. package/dist/server/server/agent/agent-storage.js.map +1 -1
  18. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  19. package/dist/server/server/agent/mcp-server.js +2 -0
  20. package/dist/server/server/agent/mcp-server.js.map +1 -1
  21. package/dist/server/server/agent/providers/claude-agent.js +1 -0
  22. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  23. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
  24. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +59 -0
  25. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
  26. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +31 -0
  27. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  28. package/dist/server/server/agent/providers/codex-app-server-agent.js +1022 -50
  29. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  30. package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
  31. package/dist/server/server/agent/providers/gemini-agent.js +1 -0
  32. package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
  33. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  34. package/dist/server/server/agent/providers/opencode-agent.js +95 -14
  35. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  36. package/dist/server/server/bootstrap.d.ts +22 -0
  37. package/dist/server/server/bootstrap.d.ts.map +1 -1
  38. package/dist/server/server/bootstrap.js +10 -0
  39. package/dist/server/server/bootstrap.js.map +1 -1
  40. package/dist/server/server/persisted-config.d.ts +4 -4
  41. package/dist/server/server/session.d.ts +26 -0
  42. package/dist/server/server/session.d.ts.map +1 -1
  43. package/dist/server/server/session.js +593 -80
  44. package/dist/server/server/session.js.map +1 -1
  45. package/dist/server/server/voice-config.d.ts +10 -0
  46. package/dist/server/server/voice-config.d.ts.map +1 -0
  47. package/dist/server/server/voice-config.js +44 -0
  48. package/dist/server/server/voice-config.js.map +1 -0
  49. package/dist/server/server/websocket-server.d.ts +24 -1
  50. package/dist/server/server/websocket-server.d.ts.map +1 -1
  51. package/dist/server/server/websocket-server.js +61 -2
  52. package/dist/server/server/websocket-server.js.map +1 -1
  53. package/dist/server/shared/messages.d.ts +8398 -2298
  54. package/dist/server/shared/messages.d.ts.map +1 -1
  55. package/dist/server/shared/messages.js +136 -0
  56. package/dist/server/shared/messages.js.map +1 -1
  57. package/package.json +3 -3
@@ -1,6 +1,7 @@
1
1
  import pino from "pino";
2
2
  import { execSync, spawn } from "node:child_process";
3
3
  import fs from "node:fs/promises";
4
+ import { createReadStream } from "node:fs";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
  import readline from "node:readline";
@@ -14,7 +15,44 @@ import { writeImageAttachment } from "./image-attachments.js";
14
15
  const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
15
16
  const TURN_START_TIMEOUT_MS = 90 * 1000;
16
17
  const TURN_COMPLETION_SETTLE_DELAY_MS = 75;
18
+ const CODEX_ROLLOUT_SPAWN_POLL_MS = 900;
17
19
  const CODEX_PROVIDER = "codex";
20
+ const CODEX_SESSION_SCAN_MAX_DEPTH = 4;
21
+ const CODEX_THREAD_LIST_SOURCE_KINDS = [
22
+ "cli",
23
+ "vscode",
24
+ "exec",
25
+ "appServer",
26
+ "subAgent",
27
+ "subAgentReview",
28
+ "subAgentCompact",
29
+ "subAgentThreadSpawn",
30
+ "subAgentOther",
31
+ "unknown",
32
+ ];
33
+ function parseCodexThreadListAllowedSourceKinds(errorMessage) {
34
+ const match = /expected one of (.+)$/i.exec(errorMessage);
35
+ if (!match) {
36
+ return null;
37
+ }
38
+ const allowed = match[1]
39
+ .split(",")
40
+ .map((value) => value.trim().replace(/^`|`$/g, ""))
41
+ .filter((value) => value.length > 0);
42
+ return allowed.length > 0 ? allowed : null;
43
+ }
44
+ function resolveCodexThreadListRetrySourceKinds(preferredSourceKinds, errorMessage) {
45
+ const allowedSourceKinds = parseCodexThreadListAllowedSourceKinds(errorMessage);
46
+ if (!allowedSourceKinds) {
47
+ return null;
48
+ }
49
+ const allowedSourceKindSet = new Set(allowedSourceKinds);
50
+ const retrySourceKinds = preferredSourceKinds.filter((value) => allowedSourceKindSet.has(value));
51
+ if (retrySourceKinds.length === 0 || retrySourceKinds.length === preferredSourceKinds.length) {
52
+ return null;
53
+ }
54
+ return retrySourceKinds;
55
+ }
18
56
  const CODEX_APP_SERVER_CAPABILITIES = {
19
57
  supportsStreaming: true,
20
58
  supportsSessionPersistence: true,
@@ -112,6 +150,41 @@ function resolveCodexLaunchPrefix(runtimeSettings) {
112
150
  function resolveCodexHomeDir() {
113
151
  return process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
114
152
  }
153
+ function resolveCodexSessionRoot() {
154
+ return process.env.CODEX_SESSION_DIR ?? path.join(resolveCodexHomeDir(), "sessions");
155
+ }
156
+ function isCodexRolloutFileName(fileName) {
157
+ return (fileName.startsWith("rollout-") &&
158
+ (fileName.endsWith(".json") || fileName.endsWith(".jsonl")));
159
+ }
160
+ async function listCodexRolloutPaths(root) {
161
+ const rolloutPaths = [];
162
+ const stack = [{ dir: root, depth: 0 }];
163
+ while (stack.length > 0) {
164
+ const current = stack.pop();
165
+ if (!current) {
166
+ continue;
167
+ }
168
+ let entries;
169
+ try {
170
+ entries = await fs.readdir(current.dir, { withFileTypes: true });
171
+ }
172
+ catch {
173
+ continue;
174
+ }
175
+ for (const entry of entries) {
176
+ const entryPath = path.join(current.dir, entry.name);
177
+ if (entry.isFile() && isCodexRolloutFileName(entry.name)) {
178
+ rolloutPaths.push(entryPath);
179
+ continue;
180
+ }
181
+ if (entry.isDirectory() && current.depth < CODEX_SESSION_SCAN_MAX_DEPTH) {
182
+ stack.push({ dir: entryPath, depth: current.depth + 1 });
183
+ }
184
+ }
185
+ }
186
+ return rolloutPaths;
187
+ }
115
188
  function tokenizeCommandArgs(args) {
116
189
  const tokens = [];
117
190
  let current = "";
@@ -196,11 +269,368 @@ function toRecord(value) {
196
269
  }
197
270
  function toNonEmptyString(value) {
198
271
  if (typeof value !== "string") {
272
+ if (typeof value === "number" && Number.isFinite(value)) {
273
+ return String(value);
274
+ }
199
275
  return null;
200
276
  }
201
277
  const trimmed = value.trim();
202
278
  return trimmed.length > 0 ? trimmed : null;
203
279
  }
280
+ function readStringField(record, keys) {
281
+ if (!record) {
282
+ return null;
283
+ }
284
+ for (const key of keys) {
285
+ const value = toNonEmptyString(record[key]);
286
+ if (value) {
287
+ return value;
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ function readNestedRecord(record, keys) {
293
+ if (!record) {
294
+ return null;
295
+ }
296
+ for (const key of keys) {
297
+ const value = toRecord(record[key]);
298
+ if (value) {
299
+ return value;
300
+ }
301
+ }
302
+ return null;
303
+ }
304
+ function readCodexNotificationProvenance(input) {
305
+ const root = toRecord(input);
306
+ const turnRecord = readNestedRecord(root, ["turn"]);
307
+ const messageRecord = readNestedRecord(root, ["msg"]);
308
+ return {
309
+ threadId: readStringField(root, ["threadId", "thread_id"]) ??
310
+ readStringField(turnRecord, ["threadId", "thread_id"]) ??
311
+ readStringField(messageRecord, ["threadId", "thread_id"]),
312
+ turnId: readStringField(root, ["turnId", "turn_id"]) ??
313
+ readStringField(turnRecord, ["turnId", "turn_id"]) ??
314
+ readStringField(messageRecord, ["turnId", "turn_id"]),
315
+ };
316
+ }
317
+ function toTimestampDate(value, fallback) {
318
+ if (typeof value === "number" && Number.isFinite(value)) {
319
+ return new Date(value * 1000);
320
+ }
321
+ if (typeof value === "string") {
322
+ const numeric = Number(value);
323
+ if (Number.isFinite(numeric)) {
324
+ return new Date(numeric * 1000);
325
+ }
326
+ const parsed = Date.parse(value);
327
+ if (!Number.isNaN(parsed)) {
328
+ return new Date(parsed);
329
+ }
330
+ }
331
+ return fallback;
332
+ }
333
+ const codexPersistedThreadMetadataCache = new Map();
334
+ const CODEX_PERSISTED_THREAD_METADATA_CACHE_LIMIT = 512;
335
+ function setCodexPersistedThreadMetadataCache(rolloutPath, entry) {
336
+ if (codexPersistedThreadMetadataCache.has(rolloutPath)) {
337
+ codexPersistedThreadMetadataCache.delete(rolloutPath);
338
+ }
339
+ codexPersistedThreadMetadataCache.set(rolloutPath, entry);
340
+ while (codexPersistedThreadMetadataCache.size >
341
+ CODEX_PERSISTED_THREAD_METADATA_CACHE_LIMIT) {
342
+ const oldestKey = codexPersistedThreadMetadataCache.keys().next().value;
343
+ if (typeof oldestKey !== "string") {
344
+ break;
345
+ }
346
+ codexPersistedThreadMetadataCache.delete(oldestKey);
347
+ }
348
+ }
349
+ function unwrapCodexPersistedThreadRecord(thread) {
350
+ const record = toRecord(thread);
351
+ if (!record) {
352
+ return null;
353
+ }
354
+ if (record.type === "session_meta") {
355
+ const payloadRecord = readNestedRecord(record, ["payload"]);
356
+ if (payloadRecord) {
357
+ return payloadRecord;
358
+ }
359
+ }
360
+ const nestedThreadRecord = readNestedRecord(record, ["thread"]);
361
+ if (nestedThreadRecord) {
362
+ return nestedThreadRecord;
363
+ }
364
+ return record;
365
+ }
366
+ function parseCodexPersistedThreadMetadata(thread, fallbackDate = new Date()) {
367
+ const threadRecord = unwrapCodexPersistedThreadRecord(thread);
368
+ const sourceRecord = readNestedRecord(threadRecord, ["source"]);
369
+ const subAgentRecord = readNestedRecord(sourceRecord, ["subAgent", "sub_agent", "subagent"]) ??
370
+ readNestedRecord(threadRecord, ["subAgent", "sub_agent", "subagent"]);
371
+ const threadSpawnRecord = readNestedRecord(subAgentRecord, ["thread_spawn", "threadSpawn"]) ??
372
+ readNestedRecord(sourceRecord, ["thread_spawn", "threadSpawn"]);
373
+ const createdAt = toTimestampDate(threadRecord?.createdAt, fallbackDate);
374
+ const updatedAt = toTimestampDate(threadRecord?.updatedAt ?? threadRecord?.createdAt, createdAt);
375
+ return {
376
+ threadId: toNonEmptyString(threadRecord?.id),
377
+ cwd: toNonEmptyString(threadRecord?.cwd),
378
+ title: toNonEmptyString(threadRecord?.preview) ?? null,
379
+ createdAt,
380
+ updatedAt,
381
+ parentSessionId: readStringField(threadRecord, [
382
+ "parentThreadId",
383
+ "parent_thread_id",
384
+ "forkedFromId",
385
+ "forked_from_id",
386
+ ]) ??
387
+ readStringField(threadSpawnRecord, [
388
+ "parentThreadId",
389
+ "parent_thread_id",
390
+ "forkedFromId",
391
+ "forked_from_id",
392
+ ]) ??
393
+ readStringField(subAgentRecord, [
394
+ "parentThreadId",
395
+ "parent_thread_id",
396
+ "forkedFromId",
397
+ "forked_from_id",
398
+ ]),
399
+ rootSessionId: readStringField(threadRecord, ["rootThreadId", "root_thread_id"]) ??
400
+ readStringField(threadSpawnRecord, ["rootThreadId", "root_thread_id"]) ??
401
+ readStringField(subAgentRecord, ["rootThreadId", "root_thread_id"]),
402
+ agentId: readStringField(threadRecord, ["agentId", "agent_id"]) ??
403
+ readStringField(threadSpawnRecord, ["agentId", "agent_id", "id"]) ??
404
+ readStringField(subAgentRecord, ["agentId", "agent_id", "id"]),
405
+ agentNickname: readStringField(threadRecord, ["agentNickname", "agent_nickname"]) ??
406
+ readStringField(threadSpawnRecord, [
407
+ "agentNickname",
408
+ "agent_nickname",
409
+ "nickname",
410
+ "name",
411
+ ]) ??
412
+ readStringField(subAgentRecord, [
413
+ "agentNickname",
414
+ "agent_nickname",
415
+ "nickname",
416
+ "name",
417
+ ]),
418
+ agentRole: readStringField(threadRecord, [
419
+ "agentRole",
420
+ "agent_role",
421
+ "agentType",
422
+ "agent_type",
423
+ ]) ??
424
+ readStringField(threadSpawnRecord, [
425
+ "agentRole",
426
+ "agent_role",
427
+ "agentType",
428
+ "agent_type",
429
+ ]) ??
430
+ readStringField(subAgentRecord, [
431
+ "agentRole",
432
+ "agent_role",
433
+ "agentType",
434
+ "agent_type",
435
+ ]),
436
+ model: readStringField(threadRecord, ["model", "modelProvider", "model_provider"]) ??
437
+ readStringField(threadSpawnRecord, ["model", "modelProvider", "model_provider"]) ??
438
+ readStringField(subAgentRecord, ["model", "modelProvider", "model_provider"]),
439
+ };
440
+ }
441
+ function mergeCodexPersistedThreadMetadata(base, incoming) {
442
+ const baseHasFallbackCwd = !base.cwd || base.cwd === process.cwd();
443
+ return {
444
+ threadId: base.threadId ?? incoming.threadId,
445
+ cwd: baseHasFallbackCwd ? incoming.cwd ?? base.cwd : base.cwd ?? incoming.cwd,
446
+ title: base.title ?? incoming.title,
447
+ createdAt: base.createdAt.getTime() <= incoming.createdAt.getTime()
448
+ ? base.createdAt
449
+ : incoming.createdAt,
450
+ updatedAt: base.updatedAt.getTime() >= incoming.updatedAt.getTime()
451
+ ? base.updatedAt
452
+ : incoming.updatedAt,
453
+ parentSessionId: base.parentSessionId ?? incoming.parentSessionId,
454
+ rootSessionId: base.rootSessionId ?? incoming.rootSessionId,
455
+ agentId: base.agentId ?? incoming.agentId,
456
+ agentNickname: base.agentNickname ?? incoming.agentNickname,
457
+ agentRole: base.agentRole ?? incoming.agentRole,
458
+ model: base.model ?? incoming.model,
459
+ };
460
+ }
461
+ async function readCodexPersistedThreadMetadataFromRolloutPath(rolloutPath, logger) {
462
+ if (rolloutPath.endsWith(".json")) {
463
+ try {
464
+ const raw = await fs.readFile(rolloutPath, "utf8");
465
+ return parseCodexPersistedThreadMetadata(JSON.parse(raw));
466
+ }
467
+ catch (error) {
468
+ logger?.debug({ err: error, rolloutPath }, "Failed to read Codex persisted thread metadata from JSON rollout path");
469
+ return null;
470
+ }
471
+ }
472
+ const input = createReadStream(rolloutPath, { encoding: "utf8" });
473
+ const lines = readline.createInterface({
474
+ input,
475
+ crlfDelay: Infinity,
476
+ });
477
+ try {
478
+ for await (const line of lines) {
479
+ const trimmed = line.trim();
480
+ if (!trimmed) {
481
+ continue;
482
+ }
483
+ const parsed = JSON.parse(trimmed);
484
+ return parseCodexPersistedThreadMetadata(parsed);
485
+ }
486
+ }
487
+ catch (error) {
488
+ logger?.debug({ err: error, rolloutPath }, "Failed to read Codex persisted thread metadata from rollout path");
489
+ }
490
+ finally {
491
+ lines.close();
492
+ input.destroy();
493
+ }
494
+ return null;
495
+ }
496
+ async function buildCodexPersistedDescriptorFromRolloutPath(input) {
497
+ let stat;
498
+ try {
499
+ stat = await fs.stat(input.rolloutPath);
500
+ }
501
+ catch {
502
+ return null;
503
+ }
504
+ const cached = codexPersistedThreadMetadataCache.get(input.rolloutPath);
505
+ let metadata = cached && cached.mtimeMs === stat.mtimeMs ? cached.metadata : undefined;
506
+ if (metadata === undefined) {
507
+ metadata = await readCodexPersistedThreadMetadataFromRolloutPath(input.rolloutPath, input.logger);
508
+ setCodexPersistedThreadMetadataCache(input.rolloutPath, {
509
+ mtimeMs: stat.mtimeMs,
510
+ metadata,
511
+ });
512
+ }
513
+ const threadId = metadata?.threadId;
514
+ if (!metadata || !threadId) {
515
+ return null;
516
+ }
517
+ const lastActivityAt = stat.mtime;
518
+ const timeline = input.includeTimeline
519
+ ? await loadCodexPersistedTimeline(threadId, { rolloutPath: input.rolloutPath, sessionRoot: resolveCodexSessionRoot() }, input.logger)
520
+ : [];
521
+ return {
522
+ provider: CODEX_PROVIDER,
523
+ sessionId: threadId,
524
+ cwd: metadata.cwd ?? process.cwd(),
525
+ title: metadata.title,
526
+ createdAt: metadata.createdAt,
527
+ lastActivityAt,
528
+ persistence: {
529
+ provider: CODEX_PROVIDER,
530
+ sessionId: threadId,
531
+ nativeHandle: threadId,
532
+ metadata: {
533
+ provider: CODEX_PROVIDER,
534
+ cwd: metadata.cwd ?? process.cwd(),
535
+ title: metadata.title,
536
+ threadId,
537
+ },
538
+ },
539
+ timeline,
540
+ model: metadata.model,
541
+ parentSessionId: metadata.parentSessionId,
542
+ rootSessionId: metadata.rootSessionId,
543
+ agentId: metadata.agentId,
544
+ agentNickname: metadata.agentNickname,
545
+ agentRole: metadata.agentRole,
546
+ };
547
+ }
548
+ async function requestCodexThreadList(input) {
549
+ try {
550
+ return (await input.client.request("thread/list", {
551
+ limit: input.limit,
552
+ ...(input.sourceKinds ? { sourceKinds: [...input.sourceKinds] } : {}),
553
+ }));
554
+ }
555
+ catch (error) {
556
+ const retrySourceKinds = error instanceof CodexAppServerRequestError && error.method === "thread/list"
557
+ ? resolveCodexThreadListRetrySourceKinds(input.sourceKinds ?? [], error.message)
558
+ : null;
559
+ if (!retrySourceKinds) {
560
+ throw error;
561
+ }
562
+ input.logger.warn({
563
+ rejectedSourceKinds: input.sourceKinds,
564
+ retrySourceKinds,
565
+ error: error instanceof Error ? error.message : String(error),
566
+ }, "Codex thread/list rejected sourceKinds; retrying with supported variants");
567
+ return (await input.client.request("thread/list", {
568
+ limit: input.limit,
569
+ sourceKinds: retrySourceKinds,
570
+ }));
571
+ }
572
+ }
573
+ async function readCodexPersistedDescriptorBySessionId(input) {
574
+ const response = (await input.client.request("thread/read", {
575
+ threadId: input.sessionId,
576
+ includeTurns: input.includeTimeline,
577
+ }));
578
+ const threadRecord = response?.thread ?? null;
579
+ if (!threadRecord) {
580
+ return null;
581
+ }
582
+ let metadata = parseCodexPersistedThreadMetadata(threadRecord);
583
+ const rolloutPath = readStringField(toRecord(threadRecord), ["path"]);
584
+ if (rolloutPath) {
585
+ const rolloutMetadata = await readCodexPersistedThreadMetadataFromRolloutPath(rolloutPath, input.logger);
586
+ if (rolloutMetadata) {
587
+ metadata = mergeCodexPersistedThreadMetadata(metadata, rolloutMetadata);
588
+ }
589
+ }
590
+ const threadId = metadata.threadId ?? input.sessionId.trim();
591
+ if (!threadId) {
592
+ return null;
593
+ }
594
+ const cwd = metadata.cwd ?? process.cwd();
595
+ let timeline = [];
596
+ if (input.includeTimeline) {
597
+ const rolloutTimeline = await loadCodexPersistedTimeline(threadId, undefined, input.logger).catch(() => []);
598
+ const turns = threadRecord.turns ?? [];
599
+ const threadTimeline = readCodexThreadTimelineFromTurns(turns, {
600
+ cwd,
601
+ });
602
+ timeline = mergeCodexThreadReadTimelineWithRolloutSpawnCalls({
603
+ threadTimeline,
604
+ rolloutTimeline,
605
+ });
606
+ }
607
+ return {
608
+ provider: CODEX_PROVIDER,
609
+ sessionId: threadId,
610
+ cwd,
611
+ title: metadata.title,
612
+ createdAt: metadata.createdAt,
613
+ lastActivityAt: metadata.updatedAt,
614
+ persistence: {
615
+ provider: CODEX_PROVIDER,
616
+ sessionId: threadId,
617
+ nativeHandle: threadId,
618
+ metadata: {
619
+ provider: CODEX_PROVIDER,
620
+ cwd,
621
+ title: metadata.title,
622
+ threadId,
623
+ },
624
+ },
625
+ timeline,
626
+ model: metadata.model,
627
+ parentSessionId: metadata.parentSessionId,
628
+ rootSessionId: metadata.rootSessionId,
629
+ agentId: metadata.agentId,
630
+ agentNickname: metadata.agentNickname,
631
+ agentRole: metadata.agentRole,
632
+ };
633
+ }
204
634
  function parseUpdatedQuestionAnswers(updatedInput) {
205
635
  const parsed = {};
206
636
  const root = toRecord(updatedInput);
@@ -586,6 +1016,12 @@ class CodexAppServerClient {
586
1016
  setNotificationHandler(handler) {
587
1017
  this.notificationHandler = handler;
588
1018
  }
1019
+ dispatchNotification(method, params) {
1020
+ if (this.disposed) {
1021
+ return;
1022
+ }
1023
+ this.notificationHandler?.(method, params);
1024
+ }
589
1025
  setRequestHandler(method, handler) {
590
1026
  this.requestHandlers.set(method, handler);
591
1027
  }
@@ -903,6 +1339,25 @@ function normalizeCodexThreadItemType(rawType) {
903
1339
  if (!rawType) {
904
1340
  return rawType;
905
1341
  }
1342
+ const normalized = rawType.replace(/[._-]/g, "").toLowerCase();
1343
+ switch (normalized) {
1344
+ case "usermessage":
1345
+ return "userMessage";
1346
+ case "agentmessage":
1347
+ return "agentMessage";
1348
+ case "reasoning":
1349
+ return "reasoning";
1350
+ case "plan":
1351
+ return "plan";
1352
+ case "commandexecution":
1353
+ return "commandExecution";
1354
+ case "filechange":
1355
+ return "fileChange";
1356
+ case "mcptoolcall":
1357
+ return "mcpToolCall";
1358
+ case "websearch":
1359
+ return "webSearch";
1360
+ }
906
1361
  switch (rawType) {
907
1362
  case "UserMessage":
908
1363
  return "userMessage";
@@ -1213,6 +1668,92 @@ function threadItemToTimeline(item, options) {
1213
1668
  return null;
1214
1669
  }
1215
1670
  }
1671
+ function readCodexThreadTimelineFromTurns(turns, options) {
1672
+ const timeline = [];
1673
+ for (const turn of turns) {
1674
+ for (const item of turn.items ?? []) {
1675
+ const timelineItem = threadItemToTimeline(item, {
1676
+ cwd: options?.cwd ?? null,
1677
+ });
1678
+ if (timelineItem) {
1679
+ timeline.push(timelineItem);
1680
+ }
1681
+ }
1682
+ }
1683
+ return timeline;
1684
+ }
1685
+ function areCodexTimelineItemsEquivalent(left, right) {
1686
+ if (left.type !== right.type) {
1687
+ return false;
1688
+ }
1689
+ switch (left.type) {
1690
+ case "user_message":
1691
+ case "assistant_message":
1692
+ case "reasoning":
1693
+ return left.text === right.text;
1694
+ case "todo":
1695
+ return JSON.stringify(left.items) === JSON.stringify(right.items);
1696
+ case "tool_call":
1697
+ if (left.callId && right.type === "tool_call" && right.callId) {
1698
+ return left.callId === right.callId;
1699
+ }
1700
+ return false;
1701
+ default:
1702
+ return false;
1703
+ }
1704
+ }
1705
+ function isCodexRolloutSpawnAgentToolCall(item) {
1706
+ return item.type === "tool_call" && item.name === "spawn_agent";
1707
+ }
1708
+ function mergeCodexThreadReadTimelineWithRolloutSpawnCalls(input) {
1709
+ if (input.rolloutTimeline.length === 0) {
1710
+ return input.threadTimeline;
1711
+ }
1712
+ const existingSpawnCallIds = new Set(input.threadTimeline.flatMap((item) => isCodexRolloutSpawnAgentToolCall(item) && item.callId ? [item.callId] : []));
1713
+ const queuedSpawnCallIds = new Set();
1714
+ const insertionsByThreadIndex = new Map();
1715
+ let searchIndex = 0;
1716
+ let lastMatchedThreadIndex = -1;
1717
+ for (const rolloutItem of input.rolloutTimeline) {
1718
+ const matchedThreadIndex = input.threadTimeline.findIndex((threadItem, index) => index >= searchIndex && areCodexTimelineItemsEquivalent(threadItem, rolloutItem));
1719
+ if (matchedThreadIndex >= 0) {
1720
+ searchIndex = matchedThreadIndex + 1;
1721
+ lastMatchedThreadIndex = matchedThreadIndex;
1722
+ continue;
1723
+ }
1724
+ if (!isCodexRolloutSpawnAgentToolCall(rolloutItem)) {
1725
+ continue;
1726
+ }
1727
+ if (rolloutItem.callId && existingSpawnCallIds.has(rolloutItem.callId)) {
1728
+ continue;
1729
+ }
1730
+ if (rolloutItem.callId && queuedSpawnCallIds.has(rolloutItem.callId)) {
1731
+ continue;
1732
+ }
1733
+ if (rolloutItem.callId) {
1734
+ queuedSpawnCallIds.add(rolloutItem.callId);
1735
+ }
1736
+ const queue = insertionsByThreadIndex.get(lastMatchedThreadIndex) ?? [];
1737
+ queue.push(rolloutItem);
1738
+ insertionsByThreadIndex.set(lastMatchedThreadIndex, queue);
1739
+ }
1740
+ if (insertionsByThreadIndex.size === 0) {
1741
+ return input.threadTimeline;
1742
+ }
1743
+ const merged = [];
1744
+ const leadingInsertions = insertionsByThreadIndex.get(-1);
1745
+ if (leadingInsertions) {
1746
+ merged.push(...leadingInsertions);
1747
+ }
1748
+ for (const [index, item] of input.threadTimeline.entries()) {
1749
+ merged.push(item);
1750
+ const insertedAfter = insertionsByThreadIndex.get(index);
1751
+ if (insertedAfter) {
1752
+ merged.push(...insertedAfter);
1753
+ }
1754
+ }
1755
+ return merged;
1756
+ }
1216
1757
  function toSandboxPolicy(type, networkAccess) {
1217
1758
  switch (type) {
1218
1759
  case "read-only":
@@ -1234,6 +1775,7 @@ const TurnStartedNotificationSchema = z.object({
1234
1775
  const TurnCompletedNotificationSchema = z.object({
1235
1776
  turn: z
1236
1777
  .object({
1778
+ id: z.string().optional(),
1237
1779
  status: z.string(),
1238
1780
  error: z
1239
1781
  .object({
@@ -1282,6 +1824,10 @@ const CodexEventTurnAbortedNotificationSchema = z.object({
1282
1824
  msg: z
1283
1825
  .object({
1284
1826
  type: z.literal("turn_aborted"),
1827
+ turn_id: z.string().optional(),
1828
+ turnId: z.string().optional(),
1829
+ thread_id: z.string().optional(),
1830
+ threadId: z.string().optional(),
1285
1831
  reason: z.string().optional(),
1286
1832
  })
1287
1833
  .passthrough(),
@@ -1290,6 +1836,12 @@ const CodexEventTaskCompleteNotificationSchema = z.object({
1290
1836
  msg: z
1291
1837
  .object({
1292
1838
  type: z.literal("task_complete"),
1839
+ turn_id: z.string().optional(),
1840
+ turnId: z.string().optional(),
1841
+ thread_id: z.string().optional(),
1842
+ threadId: z.string().optional(),
1843
+ last_agent_message: z.string().nullable().optional(),
1844
+ lastAgentMessage: z.string().nullable().optional(),
1293
1845
  })
1294
1846
  .passthrough(),
1295
1847
  }).passthrough();
@@ -1383,12 +1935,19 @@ const CodexEventTurnDiffNotificationSchema = z.object({
1383
1935
  const CodexNotificationSchema = z.union([
1384
1936
  z.object({ method: z.literal("thread/started"), params: ThreadStartedNotificationSchema }).transform(({ params }) => ({ kind: "thread_started", threadId: params.thread.id })),
1385
1937
  z.object({ method: z.literal("thread/started"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1386
- z.object({ method: z.literal("turn/started"), params: TurnStartedNotificationSchema }).transform(({ params }) => ({ kind: "turn_started", turnId: params.turn.id })),
1938
+ z.object({ method: z.literal("turn/started"), params: TurnStartedNotificationSchema }).transform(({ params }) => ({
1939
+ kind: "turn_started",
1940
+ ...readCodexNotificationProvenance(params),
1941
+ turnId: params.turn.id,
1942
+ })),
1387
1943
  z.object({ method: z.literal("turn/started"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1388
1944
  z.object({ method: z.literal("turn/completed"), params: TurnCompletedNotificationSchema }).transform(({ params }) => ({
1389
1945
  kind: "turn_completed",
1390
1946
  status: params.turn.status,
1391
1947
  errorMessage: params.turn.error?.message ?? null,
1948
+ turnId: params.turn.id ?? null,
1949
+ threadId: readCodexNotificationProvenance(params).threadId,
1950
+ lastAgentMessage: null,
1392
1951
  })),
1393
1952
  z.object({ method: z.literal("turn/completed"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1394
1953
  z.object({ method: z.literal("turn/plan/updated"), params: TurnPlanUpdatedNotificationSchema }).transform(({ params }) => ({
@@ -1403,6 +1962,7 @@ const CodexNotificationSchema = z.union([
1403
1962
  kind: "plan_delta",
1404
1963
  itemId: params.itemId,
1405
1964
  delta: params.delta,
1965
+ ...readCodexNotificationProvenance(params),
1406
1966
  })),
1407
1967
  z.object({ method: z.literal("item/plan/delta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1408
1968
  z.object({ method: z.literal("turn/diff/updated"), params: TurnDiffUpdatedNotificationSchema }).transform(({ params }) => ({ kind: "diff_updated", diff: params.diff })),
@@ -1424,6 +1984,7 @@ const CodexNotificationSchema = z.union([
1424
1984
  kind: "agent_message_delta",
1425
1985
  itemId: params.itemId,
1426
1986
  delta: params.delta,
1987
+ ...readCodexNotificationProvenance(params),
1427
1988
  })),
1428
1989
  z.object({ method: z.literal("item/agentMessage/delta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1429
1990
  z.object({
@@ -1433,11 +1994,22 @@ const CodexNotificationSchema = z.union([
1433
1994
  kind: "reasoning_delta",
1434
1995
  itemId: params.itemId,
1435
1996
  delta: params.delta,
1997
+ ...readCodexNotificationProvenance(params),
1436
1998
  })),
1437
1999
  z.object({ method: z.literal("item/reasoning/summaryTextDelta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1438
- z.object({ method: z.literal("item/completed"), params: ItemLifecycleNotificationSchema }).transform(({ params }) => ({ kind: "item_completed", source: "item", item: params.item })),
2000
+ z.object({ method: z.literal("item/completed"), params: ItemLifecycleNotificationSchema }).transform(({ params }) => ({
2001
+ kind: "item_completed",
2002
+ source: "item",
2003
+ item: params.item,
2004
+ ...readCodexNotificationProvenance(params),
2005
+ })),
1439
2006
  z.object({ method: z.literal("item/completed"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1440
- z.object({ method: z.literal("item/started"), params: ItemLifecycleNotificationSchema }).transform(({ params }) => ({ kind: "item_started", source: "item", item: params.item })),
2007
+ z.object({ method: z.literal("item/started"), params: ItemLifecycleNotificationSchema }).transform(({ params }) => ({
2008
+ kind: "item_started",
2009
+ source: "item",
2010
+ item: params.item,
2011
+ ...readCodexNotificationProvenance(params),
2012
+ })),
1441
2013
  z.object({ method: z.literal("item/started"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1442
2014
  z.object({
1443
2015
  method: z.literal("codex/event/item_started"),
@@ -1446,6 +2018,7 @@ const CodexNotificationSchema = z.union([
1446
2018
  kind: "item_started",
1447
2019
  source: "codex_event",
1448
2020
  item: params.msg.item,
2021
+ ...readCodexNotificationProvenance(params.msg),
1449
2022
  })),
1450
2023
  z.object({ method: z.literal("codex/event/item_started"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1451
2024
  z.object({
@@ -1455,6 +2028,7 @@ const CodexNotificationSchema = z.union([
1455
2028
  kind: "item_completed",
1456
2029
  source: "codex_event",
1457
2030
  item: params.msg.item,
2031
+ ...readCodexNotificationProvenance(params.msg),
1458
2032
  })),
1459
2033
  z.object({ method: z.literal("codex/event/item_completed"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1460
2034
  z.object({
@@ -1465,6 +2039,7 @@ const CodexNotificationSchema = z.union([
1465
2039
  callId: params.msg.call_id ?? null,
1466
2040
  command: params.msg.command ?? null,
1467
2041
  cwd: params.msg.cwd ?? null,
2042
+ ...readCodexNotificationProvenance(params.msg),
1468
2043
  })),
1469
2044
  z.object({ method: z.literal("codex/event/exec_command_begin"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1470
2045
  z.object({
@@ -1483,6 +2058,7 @@ const CodexNotificationSchema = z.union([
1483
2058
  exitCode: params.msg.exit_code ?? params.msg.exitCode ?? null,
1484
2059
  success: params.msg.success ?? null,
1485
2060
  stderr: params.msg.stderr ?? null,
2061
+ ...readCodexNotificationProvenance(params.msg),
1486
2062
  })),
1487
2063
  z.object({ method: z.literal("codex/event/exec_command_end"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1488
2064
  z.object({
@@ -1493,6 +2069,7 @@ const CodexNotificationSchema = z.union([
1493
2069
  callId: params.msg.call_id ?? null,
1494
2070
  stream: params.msg.stream ?? null,
1495
2071
  chunk: params.msg.chunk ?? params.msg.delta ?? null,
2072
+ ...readCodexNotificationProvenance(params.msg),
1496
2073
  })),
1497
2074
  z.object({
1498
2075
  method: z.literal("codex/event/exec_command_output_delta"),
@@ -1505,6 +2082,7 @@ const CodexNotificationSchema = z.union([
1505
2082
  kind: "patch_apply_started",
1506
2083
  callId: params.msg.call_id ?? null,
1507
2084
  changes: params.msg.changes ?? null,
2085
+ ...readCodexNotificationProvenance(params.msg),
1508
2086
  })),
1509
2087
  z.object({ method: z.literal("codex/event/patch_apply_begin"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1510
2088
  z.object({
@@ -1517,6 +2095,7 @@ const CodexNotificationSchema = z.union([
1517
2095
  stdout: params.msg.stdout ?? null,
1518
2096
  stderr: params.msg.stderr ?? null,
1519
2097
  success: params.msg.success ?? null,
2098
+ ...readCodexNotificationProvenance(params.msg),
1520
2099
  })),
1521
2100
  z.object({ method: z.literal("codex/event/patch_apply_end"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1522
2101
  z.object({
@@ -1526,6 +2105,7 @@ const CodexNotificationSchema = z.union([
1526
2105
  kind: "file_change_output_delta",
1527
2106
  itemId: params.itemId,
1528
2107
  delta: params.delta ?? params.chunk ?? null,
2108
+ ...readCodexNotificationProvenance(params),
1529
2109
  })),
1530
2110
  z.object({ method: z.literal("item/fileChange/outputDelta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1531
2111
  z.object({
@@ -1539,19 +2119,25 @@ const CodexNotificationSchema = z.union([
1539
2119
  z.object({
1540
2120
  method: z.literal("codex/event/turn_aborted"),
1541
2121
  params: CodexEventTurnAbortedNotificationSchema,
1542
- }).transform(() => ({
2122
+ }).transform(({ params }) => ({
1543
2123
  kind: "turn_completed",
1544
2124
  status: "interrupted",
1545
2125
  errorMessage: null,
2126
+ turnId: params.msg.turn_id ?? params.msg.turnId ?? null,
2127
+ threadId: params.msg.thread_id ?? params.msg.threadId ?? null,
2128
+ lastAgentMessage: null,
1546
2129
  })),
1547
2130
  z.object({ method: z.literal("codex/event/turn_aborted"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1548
2131
  z.object({
1549
2132
  method: z.literal("codex/event/task_complete"),
1550
2133
  params: CodexEventTaskCompleteNotificationSchema,
1551
- }).transform(() => ({
2134
+ }).transform(({ params }) => ({
1552
2135
  kind: "turn_completed",
1553
2136
  status: "completed",
1554
2137
  errorMessage: null,
2138
+ turnId: params.msg.turn_id ?? params.msg.turnId ?? null,
2139
+ threadId: params.msg.thread_id ?? params.msg.threadId ?? null,
2140
+ lastAgentMessage: params.msg.last_agent_message ?? params.msg.lastAgentMessage ?? null,
1555
2141
  })),
1556
2142
  z.object({ method: z.literal("codex/event/task_complete"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1557
2143
  z.object({ method: z.string(), params: z.unknown() }).transform(({ method, params }) => ({ kind: "unknown_method", method, params })),
@@ -1620,10 +2206,15 @@ export const __codexAppServerInternals = {
1620
2206
  shouldRetryInitializeWithoutExperimentalApi,
1621
2207
  shouldRetryTurnStartWithoutCollaborationMode,
1622
2208
  isCodexAppServerUnsupportedMethodError,
2209
+ parseCodexThreadListAllowedSourceKinds,
2210
+ resolveCodexThreadListRetrySourceKinds,
1623
2211
  readCodexConfiguredDefaults,
2212
+ parseCodexPersistedThreadMetadata,
2213
+ readCodexPersistedThreadMetadataFromRolloutPath,
1624
2214
  formatProposedPlanBlock,
1625
2215
  formatProposedPlanChunk,
1626
2216
  buildCompletedCodexTimelineItem,
2217
+ mergeCodexThreadReadTimelineWithRolloutSpawnCalls,
1627
2218
  normalizeCodexQuestionDescriptors,
1628
2219
  parseUpdatedQuestionAnswers,
1629
2220
  buildCodexPermissionsResponse,
@@ -1637,6 +2228,20 @@ export const __codexAppServerInternals = {
1637
2228
  "mcpServer/elicitation/request",
1638
2229
  ],
1639
2230
  };
2231
+ function normalizeToolCallNameForFallback(name) {
2232
+ return name.trim().replace(/[.\s-]+/g, "_").toLowerCase();
2233
+ }
2234
+ function isShellLikeToolCall(item) {
2235
+ const normalizedName = normalizeToolCallNameForFallback(item.name);
2236
+ return (item.detail.type === "shell" ||
2237
+ normalizedName === "shell" ||
2238
+ normalizedName === "bash");
2239
+ }
2240
+ function isSpawnAgentToolCall(item) {
2241
+ return normalizeToolCallNameForFallback(item.name).endsWith("spawn_agent");
2242
+ }
2243
+ const COMPATIBILITY_FAILED_TOOL_STATUSES = new Set(["failed", "error"]);
2244
+ const COMPATIBILITY_CANCELED_TOOL_STATUSES = new Set(["canceled", "cancelled", "interrupted"]);
1640
2245
  class CodexAppServerAgentSession {
1641
2246
  constructor(config, resumeHandle, logger, spawnAppServer) {
1642
2247
  this.resumeHandle = resumeHandle;
@@ -1659,6 +2264,7 @@ class CodexAppServerAgentSession {
1659
2264
  this.pendingFileChangeOutputDeltas = new Map();
1660
2265
  this.emittedExecCommandStartedCallIds = new Set();
1661
2266
  this.emittedExecCommandCompletedCallIds = new Set();
2267
+ this.emittedRolloutSpawnAgentCallIds = new Set();
1662
2268
  this.emittedItemStartedIds = new Set();
1663
2269
  this.emittedItemCompletedIds = new Set();
1664
2270
  this.warnedUnknownNotificationMethods = new Set();
@@ -1670,6 +2276,7 @@ class CodexAppServerAgentSession {
1670
2276
  this.cachedSkills = [];
1671
2277
  this.turnCompletionInFlight = false;
1672
2278
  this.deferredTurnCompletion = null;
2279
+ this.rolloutSpawnPollTimer = null;
1673
2280
  this.logger = logger.child({ module: "agent", provider: CODEX_PROVIDER });
1674
2281
  if (config.modeId === undefined) {
1675
2282
  throw new Error("Codex agent requires modeId to be specified");
@@ -1802,24 +2409,26 @@ class CodexAppServerAgentSession {
1802
2409
  includeTurns: true,
1803
2410
  }));
1804
2411
  const thread = response?.thread;
1805
- const threadTimeline = [];
1806
- if (thread && Array.isArray(thread.turns)) {
1807
- for (const turn of thread.turns) {
1808
- const items = Array.isArray(turn.items) ? turn.items : [];
1809
- for (const item of items) {
1810
- const timelineItem = threadItemToTimeline(item, {
1811
- cwd: this.config.cwd ?? null,
1812
- });
1813
- if (timelineItem) {
1814
- if (timelineItem.type === "tool_call") {
1815
- this.warnOnIncompleteEditToolCall(timelineItem, "thread_read", item);
1816
- }
1817
- threadTimeline.push(timelineItem);
1818
- }
2412
+ const rawTurns = thread?.turns ?? [];
2413
+ for (const turn of rawTurns) {
2414
+ const items = Array.isArray(turn.items) ? turn.items : [];
2415
+ for (const item of items) {
2416
+ const timelineItem = threadItemToTimeline(item, {
2417
+ cwd: this.config.cwd ?? null,
2418
+ });
2419
+ if (timelineItem?.type === "tool_call") {
2420
+ this.warnOnIncompleteEditToolCall(timelineItem, "thread_read", item);
1819
2421
  }
1820
2422
  }
1821
2423
  }
1822
- const timeline = rolloutTimeline.length > 0 ? rolloutTimeline : threadTimeline;
2424
+ const threadTimeline = readCodexThreadTimelineFromTurns(rawTurns, {
2425
+ cwd: this.config.cwd ?? null,
2426
+ });
2427
+ const timeline = mergeCodexThreadReadTimelineWithRolloutSpawnCalls({
2428
+ threadTimeline,
2429
+ rolloutTimeline,
2430
+ });
2431
+ this.seedRolloutSpawnAgentCallIds(timeline);
1823
2432
  if (timeline.length > 0) {
1824
2433
  this.persistedHistory = timeline;
1825
2434
  this.historyPending = true;
@@ -2489,6 +3098,10 @@ class CodexAppServerAgentSession {
2489
3098
  }
2490
3099
  clearTurnState() {
2491
3100
  this.clearDeferredTurnCompletion();
3101
+ if (this.rolloutSpawnPollTimer) {
3102
+ clearTimeout(this.rolloutSpawnPollTimer);
3103
+ this.rolloutSpawnPollTimer = null;
3104
+ }
2492
3105
  this.currentTurnId = null;
2493
3106
  this.emittedItemStartedIds.clear();
2494
3107
  this.emittedItemCompletedIds.clear();
@@ -2507,6 +3120,128 @@ class CodexAppServerAgentSession {
2507
3120
  this.pendingReasoning.size > 0 ||
2508
3121
  this.pendingPlanTexts.size > 0);
2509
3122
  }
3123
+ emitRolloutSpawnAgentCallsFromTimeline(rolloutTimeline) {
3124
+ let emitted = 0;
3125
+ for (const item of rolloutTimeline) {
3126
+ if (item.type !== "tool_call" || !item.callId) {
3127
+ continue;
3128
+ }
3129
+ if (isSpawnAgentToolCall(item)) {
3130
+ if (this.emittedRolloutSpawnAgentCallIds.has(item.callId)) {
3131
+ continue;
3132
+ }
3133
+ this.emittedRolloutSpawnAgentCallIds.add(item.callId);
3134
+ this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item });
3135
+ emitted += 1;
3136
+ continue;
3137
+ }
3138
+ if (this.hasAlreadyEmittedFallbackToolCall(item)) {
3139
+ continue;
3140
+ }
3141
+ this.markFallbackToolCallEmitted(item);
3142
+ this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item });
3143
+ emitted += 1;
3144
+ }
3145
+ return emitted;
3146
+ }
3147
+ seedRolloutSpawnAgentCallIds(timeline) {
3148
+ for (const item of timeline) {
3149
+ if (item.type === "tool_call" && isSpawnAgentToolCall(item) && item.callId) {
3150
+ this.emittedRolloutSpawnAgentCallIds.add(item.callId);
3151
+ }
3152
+ }
3153
+ }
3154
+ hasAlreadyEmittedFallbackToolCall(item) {
3155
+ if (!item.callId) {
3156
+ return false;
3157
+ }
3158
+ if (isShellLikeToolCall(item)) {
3159
+ if (item.status === "running") {
3160
+ return (this.emittedExecCommandStartedCallIds.has(item.callId) ||
3161
+ this.emittedExecCommandCompletedCallIds.has(item.callId));
3162
+ }
3163
+ return this.emittedExecCommandCompletedCallIds.has(item.callId);
3164
+ }
3165
+ if (item.status === "running") {
3166
+ return (this.emittedItemStartedIds.has(item.callId) ||
3167
+ this.emittedItemCompletedIds.has(item.callId));
3168
+ }
3169
+ return this.emittedItemCompletedIds.has(item.callId);
3170
+ }
3171
+ markFallbackToolCallEmitted(item) {
3172
+ if (!item.callId) {
3173
+ return;
3174
+ }
3175
+ if (isShellLikeToolCall(item)) {
3176
+ if (item.status === "running") {
3177
+ this.emittedExecCommandStartedCallIds.add(item.callId);
3178
+ }
3179
+ else {
3180
+ this.emittedExecCommandCompletedCallIds.add(item.callId);
3181
+ this.emittedExecCommandStartedCallIds.delete(item.callId);
3182
+ }
3183
+ return;
3184
+ }
3185
+ if (item.status === "running") {
3186
+ this.emittedItemStartedIds.add(item.callId);
3187
+ return;
3188
+ }
3189
+ this.emittedItemCompletedIds.add(item.callId);
3190
+ this.emittedItemStartedIds.delete(item.callId);
3191
+ }
3192
+ hasForeignNotificationProvenance(input) {
3193
+ return ((input.threadId !== null &&
3194
+ this.currentThreadId !== null &&
3195
+ input.threadId !== this.currentThreadId) ||
3196
+ (input.turnId !== null &&
3197
+ this.currentTurnId !== null &&
3198
+ input.turnId !== this.currentTurnId));
3199
+ }
3200
+ async emitNewRolloutSpawnAgentCalls() {
3201
+ if (!this.currentThreadId) {
3202
+ return 0;
3203
+ }
3204
+ let rolloutTimeline;
3205
+ try {
3206
+ rolloutTimeline = await loadCodexPersistedTimeline(this.currentThreadId, undefined, this.logger);
3207
+ }
3208
+ catch (error) {
3209
+ this.logger.trace({ error, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Failed to load Codex rollout timeline while reconciling spawn_agent calls");
3210
+ return 0;
3211
+ }
3212
+ return this.emitRolloutSpawnAgentCallsFromTimeline(rolloutTimeline);
3213
+ }
3214
+ scheduleRolloutSpawnAgentPoll() {
3215
+ if (!this.currentThreadId || !this.currentTurnId || this.rolloutSpawnPollTimer) {
3216
+ return;
3217
+ }
3218
+ this.rolloutSpawnPollTimer = setTimeout(() => {
3219
+ this.rolloutSpawnPollTimer = null;
3220
+ void this.pollRolloutSpawnAgentCalls();
3221
+ }, CODEX_ROLLOUT_SPAWN_POLL_MS);
3222
+ }
3223
+ async pollRolloutSpawnAgentCalls() {
3224
+ if (!this.currentThreadId || !this.currentTurnId) {
3225
+ return;
3226
+ }
3227
+ await this.emitNewRolloutSpawnAgentCalls();
3228
+ if (this.currentThreadId && this.currentTurnId) {
3229
+ this.scheduleRolloutSpawnAgentPoll();
3230
+ }
3231
+ }
3232
+ async primeRolloutSpawnAgentPolling() {
3233
+ if (!this.currentThreadId || !this.currentTurnId) {
3234
+ return;
3235
+ }
3236
+ try {
3237
+ const rolloutTimeline = await loadCodexPersistedTimeline(this.currentThreadId, undefined, this.logger);
3238
+ this.emitRolloutSpawnAgentCallsFromTimeline(rolloutTimeline);
3239
+ }
3240
+ catch (error) {
3241
+ this.logger.trace({ error, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Failed to prime Codex rollout spawn_agent polling");
3242
+ }
3243
+ this.scheduleRolloutSpawnAgentPoll();
3244
+ }
2510
3245
  emitCompletedThreadItem(item, source) {
2511
3246
  const timelineItem = buildCompletedCodexTimelineItem(item, {
2512
3247
  cwd: this.config.cwd ?? null,
@@ -2533,6 +3268,67 @@ class CodexAppServerAgentSession {
2533
3268
  }
2534
3269
  return true;
2535
3270
  }
3271
+ emitCompatibilityLifecycleNotifications(parsed) {
3272
+ if (parsed.source !== "item" || !this.client) {
3273
+ return;
3274
+ }
3275
+ const itemType = normalizeCodexThreadItemType(typeof parsed.item.type === "string" ? parsed.item.type : undefined);
3276
+ const lifecycleMethod = parsed.kind === "item_started"
3277
+ ? "codex/event/item_started"
3278
+ : "codex/event/item_completed";
3279
+ if (itemType === "agentMessage" ||
3280
+ itemType === "reasoning" ||
3281
+ itemType === "plan") {
3282
+ this.client.dispatchNotification(lifecycleMethod, {
3283
+ msg: {
3284
+ type: parsed.kind,
3285
+ item: parsed.item,
3286
+ ...(parsed.threadId ? { threadId: parsed.threadId } : {}),
3287
+ ...(parsed.turnId ? { turnId: parsed.turnId } : {}),
3288
+ },
3289
+ });
3290
+ }
3291
+ if (itemType !== "commandExecution") {
3292
+ return;
3293
+ }
3294
+ if (parsed.kind === "item_started") {
3295
+ this.client.dispatchNotification("codex/event/exec_command_begin", {
3296
+ msg: {
3297
+ type: "exec_command_begin",
3298
+ call_id: parsed.item.id ?? null,
3299
+ command: parsed.item.command ?? null,
3300
+ cwd: parsed.item.cwd ?? null,
3301
+ ...(parsed.threadId ? { threadId: parsed.threadId } : {}),
3302
+ ...(parsed.turnId ? { turnId: parsed.turnId } : {}),
3303
+ },
3304
+ });
3305
+ return;
3306
+ }
3307
+ const exitCode = typeof parsed.item.exitCode === "number" ? parsed.item.exitCode : null;
3308
+ const status = typeof parsed.item.status === "string" ? parsed.item.status.trim().toLowerCase() : "";
3309
+ const success = typeof parsed.item.success === "boolean"
3310
+ ? parsed.item.success
3311
+ : status.length > 0
3312
+ ? !COMPATIBILITY_FAILED_TOOL_STATUSES.has(status) &&
3313
+ !COMPATIBILITY_CANCELED_TOOL_STATUSES.has(status)
3314
+ : exitCode === null || exitCode === 0;
3315
+ this.client.dispatchNotification("codex/event/exec_command_end", {
3316
+ msg: {
3317
+ type: "exec_command_end",
3318
+ call_id: parsed.item.id ?? null,
3319
+ command: parsed.item.command ?? null,
3320
+ cwd: parsed.item.cwd ?? null,
3321
+ aggregated_output: typeof parsed.item.aggregatedOutput === "string"
3322
+ ? parsed.item.aggregatedOutput
3323
+ : null,
3324
+ exit_code: exitCode,
3325
+ success,
3326
+ stderr: typeof parsed.item.stderr === "string" ? parsed.item.stderr : null,
3327
+ ...(parsed.threadId ? { threadId: parsed.threadId } : {}),
3328
+ ...(parsed.turnId ? { turnId: parsed.turnId } : {}),
3329
+ },
3330
+ });
3331
+ }
2536
3332
  async reconcileTurnCompletionFromThreadRead() {
2537
3333
  if (!this.client || !this.currentThreadId) {
2538
3334
  return 0;
@@ -2620,6 +3416,15 @@ class CodexAppServerAgentSession {
2620
3416
  async finalizeTurnCompletion(parsed) {
2621
3417
  let terminalEventEmitted = false;
2622
3418
  try {
3419
+ if (parsed.status === "completed") {
3420
+ try {
3421
+ const emittedSpawnCalls = await this.emitNewRolloutSpawnAgentCalls();
3422
+ this.logger.trace({ emittedSpawnCalls, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Reconciled Codex spawn_agent rollout calls before turn completion");
3423
+ }
3424
+ catch (error) {
3425
+ this.logger.warn({ error, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Failed to reconcile Codex spawn_agent rollout calls before turn completion");
3426
+ }
3427
+ }
2623
3428
  if (parsed.status === "completed" && this.hasPendingBufferedCompletionContent()) {
2624
3429
  this.logger.trace({
2625
3430
  pendingAgentMessages: this.pendingAgentMessages.size,
@@ -2655,17 +3460,57 @@ class CodexAppServerAgentSession {
2655
3460
  handleNotification(method, params) {
2656
3461
  const parsed = CodexNotificationSchema.parse({ method, params });
2657
3462
  if (parsed.kind === "thread_started") {
3463
+ if (this.currentThreadId && this.currentThreadId !== parsed.threadId) {
3464
+ this.logger.trace({
3465
+ currentThreadId: this.currentThreadId,
3466
+ ignoredThreadId: parsed.threadId,
3467
+ turnId: this.currentTurnId,
3468
+ }, "Ignoring Codex thread_started for a different thread while session is already bound");
3469
+ return;
3470
+ }
2658
3471
  this.currentThreadId = parsed.threadId;
2659
3472
  this.emitEvent({ type: "thread_started", provider: CODEX_PROVIDER, sessionId: parsed.threadId });
2660
3473
  return;
2661
3474
  }
2662
3475
  if (parsed.kind === "turn_started") {
3476
+ if (parsed.threadId && this.currentThreadId && parsed.threadId !== this.currentThreadId) {
3477
+ this.logger.trace({
3478
+ currentThreadId: this.currentThreadId,
3479
+ ignoredThreadId: parsed.threadId,
3480
+ currentTurnId: this.currentTurnId,
3481
+ ignoredTurnId: parsed.turnId,
3482
+ }, "Ignoring Codex turn_started for a different thread");
3483
+ return;
3484
+ }
3485
+ if (this.currentTurnId && this.currentTurnId !== parsed.turnId) {
3486
+ if (this.deferredTurnCompletion) {
3487
+ this.discardDeferredTurnCompletion("turn_started");
3488
+ }
3489
+ }
2663
3490
  this.clearTurnState();
2664
3491
  this.currentTurnId = parsed.turnId;
2665
3492
  this.emitEvent({ type: "turn_started", provider: CODEX_PROVIDER });
3493
+ void this.primeRolloutSpawnAgentPolling();
2666
3494
  return;
2667
3495
  }
2668
3496
  if (parsed.kind === "turn_completed") {
3497
+ if (parsed.threadId && this.currentThreadId && parsed.threadId !== this.currentThreadId) {
3498
+ this.logger.trace({
3499
+ currentThreadId: this.currentThreadId,
3500
+ ignoredThreadId: parsed.threadId,
3501
+ turnId: parsed.turnId,
3502
+ currentTurnId: this.currentTurnId,
3503
+ }, "Ignoring Codex terminal event for a different thread");
3504
+ return;
3505
+ }
3506
+ if (parsed.turnId && this.currentTurnId && parsed.turnId !== this.currentTurnId) {
3507
+ this.logger.trace({
3508
+ currentTurnId: this.currentTurnId,
3509
+ ignoredTurnId: parsed.turnId,
3510
+ threadId: this.currentThreadId,
3511
+ }, "Ignoring Codex terminal event for a different turn");
3512
+ return;
3513
+ }
2669
3514
  // A later failed/interrupted terminal status must replace a deferred
2670
3515
  // "completed" status so we do not hide a real terminal error behind
2671
3516
  // the settle window.
@@ -2704,6 +3549,9 @@ class CodexAppServerAgentSession {
2704
3549
  return;
2705
3550
  }
2706
3551
  if (parsed.kind === "plan_delta") {
3552
+ if (this.hasForeignNotificationProvenance(parsed)) {
3553
+ return;
3554
+ }
2707
3555
  this.noteTurnActivityAfterPermission("plan_delta");
2708
3556
  const previous = this.pendingPlanTexts.get(parsed.itemId) ?? "";
2709
3557
  const next = previous + parsed.delta;
@@ -2746,12 +3594,18 @@ class CodexAppServerAgentSession {
2746
3594
  return;
2747
3595
  }
2748
3596
  if (parsed.kind === "agent_message_delta") {
3597
+ if (this.hasForeignNotificationProvenance(parsed)) {
3598
+ return;
3599
+ }
2749
3600
  this.noteTurnActivityAfterPermission("agent_message_delta");
2750
3601
  const prev = this.pendingAgentMessages.get(parsed.itemId) ?? "";
2751
3602
  this.pendingAgentMessages.set(parsed.itemId, prev + parsed.delta);
2752
3603
  return;
2753
3604
  }
2754
3605
  if (parsed.kind === "reasoning_delta") {
3606
+ if (this.hasForeignNotificationProvenance(parsed)) {
3607
+ return;
3608
+ }
2755
3609
  this.noteTurnActivityAfterPermission("reasoning_delta");
2756
3610
  const prev = this.pendingReasoning.get(parsed.itemId) ?? [];
2757
3611
  prev.push(parsed.delta);
@@ -2759,17 +3613,29 @@ class CodexAppServerAgentSession {
2759
3613
  return;
2760
3614
  }
2761
3615
  if (parsed.kind === "exec_command_output_delta") {
3616
+ if (this.hasForeignNotificationProvenance(parsed)) {
3617
+ return;
3618
+ }
2762
3619
  this.noteTurnActivityAfterPermission("exec_command_output_delta");
2763
3620
  this.appendOutputDeltaChunk(this.pendingCommandOutputDeltas, parsed.callId, parsed.chunk, { decodeBase64: true });
2764
3621
  return;
2765
3622
  }
2766
3623
  if (parsed.kind === "file_change_output_delta") {
3624
+ if (this.hasForeignNotificationProvenance(parsed)) {
3625
+ return;
3626
+ }
2767
3627
  this.noteTurnActivityAfterPermission("file_change_output_delta");
2768
3628
  this.appendOutputDeltaChunk(this.pendingFileChangeOutputDeltas, parsed.itemId, parsed.delta);
2769
3629
  return;
2770
3630
  }
2771
3631
  if (parsed.kind === "exec_command_started") {
3632
+ if (this.hasForeignNotificationProvenance(parsed)) {
3633
+ return;
3634
+ }
2772
3635
  this.noteTurnActivityAfterPermission("exec_command_started");
3636
+ if (parsed.callId && this.emittedExecCommandStartedCallIds.has(parsed.callId)) {
3637
+ return;
3638
+ }
2773
3639
  if (parsed.callId) {
2774
3640
  this.emittedExecCommandStartedCallIds.add(parsed.callId);
2775
3641
  this.pendingCommandOutputDeltas.delete(parsed.callId);
@@ -2786,7 +3652,13 @@ class CodexAppServerAgentSession {
2786
3652
  return;
2787
3653
  }
2788
3654
  if (parsed.kind === "exec_command_completed") {
3655
+ if (this.hasForeignNotificationProvenance(parsed)) {
3656
+ return;
3657
+ }
2789
3658
  this.noteTurnActivityAfterPermission("exec_command_completed");
3659
+ if (parsed.callId && this.emittedExecCommandCompletedCallIds.has(parsed.callId)) {
3660
+ return;
3661
+ }
2790
3662
  const bufferedOutput = this.consumeOutputDelta(this.pendingCommandOutputDeltas, parsed.callId);
2791
3663
  const timelineItem = mapCodexExecNotificationToToolCall({
2792
3664
  callId: parsed.callId,
@@ -2805,6 +3677,9 @@ class CodexAppServerAgentSession {
2805
3677
  return;
2806
3678
  }
2807
3679
  if (parsed.kind === "patch_apply_started") {
3680
+ if (this.hasForeignNotificationProvenance(parsed)) {
3681
+ return;
3682
+ }
2808
3683
  this.noteTurnActivityAfterPermission("patch_apply_started");
2809
3684
  if (parsed.callId) {
2810
3685
  this.pendingFileChangeOutputDeltas.delete(parsed.callId);
@@ -2825,6 +3700,9 @@ class CodexAppServerAgentSession {
2825
3700
  return;
2826
3701
  }
2827
3702
  if (parsed.kind === "patch_apply_completed") {
3703
+ if (this.hasForeignNotificationProvenance(parsed)) {
3704
+ return;
3705
+ }
2828
3706
  this.noteTurnActivityAfterPermission("patch_apply_completed");
2829
3707
  const bufferedOutput = this.consumeOutputDelta(this.pendingFileChangeOutputDeltas, parsed.callId);
2830
3708
  const timelineItem = mapCodexPatchNotificationToToolCall({
@@ -2847,6 +3725,10 @@ class CodexAppServerAgentSession {
2847
3725
  return;
2848
3726
  }
2849
3727
  if (parsed.kind === "item_completed") {
3728
+ this.emitCompatibilityLifecycleNotifications(parsed);
3729
+ if (this.hasForeignNotificationProvenance(parsed)) {
3730
+ return;
3731
+ }
2850
3732
  this.noteTurnActivityAfterPermission("item_completed");
2851
3733
  // Codex emits mirrored lifecycle notifications via both `codex/event/item_*`
2852
3734
  // and canonical `item/*`. We render only the canonical channel to avoid
@@ -2858,6 +3740,11 @@ class CodexAppServerAgentSession {
2858
3740
  return;
2859
3741
  }
2860
3742
  if (parsed.kind === "item_started") {
3743
+ this.emitCompatibilityLifecycleNotifications(parsed);
3744
+ const normalizedItemType = normalizeCodexThreadItemType(typeof parsed.item.type === "string" ? parsed.item.type : undefined);
3745
+ if (this.hasForeignNotificationProvenance(parsed)) {
3746
+ return;
3747
+ }
2861
3748
  this.noteTurnActivityAfterPermission("item_started");
2862
3749
  if (parsed.source === "codex_event") {
2863
3750
  return;
@@ -2867,7 +3754,6 @@ class CodexAppServerAgentSession {
2867
3754
  cwd: this.config.cwd ?? null,
2868
3755
  });
2869
3756
  if (timelineItem && timelineItem.type === "tool_call") {
2870
- const normalizedItemType = normalizeCodexThreadItemType(typeof parsed.item.type === "string" ? parsed.item.type : undefined);
2871
3757
  const itemId = parsed.item.id;
2872
3758
  if (normalizedItemType === "commandExecution") {
2873
3759
  const callId = timelineItem.callId || itemId;
@@ -3226,43 +4112,61 @@ export class CodexAppServerAgentClient {
3226
4112
  });
3227
4113
  client.notify("initialized", {});
3228
4114
  const limit = options?.limit ?? 20;
3229
- const response = (await client.request("thread/list", { limit }));
4115
+ const includeTimeline = options?.includeTimeline !== false;
4116
+ const response = await requestCodexThreadList({
4117
+ client,
4118
+ logger: this.logger,
4119
+ limit,
4120
+ sourceKinds: CODEX_THREAD_LIST_SOURCE_KINDS,
4121
+ });
3230
4122
  const threads = Array.isArray(response?.data) ? response.data : [];
3231
- const descriptors = [];
4123
+ const descriptorsBySessionId = new Map();
4124
+ const threadListSessionIds = new Set();
3232
4125
  for (const thread of threads.slice(0, limit)) {
3233
- const threadId = thread.id;
3234
- const cwd = thread.cwd ?? process.cwd();
3235
- const title = thread.preview ?? null;
3236
- let timeline = [];
3237
- try {
3238
- const rolloutTimeline = await loadCodexPersistedTimeline(threadId, undefined, this.logger);
3239
- const read = (await client.request("thread/read", {
3240
- threadId,
3241
- includeTurns: true,
3242
- }));
3243
- const turns = read.thread?.turns ?? [];
3244
- const itemsFromThreadRead = [];
3245
- for (const turn of turns) {
3246
- for (const item of turn.items ?? []) {
3247
- const timelineItem = threadItemToTimeline(item, { cwd });
3248
- if (timelineItem)
3249
- itemsFromThreadRead.push(timelineItem);
3250
- }
4126
+ const threadRecord = toRecord(thread);
4127
+ const rolloutPath = readStringField(threadRecord, ["path"]);
4128
+ let metadata = parseCodexPersistedThreadMetadata(thread);
4129
+ if (rolloutPath) {
4130
+ const rolloutMetadata = await readCodexPersistedThreadMetadataFromRolloutPath(rolloutPath, this.logger);
4131
+ if (rolloutMetadata) {
4132
+ metadata = mergeCodexPersistedThreadMetadata(metadata, rolloutMetadata);
3251
4133
  }
3252
- timeline =
3253
- rolloutTimeline.length > 0
3254
- ? rolloutTimeline
3255
- : itemsFromThreadRead;
3256
4134
  }
3257
- catch {
3258
- timeline = [];
4135
+ const threadId = metadata.threadId;
4136
+ if (!threadId) {
4137
+ continue;
3259
4138
  }
3260
- descriptors.push({
4139
+ threadListSessionIds.add(threadId);
4140
+ const cwd = metadata.cwd ?? process.cwd();
4141
+ const title = metadata.title;
4142
+ const createdAt = metadata.createdAt;
4143
+ const updatedAt = metadata.updatedAt;
4144
+ let timeline = [];
4145
+ if (includeTimeline) {
4146
+ try {
4147
+ const rolloutTimeline = await loadCodexPersistedTimeline(threadId, undefined, this.logger);
4148
+ const read = (await client.request("thread/read", {
4149
+ threadId,
4150
+ includeTurns: true,
4151
+ }));
4152
+ const turns = read.thread?.turns ?? [];
4153
+ const itemsFromThreadRead = readCodexThreadTimelineFromTurns(turns, { cwd });
4154
+ timeline = mergeCodexThreadReadTimelineWithRolloutSpawnCalls({
4155
+ threadTimeline: itemsFromThreadRead,
4156
+ rolloutTimeline,
4157
+ });
4158
+ }
4159
+ catch {
4160
+ timeline = [];
4161
+ }
4162
+ }
4163
+ descriptorsBySessionId.set(threadId, {
3261
4164
  provider: CODEX_PROVIDER,
3262
4165
  sessionId: threadId,
3263
4166
  cwd,
3264
4167
  title,
3265
- lastActivityAt: new Date((thread.updatedAt ?? thread.createdAt ?? 0) * 1000),
4168
+ createdAt,
4169
+ lastActivityAt: updatedAt,
3266
4170
  persistence: {
3267
4171
  provider: CODEX_PROVIDER,
3268
4172
  sessionId: threadId,
@@ -3275,9 +4179,77 @@ export class CodexAppServerAgentClient {
3275
4179
  },
3276
4180
  },
3277
4181
  timeline,
4182
+ model: metadata.model,
4183
+ parentSessionId: metadata.parentSessionId,
4184
+ rootSessionId: metadata.rootSessionId,
4185
+ agentId: metadata.agentId,
4186
+ agentNickname: metadata.agentNickname,
4187
+ agentRole: metadata.agentRole,
3278
4188
  });
3279
4189
  }
3280
- return descriptors;
4190
+ const sessionRoot = resolveCodexSessionRoot();
4191
+ const rolloutPaths = await listCodexRolloutPaths(sessionRoot);
4192
+ const rolloutPathBySessionId = new Map();
4193
+ for (const rolloutPath of rolloutPaths) {
4194
+ const descriptor = await buildCodexPersistedDescriptorFromRolloutPath({
4195
+ rolloutPath,
4196
+ includeTimeline: false,
4197
+ logger: this.logger,
4198
+ });
4199
+ if (!descriptor) {
4200
+ continue;
4201
+ }
4202
+ rolloutPathBySessionId.set(descriptor.sessionId, rolloutPath);
4203
+ if (!descriptorsBySessionId.has(descriptor.sessionId)) {
4204
+ if (!includeTimeline && !descriptor.parentSessionId) {
4205
+ continue;
4206
+ }
4207
+ descriptorsBySessionId.set(descriptor.sessionId, descriptor);
4208
+ }
4209
+ }
4210
+ const descriptors = Array.from(descriptorsBySessionId.values())
4211
+ .sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
4212
+ .slice(0, limit);
4213
+ if (!includeTimeline) {
4214
+ return descriptors;
4215
+ }
4216
+ return await Promise.all(descriptors.map(async (descriptor) => {
4217
+ if (threadListSessionIds.has(descriptor.sessionId)) {
4218
+ return descriptor;
4219
+ }
4220
+ const rolloutPath = rolloutPathBySessionId.get(descriptor.sessionId);
4221
+ if (!rolloutPath) {
4222
+ return descriptor;
4223
+ }
4224
+ return ((await buildCodexPersistedDescriptorFromRolloutPath({
4225
+ rolloutPath,
4226
+ includeTimeline: true,
4227
+ logger: this.logger,
4228
+ })) ?? descriptor);
4229
+ }));
4230
+ }
4231
+ finally {
4232
+ await client.dispose();
4233
+ }
4234
+ }
4235
+ async readPersistedAgent(options) {
4236
+ const sessionId = options.sessionId.trim();
4237
+ if (!sessionId) {
4238
+ return null;
4239
+ }
4240
+ const child = this.spawnAppServer();
4241
+ const client = new CodexAppServerClient(child, this.logger);
4242
+ try {
4243
+ await client.request("initialize", {
4244
+ clientInfo: { name: "junction", title: "Junction", version: "0.0.0" },
4245
+ });
4246
+ client.notify("initialized", {});
4247
+ return await readCodexPersistedDescriptorBySessionId({
4248
+ client,
4249
+ sessionId,
4250
+ includeTimeline: options.includeTimeline !== false,
4251
+ logger: this.logger,
4252
+ });
3281
4253
  }
3282
4254
  finally {
3283
4255
  await client.dispose();