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,330 @@
1
+ /**
2
+ * Reads Codex rollouts that Codex compressed.
3
+ *
4
+ * Codex compresses older rollouts in place to `rollout-<ts>-<id>.jsonl.zst`
5
+ * (one Zstandard frame), while its state database keeps the `.jsonl` path.
6
+ * Node ships `zlib.zstdDecompressSync` from 22.15 / 23.8; on older runtimes
7
+ * (Node 20, older VS Code Electron builds) compressed rollouts cannot be read
8
+ * and are skipped, reported once as a `read_failed` diagnostic.
9
+ *
10
+ * Every reader here takes either form: a `.jsonl` path reads as before, a
11
+ * `.jsonl.zst` path is decompressed. Compressed rollouts are complete (Codex
12
+ * only compresses rollouts it no longer writes), so they are read whole and
13
+ * never tailed.
14
+ *
15
+ * Node-only: reads files.
16
+ *
17
+ * @module providers/compressedRollouts
18
+ */
19
+ import * as fs from 'fs';
20
+ import * as zlib from 'zlib';
21
+ /** File-name suffix of a compressed rollout. */
22
+ export const COMPRESSED_ROLLOUT_SUFFIX = '.jsonl.zst';
23
+ /** `undefined`: use the runtime's; `null`: behave as if the runtime lacked it. */
24
+ let decompressOverride;
25
+ function zstdDecompressSync() {
26
+ if (decompressOverride !== undefined)
27
+ return decompressOverride;
28
+ const fn = zlib.zstdDecompressSync;
29
+ return typeof fn === 'function' ? fn.bind(zlib) : null;
30
+ }
31
+ /** Test hooks; not part of the public entry points. */
32
+ export const __compressedRolloutTesting = {
33
+ /** Replace the runtime's `zstdDecompressSync` (`null` = unavailable, `undefined` = restore). */
34
+ setDecompress(fn) {
35
+ decompressOverride = fn;
36
+ },
37
+ };
38
+ /** Whether a path (or file name) names a compressed rollout (`*.jsonl.zst`). */
39
+ export function isCompressedRolloutPath(filePath) {
40
+ return filePath.endsWith(COMPRESSED_ROLLOUT_SUFFIX);
41
+ }
42
+ /** Whether this runtime can decompress Zstandard (`zlib.zstdDecompressSync`). */
43
+ export function isZstdDecompressionAvailable() {
44
+ return zstdDecompressSync() !== null;
45
+ }
46
+ const UNAVAILABLE_MESSAGE = 'Compressed Codex rollouts (.jsonl.zst) were skipped: this Node.js runtime has no zlib.zstdDecompressSync (Node 22.15 or later).';
47
+ /** The runtime-limit diagnostic is reported once per process unless asked again. */
48
+ let reportedUnavailable = false;
49
+ function reportUnavailable(options) {
50
+ if (!options?.onDiagnostic || reportedUnavailable)
51
+ return;
52
+ reportedUnavailable = true;
53
+ options.onDiagnostic({
54
+ providerId: 'codex',
55
+ kind: 'read_failed',
56
+ severity: 'info',
57
+ phase: 'read',
58
+ message: UNAVAILABLE_MESSAGE,
59
+ });
60
+ }
61
+ function reportCorrupt(options, error) {
62
+ options?.onDiagnostic?.({
63
+ providerId: 'codex',
64
+ kind: 'read_failed',
65
+ severity: 'warning',
66
+ phase: 'read',
67
+ message: `A compressed Codex rollout could not be decompressed: ${error instanceof Error ? error.message : String(error)}`,
68
+ });
69
+ }
70
+ /**
71
+ * Whether compressed rollouts can be read here; when they cannot, reports
72
+ * the runtime limit through `onDiagnostic` (once per process). Callers that
73
+ * list rollouts use it to leave `.jsonl.zst` files out instead of listing
74
+ * sessions they cannot read.
75
+ */
76
+ export function canReadCompressedRollouts(options) {
77
+ if (zstdDecompressSync())
78
+ return true;
79
+ reportUnavailable(options);
80
+ return false;
81
+ }
82
+ /** @internal Test hook: report the runtime limit again. */
83
+ export function resetCompressedRolloutDiagnostics() {
84
+ reportedUnavailable = false;
85
+ }
86
+ function decompress(input, options) {
87
+ const fn = zstdDecompressSync();
88
+ if (!fn) {
89
+ reportUnavailable(options);
90
+ return null;
91
+ }
92
+ try {
93
+ return fn(input);
94
+ }
95
+ catch (error) {
96
+ reportCorrupt(options, error);
97
+ return null;
98
+ }
99
+ }
100
+ /**
101
+ * The whole content of a rollout: a `.jsonl` file as stored, a `.jsonl.zst`
102
+ * file decompressed. `null` when the file cannot be read or decompressed
103
+ * (the latter reported through `onDiagnostic`).
104
+ */
105
+ export function readRolloutBuffer(filePath, options) {
106
+ let raw;
107
+ try {
108
+ raw = fs.readFileSync(filePath);
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ return isCompressedRolloutPath(filePath) ? decompress(raw, options) : raw;
114
+ }
115
+ /** {@link readRolloutBuffer} decoded as UTF-8. */
116
+ export function readRolloutText(filePath, options) {
117
+ return readRolloutBuffer(filePath, options)?.toString('utf8') ?? null;
118
+ }
119
+ /** Async twin of {@link readRolloutBuffer}; decompression itself is synchronous. */
120
+ export async function readRolloutBufferAsync(filePath, options) {
121
+ let raw;
122
+ try {
123
+ raw = await fs.promises.readFile(filePath);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ return isCompressedRolloutPath(filePath) ? decompress(raw, options) : raw;
129
+ }
130
+ /** Compressed bytes read per step while looking for a decompressed prefix. */
131
+ const HEAD_STEP_BYTES = 64 * 1024;
132
+ /**
133
+ * Decompresses only as much of a compressed prefix as is needed for
134
+ * `maxBytes` of content: a truncated Zstandard frame decompresses to the
135
+ * content of its complete blocks.
136
+ */
137
+ function decompressHead(readCompressed, compressedSize, maxBytes, options) {
138
+ const fn = zstdDecompressSync();
139
+ if (!fn) {
140
+ reportUnavailable(options);
141
+ return null;
142
+ }
143
+ let length = Math.min(compressedSize, HEAD_STEP_BYTES);
144
+ for (;;) {
145
+ let out;
146
+ try {
147
+ out = fn(readCompressed(length));
148
+ }
149
+ catch (error) {
150
+ if (length >= compressedSize) {
151
+ reportCorrupt(options, error);
152
+ return null;
153
+ }
154
+ out = Buffer.alloc(0);
155
+ }
156
+ if (out.length >= maxBytes || length >= compressedSize)
157
+ return out.subarray(0, maxBytes);
158
+ length = Math.min(compressedSize, length * 4);
159
+ }
160
+ }
161
+ /**
162
+ * Up to `maxBytes` of a rollout's content from its start (decompressed for
163
+ * a `.jsonl.zst`). An empty buffer when the file cannot be read.
164
+ */
165
+ export function readRolloutHead(filePath, maxBytes, options) {
166
+ let fd;
167
+ try {
168
+ fd = fs.openSync(filePath, 'r');
169
+ }
170
+ catch {
171
+ return Buffer.alloc(0);
172
+ }
173
+ try {
174
+ const size = fs.fstatSync(fd).size;
175
+ const readPrefix = (length) => {
176
+ const buffer = Buffer.alloc(length);
177
+ const bytesRead = fs.readSync(fd, buffer, 0, length, 0);
178
+ return buffer.subarray(0, bytesRead);
179
+ };
180
+ if (!isCompressedRolloutPath(filePath))
181
+ return readPrefix(Math.min(size, Math.max(0, maxBytes)));
182
+ return decompressHead(readPrefix, size, Math.max(0, maxBytes), options) ?? Buffer.alloc(0);
183
+ }
184
+ catch {
185
+ return Buffer.alloc(0);
186
+ }
187
+ finally {
188
+ fs.closeSync(fd);
189
+ }
190
+ }
191
+ /** Async twin of {@link readRolloutHead}. */
192
+ export async function readRolloutHeadAsync(filePath, maxBytes, options) {
193
+ if (!isCompressedRolloutPath(filePath)) {
194
+ let handle = null;
195
+ try {
196
+ handle = await fs.promises.open(filePath, 'r');
197
+ const size = (await handle.stat()).size;
198
+ const length = Math.min(size, Math.max(0, maxBytes));
199
+ const buffer = Buffer.alloc(length);
200
+ const { bytesRead } = await handle.read(buffer, 0, length, 0);
201
+ return buffer.subarray(0, bytesRead);
202
+ }
203
+ catch {
204
+ return Buffer.alloc(0);
205
+ }
206
+ finally {
207
+ await handle?.close().catch(() => undefined);
208
+ }
209
+ }
210
+ // Compressed prefixes are small; one synchronous read keeps it simple.
211
+ return readRolloutHead(filePath, maxBytes, options);
212
+ }
213
+ /**
214
+ * The first `maxLines` complete lines of a rollout (decompressed for a
215
+ * `.jsonl.zst`) within `maxBytes` of content; a trailing line without a
216
+ * newline counts when the whole content fit.
217
+ */
218
+ export function readRolloutFirstLines(filePath, maxLines, maxBytes = 256 * 1024, options) {
219
+ const head = readRolloutHead(filePath, maxBytes + 1, options);
220
+ const complete = head.length <= maxBytes;
221
+ const text = head.subarray(0, maxBytes).toString('utf8');
222
+ const parts = text.split('\n');
223
+ const tail = parts.pop() ?? '';
224
+ const lines = parts.slice(0, maxLines);
225
+ if (lines.length < maxLines && complete && tail.trim())
226
+ lines.push(tail);
227
+ return lines;
228
+ }
229
+ /**
230
+ * The content size a Zstandard frame header declares (`Frame_Content_Size`),
231
+ * or `null` when the header omits it or is not a Zstandard frame. Skippable
232
+ * frames before it are not followed.
233
+ */
234
+ export function zstdFrameContentSize(header) {
235
+ if (header.length < 5 || header.readUInt32LE(0) !== 0xfd2fb528)
236
+ return null;
237
+ const descriptor = header[4];
238
+ const fcsFlag = descriptor >> 6;
239
+ const singleSegment = (descriptor >> 5) & 1;
240
+ const dictionaryIdFlag = descriptor & 3;
241
+ const offset = 5 + (singleSegment ? 0 : 1) + [0, 1, 2, 4][dictionaryIdFlag];
242
+ const fcsBytes = fcsFlag === 0 ? (singleSegment ? 1 : 0) : [0, 2, 4, 8][fcsFlag];
243
+ if (fcsBytes === 0 || header.length < offset + fcsBytes)
244
+ return null;
245
+ switch (fcsBytes) {
246
+ case 1:
247
+ return header[offset];
248
+ case 2:
249
+ return header.readUInt16LE(offset) + 256;
250
+ case 4:
251
+ return header.readUInt32LE(offset);
252
+ default: {
253
+ const value = header.readBigUInt64LE(offset);
254
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : null;
255
+ }
256
+ }
257
+ }
258
+ /**
259
+ * Stat of a rollout's content: the file's own for a `.jsonl`; for a
260
+ * `.jsonl.zst`, the decompressed size (from the frame header, else by
261
+ * decompressing) with the compressed file's mtime. Rejects like `fs.stat`
262
+ * when the file is missing; a compressed rollout that cannot be read
263
+ * rejects too.
264
+ */
265
+ export async function statRolloutContent(filePath, options) {
266
+ const stat = await fs.promises.stat(filePath);
267
+ if (!isCompressedRolloutPath(filePath))
268
+ return { size: stat.size, mtimeMs: stat.mtimeMs };
269
+ let declared = null;
270
+ let handle = null;
271
+ try {
272
+ handle = await fs.promises.open(filePath, 'r');
273
+ const header = Buffer.alloc(18);
274
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
275
+ declared = zstdFrameContentSize(header.subarray(0, bytesRead));
276
+ }
277
+ finally {
278
+ await handle?.close().catch(() => undefined);
279
+ }
280
+ if (declared !== null && isZstdDecompressionAvailable()) {
281
+ return { size: declared, mtimeMs: stat.mtimeMs };
282
+ }
283
+ const content = await readRolloutBufferAsync(filePath, options);
284
+ if (!content)
285
+ throw new Error('compressed rollout could not be read');
286
+ return { size: content.length, mtimeMs: stat.mtimeMs };
287
+ }
288
+ /** A read-only handle over a buffer, for scanning decompressed content. */
289
+ function bufferHandle(content) {
290
+ return {
291
+ async read(buffer, offset, length, position) {
292
+ if (position >= content.length || length <= 0)
293
+ return { bytesRead: 0 };
294
+ const bytesRead = content.copy(buffer, offset, position, Math.min(content.length, position + length));
295
+ return { bytesRead };
296
+ },
297
+ async close() { },
298
+ };
299
+ }
300
+ /**
301
+ * Opens a rollout for positional reads over its content: a file handle for
302
+ * a `.jsonl`, the decompressed content for a `.jsonl.zst` (with `size`, the
303
+ * content length). Rejects when the file cannot be opened or decompressed.
304
+ */
305
+ export async function openRolloutReadHandle(filePath, options) {
306
+ if (!isCompressedRolloutPath(filePath)) {
307
+ return { handle: await fs.promises.open(filePath, 'r'), size: null };
308
+ }
309
+ const content = await readRolloutBufferAsync(filePath, options);
310
+ if (!content)
311
+ throw new Error('compressed rollout could not be read');
312
+ return { handle: bufferHandle(content), size: content.length };
313
+ }
314
+ /**
315
+ * The path of a rollout as stored now: `filePath` when it exists, else its
316
+ * compressed sibling (`<filePath>.zst`) when `includeCompressed` is set and
317
+ * that exists. `null` when neither does.
318
+ */
319
+ export function resolveRolloutPath(filePath, options = {}) {
320
+ if (!filePath)
321
+ return null;
322
+ if (fs.existsSync(filePath))
323
+ return filePath;
324
+ if (options.includeCompressed && !isCompressedRolloutPath(filePath)) {
325
+ const compressed = `${filePath}.zst`;
326
+ if (filePath.endsWith('.jsonl') && fs.existsSync(compressed))
327
+ return compressed;
328
+ }
329
+ return null;
330
+ }
@@ -37,7 +37,10 @@ export function createSessionProviders(options = {}) {
37
37
  ? new ClaudeCodeProvider(providerOptions)
38
38
  : providerId === 'opencode'
39
39
  ? new OpenCodeProvider(providerOptions)
40
- : new CodexProvider(providerOptions));
40
+ : new CodexProvider({
41
+ ...providerOptions,
42
+ includeCompressedRollouts: options.includeCompressedRollouts === true,
43
+ }));
41
44
  }
