sidekick-shared 0.29.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +171 -60
  3. package/dist/aggregation/EventAggregator.d.ts +79 -4
  4. package/dist/aggregation/EventAggregator.js +391 -37
  5. package/dist/aggregation/types.d.ts +27 -3
  6. package/dist/browser.d.ts +7 -3
  7. package/dist/browser.js +23 -3
  8. package/dist/context/sessionContext.js +5 -1
  9. package/dist/esm/aggregation/EventAggregator.js +392 -38
  10. package/dist/esm/browser.js +3 -1
  11. package/dist/esm/context/sessionContext.js +6 -2
  12. package/dist/esm/fileContinuity.js +149 -0
  13. package/dist/esm/index.js +5 -2
  14. package/dist/esm/node.js +2 -0
  15. package/dist/esm/parsers/codexParser.js +192 -3
  16. package/dist/esm/parsers/subagentEndState.js +61 -0
  17. package/dist/esm/parsers/subagentScanner.js +4 -0
  18. package/dist/esm/parsers/subagentTraceParser.js +6 -0
  19. package/dist/esm/promptHistory.js +22 -11
  20. package/dist/esm/providers/claudeCode.js +50 -27
  21. package/dist/esm/providers/codex.js +153 -163
  22. package/dist/esm/providers/codexCompressedReader.js +184 -0
  23. package/dist/esm/providers/codexDatabase.js +47 -2
  24. package/dist/esm/providers/codexRollouts.js +20 -2
  25. package/dist/esm/providers/codexSubagents.js +456 -0
  26. package/dist/esm/providers/compressedRollouts.js +330 -0
  27. package/dist/esm/providers/factory.js +4 -1
  28. package/dist/esm/providers/rolloutWalker.js +32 -4
  29. package/dist/esm/schemas/sessionEvent.js +32 -4
  30. package/dist/esm/sessionMonitor.js +7 -1
  31. package/dist/esm/sessionPreviews.js +9 -0
  32. package/dist/esm/sessionPromptHistory.js +5 -3
  33. package/dist/esm/subagentActivity.js +125 -0
  34. package/dist/esm/subagentDisplayState.js +81 -0
  35. package/dist/esm/subagentSpawn.js +206 -6
  36. package/dist/esm/testing/codexSubagentFixtures.js +336 -0
  37. package/dist/esm/toolCallOrigins.js +17 -10
  38. package/dist/esm/toolCategory.js +6 -2
  39. package/dist/esm/transcript.js +6 -2
  40. package/dist/esm/turns/assistantTurn.js +6 -3
  41. package/dist/esm/usage/usageEvents.js +4 -1
  42. package/dist/esm/watchers/eventBridge.js +31 -1
  43. package/dist/esm/watchers/jsonlTail.js +11 -42
  44. package/dist/esm/watchers/jsonlWatcher.js +19 -9
  45. package/dist/esm/watchers/providerReaderWatcher.js +5 -4
  46. package/dist/fileContinuity.d.ts +95 -0
  47. package/dist/fileContinuity.js +187 -0
  48. package/dist/index.d.ts +14 -7
  49. package/dist/index.js +39 -10
  50. package/dist/node.d.ts +4 -0
  51. package/dist/node.js +16 -2
  52. package/dist/parsers/codexParser.d.ts +44 -0
  53. package/dist/parsers/codexParser.js +192 -3
  54. package/dist/parsers/subagentEndState.d.ts +20 -0
  55. package/dist/parsers/subagentEndState.js +65 -0
  56. package/dist/parsers/subagentScanner.js +4 -0
  57. package/dist/parsers/subagentTraceParser.d.ts +8 -0
  58. package/dist/parsers/subagentTraceParser.js +6 -0
  59. package/dist/promptHistory.d.ts +13 -3
  60. package/dist/promptHistory.js +22 -11
  61. package/dist/providers/claudeCode.js +50 -27
  62. package/dist/providers/codex.d.ts +35 -5
  63. package/dist/providers/codex.js +150 -160
  64. package/dist/providers/codexCompressedReader.d.ts +58 -0
  65. package/dist/providers/codexCompressedReader.js +221 -0
  66. package/dist/providers/codexDatabase.d.ts +25 -0
  67. package/dist/providers/codexDatabase.js +48 -2
  68. package/dist/providers/codexRollouts.d.ts +16 -2
  69. package/dist/providers/codexRollouts.js +20 -2
  70. package/dist/providers/codexSubagents.d.ts +66 -0
  71. package/dist/providers/codexSubagents.js +495 -0
  72. package/dist/providers/compressedRollouts.d.ts +114 -0
  73. package/dist/providers/compressedRollouts.js +380 -0
  74. package/dist/providers/factory.d.ts +5 -0
  75. package/dist/providers/factory.js +4 -1
  76. package/dist/providers/rolloutWalker.d.ts +15 -2
  77. package/dist/providers/rolloutWalker.js +33 -4
  78. package/dist/providers/types.d.ts +22 -1
  79. package/dist/schemas/sessionEvent.d.ts +26 -2
  80. package/dist/schemas/sessionEvent.js +31 -3
  81. package/dist/sessionMonitor.d.ts +6 -0
  82. package/dist/sessionMonitor.js +7 -1
  83. package/dist/sessionPreviews.js +9 -0
  84. package/dist/sessionPromptHistory.d.ts +5 -0
  85. package/dist/sessionPromptHistory.js +5 -36
  86. package/dist/subagentActivity.d.ts +91 -0
  87. package/dist/subagentActivity.js +132 -0
  88. package/dist/subagentDisplayState.d.ts +81 -0
  89. package/dist/subagentDisplayState.js +86 -0
  90. package/dist/subagentSpawn.d.ts +100 -5
  91. package/dist/subagentSpawn.js +218 -7
  92. package/dist/testing/codexSubagentFixtures.d.ts +48 -0
  93. package/dist/testing/codexSubagentFixtures.js +344 -0
  94. package/dist/toolCallOrigins.d.ts +5 -0
  95. package/dist/toolCallOrigins.js +17 -10
  96. package/dist/toolCategory.js +6 -2
  97. package/dist/transcript.js +5 -1
  98. package/dist/turns/assistantTurn.d.ts +4 -1
  99. package/dist/turns/assistantTurn.js +5 -2
  100. package/dist/types/codex.d.ts +71 -2
  101. package/dist/types/sessionEvent.d.ts +38 -3
  102. package/dist/usage/usageEvents.d.ts +4 -1
  103. package/dist/usage/usageEvents.js +4 -1
  104. package/dist/watchers/eventBridge.js +30 -0
  105. package/dist/watchers/jsonlTail.js +11 -42
  106. package/dist/watchers/jsonlWatcher.d.ts +5 -1
  107. package/dist/watchers/jsonlWatcher.js +18 -8
  108. package/dist/watchers/providerReaderWatcher.js +5 -4
  109. package/package.json +1 -1
