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