sidekick-shared 0.29.1 → 0.30.0

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 (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +171 -60
  3. package/dist/aggregation/EventAggregator.d.ts +79 -4
  4. package/dist/aggregation/EventAggregator.js +391 -37
  5. package/dist/aggregation/types.d.ts +27 -3
  6. package/dist/browser.d.ts +7 -3
  7. package/dist/browser.js +23 -3
  8. package/dist/context/sessionContext.js +5 -1
  9. package/dist/esm/aggregation/EventAggregator.js +392 -38
  10. package/dist/esm/browser.js +3 -1
  11. package/dist/esm/context/sessionContext.js +6 -2
  12. package/dist/esm/fileContinuity.js +149 -0
  13. package/dist/esm/index.js +5 -2
  14. package/dist/esm/node.js +2 -0
  15. package/dist/esm/parsers/codexParser.js +192 -3
  16. package/dist/esm/parsers/subagentEndState.js +61 -0
  17. package/dist/esm/parsers/subagentScanner.js +4 -0
  18. package/dist/esm/parsers/subagentTraceParser.js +6 -0
  19. package/dist/esm/promptHistory.js +22 -11
  20. package/dist/esm/providers/claudeCode.js +50 -27
  21. package/dist/esm/providers/codex.js +153 -163
  22. package/dist/esm/providers/codexCompressedReader.js +184 -0
  23. package/dist/esm/providers/codexDatabase.js +47 -2
  24. package/dist/esm/providers/codexRollouts.js +20 -2
  25. package/dist/esm/providers/codexSubagents.js +456 -0
  26. package/dist/esm/providers/compressedRollouts.js +330 -0
  27. package/dist/esm/providers/factory.js +4 -1
  28. package/dist/esm/providers/rolloutWalker.js +32 -4
  29. package/dist/esm/schemas/sessionEvent.js +32 -4
  30. package/dist/esm/sessionMonitor.js +7 -1
  31. package/dist/esm/sessionPreviews.js +9 -0
  32. package/dist/esm/sessionPromptHistory.js +5 -3
  33. package/dist/esm/subagentActivity.js +125 -0
  34. package/dist/esm/subagentDisplayState.js +81 -0
  35. package/dist/esm/subagentSpawn.js +206 -6
  36. package/dist/esm/testing/codexSubagentFixtures.js +336 -0
  37. package/dist/esm/toolCallOrigins.js +17 -10
  38. package/dist/esm/toolCategory.js +6 -2
  39. package/dist/esm/transcript.js +6 -2
  40. package/dist/esm/turns/assistantTurn.js +6 -3
  41. package/dist/esm/usage/usageEvents.js +4 -1
  42. package/dist/esm/watchers/eventBridge.js +31 -1
  43. package/dist/esm/watchers/jsonlTail.js +11 -42
  44. package/dist/esm/watchers/jsonlWatcher.js +19 -9
  45. package/dist/esm/watchers/providerReaderWatcher.js +5 -4
  46. package/dist/fileContinuity.d.ts +95 -0
  47. package/dist/fileContinuity.js +187 -0
  48. package/dist/index.d.ts +14 -7
  49. package/dist/index.js +39 -10
  50. package/dist/node.d.ts +4 -0
  51. package/dist/node.js +16 -2
  52. package/dist/parsers/codexParser.d.ts +44 -0
  53. package/dist/parsers/codexParser.js +192 -3
  54. package/dist/parsers/subagentEndState.d.ts +20 -0
  55. package/dist/parsers/subagentEndState.js +65 -0
  56. package/dist/parsers/subagentScanner.js +4 -0
  57. package/dist/parsers/subagentTraceParser.d.ts +8 -0
  58. package/dist/parsers/subagentTraceParser.js +6 -0
  59. package/dist/promptHistory.d.ts +13 -3
  60. package/dist/promptHistory.js +22 -11
  61. package/dist/providers/claudeCode.js +50 -27
  62. package/dist/providers/codex.d.ts +35 -5
  63. package/dist/providers/codex.js +150 -160
  64. package/dist/providers/codexCompressedReader.d.ts +58 -0
  65. package/dist/providers/codexCompressedReader.js +221 -0
  66. package/dist/providers/codexDatabase.d.ts +25 -0
  67. package/dist/providers/codexDatabase.js +48 -2
  68. package/dist/providers/codexRollouts.d.ts +16 -2
  69. package/dist/providers/codexRollouts.js +20 -2
  70. package/dist/providers/codexSubagents.d.ts +66 -0
  71. package/dist/providers/codexSubagents.js +495 -0
  72. package/dist/providers/compressedRollouts.d.ts +114 -0
  73. package/dist/providers/compressedRollouts.js +380 -0
  74. package/dist/providers/factory.d.ts +5 -0
  75. package/dist/providers/factory.js +4 -1
  76. package/dist/providers/rolloutWalker.d.ts +15 -2
  77. package/dist/providers/rolloutWalker.js +33 -4
  78. package/dist/providers/types.d.ts +22 -1
  79. package/dist/schemas/sessionEvent.d.ts +26 -2
  80. package/dist/schemas/sessionEvent.js +31 -3
  81. package/dist/sessionMonitor.d.ts +6 -0
  82. package/dist/sessionMonitor.js +7 -1
  83. package/dist/sessionPreviews.js +9 -0
  84. package/dist/sessionPromptHistory.d.ts +5 -0
  85. package/dist/sessionPromptHistory.js +5 -36
  86. package/dist/subagentActivity.d.ts +91 -0
  87. package/dist/subagentActivity.js +132 -0
  88. package/dist/subagentDisplayState.d.ts +81 -0
  89. package/dist/subagentDisplayState.js +86 -0
  90. package/dist/subagentSpawn.d.ts +100 -5
  91. package/dist/subagentSpawn.js +218 -7
  92. package/dist/testing/codexSubagentFixtures.d.ts +48 -0
  93. package/dist/testing/codexSubagentFixtures.js +344 -0
  94. package/dist/toolCallOrigins.d.ts +5 -0
  95. package/dist/toolCallOrigins.js +17 -10
  96. package/dist/toolCategory.js +6 -2
  97. package/dist/transcript.js +5 -1
  98. package/dist/turns/assistantTurn.d.ts +4 -1
  99. package/dist/turns/assistantTurn.js +5 -2
  100. package/dist/types/codex.d.ts +71 -2
  101. package/dist/types/sessionEvent.d.ts +38 -3
  102. package/dist/usage/usageEvents.d.ts +4 -1
  103. package/dist/usage/usageEvents.js +4 -1
  104. package/dist/watchers/eventBridge.js +30 -0
  105. package/dist/watchers/jsonlTail.js +11 -42
  106. package/dist/watchers/jsonlWatcher.d.ts +5 -1
  107. package/dist/watchers/jsonlWatcher.js +18 -8
  108. package/dist/watchers/providerReaderWatcher.js +5 -4
  109. package/package.json +1 -1
@@ -5,7 +5,9 @@
5
5
  * Implements SessionProviderBase for monitoring Codex CLI sessions.
6
6
  * Codex stores sessions as JSONL rollout files in:
7
7
  * ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl
8
- * with an optional SQLite index at ~/.codex/state.sqlite.
8
+ * with an optional SQLite index at ~/.codex/state.sqlite. Older rollouts are
9
+ * compressed in place to `….jsonl.zst`; those are read only with the
10
+ * `includeCompressedRollouts` option (see `providers/compressedRollouts`).
9
11
  *
10
12
  * Ported from sidekick-vscode/src/services/providers/CodexSessionProvider.ts
11
13
  */
@@ -51,17 +53,20 @@ const sessionContext_1 = require("../context/sessionContext");
51
53
  const sessionStats_1 = require("../sessionStats");
52
54
  const codexParser_1 = require("../parsers/codexParser");
53
55
  const codexDatabase_1 = require("./codexDatabase");
54
- const subagentStatsCache_1 = require("../parsers/subagentStatsCache");
55
56
  const rolloutWalker_1 = require("./rolloutWalker");
57
+ const compressedRollouts_1 = require("./compressedRollouts");
58
+ const codexCompressedReader_1 = require("./codexCompressedReader");
59
+ const codexSubagents_1 = require("./codexSubagents");
60
+ const codexRolloutOrigin_1 = require("../parsers/codexRolloutOrigin");
56
61
  const codexPaths_1 = require("../codexPaths");
57
62
  const codexRollouts_1 = require("./codexRollouts");
58
63
  Object.defineProperty(exports, "findCodexRolloutFile", { enumerable: true, get: function () { return codexRollouts_1.findCodexRolloutFile; } });
59
64
  const directoryListingCache_1 = require("./directoryListingCache");
65
+ const fileContinuity_1 = require("../fileContinuity");
60
66
  const diagnostics_1 = require("./diagnostics");
61
67
  const modelContext_1 = require("../modelContext");
62
68
  const observedContextWindows_1 = require("../observedContextWindows");
63
69
  const usageNormalization_1 = require("../usageNormalization");
64
- const subagentUsage_1 = require("../usage/subagentUsage");
65
70
  // ---------------------------------------------------------------------------
66
71
  // Helper functions
67
72
  // ---------------------------------------------------------------------------
@@ -74,8 +79,8 @@ function truncate(text, maxLen) {
74
79
  return trimmed;
75
80
  }
76
81
  /** Every rollout under a directory, newest first (one capped walk). */
77
- function findRolloutFiles(dir) {
78
- return (0, rolloutWalker_1.walkRolloutFiles)([dir]);
82
+ function findRolloutFiles(dir, includeCompressed = false) {
83
+ return (0, rolloutWalker_1.walkRolloutFiles)([dir], { includeCompressed });
79
84
  }
80
85
  function hasRolloutFile(dir) {
81
86
  return (0, rolloutWalker_1.walkRolloutFiles)([dir], { limit: 1 }).length > 0;
@@ -84,6 +89,10 @@ function hasRolloutFile(dir) {
84
89
  function findRolloutFilesInConfiguredHomes(options = {}) {
85
90
  return (0, rolloutWalker_1.walkRolloutFiles)((0, codexRollouts_1.getSessionsDirs)(), options);
86
91
  }
92
+ /** Whether a rollout's session_meta says a subagent wrote it. */
93
+ function isSubagentRollout(rolloutPath) {
94
+ return (0, codexRolloutOrigin_1.codexRolloutOrigin)((0, codexRollouts_1.readSessionMeta)(rolloutPath)).isSubagent;
95
+ }
87
96
  /** Pick the best sessions directory for UI/search entrypoints. */
88
97
  function getSessionsDir() {
89
98
  const dirs = (0, codexPaths_1.getCodexSessionsDirs)(undefined, { includeArchived: false });
@@ -160,19 +169,16 @@ function extractFirstUserMessageFromLines(lines) {
160
169
  return null;
161
170
  }
162
171
  async function extractFirstUserMessageAsync(rolloutPath) {
163
- let handle = null;
164
172
  try {
165
- handle = await fs.promises.open(rolloutPath, 'r');
166
- const buffer = Buffer.alloc(256 * 1024);
167
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
168
- return extractFirstUserMessageFromLines(buffer.toString('utf8', 0, bytesRead).split('\n').slice(0, 20));
173
+ // A compressed rollout's head is decompressed; a plain one is read as is.
174
+ const head = await (0, compressedRollouts_1.readRolloutHeadAsync)(rolloutPath, 256 * 1024);
175
+ if (head.length === 0)
176
+ return null;
177
+ return extractFirstUserMessageFromLines(head.toString('utf8').split('\n').slice(0, 20));
169
178
  }
170
179
  catch {
171
180
  return null;
172
181
  }
173
- finally {
174
- await handle?.close().catch(() => undefined);
175
- }
176
182
  }
177
183
  /** Extract searchable text from a rollout line. */
178
184
  function extractSearchableText(line) {
@@ -304,7 +310,15 @@ class CodexReader {
304
310
  /** Cursor after the last complete line, safe to persist across restarts. */
305
311
  committedPosition = 0;
306
312
  lineBuffer = '';
307
- _wasTruncated = false;
313
+ /** Why the last `readNew()` started over from offset 0, if it did. */
314
+ _lastReset = null;
315
+ /**
316
+ * A restart found while settling a seek (the file is now shorter than the
317
+ * position sought); the next `readNew()` reports it.
318
+ */
319
+ pendingRestart = null;
320
+ /** Whether the rollout is still the one the positions refer to. */
321
+ continuity = new fileContinuity_1.FileContinuity();
308
322
  decoder = new string_decoder_1.StringDecoder('utf8');
309
323
  /**
310
324
  * Position from `seekTo()` whose prefix still has to be parsed for model,
@@ -317,7 +331,7 @@ class CodexReader {
317
331
  this.onContextWindowLimit = onContextWindowLimit;
318
332
  this.onRateLimits = onRateLimits;
319
333
  this.onTokenUsage = onTokenUsage;
320
- this.parser = new codexParser_1.CodexRolloutParser();
334
+ this.parser = new codexParser_1.CodexRolloutParser({ includeSubagentActivity: true });
321
335
  }
322
336
  /** Get the parser's session metadata. */
323
337
  getSessionMeta() {
@@ -326,36 +340,32 @@ class CodexReader {
326
340
  }
327
341
  readNew(endPosition) {
328
342
  this.settleSeek();
329
- this._wasTruncated = false;
343
+ this._lastReset = null;
330
344
  const events = [];
345
+ let fd = null;
331
346
  try {
332
- if (!fs.existsSync(this.rolloutPath))
333
- return [];
334
- const stats = fs.statSync(this.rolloutPath);
335
- const currentSize = Math.min(stats.size, endPosition ?? stats.size);
336
- // Handle truncation
337
- if (currentSize < this.filePosition) {
338
- this._wasTruncated = true;
339
- this.filePosition = 0;
340
- this.committedPosition = 0;
341
- this.lineBuffer = '';
342
- this.parser.reset();
343
- this.decoder = new string_decoder_1.StringDecoder('utf8');
347
+ // Open first and stat the descriptor: the size and identity checked
348
+ // are those of the file actually read, even if the path is replaced.
349
+ fd = fs.openSync(this.rolloutPath, 'r');
350
+ const stats = fs.fstatSync(fd);
351
+ if (this.pendingRestart) {
352
+ this._lastReset = this.pendingRestart;
353
+ this.pendingRestart = null;
344
354
  }
355
+ const reason = this.continuity.check(fd, stats, this.filePosition, this.committedPosition);
356
+ // Truncated, rewritten, or replaced: what was read no longer describes
357
+ // the rollout, so start over from its beginning.
358
+ if (reason !== null)
359
+ this.restart(reason);
360
+ this.continuity.adopt(stats);
361
+ const currentSize = Math.min(stats.size, endPosition ?? stats.size);
345
362
  // No new content
346
363
  if (currentSize <= this.filePosition)
347
- return [];
364
+ return events;
348
365
  // Read new bytes
349
- const fd = fs.openSync(this.rolloutPath, 'r');
350
366
  const bufferSize = currentSize - this.filePosition;
351
367
  const buffer = Buffer.alloc(bufferSize);
352
- let bytesRead = 0;
353
- try {
354
- bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
355
- }
356
- finally {
357
- fs.closeSync(fd);
358
- }
368
+ const bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
359
369
  const bytes = buffer.subarray(0, bytesRead);
360
370
  const newline = bytes.lastIndexOf(0x0a);
361
371
  if (newline >= 0)
@@ -382,10 +392,30 @@ class CodexReader {
382
392
  this.propagateParserState();
383
393
  }
384
394
  catch {
385
- // Skip read errors
395
+ // Skip read errors (a missing rollout reads as no events)
396
+ }
397
+ finally {
398
+ if (fd !== null) {
399
+ try {
400
+ fs.closeSync(fd);
401
+ }
402
+ catch {
403
+ // Ignore close errors.
404
+ }
405
+ }
386
406
  }
387
407
  return events;
388
408
  }
409
+ /** Starts over from offset 0 of a rollout that was truncated, rewritten, or replaced. */
410
+ restart(reason) {
411
+ this._lastReset = { reason, previousPosition: this.committedPosition };
412
+ this.filePosition = 0;
413
+ this.committedPosition = 0;
414
+ this.lineBuffer = '';
415
+ this.parser.reset();
416
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
417
+ this.pendingSeek = null;
418
+ }
389
419
  /** Hands the parser's context window, rate limits, and last usage to the provider. */
390
420
  propagateParserState() {
391
421
  // Propagate model_context_window from token_count events to the provider.
@@ -434,8 +464,11 @@ class CodexReader {
434
464
  this.lineBuffer = '';
435
465
  this.parser.reset();
436
466
  this.decoder = new string_decoder_1.StringDecoder('utf8');
437
- this._wasTruncated = false;
467
+ this._lastReset = null;
468
+ this.pendingRestart = null;
438
469
  this.pendingSeek = null;
470
+ // The next read adopts whatever rollout has the path.
471
+ this.continuity.forget();
439
472
  }
440
473
  exists() {
441
474
  return fs.existsSync(this.rolloutPath);
@@ -468,7 +501,10 @@ class CodexReader {
468
501
  this.pendingSeek = position;
469
502
  }
470
503
  wasTruncated() {
471
- return this._wasTruncated;
504
+ return this._lastReset !== null;
505
+ }
506
+ lastReset() {
507
+ return this._lastReset ? { ...this._lastReset } : null;
472
508
  }
473
509
  /** The parser's continuation state (see `CodexRolloutParser.exportResumeState`). */
474
510
  getResumeState() {
@@ -497,11 +533,14 @@ class CodexReader {
497
533
  return;
498
534
  this.reset();
499
535
  this.readNew(position);
536
+ if (this.filePosition < position) {
537
+ // The rollout no longer reaches the position sought: it was truncated
538
+ // or replaced since. The next read starts over and reports it.
539
+ this.reset();
540
+ this.pendingRestart = { reason: 'truncated', previousPosition: position };
541
+ }
500
542
  }
501
543
  }
502
- // ---------------------------------------------------------------------------
503
- // CodexProvider — full SessionProviderBase implementation
504
- // ---------------------------------------------------------------------------
505
544
  /**
506
545
  * Session provider for Codex CLI.
507
546
  *
@@ -520,8 +559,26 @@ class CodexProvider {
520
559
  lastTokenUsageData = null;
521
560
  /** Dated rollout-directory listings reused across async enumerations. */
522
561
  directoryCache = new directoryListingCache_1.DirectoryListingCache();
562
+ /** See {@link CodexProviderOptions.includeCompressedRollouts}. */
563
+ compressedRequested;
564
+ onDiagnostic;
523
565
  constructor(options = {}) {
524
566
  this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
567
+ this.compressedRequested = options.includeCompressedRollouts === true;
568
+ this.onDiagnostic = options.onDiagnostic;
569
+ }
570
+ /**
571
+ * Whether compressed rollouts are discovered and read: requested, and the
572
+ * runtime can decompress them (otherwise reported once, on first use).
573
+ */
574
+ get includeCompressed() {
575
+ return (this.compressedRequested && (0, compressedRollouts_1.canReadCompressedRollouts)({ onDiagnostic: this.onDiagnostic }));
576
+ }
577
+ /** A stored rollout path as it exists now (its `.zst` when compressed and enabled). */
578
+ resolveRollout(rolloutPath) {
579
+ return rolloutPath
580
+ ? (0, compressedRollouts_1.resolveRolloutPath)(rolloutPath, { includeCompressed: this.includeCompressed })
581
+ : null;
525
582
  }
526
583
  // --- Database ---
527
584
  /** Lazy-initialize database connection. */
@@ -619,10 +676,11 @@ class CodexProvider {
619
676
  return thread.rollout_path;
620
677
  }
621
678
  }
622
- // File scan fallback (newest first; cwd reads are cached per path)
679
+ // File scan fallback (newest first; cwd reads are cached per path).
680
+ // Subagent rollouts share their parent's cwd but are not the session.
623
681
  for (const file of findRolloutFilesInConfiguredHomes()) {
624
682
  const cwd = (0, codexRollouts_1.readSessionCwd)(file.path);
625
- if (cwd && (0, codexRollouts_1.cwdMatches)(cwd, workspacePath)) {
683
+ if (cwd && (0, codexRollouts_1.cwdMatches)(cwd, workspacePath) && !isSubagentRollout(file.path)) {
626
684
  this.recordDatabaseFallback('findActiveSession', true);
627
685
  return file.path;
628
686
  }
@@ -636,15 +694,17 @@ class CodexProvider {
636
694
  if (db) {
637
695
  const threads = db.getThreadsByCwd(workspacePath);
638
696
  const dbPaths = threads
639
- .filter((t) => t.rollout_path && fs.existsSync(t.rollout_path))
640
- .map((t) => t.rollout_path);
697
+ .map((t) => this.resolveRollout(t.rollout_path))
698
+ .filter((rolloutPath) => rolloutPath !== null);
641
699
  if (dbPaths.length > 0) {
642
700
  this.recordDatabaseFallback('findAllSessions', true);
643
701
  return dbPaths;
644
702
  }
645
703
  }
646
704
  // File scan (newest first; cwd reads are cached per path)
647
- const sessions = findRolloutFilesInConfiguredHomes()
705
+ const sessions = findRolloutFilesInConfiguredHomes({
706
+ includeCompressed: this.includeCompressed,
707
+ })
648
708
  .filter((f) => {
649
709
  const cwd = (0, codexRollouts_1.readSessionCwd)(f.path);
650
710
  return cwd !== null && (0, codexRollouts_1.cwdMatches)(cwd, workspacePath);
@@ -655,7 +715,7 @@ class CodexProvider {
655
715
  }
656
716
  findSessionById(_workspacePath, sessionId) {
657
717
  try {
658
- const found = (0, codexRollouts_1.findCodexRolloutFile)(sessionId);
718
+ const found = (0, codexRollouts_1.findCodexRolloutFile)(sessionId, { includeCompressed: this.includeCompressed });
659
719
  this.recordDatabaseFallback('findSessionById', true);
660
720
  return found;
661
721
  }
@@ -669,15 +729,15 @@ class CodexProvider {
669
729
  return this.findAllSessions(workspacePath);
670
730
  }
671
731
  findSessionsInDirectory(dir) {
672
- return findRolloutFiles(dir).map((f) => f.path);
732
+ return findRolloutFiles(dir, this.includeCompressed).map((f) => f.path);
673
733
  }
674
734
  listAllSessionFiles() {
675
- const files = findRolloutFilesInConfiguredHomes();
735
+ const files = findRolloutFilesInConfiguredHomes({ includeCompressed: this.includeCompressed });
676
736
  this.recordDatabaseFallback('listAllSessionFiles', true);
677
737
  return files;
678
738
  }
679
739
  async listSessionFilesAsync(workspacePath, options = {}) {
680
- const results = await (0, codexRollouts_1.listCodexSessionFilesAsync)(workspacePath, options, this.directoryCache);
740
+ const results = await (0, codexRollouts_1.listCodexSessionFilesAsync)(workspacePath, { ...options, includeCompressed: this.includeCompressed }, this.directoryCache);
681
741
  // Filesystem enumeration bypasses the database, so record that here as
682
742
  // the sync listing paths do; otherwise `getLastOperationStatus()` keeps
683
743
  // describing whichever operation ran before this one.
@@ -697,7 +757,7 @@ class CodexProvider {
697
757
  const name = segments[segments.length - 1];
698
758
  if (segments.length - 1 > rolloutWalker_1.DEFAULT_ROLLOUT_WALK_MAX_DEPTH)
699
759
  return { status: 'ignored' };
700
- if (!(0, rolloutWalker_1.isRolloutFile)(name)) {
760
+ if (!this.isSessionFile(name)) {
701
761
  this.directoryCache.invalidate(fullPath);
702
762
  try {
703
763
  const stat = await fs.promises.stat(fullPath);
@@ -747,7 +807,9 @@ class CodexProvider {
747
807
  // after its last update can be missing from it, so older files are not
748
808
  // opened; the cwd read itself is cached per path either way.
749
809
  const indexedUpTo = db ? db.getDbMtime() : 0;
750
- for (const file of findRolloutFilesInConfiguredHomes()) {
810
+ for (const file of findRolloutFilesInConfiguredHomes({
811
+ includeCompressed: this.includeCompressed,
812
+ })) {
751
813
  if (db && indexedUpTo > 0 && file.mtime.getTime() <= indexedUpTo)
752
814
  continue;
753
815
  const cwd = (0, codexRollouts_1.readSessionCwd)(file.path);
@@ -789,7 +851,7 @@ class CodexProvider {
789
851
  }
790
852
  // --- File identification ---
791
853
  isSessionFile(filename) {
792
- return (0, rolloutWalker_1.isRolloutFile)(filename);
854
+ return (0, rolloutWalker_1.isRolloutFile)(filename) || (this.includeCompressed && (0, rolloutWalker_1.isCompressedRolloutFile)(filename));
793
855
  }
794
856
  getSessionId(sessionPath) {
795
857
  return (0, rolloutWalker_1.extractRolloutSessionId)(path.basename(sessionPath));
@@ -850,6 +912,17 @@ class CodexProvider {
850
912
  }
851
913
  // --- Data reading ---
852
914
  createReader(sessionPath) {
915
+ if ((0, compressedRollouts_1.isCompressedRolloutPath)(sessionPath)) {
916
+ // Compressed rollouts are complete: read whole, never tailed.
917
+ return new codexCompressedReader_1.CompressedCodexRolloutReader(sessionPath, {
918
+ onContextWindowLimit: (limit) => {
919
+ // Historical content: keep the live limit, do not persist it again.
920
+ if (this.dynamicContextWindowLimit === null)
921
+ this.dynamicContextWindowLimit = limit;
922
+ },
923
+ onDiagnostic: this.onDiagnostic,
924
+ });
925
+ }
853
926
  return new CodexReader(sessionPath, (limit, modelId) => {
854
927
  this.dynamicContextWindowLimit = limit;
855
928
  if (modelId) {
@@ -866,119 +939,36 @@ class CodexProvider {
866
939
  });
867
940
  }
868
941
  // --- Subagent support ---
869
- scanSubagents(_sessionDir, sessionId) {
870
- // Codex tracks forked sessions via forked_from_id in session_meta.
871
- // Use DB when available, fall back to filesystem scanning.
872
- const subagents = [];
873
- // DB approach: spawned subagents (parent recorded in `threads.source`),
874
- // plus forked children on older schemas that carry `forked_from_id`.
875
- const db = this.ensureDb();
876
- if (db) {
877
- const seen = new Set();
878
- const children = [
879
- ...db.getSpawnedChildThreads(sessionId),
880
- ...db.getThreadsByForkedFromId(sessionId),
881
- ].filter((thread) => !seen.has(thread.id) && seen.add(thread.id));
882
- for (const thread of children) {
883
- const built = (0, subagentStatsCache_1.cachedSubagentStats)(thread.rollout_path, () => this.buildSubagentStats(thread.id, thread.rollout_path, thread.title || thread.agent_nickname));
884
- if (built) {
885
- subagents.push(thread.agent_role && !built.agentType
886
- ? { ...built, agentType: thread.agent_role }
887
- : built);
888
- }
889
- }
890
- if (children.length > 0)
891
- return subagents;
892
- }
893
- // Filesystem fallback: scan recent rollout files for a matching
894
- // forked_from_id. The walk stops after the 50 newest-dated files instead
895
- // of enumerating the whole history and slicing afterwards.
896
- for (const file of findRolloutFilesInConfiguredHomes({ limit: 50 })) {
897
- const meta = (0, codexRollouts_1.readSessionMeta)(file.path);
898
- if (meta &&
899
- (meta.forked_from_id === sessionId || (0, codexDatabase_1.spawnParentFromSource)(meta.source) === sessionId)) {
900
- const childId = (0, rolloutWalker_1.extractRolloutSessionId)(path.basename(file.path));
901
- // `source` is a plain string for top-level sessions but an object
902
- // (`{ subagent: { thread_spawn: … } }`) for spawned children.
903
- const description = typeof meta.source === 'string' ? meta.source : undefined;
904
- const stats = (0, subagentStatsCache_1.cachedSubagentStats)(file.path, () => this.buildSubagentStats(childId, file.path, description));
905
- if (stats)
906
- subagents.push(stats);
907
- }
908
- }
909
- return subagents;
910
- }
911
942
  /**
912
- * Builds SubagentStats for a forked session by reading its rollout file.
943
+ * Every subagent below the session, any depth: spawned threads (the
944
+ * database's `threads.source`, else the newest rollouts' session_meta)
945
+ * and, on older schemas, forks (`forked_from_id`). Each carries its
946
+ * rollout (`filePath`), the parent's spawn call (`spawnToolUseId`),
947
+ * `agentNickname`, `agentPath`, `parentAgentId`, and `depth`; tool calls
948
+ * and tokens come from the child's own records, not the parent history a
949
+ * forked child embeds.
913
950
  */
914
- buildSubagentStats(agentId, rolloutPath, description) {
915
- if (!rolloutPath || !fs.existsSync(rolloutPath))
916
- return null;
917
- const stats = {
918
- agentId,
919
- description: description || undefined,
920
- toolCalls: [],
921
- inputTokens: 0,
922
- outputTokens: 0,
923
- };
924
- try {
925
- const parser = new codexParser_1.CodexRolloutParser();
926
- const content = fs.readFileSync(rolloutPath, 'utf8');
927
- const lines = content.split('\n');
928
- for (const line of lines) {
929
- const trimmed = line.trim();
930
- if (!trimmed)
931
- continue;
932
- try {
933
- const parsed = JSON.parse(trimmed);
934
- // Track timestamps
935
- if (parsed.timestamp) {
936
- const ts = new Date(parsed.timestamp);
937
- if (!stats.startTime || ts < stats.startTime)
938
- stats.startTime = ts;
939
- if (!stats.endTime || ts > stats.endTime)
940
- stats.endTime = ts;
941
- }
942
- const events = parser.convertLine(parsed);
943
- for (const event of events) {
944
- // Collect tool calls
945
- if (event.type === 'assistant' && Array.isArray(event.message.content)) {
946
- for (const block of event.message.content) {
947
- if (block.type === 'tool_use' && typeof block.name === 'string') {
948
- stats.toolCalls.push({
949
- name: block.name,
950
- input: block.input || {},
951
- timestamp: new Date(event.timestamp),
952
- });
953
- }
954
- }
955
- }
956
- // Accumulate tokens from usage events (duplicate token_count events
957
- // are already dropped by the parser).
958
- const usage = (0, usageNormalization_1.extractNormalizedUsage)(event);
959
- if (usage)
960
- (0, subagentUsage_1.addUsageToSubagent)(stats, usage);
961
- }
962
- }
963
- catch {
964
- /* skip malformed lines */
965
- }
966
- }
967
- if (stats.startTime && stats.endTime) {
968
- stats.durationMs = stats.endTime.getTime() - stats.startTime.getTime();
969
- }
970
- }
971
- catch {
972
- /* skip unreadable files */
973
- }
974
- return stats;
951
+ scanSubagents(_sessionDir, sessionId) {
952
+ const db = this.ensureDb();
953
+ return (0, codexSubagents_1.discoverCodexSubagents)(sessionId, {
954
+ db,
955
+ includeCompressed: this.includeCompressed,
956
+ onDiagnostic: this.onDiagnostic,
957
+ sessionRolloutPath: () => this.resolveRollout(db?.getThread(sessionId)?.rollout_path) ??
958
+ (0, codexRollouts_1.findCodexRolloutFile)(sessionId, { includeCompressed: this.includeCompressed }),
959
+ // The walk stops after the 50 newest-dated files instead of
960
+ // enumerating the whole history and slicing afterwards.
961
+ fallbackFiles: () => findRolloutFilesInConfiguredHomes({ limit: 50, includeCompressed: this.includeCompressed }),
962
+ });
975
963
  }
976
964
  // --- Cross-session search ---
977
965
  searchInSession(sessionPath, query, maxResults) {
978
966
  const results = [];
979
967
  const queryLower = query.toLowerCase();
980
968
  try {
981
- const content = fs.readFileSync(sessionPath, 'utf8');
969
+ const content = (0, compressedRollouts_1.readRolloutText)(sessionPath, { onDiagnostic: this.onDiagnostic });
970
+ if (content === null)
971
+ return results;
982
972
  const lines = content.split('\n');
983
973
  for (const line of lines) {
984
974
  if (results.length >= maxResults)
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Session reader for a compressed Codex rollout (`rollout-…jsonl.zst`).
3
+ *
4
+ * Codex compresses only rollouts it no longer writes, so the content is
5
+ * decompressed and parsed whole on the first read; later reads cost one
6
+ * stat and return nothing until the file changes (then the rollout is read
7
+ * again from the start, reported as `replaced`). Positions are offsets in
8
+ * the decompressed content. Live tailing stays with the `.jsonl` reader.
9
+ *
10
+ * Node-only: reads files.
11
+ *
12
+ * @module providers/codexCompressedReader
13
+ */
14
+ import { type CodexRolloutResumeState } from '../parsers/codexParser';
15
+ import type { CodexRateLimits, CodexSessionMeta } from '../types/codex';
16
+ import type { SessionEvent, TokenUsage } from '../types/sessionEvent';
17
+ import { type ReadRolloutOptions } from './compressedRollouts';
18
+ import type { SessionReader } from './types';
19
+ type ReaderReset = NonNullable<ReturnType<NonNullable<SessionReader['lastReset']>>>;
20
+ export interface CompressedCodexReaderCallbacks {
21
+ onContextWindowLimit?: (limit: number, modelId?: string) => void;
22
+ onRateLimits?: (limits: CodexRateLimits) => void;
23
+ onTokenUsage?: (usage: TokenUsage) => void;
24
+ onDiagnostic?: ReadRolloutOptions['onDiagnostic'];
25
+ }
26
+ /** Reads a `.jsonl.zst` rollout through a {@link CodexRolloutParser}. */
27
+ export declare class CompressedCodexRolloutReader implements SessionReader {
28
+ private readonly rolloutPath;
29
+ private readonly callbacks;
30
+ private parser;
31
+ /** Offset after the last line parsed (the content is complete, so its end). */
32
+ private position;
33
+ /** Size and mtime of the compressed file the position refers to. */
34
+ private source;
35
+ private reset_;
36
+ /** Position from `seekTo()` whose prefix still has to be parsed for context. */
37
+ private pendingSeek;
38
+ constructor(rolloutPath: string, callbacks?: CompressedCodexReaderCallbacks);
39
+ getSessionMeta(): CodexSessionMeta | null;
40
+ readNew(): SessionEvent[];
41
+ readAll(): SessionEvent[];
42
+ reset(): void;
43
+ exists(): boolean;
44
+ flush(): void;
45
+ getPosition(): number;
46
+ seekTo(position: number): void;
47
+ wasTruncated(): boolean;
48
+ lastReset(): ReaderReset | null;
49
+ getResumeState(): CodexRolloutResumeState;
50
+ restoreResumeState(state: unknown): boolean;
51
+ /** Parses the content past `position`; `emit` false only rebuilds parser state. */
52
+ private readFrom;
53
+ /** Rebuilds parser context for a pending `seekTo()` position. */
54
+ private settleSeek;
55
+ /** Hands the parser's context window, rate limits, and last usage to the provider. */
56
+ private propagateParserState;
57
+ }
58
+ export {};