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
@@ -0,0 +1,380 @@
1
+ "use strict";
2
+ /**
3
+ * Reads Codex rollouts that Codex compressed.
4
+ *
5
+ * Codex compresses older rollouts in place to `rollout-<ts>-<id>.jsonl.zst`
6
+ * (one Zstandard frame), while its state database keeps the `.jsonl` path.
7
+ * Node ships `zlib.zstdDecompressSync` from 22.15 / 23.8; on older runtimes
8
+ * (Node 20, older VS Code Electron builds) compressed rollouts cannot be read
9
+ * and are skipped, reported once as a `read_failed` diagnostic.
10
+ *
11
+ * Every reader here takes either form: a `.jsonl` path reads as before, a
12
+ * `.jsonl.zst` path is decompressed. Compressed rollouts are complete (Codex
13
+ * only compresses rollouts it no longer writes), so they are read whole and
14
+ * never tailed.
15
+ *
16
+ * Node-only: reads files.
17
+ *
18
+ * @module providers/compressedRollouts
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.__compressedRolloutTesting = exports.COMPRESSED_ROLLOUT_SUFFIX = void 0;
55
+ exports.isCompressedRolloutPath = isCompressedRolloutPath;
56
+ exports.isZstdDecompressionAvailable = isZstdDecompressionAvailable;
57
+ exports.canReadCompressedRollouts = canReadCompressedRollouts;
58
+ exports.resetCompressedRolloutDiagnostics = resetCompressedRolloutDiagnostics;
59
+ exports.readRolloutBuffer = readRolloutBuffer;
60
+ exports.readRolloutText = readRolloutText;
61
+ exports.readRolloutBufferAsync = readRolloutBufferAsync;
62
+ exports.readRolloutHead = readRolloutHead;
63
+ exports.readRolloutHeadAsync = readRolloutHeadAsync;
64
+ exports.readRolloutFirstLines = readRolloutFirstLines;
65
+ exports.zstdFrameContentSize = zstdFrameContentSize;
66
+ exports.statRolloutContent = statRolloutContent;
67
+ exports.openRolloutReadHandle = openRolloutReadHandle;
68
+ exports.resolveRolloutPath = resolveRolloutPath;
69
+ const fs = __importStar(require("fs"));
70
+ const zlib = __importStar(require("zlib"));
71
+ /** File-name suffix of a compressed rollout. */
72
+ exports.COMPRESSED_ROLLOUT_SUFFIX = '.jsonl.zst';
73
+ /** `undefined`: use the runtime's; `null`: behave as if the runtime lacked it. */
74
+ let decompressOverride;
75
+ function zstdDecompressSync() {
76
+ if (decompressOverride !== undefined)
77
+ return decompressOverride;
78
+ const fn = zlib.zstdDecompressSync;
79
+ return typeof fn === 'function' ? fn.bind(zlib) : null;
80
+ }
81
+ /** Test hooks; not part of the public entry points. */
82
+ exports.__compressedRolloutTesting = {
83
+ /** Replace the runtime's `zstdDecompressSync` (`null` = unavailable, `undefined` = restore). */
84
+ setDecompress(fn) {
85
+ decompressOverride = fn;
86
+ },
87
+ };
88
+ /** Whether a path (or file name) names a compressed rollout (`*.jsonl.zst`). */
89
+ function isCompressedRolloutPath(filePath) {
90
+ return filePath.endsWith(exports.COMPRESSED_ROLLOUT_SUFFIX);
91
+ }
92
+ /** Whether this runtime can decompress Zstandard (`zlib.zstdDecompressSync`). */
93
+ function isZstdDecompressionAvailable() {
94
+ return zstdDecompressSync() !== null;
95
+ }
96
+ const UNAVAILABLE_MESSAGE = 'Compressed Codex rollouts (.jsonl.zst) were skipped: this Node.js runtime has no zlib.zstdDecompressSync (Node 22.15 or later).';
97
+ /** The runtime-limit diagnostic is reported once per process unless asked again. */
98
+ let reportedUnavailable = false;
99
+ function reportUnavailable(options) {
100
+ if (!options?.onDiagnostic || reportedUnavailable)
101
+ return;
102
+ reportedUnavailable = true;
103
+ options.onDiagnostic({
104
+ providerId: 'codex',
105
+ kind: 'read_failed',
106
+ severity: 'info',
107
+ phase: 'read',
108
+ message: UNAVAILABLE_MESSAGE,
109
+ });
110
+ }
111
+ function reportCorrupt(options, error) {
112
+ options?.onDiagnostic?.({
113
+ providerId: 'codex',
114
+ kind: 'read_failed',
115
+ severity: 'warning',
116
+ phase: 'read',
117
+ message: `A compressed Codex rollout could not be decompressed: ${error instanceof Error ? error.message : String(error)}`,
118
+ });
119
+ }
120
+ /**
121
+ * Whether compressed rollouts can be read here; when they cannot, reports
122
+ * the runtime limit through `onDiagnostic` (once per process). Callers that
123
+ * list rollouts use it to leave `.jsonl.zst` files out instead of listing
124
+ * sessions they cannot read.
125
+ */
126
+ function canReadCompressedRollouts(options) {
127
+ if (zstdDecompressSync())
128
+ return true;
129
+ reportUnavailable(options);
130
+ return false;
131
+ }
132
+ /** @internal Test hook: report the runtime limit again. */
133
+ function resetCompressedRolloutDiagnostics() {
134
+ reportedUnavailable = false;
135
+ }
136
+ function decompress(input, options) {
137
+ const fn = zstdDecompressSync();
138
+ if (!fn) {
139
+ reportUnavailable(options);
140
+ return null;
141
+ }
142
+ try {
143
+ return fn(input);
144
+ }
145
+ catch (error) {
146
+ reportCorrupt(options, error);
147
+ return null;
148
+ }
149
+ }
150
+ /**
151
+ * The whole content of a rollout: a `.jsonl` file as stored, a `.jsonl.zst`
152
+ * file decompressed. `null` when the file cannot be read or decompressed
153
+ * (the latter reported through `onDiagnostic`).
154
+ */
155
+ function readRolloutBuffer(filePath, options) {
156
+ let raw;
157
+ try {
158
+ raw = fs.readFileSync(filePath);
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ return isCompressedRolloutPath(filePath) ? decompress(raw, options) : raw;
164
+ }
165
+ /** {@link readRolloutBuffer} decoded as UTF-8. */
166
+ function readRolloutText(filePath, options) {
167
+ return readRolloutBuffer(filePath, options)?.toString('utf8') ?? null;
168
+ }
169
+ /** Async twin of {@link readRolloutBuffer}; decompression itself is synchronous. */
170
+ async function readRolloutBufferAsync(filePath, options) {
171
+ let raw;
172
+ try {
173
+ raw = await fs.promises.readFile(filePath);
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ return isCompressedRolloutPath(filePath) ? decompress(raw, options) : raw;
179
+ }
180
+ /** Compressed bytes read per step while looking for a decompressed prefix. */
181
+ const HEAD_STEP_BYTES = 64 * 1024;
182
+ /**
183
+ * Decompresses only as much of a compressed prefix as is needed for
184
+ * `maxBytes` of content: a truncated Zstandard frame decompresses to the
185
+ * content of its complete blocks.
186
+ */
187
+ function decompressHead(readCompressed, compressedSize, maxBytes, options) {
188
+ const fn = zstdDecompressSync();
189
+ if (!fn) {
190
+ reportUnavailable(options);
191
+ return null;
192
+ }
193
+ let length = Math.min(compressedSize, HEAD_STEP_BYTES);
194
+ for (;;) {
195
+ let out;
196
+ try {
197
+ out = fn(readCompressed(length));
198
+ }
199
+ catch (error) {
200
+ if (length >= compressedSize) {
201
+ reportCorrupt(options, error);
202
+ return null;
203
+ }
204
+ out = Buffer.alloc(0);
205
+ }
206
+ if (out.length >= maxBytes || length >= compressedSize)
207
+ return out.subarray(0, maxBytes);
208
+ length = Math.min(compressedSize, length * 4);
209
+ }
210
+ }
211
+ /**
212
+ * Up to `maxBytes` of a rollout's content from its start (decompressed for
213
+ * a `.jsonl.zst`). An empty buffer when the file cannot be read.
214
+ */
215
+ function readRolloutHead(filePath, maxBytes, options) {
216
+ let fd;
217
+ try {
218
+ fd = fs.openSync(filePath, 'r');
219
+ }
220
+ catch {
221
+ return Buffer.alloc(0);
222
+ }
223
+ try {
224
+ const size = fs.fstatSync(fd).size;
225
+ const readPrefix = (length) => {
226
+ const buffer = Buffer.alloc(length);
227
+ const bytesRead = fs.readSync(fd, buffer, 0, length, 0);
228
+ return buffer.subarray(0, bytesRead);
229
+ };
230
+ if (!isCompressedRolloutPath(filePath))
231
+ return readPrefix(Math.min(size, Math.max(0, maxBytes)));
232
+ return decompressHead(readPrefix, size, Math.max(0, maxBytes), options) ?? Buffer.alloc(0);
233
+ }
234
+ catch {
235
+ return Buffer.alloc(0);
236
+ }
237
+ finally {
238
+ fs.closeSync(fd);
239
+ }
240
+ }
241
+ /** Async twin of {@link readRolloutHead}. */
242
+ async function readRolloutHeadAsync(filePath, maxBytes, options) {
243
+ if (!isCompressedRolloutPath(filePath)) {
244
+ let handle = null;
245
+ try {
246
+ handle = await fs.promises.open(filePath, 'r');
247
+ const size = (await handle.stat()).size;
248
+ const length = Math.min(size, Math.max(0, maxBytes));
249
+ const buffer = Buffer.alloc(length);
250
+ const { bytesRead } = await handle.read(buffer, 0, length, 0);
251
+ return buffer.subarray(0, bytesRead);
252
+ }
253
+ catch {
254
+ return Buffer.alloc(0);
255
+ }
256
+ finally {
257
+ await handle?.close().catch(() => undefined);
258
+ }
259
+ }
260
+ // Compressed prefixes are small; one synchronous read keeps it simple.
261
+ return readRolloutHead(filePath, maxBytes, options);
262
+ }
263
+ /**
264
+ * The first `maxLines` complete lines of a rollout (decompressed for a
265
+ * `.jsonl.zst`) within `maxBytes` of content; a trailing line without a
266
+ * newline counts when the whole content fit.
267
+ */
268
+ function readRolloutFirstLines(filePath, maxLines, maxBytes = 256 * 1024, options) {
269
+ const head = readRolloutHead(filePath, maxBytes + 1, options);
270
+ const complete = head.length <= maxBytes;
271
+ const text = head.subarray(0, maxBytes).toString('utf8');
272
+ const parts = text.split('\n');
273
+ const tail = parts.pop() ?? '';
274
+ const lines = parts.slice(0, maxLines);
275
+ if (lines.length < maxLines && complete && tail.trim())
276
+ lines.push(tail);
277
+ return lines;
278
+ }
279
+ /**
280
+ * The content size a Zstandard frame header declares (`Frame_Content_Size`),
281
+ * or `null` when the header omits it or is not a Zstandard frame. Skippable
282
+ * frames before it are not followed.
283
+ */
284
+ function zstdFrameContentSize(header) {
285
+ if (header.length < 5 || header.readUInt32LE(0) !== 0xfd2fb528)
286
+ return null;
287
+ const descriptor = header[4];
288
+ const fcsFlag = descriptor >> 6;
289
+ const singleSegment = (descriptor >> 5) & 1;
290
+ const dictionaryIdFlag = descriptor & 3;
291
+ const offset = 5 + (singleSegment ? 0 : 1) + [0, 1, 2, 4][dictionaryIdFlag];
292
+ const fcsBytes = fcsFlag === 0 ? (singleSegment ? 1 : 0) : [0, 2, 4, 8][fcsFlag];
293
+ if (fcsBytes === 0 || header.length < offset + fcsBytes)
294
+ return null;
295
+ switch (fcsBytes) {
296
+ case 1:
297
+ return header[offset];
298
+ case 2:
299
+ return header.readUInt16LE(offset) + 256;
300
+ case 4:
301
+ return header.readUInt32LE(offset);
302
+ default: {
303
+ const value = header.readBigUInt64LE(offset);
304
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : null;
305
+ }
306
+ }
307
+ }
308
+ /**
309
+ * Stat of a rollout's content: the file's own for a `.jsonl`; for a
310
+ * `.jsonl.zst`, the decompressed size (from the frame header, else by
311
+ * decompressing) with the compressed file's mtime. Rejects like `fs.stat`
312
+ * when the file is missing; a compressed rollout that cannot be read
313
+ * rejects too.
314
+ */
315
+ async function statRolloutContent(filePath, options) {
316
+ const stat = await fs.promises.stat(filePath);
317
+ if (!isCompressedRolloutPath(filePath))
318
+ return { size: stat.size, mtimeMs: stat.mtimeMs };
319
+ let declared = null;
320
+ let handle = null;
321
+ try {
322
+ handle = await fs.promises.open(filePath, 'r');
323
+ const header = Buffer.alloc(18);
324
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
325
+ declared = zstdFrameContentSize(header.subarray(0, bytesRead));
326
+ }
327
+ finally {
328
+ await handle?.close().catch(() => undefined);
329
+ }
330
+ if (declared !== null && isZstdDecompressionAvailable()) {
331
+ return { size: declared, mtimeMs: stat.mtimeMs };
332
+ }
333
+ const content = await readRolloutBufferAsync(filePath, options);
334
+ if (!content)
335
+ throw new Error('compressed rollout could not be read');
336
+ return { size: content.length, mtimeMs: stat.mtimeMs };
337
+ }
338
+ /** A read-only handle over a buffer, for scanning decompressed content. */
339
+ function bufferHandle(content) {
340
+ return {
341
+ async read(buffer, offset, length, position) {
342
+ if (position >= content.length || length <= 0)
343
+ return { bytesRead: 0 };
344
+ const bytesRead = content.copy(buffer, offset, position, Math.min(content.length, position + length));
345
+ return { bytesRead };
346
+ },
347
+ async close() { },
348
+ };
349
+ }
350
+ /**
351
+ * Opens a rollout for positional reads over its content: a file handle for
352
+ * a `.jsonl`, the decompressed content for a `.jsonl.zst` (with `size`, the
353
+ * content length). Rejects when the file cannot be opened or decompressed.
354
+ */
355
+ async function openRolloutReadHandle(filePath, options) {
356
+ if (!isCompressedRolloutPath(filePath)) {
357
+ return { handle: await fs.promises.open(filePath, 'r'), size: null };
358
+ }
359
+ const content = await readRolloutBufferAsync(filePath, options);
360
+ if (!content)
361
+ throw new Error('compressed rollout could not be read');
362
+ return { handle: bufferHandle(content), size: content.length };
363
+ }
364
+ /**
365
+ * The path of a rollout as stored now: `filePath` when it exists, else its
366
+ * compressed sibling (`<filePath>.zst`) when `includeCompressed` is set and
367
+ * that exists. `null` when neither does.
368
+ */
369
+ function resolveRolloutPath(filePath, options = {}) {
370
+ if (!filePath)
371
+ return null;
372
+ if (fs.existsSync(filePath))
373
+ return filePath;
374
+ if (options.includeCompressed && !isCompressedRolloutPath(filePath)) {
375
+ const compressed = `${filePath}.zst`;
376
+ if (filePath.endsWith('.jsonl') && fs.existsSync(compressed))
377
+ return compressed;
378
+ }
379
+ return null;
380
+ }
@@ -2,6 +2,11 @@ import type { ProviderId, SessionProviderBase, SessionProviderDiagnostic } from
2
2
  export interface CreateSessionProvidersOptions {
3
3
  providerIds?: readonly ProviderId[];
4
4
  onDiagnostic?: (diagnostic: SessionProviderDiagnostic) => void;
5
+ /**
6
+ * Codex: also discover and read compressed rollouts (`….jsonl.zst`); see
7
+ * `CodexProviderOptions.includeCompressedRollouts`. Default false.
8
+ */
9
+ includeCompressedRollouts?: boolean;
5
10
  }
6
11
  export interface CreateSessionProvidersResult {
7
12
  providers: SessionProviderBase[];
@@ -40,7 +40,10 @@ function createSessionProviders(options = {}) {
40
40
  ? new claudeCode_1.ClaudeCodeProvider(providerOptions)
41
41
  : providerId === 'opencode'
42
42
  ? new openCode_1.OpenCodeProvider(providerOptions)
43
- : new codex_1.CodexProvider(providerOptions));
43
+ : new codex_1.CodexProvider({
44
+ ...providerOptions,
45
+ includeCompressedRollouts: options.includeCompressedRollouts === true,
46
+ }));
44
47
  }
45
48
  catch {
46
49
  const diagnostic = {
@@ -15,6 +15,10 @@
15
15
  * order over the whole history omit both (the `maxFiles` cap still applies);
16
16
  * a live session missed this way is picked up by its next watch event.
17
17
  *
18
+ * Codex compresses older rollouts to `rollout-…jsonl.zst`. Those are skipped
19
+ * unless `includeCompressed` is set; a compressed rollout whose `.jsonl` is
20
+ * still beside it is skipped either way (the `.jsonl` is the one to read).
21
+ *
18
22
  * The async walker accepts a {@link DirectoryListingCache}: unchanged
19
23
  * directories are then one stat each instead of a readdir plus a stat per
20
24
  * file, which is what makes repeated discovery over a large history cheap.
@@ -45,6 +49,13 @@ export interface WalkRolloutFilesOptions {
45
49
  sessionId?: string;
46
50
  /** Include zero-byte files (default false). */
47
51
  includeEmpty?: boolean;
52
+ /**
53
+ * Also return compressed rollouts (`rollout-…jsonl.zst`; default false).
54
+ * Reading them needs `zlib.zstdDecompressSync` (see
55
+ * `providers/compressedRollouts`). A `cache` remembers which names a walk
56
+ * accepted, so do not share one between walks that differ in this option.
57
+ */
58
+ includeCompressed?: boolean;
48
59
  /** Reuse directory listings across walks (async walker only). */
49
60
  cache?: DirectoryListingCache;
50
61
  /** With `cache`: re-stat recently modified files even in unchanged directories. */
@@ -54,11 +65,13 @@ export interface WalkRolloutFilesOptions {
54
65
  }
55
66
  export declare const DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
56
67
  export declare const DEFAULT_ROLLOUT_WALK_MAX_FILES = 20000;
57
- /** Test if a filename is a Codex rollout file. */
68
+ /** Test if a filename is a Codex rollout file (`rollout-….jsonl`; not compressed ones). */
58
69
  export declare function isRolloutFile(filename: string): boolean;
70
+ /** Test if a filename is a compressed Codex rollout (`rollout-….jsonl.zst`). */
71
+ export declare function isCompressedRolloutFile(filename: string): boolean;
59
72
  /**
60
73
  * Extract the session UUID from a rollout filename.
61
- * Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
74
+ * Format: rollout-<timestamp>-<uuid>.jsonl (or .jsonl.zst) -> <uuid>
62
75
  */
63
76
  export declare function extractRolloutSessionId(filename: string): string;
64
77
  /**
@@ -16,6 +16,10 @@
16
16
  * order over the whole history omit both (the `maxFiles` cap still applies);
17
17
  * a live session missed this way is picked up by its next watch event.
18
18
  *
19
+ * Codex compresses older rollouts to `rollout-…jsonl.zst`. Those are skipped
20
+ * unless `includeCompressed` is set; a compressed rollout whose `.jsonl` is
21
+ * still beside it is skipped either way (the `.jsonl` is the one to read).
22
+ *
19
23
  * The async walker accepts a {@link DirectoryListingCache}: unchanged
20
24
  * directories are then one stat each instead of a readdir plus a stat per
21
25
  * file, which is what makes repeated discovery over a large history cheap.
@@ -58,6 +62,7 @@ var __importStar = (this && this.__importStar) || (function () {
58
62
  Object.defineProperty(exports, "__esModule", { value: true });
59
63
  exports.DEFAULT_ROLLOUT_WALK_MAX_FILES = exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH = void 0;
60
64
  exports.isRolloutFile = isRolloutFile;
65
+ exports.isCompressedRolloutFile = isCompressedRolloutFile;
61
66
  exports.extractRolloutSessionId = extractRolloutSessionId;
62
67
  exports.isDatedDirectoryBefore = isDatedDirectoryBefore;
63
68
  exports.walkRolloutFiles = walkRolloutFiles;
@@ -66,16 +71,23 @@ const fs = __importStar(require("fs"));
66
71
  const path = __importStar(require("path"));
67
72
  exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
68
73
  exports.DEFAULT_ROLLOUT_WALK_MAX_FILES = 20_000;
69
- /** Test if a filename is a Codex rollout file. */
74
+ /** Test if a filename is a Codex rollout file (`rollout-….jsonl`; not compressed ones). */
70
75
  function isRolloutFile(filename) {
71
76
  return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
72
77
  }
78
+ /** Test if a filename is a compressed Codex rollout (`rollout-….jsonl.zst`). */
79
+ function isCompressedRolloutFile(filename) {
80
+ return filename.startsWith('rollout-') && filename.endsWith('.jsonl.zst');
81
+ }
73
82
  /**
74
83
  * Extract the session UUID from a rollout filename.
75
- * Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
84
+ * Format: rollout-<timestamp>-<uuid>.jsonl (or .jsonl.zst) -> <uuid>
76
85
  */
77
86
  function extractRolloutSessionId(filename) {
78
- const base = path.basename(filename, '.jsonl');
87
+ const name = path.basename(filename);
88
+ const base = name.endsWith('.jsonl.zst')
89
+ ? name.slice(0, -'.jsonl.zst'.length)
90
+ : path.basename(name, '.jsonl');
79
91
  // rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
80
92
  const parts = base.split('-');
81
93
  // The UUID is typically the last 5 segments (8-4-4-4-12)
@@ -99,6 +111,7 @@ function walkState(options) {
99
111
  since: options.since !== undefined && Number.isFinite(options.since) ? options.since : null,
100
112
  sessionId: options.sessionId ? options.sessionId.trim().toLowerCase() : null,
101
113
  includeEmpty: options.includeEmpty ?? false,
114
+ includeCompressed: options.includeCompressed ?? false,
102
115
  cache: options.cache ?? null,
103
116
  revalidateRecent: options.revalidateRecent ?? false,
104
117
  stats: options.stats,
@@ -109,10 +122,15 @@ function byNameDescending(a, b) {
109
122
  return b.name.localeCompare(a.name);
110
123
  }
111
124
  function matchesFilter(state, name) {
112
- if (!isRolloutFile(name))
125
+ if (!isRolloutFile(name) && !(state.includeCompressed && isCompressedRolloutFile(name))) {
113
126
  return false;
127
+ }
114
128
  return (state.sessionId === null || extractRolloutSessionId(name).toLowerCase() === state.sessionId);
115
129
  }
130
+ /** A compressed rollout whose uncompressed rollout is listed beside it. */
131
+ function shadowedByPlain(name, has) {
132
+ return isCompressedRolloutFile(name) && has(name.slice(0, -'.zst'.length));
133
+ }
116
134
  function record(state, fullPath, name, stat) {
117
135
  if (!state.includeEmpty && stat.size <= 0)
118
136
  return;
@@ -171,11 +189,14 @@ function visitSync(state, dir, depth, segments) {
171
189
  return;
172
190
  }
173
191
  entries.sort(byNameDescending);
192
+ const names = state.includeCompressed ? new Set(entries.map((entry) => entry.name)) : null;
174
193
  for (const entry of entries) {
175
194
  if (state.results.length >= state.cap)
176
195
  return;
177
196
  if (!entry.isFile() || !matchesFilter(state, entry.name))
178
197
  continue;
198
+ if (names && shadowedByPlain(entry.name, (name) => names.has(name)))
199
+ continue;
179
200
  const fullPath = path.join(dir, entry.name);
180
201
  try {
181
202
  record(state, fullPath, entry.name, fs.statSync(fullPath));
@@ -226,6 +247,11 @@ async function visitCached(state, cache, dir, depth, segments) {
226
247
  const stat = listing.files.get(name);
227
248
  if (!stat)
228
249
  continue;
250
+ // The listing may come from a walk with other options: filter again.
251
+ if (!matchesFilter(state, name))
252
+ continue;
253
+ if (shadowedByPlain(name, (other) => listing.files.has(other)))
254
+ continue;
229
255
  record(state, path.join(dir, name), name, { size: stat.sizeBytes, mtimeMs: stat.mtimeMs });
230
256
  }
231
257
  await yieldToEventLoop();
@@ -250,12 +276,15 @@ async function visitAsync(state, dir, depth, segments) {
250
276
  return;
251
277
  }
252
278
  entries.sort(byNameDescending);
279
+ const names = state.includeCompressed ? new Set(entries.map((entry) => entry.name)) : null;
253
280
  let sinceYield = 0;
254
281
  for (const entry of entries) {
255
282
  if (state.results.length >= state.cap)
256
283
  return;
257
284
  if (!entry.isFile() || !matchesFilter(state, entry.name))
258
285
  continue;
286
+ if (names && shadowedByPlain(entry.name, (name) => names.has(name)))
287
+ continue;
259
288
  const fullPath = path.join(dir, entry.name);
260
289
  try {
261
290
  const stat = await fs.promises.stat(fullPath);
@@ -12,6 +12,7 @@ import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '
12
12
  import type { SessionEvent, SubagentStats, TokenUsage, ContextAttribution } from '../types/sessionEvent';
13
13
  import { SESSION_PROVIDER_IDS } from '../providerIds';
14
14
  import type { AggregatedCostProvenance } from '../aggregation/types';
15
+ import type { JsonlTailResetReason } from '../watchers/jsonlTail';
15
16
  /** Stable runtime list for consumers that must enumerate session providers. */
16
17
  export { SESSION_PROVIDER_IDS } from '../providerIds';
17
18
  export type ProviderId = (typeof SESSION_PROVIDER_IDS)[number];
@@ -163,6 +164,12 @@ export interface ProviderRuntimeStatus {
163
164
  kind: 'available' | 'home_unavailable' | 'config_unreadable' | 'db_missing' | 'sqlite_missing' | 'sqlite_blocked' | 'query_failed';
164
165
  message?: string;
165
166
  }
167
+ /** Why a {@link SessionReader} started over from offset 0 (see `SessionReader.lastReset`). */
168
+ export interface SessionReaderReset {
169
+ reason: JsonlTailResetReason;
170
+ /** The committed position (`getPosition()`) before the restart. */
171
+ previousPosition: number;
172
+ }
166
173
  /**
167
174
  * Incremental reader for session data.
168
175
  *
@@ -184,8 +191,22 @@ export interface SessionReader {
184
191
  getPosition(): number;
185
192
  /** Seek to a specific position (byte offset for JSONL, timestamp for DB-backed). */
186
193
  seekTo(position: number): void;
187
- /** Check if file was truncated (size < position). */
194
+ /**
195
+ * Whether the last `readNew()` restarted from offset 0 because the file was
196
+ * truncated, rewritten, or replaced. Its events are then the session from
197
+ * the beginning: discard anything built from earlier reads before
198
+ * processing them. (Before 0.30.0 the built-in readers reported only a
199
+ * file that shrank below the read position.)
200
+ */
188
201
  wasTruncated(): boolean;
202
+ /**
203
+ * Why the last `readNew()` restarted from offset 0, and the committed
204
+ * position it left: `truncated` (the file shrank below the read
205
+ * position), `rewritten` (its first bytes, or the byte before the resume
206
+ * point, changed), or `replaced` (another file now has the path). `null`
207
+ * when it continued where it was. Optional; the JSONL readers implement it.
208
+ */
209
+ lastReset?(): SessionReaderReset | null;
189
210
  /**
190
211
  * Parse state the reader needs, beyond `getPosition()`, to continue
191
212
  * correctly after a restart: JSON-serializable data to persist together