@@ -69,11 +69,11 @@ exports.resolveRoots = resolveRoots;
69
69
  exports.listCandidates = listCandidates;
70
70
  exports.scanFile = scanFile;
71
71
  const crypto_1 = require("crypto");
72
- const fs = __importStar(require("fs"));
73
72
  const path = __importStar(require("path"));
74
73
  const humanPrompt_1 = require("./humanPrompt");
75
74
  const claudeListing_1 = require("./providers/claudeListing");
76
75
  const codexRollouts_1 = require("./providers/codexRollouts");
76
+ const compressedRollouts_1 = require("./providers/compressedRollouts");
77
77
  const directoryListingCache_1 = require("./providers/directoryListingCache");
78
78
  const workspaceRoots_1 = require("./workspaceRoots");
79
79
  const fileHead_1 = require("./fileHead");
@@ -150,7 +150,9 @@ async function collectPromptHistory(options) {
150
150
  return finish();
151
151
  const sinceMs = options.since ? options.since.getTime() : null;
152
152
  const providers = options.providers ?? ALL_PROVIDERS;
153
- const candidates = rotate(await listCandidates(providers, listingRoots, sinceMs), previousNext);
153
+ const candidates = rotate(await listCandidates(providers, listingRoots, sinceMs, {
154
+ includeCompressedRollouts: options.includeCompressedRollouts,
155
+ }), previousNext);
154
156
  const moved = movedSessionKeys(previous, candidates);
155
157
  const context = {
156
158
  roots,
@@ -186,7 +188,8 @@ async function collectPromptHistory(options) {
186
188
  }
187
189
  let stat;
188
190
  try {
189
- stat = await fs.promises.stat(candidate.filePath);
191
+ // A compressed rollout's size is its decompressed content's.
192
+ stat = await (0, compressedRollouts_1.statRolloutContent)(candidate.filePath);
190
193
  }
191
194
  catch {
192
195
  continue;
@@ -291,7 +294,7 @@ async function collectPromptHistory(options) {
291
294
  */
292
295
  async function readPromptOrdinalMarks(provider, sessionId, filePath) {
293
296
  const marks = [];
294
- const stat = await fs.promises.stat(filePath);
297
+ const stat = await (0, compressedRollouts_1.statRolloutContent)(filePath);
295
298
  const unbounded = Number.MAX_SAFE_INTEGER;
296
299
  const context = {
297
300
  roots: [],
@@ -480,19 +483,24 @@ function scopedCwd(cwd, roots, realpaths) {
480
483
  return null;
481
484
  }
482
485
  /** @internal */
483
- async function listCandidates(providers, listingRoots, sinceMs) {
486
+ async function listCandidates(providers, listingRoots, sinceMs, options = {}) {
484
487
  const byPath = new Map();
485
488
  const listOptions = sinceMs !== null ? { since: sinceMs } : {};
486
489
  for (const provider of [...new Set(providers)]) {
487
490
  if (!ALL_PROVIDERS.includes(provider))
488
491
  continue;
489
492
  // Listings are only candidate supersets; scope is re-checked per prompt.
490
- const list = provider === 'claude-code' ? claudeListing_1.listClaudeSessionFilesAsync : codexRollouts_1.listCodexSessionFilesAsync;
491
493
  const cache = new directoryListingCache_1.DirectoryListingCache();
492
494
  for (const root of listingRoots) {
493
495
  let files;
494
496
  try {
495
- files = await list(root, listOptions, cache);
497
+ files =
498
+ provider === 'claude-code'
499
+ ? await (0, claudeListing_1.listClaudeSessionFilesAsync)(root, listOptions, cache)
500
+ : await (0, codexRollouts_1.listCodexSessionFilesAsync)(root, {
501
+ ...listOptions,
502
+ includeCompressed: options.includeCompressedRollouts === true && (0, compressedRollouts_1.canReadCompressedRollouts)(),
503
+ }, cache);
496
504
  }
497
505
  catch {
498
506
  files = [];
@@ -514,11 +522,11 @@ async function listCandidates(providers, listingRoots, sinceMs) {
514
522
  compareStrings(left.sessionId, right.sessionId) ||
515
523
  compareStrings(left.key, right.key));
516
524
  }
517
- /** True when the file's first bytes still match a recorded head. */
525
+ /** True when the file's first bytes (a compressed rollout's content) still match a recorded head. */
518
526
  async function fileStartsWith(filePath, head) {
519
527
  let handle;
520
528
  try {
521
- handle = await fs.promises.open(filePath, 'r');
529
+ ({ handle } = await (0, compressedRollouts_1.openRolloutReadHandle)(filePath));
522
530
  return (0, fileHead_1.headMatches)(head, await readAt(handle, 0, Math.min(fileHead_1.HEAD_BYTES, head.bytes)));
523
531
  }
524
532
  catch {
@@ -591,8 +599,11 @@ async function verifyFile(handle, size, session) {
591
599
  return { head: (0, fileHead_1.fileHeadOf)(head), bytesRead };
592
600
  }
593
601
  /** @internal */
594
- async function scanFile(candidate, stat, prior, budget, context) {
595
- const handle = await fs.promises.open(candidate.filePath, 'r');
602
+ async function scanFile(candidate, fileStat, prior, budget, context) {
603
+ // A compressed rollout is scanned over its decompressed content.
604
+ const opened = await (0, compressedRollouts_1.openRolloutReadHandle)(candidate.filePath);
605
+ const handle = opened.handle;
606
+ const stat = opened.size === null ? fileStat : { size: opened.size, mtimeMs: fileStat.mtimeMs };
596
607
  try {
597
608
  let bytesRead = 0;
598
609
  const verified = await verifyFile(handle, stat.size, prior);
@@ -49,6 +49,7 @@ const string_decoder_1 = require("string_decoder");
49
49
  const sessionContext_1 = require("../context/sessionContext");
50
50
  const sessionStats_1 = require("../sessionStats");
51
51
  const jsonl_1 = require("../parsers/jsonl");
52
+ const fileContinuity_1 = require("../fileContinuity");
52
53
  const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
53
54
  const diagnostics_1 = require("./diagnostics");
54
55
  const directoryListingCache_1 = require("./directoryListingCache");
@@ -146,7 +147,10 @@ class ClaudeCodeReader {
146
147
  /** Cursor after the last complete line, safe to persist across restarts. */
147
148
  committedPosition = 0;
148
149
  events = [];
149
- _wasTruncated = false;
150
+ /** Why the last `readNew()` started over from offset 0, if it did. */
151
+ _lastReset = null;
152
+ /** Whether the file is still the one the positions refer to. */
153
+ continuity = new fileContinuity_1.FileContinuity();
150
154
  decoder = new string_decoder_1.StringDecoder('utf8');
151
155
  /** Split lines of one response repeat its usage; count each response once. */
152
156
  usageDeduper;
@@ -167,37 +171,28 @@ class ClaudeCodeReader {
167
171
  }
168
172
  readNew() {
169
173
  this.events = [];
170
- this._wasTruncated = false;
174
+ this._lastReset = null;
175
+ let fd = null;
171
176
  try {
172
- if (!fs.existsSync(this.sessionPath)) {
173
- return [];
174
- }
175
- const stats = fs.statSync(this.sessionPath);
177
+ // Open first and stat the descriptor: the size and identity checked
178
+ // are those of the file actually read, even if the path is replaced.
179
+ fd = fs.openSync(this.sessionPath, 'r');
180
+ const stats = fs.fstatSync(fd);
181
+ const reason = this.continuity.check(fd, stats, this.filePosition, this.committedPosition);
182
+ // Truncated, rewritten, or replaced: what was read no longer describes
183
+ // the file, so start over from its beginning.
184
+ if (reason !== null)
185
+ this.restart(reason);
186
+ this.continuity.adopt(stats);
176
187
  const currentSize = stats.size;
177
- // Handle truncation
178
- if (currentSize < this.filePosition) {
179
- this._wasTruncated = true;
180
- this.filePosition = 0;
181
- this.committedPosition = 0;
182
- this.parser.reset();
183
- this.decoder = new string_decoder_1.StringDecoder('utf8');
184
- this.usageDeduper.reset();
185
- }
186
188
  // No new content
187
189
  if (currentSize <= this.filePosition) {
188
- return [];
190
+ return this.events;
189
191
  }
190
192
  // Read new bytes from last position
191
- const fd = fs.openSync(this.sessionPath, 'r');
192
193
  const bufferSize = currentSize - this.filePosition;
193
194
  const buffer = Buffer.alloc(bufferSize);
194
- let bytesRead = 0;
195
- try {
196
- bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
197
- }
198
- finally {
199
- fs.closeSync(fd);
200
- }
195
+ const bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
201
196
  const bytes = buffer.subarray(0, bytesRead);
202
197
  const newline = bytes.lastIndexOf(0x0a);
203
198
  if (newline >= 0)
@@ -207,10 +202,31 @@ class ClaudeCodeReader {
207
202
  this.filePosition += bytesRead;
208
203
  }
209
204
  catch (error) {
210
- console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
205
+ if (error?.code !== 'ENOENT') {
206
+ console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
207
+ }
208
+ }
209
+ finally {
210
+ if (fd !== null) {
211
+ try {
212
+ fs.closeSync(fd);
213
+ }
214
+ catch {
215
+ // Ignore close errors.
216
+ }
217
+ }
211
218
  }
212
219
  return this.events;
213
220
  }
221
+ /** Starts over from offset 0 of a file that was truncated, rewritten, or replaced. */
222
+ restart(reason) {
223
+ this._lastReset = { reason, previousPosition: this.committedPosition };
224
+ this.filePosition = 0;
225
+ this.committedPosition = 0;
226
+ this.parser.reset();
227
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
228
+ this.usageDeduper.reset();
229
+ }
214
230
  readAll() {
215
231
  this.reset();
216
232
  return this.readNew();
@@ -221,7 +237,8 @@ class ClaudeCodeReader {
221
237
  this.parser.reset();
222
238
  this.decoder = new string_decoder_1.StringDecoder('utf8');
223
239
  this.usageDeduper.reset();
224
- this._wasTruncated = false;
240
+ this._lastReset = null;
241
+ this.continuity.forget();
225
242
  }
226
243
  exists() {
227
244
  return fs.existsSync(this.sessionPath);
@@ -241,9 +258,15 @@ class ClaudeCodeReader {
241
258
  this.committedPosition = position;
242
259
  this.parser.reset();
243
260
  this.decoder = new string_decoder_1.StringDecoder('utf8');
261
+ // The position comes from elsewhere (a snapshot): adopt the file the next
262
+ // read finds rather than compare it with one read before.
263
+ this.continuity.forget();
244
264
  }
245
265
  wasTruncated() {
246
- return this._wasTruncated;
266
+ return this._lastReset !== null;
267
+ }
268
+ lastReset() {
269
+ return this._lastReset ? { ...this._lastReset } : null;
247
270
  }
248
271
  /** The usage deduper's recent message ids, so a resumed reader does not recount a split response. */
249
272
  getResumeState() {
@@ -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
  */
@@ -15,6 +17,19 @@ import type { SubagentStats, TokenUsage } from '../types/sessionEvent';
15
17
  import type { CodexRateLimits } from '../types/codex';
16
18
  export { findCodexRolloutFile };
17
19
  export type { FindCodexRolloutFileOptions } from './codexRollouts';
20
+ /** Options for {@link CodexProvider}; every field is optional and additive. */
21
+ export interface CodexProviderOptions extends SessionProviderOptions {
22
+ /**
23
+ * Also discover and read compressed rollouts (`rollout-….jsonl.zst`,
24
+ * which Codex writes for older sessions). Listings, session lookups,
25
+ * labels, `createReader()` (a whole-file reader; live tailing stays
26
+ * `.jsonl`-only), search, and subagent stats then include them. Reading
27
+ * needs `zlib.zstdDecompressSync` (Node 22.15+); without it they are
28
+ * skipped and reported once through `onDiagnostic`. Default false, so
29
+ * session and history counts are unchanged.
30
+ */
31
+ includeCompressedRollouts?: boolean;
32
+ }
18
33
  /**
19
34
  * Session provider for Codex CLI.
20
35
  *
@@ -33,7 +48,17 @@ export declare class CodexProvider implements SessionProviderBase {
33
48
  private lastTokenUsageData;
34
49
  /** Dated rollout-directory listings reused across async enumerations. */
35
50
  private readonly directoryCache;
36
- constructor(options?: SessionProviderOptions);
51
+ /** See {@link CodexProviderOptions.includeCompressedRollouts}. */
52
+ private readonly compressedRequested;
53
+ private readonly onDiagnostic;
54
+ constructor(options?: CodexProviderOptions);
55
+ /**
56
+ * Whether compressed rollouts are discovered and read: requested, and the
57
+ * runtime can decompress them (otherwise reported once, on first use).
58
+ */
59
+ private get includeCompressed();
60
+ /** A stored rollout path as it exists now (its `.zst` when compressed and enabled). */
61
+ private resolveRollout;
37
62
  /** Lazy-initialize database connection. */
38
63
  private ensureDb;
39
64
  private ensureDbAsync;
@@ -70,11 +95,16 @@ export declare class CodexProvider implements SessionProviderBase {
70
95
  extractSessionLabel(sessionPath: string): string | null;
71
96
  extractSessionLabelsAsync(sessionPaths: readonly string[]): Promise<Map<string, string | null>>;
72
97
  createReader(sessionPath: string): SessionReader;
73
- scanSubagents(_sessionDir: string, sessionId: string): SubagentStats[];
74
98
  /**
75
- * Builds SubagentStats for a forked session by reading its rollout file.
99
+ * Every subagent below the session, any depth: spawned threads (the
100
+ * database's `threads.source`, else the newest rollouts' session_meta)
101
+ * and, on older schemas, forks (`forked_from_id`). Each carries its
102
+ * rollout (`filePath`), the parent's spawn call (`spawnToolUseId`),
103
+ * `agentNickname`, `agentPath`, `parentAgentId`, and `depth`; tool calls
104
+ * and tokens come from the child's own records, not the parent history a
105
+ * forked child embeds.
76
106
  */
77
- private buildSubagentStats;
107
+ scanSubagents(_sessionDir: string, sessionId: string): SubagentStats[];
78
108
  searchInSession(sessionPath: string, query: string, maxResults: number): SearchHit[];
79
109
  getProjectsBaseDir(): string;
80
110
  getWatchRoots(): string[];