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
@@ -24,11 +24,11 @@
24
24
  * answering call brings model or usage (`metadataStatus` says how settled).
25
25
  */
26
26
  import { createHash } from 'crypto';
27
- import * as fs from 'fs';
28
27
  import * as path from 'path';
29
28
  import { HUMAN_CODEX_SOURCES, humanPromptText } from './humanPrompt';
30
29
  import { listClaudeSessionFilesAsync } from './providers/claudeListing';
31
30
  import { listCodexSessionFilesAsync } from './providers/codexRollouts';
31
+ import { canReadCompressedRollouts, openRolloutReadHandle, statRolloutContent, } from './providers/compressedRollouts';
32
32
  import { DirectoryListingCache } from './providers/directoryListingCache';
33
33
  import { realpathOrNull, resolveWorkspaceRoots } from './workspaceRoots';
34
34
  import { fileHeadOf, HEAD_BYTES, headMatches } from './fileHead';
@@ -105,7 +105,9 @@ export async function collectPromptHistory(options) {
105
105
  return finish();
106
106
  const sinceMs = options.since ? options.since.getTime() : null;
107
107
  const providers = options.providers ?? ALL_PROVIDERS;
108
- const candidates = rotate(await listCandidates(providers, listingRoots, sinceMs), previousNext);
108
+ const candidates = rotate(await listCandidates(providers, listingRoots, sinceMs, {
109
+ includeCompressedRollouts: options.includeCompressedRollouts,
110
+ }), previousNext);
109
111
  const moved = movedSessionKeys(previous, candidates);
110
112
  const context = {
111
113
  roots,
@@ -141,7 +143,8 @@ export async function collectPromptHistory(options) {
141
143
  }
142
144
  let stat;
143
145
  try {
144
- stat = await fs.promises.stat(candidate.filePath);
146
+ // A compressed rollout's size is its decompressed content's.
147
+ stat = await statRolloutContent(candidate.filePath);
145
148
  }
146
149
  catch {
147
150
  continue;
@@ -246,7 +249,7 @@ export async function collectPromptHistory(options) {
246
249
  */
247
250
  export async function readPromptOrdinalMarks(provider, sessionId, filePath) {
248
251
  const marks = [];
249
- const stat = await fs.promises.stat(filePath);
252
+ const stat = await statRolloutContent(filePath);
250
253
  const unbounded = Number.MAX_SAFE_INTEGER;
251
254
  const context = {
252
255
  roots: [],
@@ -435,19 +438,24 @@ function scopedCwd(cwd, roots, realpaths) {
435
438
  return null;
436
439
  }
437
440
  /** @internal */
438
- export async function listCandidates(providers, listingRoots, sinceMs) {
441
+ export async function listCandidates(providers, listingRoots, sinceMs, options = {}) {
439
442
  const byPath = new Map();
440
443
  const listOptions = sinceMs !== null ? { since: sinceMs } : {};
441
444
  for (const provider of [...new Set(providers)]) {
442
445
  if (!ALL_PROVIDERS.includes(provider))
443
446
  continue;
444
447
  // Listings are only candidate supersets; scope is re-checked per prompt.
445
- const list = provider === 'claude-code' ? listClaudeSessionFilesAsync : listCodexSessionFilesAsync;
446
448
  const cache = new DirectoryListingCache();
447
449
  for (const root of listingRoots) {
448
450
  let files;
449
451
  try {
450
- files = await list(root, listOptions, cache);
452
+ files =
453
+ provider === 'claude-code'
454
+ ? await listClaudeSessionFilesAsync(root, listOptions, cache)
455
+ : await listCodexSessionFilesAsync(root, {
456
+ ...listOptions,
457
+ includeCompressed: options.includeCompressedRollouts === true && canReadCompressedRollouts(),
458
+ }, cache);
451
459
  }
452
460
  catch {
453
461
  files = [];
@@ -469,11 +477,11 @@ export async function listCandidates(providers, listingRoots, sinceMs) {
469
477
  compareStrings(left.sessionId, right.sessionId) ||
470
478
  compareStrings(left.key, right.key));
471
479
  }
472
- /** True when the file's first bytes still match a recorded head. */
480
+ /** True when the file's first bytes (a compressed rollout's content) still match a recorded head. */
473
481
  async function fileStartsWith(filePath, head) {
474
482
  let handle;
475
483
  try {
476
- handle = await fs.promises.open(filePath, 'r');
484
+ ({ handle } = await openRolloutReadHandle(filePath));
477
485
  return headMatches(head, await readAt(handle, 0, Math.min(HEAD_BYTES, head.bytes)));
478
486
  }
479
487
  catch {
@@ -546,8 +554,11 @@ async function verifyFile(handle, size, session) {
546
554
  return { head: fileHeadOf(head), bytesRead };
547
555
  }
548
556
  /** @internal */
549
- export async function scanFile(candidate, stat, prior, budget, context) {
550
- const handle = await fs.promises.open(candidate.filePath, 'r');
557
+ export async function scanFile(candidate, fileStat, prior, budget, context) {
558
+ // A compressed rollout is scanned over its decompressed content.
559
+ const opened = await openRolloutReadHandle(candidate.filePath);
560
+ const handle = opened.handle;
561
+ const stat = opened.size === null ? fileStat : { size: opened.size, mtimeMs: fileStat.mtimeMs };
551
562
  try {
552
563
  let bytesRead = 0;
553
564
  const verified = await verifyFile(handle, stat.size, prior);
@@ -13,6 +13,7 @@ import { StringDecoder } from 'string_decoder';
13
13
  import { readSessionContextSnapshot } from '../context/sessionContext';
14
14
  import { readSessionFileStats } from '../sessionStats';
15
15
  import { JsonlParser } from '../parsers/jsonl';
16
+ import { FileContinuity } from '../fileContinuity';
16
17
  import { ClaudeUsageDeduper, normalizeClaudeUsage, } from '../usage/claudeUsageDedupe';
17
18
  import { ProviderDiagnosticTracker } from './diagnostics';
18
19
  import { DirectoryListingCache, splitWatchedPath } from './directoryListingCache';
@@ -110,7 +111,10 @@ class ClaudeCodeReader {
110
111
  /** Cursor after the last complete line, safe to persist across restarts. */
111
112
  committedPosition = 0;
112
113
  events = [];
113
- _wasTruncated = false;
114
+ /** Why the last `readNew()` started over from offset 0, if it did. */
115
+ _lastReset = null;
116
+ /** Whether the file is still the one the positions refer to. */
117
+ continuity = new FileContinuity();
114
118
  decoder = new StringDecoder('utf8');
115
119
  /** Split lines of one response repeat its usage; count each response once. */
116
120
  usageDeduper;
@@ -131,37 +135,28 @@ class ClaudeCodeReader {
131
135
  }
132
136
  readNew() {
133
137
  this.events = [];
134
- this._wasTruncated = false;
138
+ this._lastReset = null;
139
+ let fd = null;
135
140
  try {
136
- if (!fs.existsSync(this.sessionPath)) {
137
- return [];
138
- }
139
- const stats = fs.statSync(this.sessionPath);
141
+ // Open first and stat the descriptor: the size and identity checked
142
+ // are those of the file actually read, even if the path is replaced.
143
+ fd = fs.openSync(this.sessionPath, 'r');
144
+ const stats = fs.fstatSync(fd);
145
+ const reason = this.continuity.check(fd, stats, this.filePosition, this.committedPosition);
146
+ // Truncated, rewritten, or replaced: what was read no longer describes
147
+ // the file, so start over from its beginning.
148
+ if (reason !== null)
149
+ this.restart(reason);
150
+ this.continuity.adopt(stats);
140
151
  const currentSize = stats.size;
141
- // Handle truncation
142
- if (currentSize < this.filePosition) {
143
- this._wasTruncated = true;
144
- this.filePosition = 0;
145
- this.committedPosition = 0;
146
- this.parser.reset();
147
- this.decoder = new StringDecoder('utf8');
148
- this.usageDeduper.reset();
149
- }
150
152
  // No new content
151
153
  if (currentSize <= this.filePosition) {
152
- return [];
154
+ return this.events;
153
155
  }
154
156
  // Read new bytes from last position
155
- const fd = fs.openSync(this.sessionPath, 'r');
156
157
  const bufferSize = currentSize - this.filePosition;
157
158
  const buffer = Buffer.alloc(bufferSize);
158
- let bytesRead = 0;
159
- try {
160
- bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
161
- }
162
- finally {
163
- fs.closeSync(fd);
164
- }
159
+ const bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
165
160
  const bytes = buffer.subarray(0, bytesRead);
166
161
  const newline = bytes.lastIndexOf(0x0a);
167
162
  if (newline >= 0)
@@ -171,10 +166,31 @@ class ClaudeCodeReader {
171
166
  this.filePosition += bytesRead;
172
167
  }
173
168
  catch (error) {
174
- console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
169
+ if (error?.code !== 'ENOENT') {
170
+ console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
171
+ }
172
+ }
173
+ finally {
174
+ if (fd !== null) {
175
+ try {
176
+ fs.closeSync(fd);
177
+ }
178
+ catch {
179
+ // Ignore close errors.
180
+ }
181
+ }
175
182
  }
176
183
  return this.events;
177
184
  }
185
+ /** Starts over from offset 0 of a file that was truncated, rewritten, or replaced. */
186
+ restart(reason) {
187
+ this._lastReset = { reason, previousPosition: this.committedPosition };
188
+ this.filePosition = 0;
189
+ this.committedPosition = 0;
190
+ this.parser.reset();
191
+ this.decoder = new StringDecoder('utf8');
192
+ this.usageDeduper.reset();
193
+ }
178
194
  readAll() {
179
195
  this.reset();
180
196
  return this.readNew();
@@ -185,7 +201,8 @@ class ClaudeCodeReader {
185
201
  this.parser.reset();
186
202
  this.decoder = new StringDecoder('utf8');
187
203
  this.usageDeduper.reset();
188
- this._wasTruncated = false;
204
+ this._lastReset = null;
205
+ this.continuity.forget();
189
206
  }
190
207
  exists() {
191
208
  return fs.existsSync(this.sessionPath);
@@ -205,9 +222,15 @@ class ClaudeCodeReader {
205
222
  this.committedPosition = position;
206
223
  this.parser.reset();
207
224
  this.decoder = new StringDecoder('utf8');
225
+ // The position comes from elsewhere (a snapshot): adopt the file the next
226
+ // read finds rather than compare it with one read before.
227
+ this.continuity.forget();
208
228
  }
209
229
  wasTruncated() {
210
- return this._wasTruncated;
230
+ return this._lastReset !== null;
231
+ }
232
+ lastReset() {
233
+ return this._lastReset ? { ...this._lastReset } : null;
211
234
  }
212
235
  /** The usage deduper's recent message ids, so a resumed reader does not recount a split response. */
213
236
  getResumeState() {