42
45
  catch {
43
46
  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.
@@ -25,16 +29,23 @@ import * as fs from 'fs';
25
29
  import * as path from 'path';
26
30
  export const DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
27
31
  export const DEFAULT_ROLLOUT_WALK_MAX_FILES = 20_000;
28
- /** Test if a filename is a Codex rollout file. */
32
+ /** Test if a filename is a Codex rollout file (`rollout-….jsonl`; not compressed ones). */
29
33
  export function isRolloutFile(filename) {
30
34
  return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
31
35
  }
36
+ /** Test if a filename is a compressed Codex rollout (`rollout-….jsonl.zst`). */
37
+ export function isCompressedRolloutFile(filename) {
38
+ return filename.startsWith('rollout-') && filename.endsWith('.jsonl.zst');
39
+ }
32
40
  /**
33
41
  * Extract the session UUID from a rollout filename.
34
- * Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
42
+ * Format: rollout-<timestamp>-<uuid>.jsonl (or .jsonl.zst) -> <uuid>
35
43
  */
36
44
  export function extractRolloutSessionId(filename) {
37
- const base = path.basename(filename, '.jsonl');
45
+ const name = path.basename(filename);
46
+ const base = name.endsWith('.jsonl.zst')
47
+ ? name.slice(0, -'.jsonl.zst'.length)
48
+ : path.basename(name, '.jsonl');
38
49
  // rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
39
50
  const parts = base.split('-');
40
51
  // The UUID is typically the last 5 segments (8-4-4-4-12)
@@ -58,6 +69,7 @@ function walkState(options) {
58
69
  since: options.since !== undefined && Number.isFinite(options.since) ? options.since : null,
59
70
  sessionId: options.sessionId ? options.sessionId.trim().toLowerCase() : null,
60
71
  includeEmpty: options.includeEmpty ?? false,
72
+ includeCompressed: options.includeCompressed ?? false,
61
73
  cache: options.cache ?? null,
62
74
  revalidateRecent: options.revalidateRecent ?? false,
63
75
  stats: options.stats,
@@ -68,10 +80,15 @@ function byNameDescending(a, b) {
68
80
  return b.name.localeCompare(a.name);
69
81
  }
70
82
  function matchesFilter(state, name) {
71
- if (!isRolloutFile(name))
83
+ if (!isRolloutFile(name) && !(state.includeCompressed && isCompressedRolloutFile(name))) {
72
84
  return false;
85
+ }
73
86
  return (state.sessionId === null || extractRolloutSessionId(name).toLowerCase() === state.sessionId);
74
87
  }
88
+ /** A compressed rollout whose uncompressed rollout is listed beside it. */
89
+ function shadowedByPlain(name, has) {
90
+ return isCompressedRolloutFile(name) && has(name.slice(0, -'.zst'.length));
91
+ }
75
92
  function record(state, fullPath, name, stat) {
76
93
  if (!state.includeEmpty && stat.size <= 0)
77
94
  return;
@@ -130,11 +147,14 @@ function visitSync(state, dir, depth, segments) {
130
147
  return;
131
148
  }
132
149
  entries.sort(byNameDescending);
150
+ const names = state.includeCompressed ? new Set(entries.map((entry) => entry.name)) : null;
133
151
  for (const entry of entries) {
134
152
  if (state.results.length >= state.cap)
135
153
  return;
136
154
  if (!entry.isFile() || !matchesFilter(state, entry.name))
137
155
  continue;
156
+ if (names && shadowedByPlain(entry.name, (name) => names.has(name)))
157
+ continue;
138
158
  const fullPath = path.join(dir, entry.name);
139
159
  try {
140
160
  record(state, fullPath, entry.name, fs.statSync(fullPath));
@@ -185,6 +205,11 @@ async function visitCached(state, cache, dir, depth, segments) {
185
205
  const stat = listing.files.get(name);
186
206
  if (!stat)
187
207
  continue;
208
+ // The listing may come from a walk with other options: filter again.
209
+ if (!matchesFilter(state, name))
210
+ continue;
211
+ if (shadowedByPlain(name, (other) => listing.files.has(other)))
212
+ continue;
188
213
  record(state, path.join(dir, name), name, { size: stat.sizeBytes, mtimeMs: stat.mtimeMs });
189
214
  }
190
215
  await yieldToEventLoop();
@@ -209,12 +234,15 @@ async function visitAsync(state, dir, depth, segments) {
209
234
  return;
210
235
  }
211
236
  entries.sort(byNameDescending);
237
+ const names = state.includeCompressed ? new Set(entries.map((entry) => entry.name)) : null;
212
238
  let sinceYield = 0;
213
239
  for (const entry of entries) {
214
240
  if (state.results.length >= state.cap)
215
241
  return;
216
242
  if (!entry.isFile() || !matchesFilter(state, entry.name))
217
243
  continue;
244
+ if (names && shadowedByPlain(entry.name, (name) => names.has(name)))
245
+ continue;
218
246
  const fullPath = path.join(dir, entry.name);
219
247
  try {
220
248
  const stat = await fs.promises.stat(fullPath);
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { z } from 'zod';
10
10
  import { normalizedUsageSchema } from './usageNormalization';
11
- import { taskNotificationPromptFromAttachment } from '../subagentSpawn';
11
+ import { TASK_STATUS_BLOCK_TYPE, TASK_STATUS_ORIGIN_KIND, isFinalTaskStatus, taskNotificationPromptFromAttachment, taskStatusFromAttachment, } from '../subagentSpawn';
12
12
  // ── MessageUsage ──
13
13
  export const messageUsageSchema = z.object({
14
14
  input_tokens: z.number(),
@@ -41,6 +41,8 @@ export const permissionModeSchema = z.enum([
41
41
  'acceptEdits',
42
42
  'bypassPermissions',
43
43
  'plan',
44
+ 'auto',
45
+ 'dontAsk',
44
46
  ]);
45
47
  // ── SessionEvent ──
46
48
  const eventBaseShape = {
@@ -53,8 +55,8 @@ const eventBaseShape = {
53
55
  promptSource: z.string().optional(),
54
56
  cwd: z.string().optional(),
55
57
  gitBranch: z.string().optional(),
56
- // A mode this release doesn't know (Claude Code added `auto`) is dropped
57
- // rather than rejecting the whole line with its prompt or tool results.
58
+ // A mode this release doesn't know is dropped rather than rejecting the
59
+ // whole line with its prompt or tool results.
58
60
  permissionMode: permissionModeSchema.optional().catch(undefined),
59
61
  rateLimits: z
60
62
  .object({
@@ -144,6 +146,9 @@ export const systemSessionEventSchema = z.object({
144
146
  type: z.literal('system'),
145
147
  message: sessionMessageSchema.optional(),
146
148
  ...eventBaseShape,
149
+ // Claude Code's system-line subtype (`agents_killed`, `turn_duration`, …);
150
+ // a malformed one is dropped rather than rejecting the line.
151
+ subtype: z.string().optional().catch(undefined),
147
152
  });
148
153
  export const toolUseSessionEventSchema = z.object({
149
154
  type: z.literal('tool_use'),
@@ -183,7 +188,7 @@ function extractSessionEventsWithProvenance(raw, depth, inherited, options) {
183
188
  if (direct.success)
184
189
  return [{ ...inherited, ...direct.data }];
185
190
  if (options.includeTaskNotifications) {
186
- const notification = taskNotificationAttachmentEvent(raw);
191
+ const notification = taskNotificationAttachmentEvent(raw) ?? taskStatusAttachmentEvent(raw);
187
192
  if (notification)
188
193
  return [{ ...inherited, ...notification }];
189
194
  }
@@ -220,6 +225,29 @@ function taskNotificationAttachmentEvent(raw) {
220
225
  const parsed = systemSessionEventSchema.safeParse(candidate);
221
226
  return parsed.success ? parsed.data : null;
222
227
  }
228
+ /** The `system` event for a background agent's final `task_status` attachment, or `null`. */
229
+ function taskStatusAttachmentEvent(raw) {
230
+ const status = taskStatusFromAttachment(raw);
231
+ if (!status || !isFinalTaskStatus(status.status))
232
+ return null;
233
+ const line = raw;
234
+ const candidate = {
235
+ type: 'system',
236
+ timestamp: line.timestamp,
237
+ origin: { kind: TASK_STATUS_ORIGIN_KIND },
238
+ message: {
239
+ role: 'system',
240
+ sourceLabel: 'task status',
241
+ content: [{ type: TASK_STATUS_BLOCK_TYPE, ...status }],
242
+ },
243
+ };
244
+ for (const key of ['entrypoint', 'isSidechain', 'cwd', 'gitBranch']) {
245
+ if (line[key] !== undefined)
246
+ candidate[key] = line[key];
247
+ }
248
+ const parsed = systemSessionEventSchema.safeParse(candidate);
249
+ return parsed.success ? parsed.data : null;
250
+ }
223
251
  function progressProvenance(raw) {
224
252
  const value = raw;
225
253
  const provenance = {};
@@ -31,12 +31,18 @@ export class SessionMonitor {
31
31
  const sessionPath = this.provider.findActiveSession(workspacePath);
32
32
  return sessionPath ? this.attach(sessionPath, replay) : null;
33
33
  }
34
+ /**
35
+ * Reads and aggregates what the session gained since the last call. When
36
+ * the reader had to start over (the file was truncated, rewritten, or
37
+ * replaced) the aggregator is reset first and the returned events are the
38
+ * session from the beginning.
39
+ */
34
40
  poll() {
35
41
  if (!this.reader)
36
42
  return [];
43
+ const events = this.reader.readNew();
37
44
  if (this.reader.wasTruncated())
38
45
  this.aggregator.reset();
39
- const events = this.reader.readNew();
40
46
  this.consume(events);
41
47
  return events;
42
48
  }
@@ -18,6 +18,7 @@
18
18
  * @module sessionPreviews
19
19
  */
20
20
  import * as fs from 'fs';
21
+ import { isCompressedRolloutPath, readRolloutHead } from './providers/compressedRollouts';
21
22
  function emptyPrefixScan() {
22
23
  return { firstTimestamp: null, workspacePath: null };
23
24
  }
@@ -415,6 +416,12 @@ function scanPrefix(sessionPath, maxPrefixBytes) {
415
416
  const result = { firstTimestamp: null, workspacePath: null };
416
417
  let content;
417
418
  let sawWholeFile;
419
+ if (isCompressedRolloutPath(sessionPath)) {
420
+ // A compressed Codex rollout: scan its decompressed prefix.
421
+ const head = readRolloutHead(sessionPath, Math.max(1, maxPrefixBytes) + 1);
422
+ parsePrefixInto(result, head.subarray(0, Math.max(1, maxPrefixBytes)).toString('utf8'), head.length <= maxPrefixBytes);
423
+ return result;
424
+ }
418
425
  try {
419
426
  const descriptor = fs.openSync(sessionPath, 'r');
420
427
  try {
@@ -470,6 +477,8 @@ function scanPrefix(sessionPath, maxPrefixBytes) {
470
477
  }
471
478
  async function scanPrefixAsync(sessionPath, maxPrefixBytes) {
472
479
  const result = { firstTimestamp: null, workspacePath: null };
480
+ if (isCompressedRolloutPath(sessionPath))
481
+ return scanPrefix(sessionPath, maxPrefixBytes);
473
482
  let handle = null;
474
483
  try {
475
484
  handle = await fs.promises.open(sessionPath, 'r');
@@ -13,7 +13,7 @@
13
13
  * `sessionIds` continues. Scope fails closed as in `collectPromptHistory()`,
14
14
  * and replies or signals that belong to an out-of-scope prompt are dropped.
15
15
  */
16
- import * as fs from 'fs';
16
+ import { statRolloutContent } from './providers/compressedRollouts';
17
17
  import { DEFAULT_BOUNDS, PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES, compareStrings, emptyStats, listCandidates, resolveRoots, scanFile, } from './promptHistory';
18
18
  import { createSessionRecordExtractor, } from './sessionPromptSignals';
19
19
  const DEFAULT_SESSION_BOUNDS = {
@@ -188,7 +188,9 @@ function definedBounds(bounds) {
188
188
  async function selectCandidates(options, listingRoots) {
189
189
  const sinceMs = options.since ? options.since.getTime() : null;
190
190
  const providers = options.providers ?? ['claude-code', 'codex'];
191
- let candidates = await listCandidates(providers, listingRoots, sinceMs);
191
+ let candidates = await listCandidates(providers, listingRoots, sinceMs, {
192
+ includeCompressedRollouts: options.includeCompressedRollouts,
193
+ });
192
194
  if (options.sessionIds || options.sessionRefs) {
193
195
  const ids = new Set(options.sessionIds ?? []);
194
196
  const keys = new Set();
@@ -206,7 +208,7 @@ async function selectCandidates(options, listingRoots) {
206
208
  const selected = [];
207
209
  for (const candidate of candidates) {
208
210
  try {
209
- selected.push({ candidate, stat: await fs.promises.stat(candidate.filePath) });
211
+ selected.push({ candidate, stat: await statRolloutContent(candidate.filePath) });
210
212
  }
211
213
  catch {
212
214
  // Gone since it was listed.