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.
- package/CHANGELOG.md +26 -0
- package/README.md +171 -60
- package/dist/aggregation/EventAggregator.d.ts +79 -4
- package/dist/aggregation/EventAggregator.js +391 -37
- package/dist/aggregation/types.d.ts +27 -3
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +23 -3
- package/dist/context/sessionContext.js +5 -1
- package/dist/esm/aggregation/EventAggregator.js +392 -38
- package/dist/esm/browser.js +3 -1
- package/dist/esm/context/sessionContext.js +6 -2
- package/dist/esm/fileContinuity.js +149 -0
- package/dist/esm/index.js +5 -2
- package/dist/esm/node.js +2 -0
- package/dist/esm/parsers/codexParser.js +192 -3
- package/dist/esm/parsers/subagentEndState.js +61 -0
- package/dist/esm/parsers/subagentScanner.js +4 -0
- package/dist/esm/parsers/subagentTraceParser.js +6 -0
- package/dist/esm/promptHistory.js +22 -11
- package/dist/esm/providers/claudeCode.js +50 -27
- package/dist/esm/providers/codex.js +153 -163
- package/dist/esm/providers/codexCompressedReader.js +184 -0
- package/dist/esm/providers/codexDatabase.js +47 -2
- package/dist/esm/providers/codexRollouts.js +20 -2
- package/dist/esm/providers/codexSubagents.js +456 -0
- package/dist/esm/providers/compressedRollouts.js +330 -0
- package/dist/esm/providers/factory.js +4 -1
- package/dist/esm/providers/rolloutWalker.js +32 -4
- package/dist/esm/schemas/sessionEvent.js +32 -4
- package/dist/esm/sessionMonitor.js +7 -1
- package/dist/esm/sessionPreviews.js +9 -0
- package/dist/esm/sessionPromptHistory.js +5 -3
- package/dist/esm/subagentActivity.js +125 -0
- package/dist/esm/subagentDisplayState.js +81 -0
- package/dist/esm/subagentSpawn.js +206 -6
- package/dist/esm/testing/codexSubagentFixtures.js +336 -0
- package/dist/esm/toolCallOrigins.js +17 -10
- package/dist/esm/toolCategory.js +6 -2
- package/dist/esm/transcript.js +6 -2
- package/dist/esm/turns/assistantTurn.js +6 -3
- package/dist/esm/usage/usageEvents.js +4 -1
- package/dist/esm/watchers/eventBridge.js +31 -1
- package/dist/esm/watchers/jsonlTail.js +11 -42
- package/dist/esm/watchers/jsonlWatcher.js +19 -9
- package/dist/esm/watchers/providerReaderWatcher.js +5 -4
- package/dist/fileContinuity.d.ts +95 -0
- package/dist/fileContinuity.js +187 -0
- package/dist/index.d.ts +14 -7
- package/dist/index.js +39 -10
- package/dist/node.d.ts +4 -0
- package/dist/node.js +16 -2
- package/dist/parsers/codexParser.d.ts +44 -0
- package/dist/parsers/codexParser.js +192 -3
- package/dist/parsers/subagentEndState.d.ts +20 -0
- package/dist/parsers/subagentEndState.js +65 -0
- package/dist/parsers/subagentScanner.js +4 -0
- package/dist/parsers/subagentTraceParser.d.ts +8 -0
- package/dist/parsers/subagentTraceParser.js +6 -0
- package/dist/promptHistory.d.ts +13 -3
- package/dist/promptHistory.js +22 -11
- package/dist/providers/claudeCode.js +50 -27
- package/dist/providers/codex.d.ts +35 -5
- package/dist/providers/codex.js +150 -160
- package/dist/providers/codexCompressedReader.d.ts +58 -0
- package/dist/providers/codexCompressedReader.js +221 -0
- package/dist/providers/codexDatabase.d.ts +25 -0
- package/dist/providers/codexDatabase.js +48 -2
- package/dist/providers/codexRollouts.d.ts +16 -2
- package/dist/providers/codexRollouts.js +20 -2
- package/dist/providers/codexSubagents.d.ts +66 -0
- package/dist/providers/codexSubagents.js +495 -0
- package/dist/providers/compressedRollouts.d.ts +114 -0
- package/dist/providers/compressedRollouts.js +380 -0
- package/dist/providers/factory.d.ts +5 -0
- package/dist/providers/factory.js +4 -1
- package/dist/providers/rolloutWalker.d.ts +15 -2
- package/dist/providers/rolloutWalker.js +33 -4
- package/dist/providers/types.d.ts +22 -1
- package/dist/schemas/sessionEvent.d.ts +26 -2
- package/dist/schemas/sessionEvent.js +31 -3
- package/dist/sessionMonitor.d.ts +6 -0
- package/dist/sessionMonitor.js +7 -1
- package/dist/sessionPreviews.js +9 -0
- package/dist/sessionPromptHistory.d.ts +5 -0
- package/dist/sessionPromptHistory.js +5 -36
- package/dist/subagentActivity.d.ts +91 -0
- package/dist/subagentActivity.js +132 -0
- package/dist/subagentDisplayState.d.ts +81 -0
- package/dist/subagentDisplayState.js +86 -0
- package/dist/subagentSpawn.d.ts +100 -5
- package/dist/subagentSpawn.js +218 -7
- package/dist/testing/codexSubagentFixtures.d.ts +48 -0
- package/dist/testing/codexSubagentFixtures.js +344 -0
- package/dist/toolCallOrigins.d.ts +5 -0
- package/dist/toolCallOrigins.js +17 -10
- package/dist/toolCategory.js +6 -2
- package/dist/transcript.js +5 -1
- package/dist/turns/assistantTurn.d.ts +4 -1
- package/dist/turns/assistantTurn.js +5 -2
- package/dist/types/codex.d.ts +71 -2
- package/dist/types/sessionEvent.d.ts +38 -3
- package/dist/usage/usageEvents.d.ts +4 -1
- package/dist/usage/usageEvents.js +4 -1
- package/dist/watchers/eventBridge.js +30 -0
- package/dist/watchers/jsonlTail.js +11 -42
- package/dist/watchers/jsonlWatcher.d.ts +5 -1
- package/dist/watchers/jsonlWatcher.js +18 -8
- package/dist/watchers/providerReaderWatcher.js +5 -4
- 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(
|
|
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
|
|
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
|
|
57
|
-
//
|
|
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
|
|
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
|
|
211
|
+
selected.push({ candidate, stat: await statRolloutContent(candidate.filePath) });
|
|
210
212
|
}
|
|
211
213
|
catch {
|
|
212
214
|
// Gone since it was listed.
|