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
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Implements SessionProviderBase for monitoring Codex CLI sessions.
|
|
5
5
|
* Codex stores sessions as JSONL rollout files in:
|
|
6
6
|
* ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl
|
|
7
|
-
* with an optional SQLite index at ~/.codex/state.sqlite.
|
|
7
|
+
* with an optional SQLite index at ~/.codex/state.sqlite. Older rollouts are
|
|
8
|
+
* compressed in place to `….jsonl.zst`; those are read only with the
|
|
9
|
+
* `includeCompressedRollouts` option (see `providers/compressedRollouts`).
|
|
8
10
|
*
|
|
9
11
|
* Ported from sidekick-vscode/src/services/providers/CodexSessionProvider.ts
|
|
10
12
|
*/
|
|
@@ -14,17 +16,20 @@ import { StringDecoder } from 'string_decoder';
|
|
|
14
16
|
import { readSessionContextSnapshot } from '../context/sessionContext';
|
|
15
17
|
import { readSessionFileStats } from '../sessionStats';
|
|
16
18
|
import { CodexRolloutParser } from '../parsers/codexParser';
|
|
17
|
-
import { CodexDatabase
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
19
|
+
import { CodexDatabase } from './codexDatabase';
|
|
20
|
+
import { DEFAULT_ROLLOUT_WALK_MAX_DEPTH, extractRolloutSessionId as extractSessionId, isCompressedRolloutFile, isRolloutFile, walkRolloutFiles, } from './rolloutWalker';
|
|
21
|
+
import { canReadCompressedRollouts, isCompressedRolloutPath, readRolloutHeadAsync, readRolloutText, resolveRolloutPath, } from './compressedRollouts';
|
|
22
|
+
import { CompressedCodexRolloutReader } from './codexCompressedReader';
|
|
23
|
+
import { discoverCodexSubagents } from './codexSubagents';
|
|
24
|
+
import { codexRolloutOrigin } from '../parsers/codexRolloutOrigin';
|
|
20
25
|
import { getCodexSessionsDirs } from '../codexPaths';
|
|
21
26
|
import { cwdMatches, findCodexRolloutFile, getSessionsDirs, listCodexSessionFilesAsync, normalizePath, readFirstLines, readSessionCwd, readSessionCwdAsync, readSessionMeta, } from './codexRollouts';
|
|
22
27
|
import { DirectoryListingCache, splitWatchedPath } from './directoryListingCache';
|
|
28
|
+
import { FileContinuity } from '../fileContinuity';
|
|
23
29
|
import { ProviderDiagnosticTracker, diagnosticFromRuntimeStatus } from './diagnostics';
|
|
24
30
|
import { getModelContextWindowSize } from '../modelContext';
|
|
25
31
|
import { recordObservedContextWindow } from '../observedContextWindows';
|
|
26
|
-
import {
|
|
27
|
-
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
32
|
+
import { normalizeProviderUsage } from '../usageNormalization';
|
|
28
33
|
// ---------------------------------------------------------------------------
|
|
29
34
|
// Helper functions
|
|
30
35
|
// ---------------------------------------------------------------------------
|
|
@@ -37,8 +42,8 @@ function truncate(text, maxLen) {
|
|
|
37
42
|
return trimmed;
|
|
38
43
|
}
|
|
39
44
|
/** Every rollout under a directory, newest first (one capped walk). */
|
|
40
|
-
function findRolloutFiles(dir) {
|
|
41
|
-
return walkRolloutFiles([dir]);
|
|
45
|
+
function findRolloutFiles(dir, includeCompressed = false) {
|
|
46
|
+
return walkRolloutFiles([dir], { includeCompressed });
|
|
42
47
|
}
|
|
43
48
|
function hasRolloutFile(dir) {
|
|
44
49
|
return walkRolloutFiles([dir], { limit: 1 }).length > 0;
|
|
@@ -47,6 +52,10 @@ function hasRolloutFile(dir) {
|
|
|
47
52
|
function findRolloutFilesInConfiguredHomes(options = {}) {
|
|
48
53
|
return walkRolloutFiles(getSessionsDirs(), options);
|
|
49
54
|
}
|
|
55
|
+
/** Whether a rollout's session_meta says a subagent wrote it. */
|
|
56
|
+
function isSubagentRollout(rolloutPath) {
|
|
57
|
+
return codexRolloutOrigin(readSessionMeta(rolloutPath)).isSubagent;
|
|
58
|
+
}
|
|
50
59
|
export { findCodexRolloutFile };
|
|
51
60
|
/** Pick the best sessions directory for UI/search entrypoints. */
|
|
52
61
|
function getSessionsDir() {
|
|
@@ -124,19 +133,16 @@ function extractFirstUserMessageFromLines(lines) {
|
|
|
124
133
|
return null;
|
|
125
134
|
}
|
|
126
135
|
async function extractFirstUserMessageAsync(rolloutPath) {
|
|
127
|
-
let handle = null;
|
|
128
136
|
try {
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
// A compressed rollout's head is decompressed; a plain one is read as is.
|
|
138
|
+
const head = await readRolloutHeadAsync(rolloutPath, 256 * 1024);
|
|
139
|
+
if (head.length === 0)
|
|
140
|
+
return null;
|
|
141
|
+
return extractFirstUserMessageFromLines(head.toString('utf8').split('\n').slice(0, 20));
|
|
133
142
|
}
|
|
134
143
|
catch {
|
|
135
144
|
return null;
|
|
136
145
|
}
|
|
137
|
-
finally {
|
|
138
|
-
await handle?.close().catch(() => undefined);
|
|
139
|
-
}
|
|
140
146
|
}
|
|
141
147
|
/** Extract searchable text from a rollout line. */
|
|
142
148
|
function extractSearchableText(line) {
|
|
@@ -268,7 +274,15 @@ class CodexReader {
|
|
|
268
274
|
/** Cursor after the last complete line, safe to persist across restarts. */
|
|
269
275
|
committedPosition = 0;
|
|
270
276
|
lineBuffer = '';
|
|
271
|
-
|
|
277
|
+
/** Why the last `readNew()` started over from offset 0, if it did. */
|
|
278
|
+
_lastReset = null;
|
|
279
|
+
/**
|
|
280
|
+
* A restart found while settling a seek (the file is now shorter than the
|
|
281
|
+
* position sought); the next `readNew()` reports it.
|
|
282
|
+
*/
|
|
283
|
+
pendingRestart = null;
|
|
284
|
+
/** Whether the rollout is still the one the positions refer to. */
|
|
285
|
+
continuity = new FileContinuity();
|
|
272
286
|
decoder = new StringDecoder('utf8');
|
|
273
287
|
/**
|
|
274
288
|
* Position from `seekTo()` whose prefix still has to be parsed for model,
|
|
@@ -281,7 +295,7 @@ class CodexReader {
|
|
|
281
295
|
this.onContextWindowLimit = onContextWindowLimit;
|
|
282
296
|
this.onRateLimits = onRateLimits;
|
|
283
297
|
this.onTokenUsage = onTokenUsage;
|
|
284
|
-
this.parser = new CodexRolloutParser();
|
|
298
|
+
this.parser = new CodexRolloutParser({ includeSubagentActivity: true });
|
|
285
299
|
}
|
|
286
300
|
/** Get the parser's session metadata. */
|
|
287
301
|
getSessionMeta() {
|
|
@@ -290,36 +304,32 @@ class CodexReader {
|
|
|
290
304
|
}
|
|
291
305
|
readNew(endPosition) {
|
|
292
306
|
this.settleSeek();
|
|
293
|
-
this.
|
|
307
|
+
this._lastReset = null;
|
|
294
308
|
const events = [];
|
|
309
|
+
let fd = null;
|
|
295
310
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.
|
|
303
|
-
this.filePosition = 0;
|
|
304
|
-
this.committedPosition = 0;
|
|
305
|
-
this.lineBuffer = '';
|
|
306
|
-
this.parser.reset();
|
|
307
|
-
this.decoder = new StringDecoder('utf8');
|
|
311
|
+
// Open first and stat the descriptor: the size and identity checked
|
|
312
|
+
// are those of the file actually read, even if the path is replaced.
|
|
313
|
+
fd = fs.openSync(this.rolloutPath, 'r');
|
|
314
|
+
const stats = fs.fstatSync(fd);
|
|
315
|
+
if (this.pendingRestart) {
|
|
316
|
+
this._lastReset = this.pendingRestart;
|
|
317
|
+
this.pendingRestart = null;
|
|
308
318
|
}
|
|
319
|
+
const reason = this.continuity.check(fd, stats, this.filePosition, this.committedPosition);
|
|
320
|
+
// Truncated, rewritten, or replaced: what was read no longer describes
|
|
321
|
+
// the rollout, so start over from its beginning.
|
|
322
|
+
if (reason !== null)
|
|
323
|
+
this.restart(reason);
|
|
324
|
+
this.continuity.adopt(stats);
|
|
325
|
+
const currentSize = Math.min(stats.size, endPosition ?? stats.size);
|
|
309
326
|
// No new content
|
|
310
327
|
if (currentSize <= this.filePosition)
|
|
311
|
-
return
|
|
328
|
+
return events;
|
|
312
329
|
// Read new bytes
|
|
313
|
-
const fd = fs.openSync(this.rolloutPath, 'r');
|
|
314
330
|
const bufferSize = currentSize - this.filePosition;
|
|
315
331
|
const buffer = Buffer.alloc(bufferSize);
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
319
|
-
}
|
|
320
|
-
finally {
|
|
321
|
-
fs.closeSync(fd);
|
|
322
|
-
}
|
|
332
|
+
const bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
323
333
|
const bytes = buffer.subarray(0, bytesRead);
|
|
324
334
|
const newline = bytes.lastIndexOf(0x0a);
|
|
325
335
|
if (newline >= 0)
|
|
@@ -346,10 +356,30 @@ class CodexReader {
|
|
|
346
356
|
this.propagateParserState();
|
|
347
357
|
}
|
|
348
358
|
catch {
|
|
349
|
-
// Skip read errors
|
|
359
|
+
// Skip read errors (a missing rollout reads as no events)
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
if (fd !== null) {
|
|
363
|
+
try {
|
|
364
|
+
fs.closeSync(fd);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Ignore close errors.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
350
370
|
}
|
|
351
371
|
return events;
|
|
352
372
|
}
|
|
373
|
+
/** Starts over from offset 0 of a rollout that was truncated, rewritten, or replaced. */
|
|
374
|
+
restart(reason) {
|
|
375
|
+
this._lastReset = { reason, previousPosition: this.committedPosition };
|
|
376
|
+
this.filePosition = 0;
|
|
377
|
+
this.committedPosition = 0;
|
|
378
|
+
this.lineBuffer = '';
|
|
379
|
+
this.parser.reset();
|
|
380
|
+
this.decoder = new StringDecoder('utf8');
|
|
381
|
+
this.pendingSeek = null;
|
|
382
|
+
}
|
|
353
383
|
/** Hands the parser's context window, rate limits, and last usage to the provider. */
|
|
354
384
|
propagateParserState() {
|
|
355
385
|
// Propagate model_context_window from token_count events to the provider.
|
|
@@ -398,8 +428,11 @@ class CodexReader {
|
|
|
398
428
|
this.lineBuffer = '';
|
|
399
429
|
this.parser.reset();
|
|
400
430
|
this.decoder = new StringDecoder('utf8');
|
|
401
|
-
this.
|
|
431
|
+
this._lastReset = null;
|
|
432
|
+
this.pendingRestart = null;
|
|
402
433
|
this.pendingSeek = null;
|
|
434
|
+
// The next read adopts whatever rollout has the path.
|
|
435
|
+
this.continuity.forget();
|
|
403
436
|
}
|
|
404
437
|
exists() {
|
|
405
438
|
return fs.existsSync(this.rolloutPath);
|
|
@@ -432,7 +465,10 @@ class CodexReader {
|
|
|
432
465
|
this.pendingSeek = position;
|
|
433
466
|
}
|
|
434
467
|
wasTruncated() {
|
|
435
|
-
return this.
|
|
468
|
+
return this._lastReset !== null;
|
|
469
|
+
}
|
|
470
|
+
lastReset() {
|
|
471
|
+
return this._lastReset ? { ...this._lastReset } : null;
|
|
436
472
|
}
|
|
437
473
|
/** The parser's continuation state (see `CodexRolloutParser.exportResumeState`). */
|
|
438
474
|
getResumeState() {
|
|
@@ -461,11 +497,14 @@ class CodexReader {
|
|
|
461
497
|
return;
|
|
462
498
|
this.reset();
|
|
463
499
|
this.readNew(position);
|
|
500
|
+
if (this.filePosition < position) {
|
|
501
|
+
// The rollout no longer reaches the position sought: it was truncated
|
|
502
|
+
// or replaced since. The next read starts over and reports it.
|
|
503
|
+
this.reset();
|
|
504
|
+
this.pendingRestart = { reason: 'truncated', previousPosition: position };
|
|
505
|
+
}
|
|
464
506
|
}
|
|
465
507
|
}
|
|
466
|
-
// ---------------------------------------------------------------------------
|
|
467
|
-
// CodexProvider — full SessionProviderBase implementation
|
|
468
|
-
// ---------------------------------------------------------------------------
|
|
469
508
|
/**
|
|
470
509
|
* Session provider for Codex CLI.
|
|
471
510
|
*
|
|
@@ -484,8 +523,26 @@ export class CodexProvider {
|
|
|
484
523
|
lastTokenUsageData = null;
|
|
485
524
|
/** Dated rollout-directory listings reused across async enumerations. */
|
|
486
525
|
directoryCache = new DirectoryListingCache();
|
|
526
|
+
/** See {@link CodexProviderOptions.includeCompressedRollouts}. */
|
|
527
|
+
compressedRequested;
|
|
528
|
+
onDiagnostic;
|
|
487
529
|
constructor(options = {}) {
|
|
488
530
|
this.diagnostics = new ProviderDiagnosticTracker(this.id, options);
|
|
531
|
+
this.compressedRequested = options.includeCompressedRollouts === true;
|
|
532
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Whether compressed rollouts are discovered and read: requested, and the
|
|
536
|
+
* runtime can decompress them (otherwise reported once, on first use).
|
|
537
|
+
*/
|
|
538
|
+
get includeCompressed() {
|
|
539
|
+
return (this.compressedRequested && canReadCompressedRollouts({ onDiagnostic: this.onDiagnostic }));
|
|
540
|
+
}
|
|
541
|
+
/** A stored rollout path as it exists now (its `.zst` when compressed and enabled). */
|
|
542
|
+
resolveRollout(rolloutPath) {
|
|
543
|
+
return rolloutPath
|
|
544
|
+
? resolveRolloutPath(rolloutPath, { includeCompressed: this.includeCompressed })
|
|
545
|
+
: null;
|
|
489
546
|
}
|
|
490
547
|
// --- Database ---
|
|
491
548
|
/** Lazy-initialize database connection. */
|
|
@@ -583,10 +640,11 @@ export class CodexProvider {
|
|
|
583
640
|
return thread.rollout_path;
|
|
584
641
|
}
|
|
585
642
|
}
|
|
586
|
-
// File scan fallback (newest first; cwd reads are cached per path)
|
|
643
|
+
// File scan fallback (newest first; cwd reads are cached per path).
|
|
644
|
+
// Subagent rollouts share their parent's cwd but are not the session.
|
|
587
645
|
for (const file of findRolloutFilesInConfiguredHomes()) {
|
|
588
646
|
const cwd = readSessionCwd(file.path);
|
|
589
|
-
if (cwd && cwdMatches(cwd, workspacePath)) {
|
|
647
|
+
if (cwd && cwdMatches(cwd, workspacePath) && !isSubagentRollout(file.path)) {
|
|
590
648
|
this.recordDatabaseFallback('findActiveSession', true);
|
|
591
649
|
return file.path;
|
|
592
650
|
}
|
|
@@ -600,15 +658,17 @@ export class CodexProvider {
|
|
|
600
658
|
if (db) {
|
|
601
659
|
const threads = db.getThreadsByCwd(workspacePath);
|
|
602
660
|
const dbPaths = threads
|
|
603
|
-
.
|
|
604
|
-
.
|
|
661
|
+
.map((t) => this.resolveRollout(t.rollout_path))
|
|
662
|
+
.filter((rolloutPath) => rolloutPath !== null);
|
|
605
663
|
if (dbPaths.length > 0) {
|
|
606
664
|
this.recordDatabaseFallback('findAllSessions', true);
|
|
607
665
|
return dbPaths;
|
|
608
666
|
}
|
|
609
667
|
}
|
|
610
668
|
// File scan (newest first; cwd reads are cached per path)
|
|
611
|
-
const sessions = findRolloutFilesInConfiguredHomes(
|
|
669
|
+
const sessions = findRolloutFilesInConfiguredHomes({
|
|
670
|
+
includeCompressed: this.includeCompressed,
|
|
671
|
+
})
|
|
612
672
|
.filter((f) => {
|
|
613
673
|
const cwd = readSessionCwd(f.path);
|
|
614
674
|
return cwd !== null && cwdMatches(cwd, workspacePath);
|
|
@@ -619,7 +679,7 @@ export class CodexProvider {
|
|
|
619
679
|
}
|
|
620
680
|
findSessionById(_workspacePath, sessionId) {
|
|
621
681
|
try {
|
|
622
|
-
const found = findCodexRolloutFile(sessionId);
|
|
682
|
+
const found = findCodexRolloutFile(sessionId, { includeCompressed: this.includeCompressed });
|
|
623
683
|
this.recordDatabaseFallback('findSessionById', true);
|
|
624
684
|
return found;
|
|
625
685
|
}
|
|
@@ -633,15 +693,15 @@ export class CodexProvider {
|
|
|
633
693
|
return this.findAllSessions(workspacePath);
|
|
634
694
|
}
|
|
635
695
|
findSessionsInDirectory(dir) {
|
|
636
|
-
return findRolloutFiles(dir).map((f) => f.path);
|
|
696
|
+
return findRolloutFiles(dir, this.includeCompressed).map((f) => f.path);
|
|
637
697
|
}
|
|
638
698
|
listAllSessionFiles() {
|
|
639
|
-
const files = findRolloutFilesInConfiguredHomes();
|
|
699
|
+
const files = findRolloutFilesInConfiguredHomes({ includeCompressed: this.includeCompressed });
|
|
640
700
|
this.recordDatabaseFallback('listAllSessionFiles', true);
|
|
641
701
|
return files;
|
|
642
702
|
}
|
|
643
703
|
async listSessionFilesAsync(workspacePath, options = {}) {
|
|
644
|
-
const results = await listCodexSessionFilesAsync(workspacePath, options, this.directoryCache);
|
|
704
|
+
const results = await listCodexSessionFilesAsync(workspacePath, { ...options, includeCompressed: this.includeCompressed }, this.directoryCache);
|
|
645
705
|
// Filesystem enumeration bypasses the database, so record that here as
|
|
646
706
|
// the sync listing paths do; otherwise `getLastOperationStatus()` keeps
|
|
647
707
|
// describing whichever operation ran before this one.
|
|
@@ -661,7 +721,7 @@ export class CodexProvider {
|
|
|
661
721
|
const name = segments[segments.length - 1];
|
|
662
722
|
if (segments.length - 1 > DEFAULT_ROLLOUT_WALK_MAX_DEPTH)
|
|
663
723
|
return { status: 'ignored' };
|
|
664
|
-
if (!
|
|
724
|
+
if (!this.isSessionFile(name)) {
|
|
665
725
|
this.directoryCache.invalidate(fullPath);
|
|
666
726
|
try {
|
|
667
727
|
const stat = await fs.promises.stat(fullPath);
|
|
@@ -711,7 +771,9 @@ export class CodexProvider {
|
|
|
711
771
|
// after its last update can be missing from it, so older files are not
|
|
712
772
|
// opened; the cwd read itself is cached per path either way.
|
|
713
773
|
const indexedUpTo = db ? db.getDbMtime() : 0;
|
|
714
|
-
for (const file of findRolloutFilesInConfiguredHomes(
|
|
774
|
+
for (const file of findRolloutFilesInConfiguredHomes({
|
|
775
|
+
includeCompressed: this.includeCompressed,
|
|
776
|
+
})) {
|
|
715
777
|
if (db && indexedUpTo > 0 && file.mtime.getTime() <= indexedUpTo)
|
|
716
778
|
continue;
|
|
717
779
|
const cwd = readSessionCwd(file.path);
|
|
@@ -753,7 +815,7 @@ export class CodexProvider {
|
|
|
753
815
|
}
|
|
754
816
|
// --- File identification ---
|
|
755
817
|
isSessionFile(filename) {
|
|
756
|
-
return isRolloutFile(filename);
|
|
818
|
+
return isRolloutFile(filename) || (this.includeCompressed && isCompressedRolloutFile(filename));
|
|
757
819
|
}
|
|
758
820
|
getSessionId(sessionPath) {
|
|
759
821
|
return extractSessionId(path.basename(sessionPath));
|
|
@@ -814,6 +876,17 @@ export class CodexProvider {
|
|
|
814
876
|
}
|
|
815
877
|
// --- Data reading ---
|
|
816
878
|
createReader(sessionPath) {
|
|
879
|
+
if (isCompressedRolloutPath(sessionPath)) {
|
|
880
|
+
// Compressed rollouts are complete: read whole, never tailed.
|
|
881
|
+
return new CompressedCodexRolloutReader(sessionPath, {
|
|
882
|
+
onContextWindowLimit: (limit) => {
|
|
883
|
+
// Historical content: keep the live limit, do not persist it again.
|
|
884
|
+
if (this.dynamicContextWindowLimit === null)
|
|
885
|
+
this.dynamicContextWindowLimit = limit;
|
|
886
|
+
},
|
|
887
|
+
onDiagnostic: this.onDiagnostic,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
817
890
|
return new CodexReader(sessionPath, (limit, modelId) => {
|
|
818
891
|
this.dynamicContextWindowLimit = limit;
|
|
819
892
|
if (modelId) {
|
|
@@ -830,119 +903,36 @@ export class CodexProvider {
|
|
|
830
903
|
});
|
|
831
904
|
}
|
|
832
905
|
// --- Subagent support ---
|
|
833
|
-
scanSubagents(_sessionDir, sessionId) {
|
|
834
|
-
// Codex tracks forked sessions via forked_from_id in session_meta.
|
|
835
|
-
// Use DB when available, fall back to filesystem scanning.
|
|
836
|
-
const subagents = [];
|
|
837
|
-
// DB approach: spawned subagents (parent recorded in `threads.source`),
|
|
838
|
-
// plus forked children on older schemas that carry `forked_from_id`.
|
|
839
|
-
const db = this.ensureDb();
|
|
840
|
-
if (db) {
|
|
841
|
-
const seen = new Set();
|
|
842
|
-
const children = [
|
|
843
|
-
...db.getSpawnedChildThreads(sessionId),
|
|
844
|
-
...db.getThreadsByForkedFromId(sessionId),
|
|
845
|
-
].filter((thread) => !seen.has(thread.id) && seen.add(thread.id));
|
|
846
|
-
for (const thread of children) {
|
|
847
|
-
const built = cachedSubagentStats(thread.rollout_path, () => this.buildSubagentStats(thread.id, thread.rollout_path, thread.title || thread.agent_nickname));
|
|
848
|
-
if (built) {
|
|
849
|
-
subagents.push(thread.agent_role && !built.agentType
|
|
850
|
-
? { ...built, agentType: thread.agent_role }
|
|
851
|
-
: built);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
if (children.length > 0)
|
|
855
|
-
return subagents;
|
|
856
|
-
}
|
|
857
|
-
// Filesystem fallback: scan recent rollout files for a matching
|
|
858
|
-
// forked_from_id. The walk stops after the 50 newest-dated files instead
|
|
859
|
-
// of enumerating the whole history and slicing afterwards.
|
|
860
|
-
for (const file of findRolloutFilesInConfiguredHomes({ limit: 50 })) {
|
|
861
|
-
const meta = readSessionMeta(file.path);
|
|
862
|
-
if (meta &&
|
|
863
|
-
(meta.forked_from_id === sessionId || spawnParentFromSource(meta.source) === sessionId)) {
|
|
864
|
-
const childId = extractSessionId(path.basename(file.path));
|
|
865
|
-
// `source` is a plain string for top-level sessions but an object
|
|
866
|
-
// (`{ subagent: { thread_spawn: … } }`) for spawned children.
|
|
867
|
-
const description = typeof meta.source === 'string' ? meta.source : undefined;
|
|
868
|
-
const stats = cachedSubagentStats(file.path, () => this.buildSubagentStats(childId, file.path, description));
|
|
869
|
-
if (stats)
|
|
870
|
-
subagents.push(stats);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
return subagents;
|
|
874
|
-
}
|
|
875
906
|
/**
|
|
876
|
-
*
|
|
907
|
+
* Every subagent below the session, any depth: spawned threads (the
|
|
908
|
+
* database's `threads.source`, else the newest rollouts' session_meta)
|
|
909
|
+
* and, on older schemas, forks (`forked_from_id`). Each carries its
|
|
910
|
+
* rollout (`filePath`), the parent's spawn call (`spawnToolUseId`),
|
|
911
|
+
* `agentNickname`, `agentPath`, `parentAgentId`, and `depth`; tool calls
|
|
912
|
+
* and tokens come from the child's own records, not the parent history a
|
|
913
|
+
* forked child embeds.
|
|
877
914
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const content = fs.readFileSync(rolloutPath, 'utf8');
|
|
891
|
-
const lines = content.split('\n');
|
|
892
|
-
for (const line of lines) {
|
|
893
|
-
const trimmed = line.trim();
|
|
894
|
-
if (!trimmed)
|
|
895
|
-
continue;
|
|
896
|
-
try {
|
|
897
|
-
const parsed = JSON.parse(trimmed);
|
|
898
|
-
// Track timestamps
|
|
899
|
-
if (parsed.timestamp) {
|
|
900
|
-
const ts = new Date(parsed.timestamp);
|
|
901
|
-
if (!stats.startTime || ts < stats.startTime)
|
|
902
|
-
stats.startTime = ts;
|
|
903
|
-
if (!stats.endTime || ts > stats.endTime)
|
|
904
|
-
stats.endTime = ts;
|
|
905
|
-
}
|
|
906
|
-
const events = parser.convertLine(parsed);
|
|
907
|
-
for (const event of events) {
|
|
908
|
-
// Collect tool calls
|
|
909
|
-
if (event.type === 'assistant' && Array.isArray(event.message.content)) {
|
|
910
|
-
for (const block of event.message.content) {
|
|
911
|
-
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
912
|
-
stats.toolCalls.push({
|
|
913
|
-
name: block.name,
|
|
914
|
-
input: block.input || {},
|
|
915
|
-
timestamp: new Date(event.timestamp),
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
// Accumulate tokens from usage events (duplicate token_count events
|
|
921
|
-
// are already dropped by the parser).
|
|
922
|
-
const usage = extractNormalizedUsage(event);
|
|
923
|
-
if (usage)
|
|
924
|
-
addUsageToSubagent(stats, usage);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
catch {
|
|
928
|
-
/* skip malformed lines */
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
if (stats.startTime && stats.endTime) {
|
|
932
|
-
stats.durationMs = stats.endTime.getTime() - stats.startTime.getTime();
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
catch {
|
|
936
|
-
/* skip unreadable files */
|
|
937
|
-
}
|
|
938
|
-
return stats;
|
|
915
|
+
scanSubagents(_sessionDir, sessionId) {
|
|
916
|
+
const db = this.ensureDb();
|
|
917
|
+
return discoverCodexSubagents(sessionId, {
|
|
918
|
+
db,
|
|
919
|
+
includeCompressed: this.includeCompressed,
|
|
920
|
+
onDiagnostic: this.onDiagnostic,
|
|
921
|
+
sessionRolloutPath: () => this.resolveRollout(db?.getThread(sessionId)?.rollout_path) ??
|
|
922
|
+
findCodexRolloutFile(sessionId, { includeCompressed: this.includeCompressed }),
|
|
923
|
+
// The walk stops after the 50 newest-dated files instead of
|
|
924
|
+
// enumerating the whole history and slicing afterwards.
|
|
925
|
+
fallbackFiles: () => findRolloutFilesInConfiguredHomes({ limit: 50, includeCompressed: this.includeCompressed }),
|
|
926
|
+
});
|
|
939
927
|
}
|
|
940
928
|
// --- Cross-session search ---
|
|
941
929
|
searchInSession(sessionPath, query, maxResults) {
|
|
942
930
|
const results = [];
|
|
943
931
|
const queryLower = query.toLowerCase();
|
|
944
932
|
try {
|
|
945
|
-
const content =
|
|
933
|
+
const content = readRolloutText(sessionPath, { onDiagnostic: this.onDiagnostic });
|
|
934
|
+
if (content === null)
|
|
935
|
+
return results;
|
|
946
936
|
const lines = content.split('\n');
|
|
947
937
|
for (const line of lines) {
|
|
948
938
|
if (results.length >= maxResults)
|