sidekick-shared 0.24.4 → 0.25.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 (98) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +200 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +151 -0
  7. package/dist/accountManager.d.ts +13 -0
  8. package/dist/accountManager.js +60 -32
  9. package/dist/accountRegistry.d.ts +3 -1
  10. package/dist/accountRegistry.js +64 -41
  11. package/dist/accounts.js +40 -19
  12. package/dist/autoSwitch.d.ts +2 -1
  13. package/dist/autoSwitch.js +32 -7
  14. package/dist/browser.d.ts +10 -3
  15. package/dist/browser.js +17 -1
  16. package/dist/codexProfiles.d.ts +14 -0
  17. package/dist/codexProfiles.js +268 -116
  18. package/dist/codexQuotaWatcher.d.ts +6 -0
  19. package/dist/codexQuotaWatcher.js +101 -47
  20. package/dist/context/sessionContext.js +2 -4
  21. package/dist/ensureDefaultAccounts.js +4 -4
  22. package/dist/index.d.ts +32 -15
  23. package/dist/index.js +54 -9
  24. package/dist/modelAliases.d.ts +13 -0
  25. package/dist/modelAliases.js +58 -0
  26. package/dist/modelCatalog.d.ts +32 -0
  27. package/dist/modelCatalog.js +145 -0
  28. package/dist/modelContext.d.ts +36 -3
  29. package/dist/modelContext.js +109 -34
  30. package/dist/modelInfo.d.ts +30 -1
  31. package/dist/modelInfo.js +116 -26
  32. package/dist/multiProviderQuotaService.d.ts +8 -0
  33. package/dist/multiProviderQuotaService.js +64 -3
  34. package/dist/node.d.ts +8 -2
  35. package/dist/node.js +11 -1
  36. package/dist/observedContextWindows.d.ts +13 -0
  37. package/dist/observedContextWindows.js +13 -0
  38. package/dist/observedSessionCollector.d.ts +75 -4
  39. package/dist/observedSessionCollector.js +452 -42
  40. package/dist/parsers/codexHistory.d.ts +32 -0
  41. package/dist/parsers/codexHistory.js +125 -0
  42. package/dist/parsers/mcpToolName.d.ts +26 -0
  43. package/dist/parsers/mcpToolName.js +31 -0
  44. package/dist/parsers/sessionActivityDetector.d.ts +6 -0
  45. package/dist/parsers/sessionActivityDetector.js +17 -0
  46. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  47. package/dist/parsers/sessionPathResolver.js +11 -4
  48. package/dist/paths.d.ts +7 -3
  49. package/dist/paths.js +7 -3
  50. package/dist/providerIds.d.ts +11 -0
  51. package/dist/providerIds.js +9 -0
  52. package/dist/providerQuota.d.ts +3 -1
  53. package/dist/providerQuota.js +3 -0
  54. package/dist/providers/claudeCode.d.ts +11 -1
  55. package/dist/providers/claudeCode.js +222 -39
  56. package/dist/providers/codex.d.ts +39 -1
  57. package/dist/providers/codex.js +326 -39
  58. package/dist/providers/codexDatabase.d.ts +9 -0
  59. package/dist/providers/codexDatabase.js +107 -2
  60. package/dist/providers/diagnostics.d.ts +13 -0
  61. package/dist/providers/diagnostics.js +88 -0
  62. package/dist/providers/factory.d.ts +14 -0
  63. package/dist/providers/factory.js +57 -0
  64. package/dist/providers/openCode.d.ts +20 -1
  65. package/dist/providers/openCode.js +361 -11
  66. package/dist/providers/openCodeDatabase.d.ts +10 -0
  67. package/dist/providers/openCodeDatabase.js +69 -0
  68. package/dist/providers/types.d.ts +57 -2
  69. package/dist/providers/types.js +4 -0
  70. package/dist/quota.d.ts +4 -1
  71. package/dist/quota.js +3 -7
  72. package/dist/quotaHistory.d.ts +3 -2
  73. package/dist/quotaHistory.js +2 -2
  74. package/dist/quotaPoller.d.ts +13 -1
  75. package/dist/quotaPoller.js +65 -3
  76. package/dist/quotaSnapshots.js +8 -77
  77. package/dist/schemas/accountManager.d.ts +2 -2
  78. package/dist/schemas/accountManager.js +2 -4
  79. package/dist/schemas/accountStatus.d.ts +3 -0
  80. package/dist/schemas/accountStatus.js +1 -0
  81. package/dist/schemas/index.d.ts +1 -0
  82. package/dist/schemas/index.js +7 -2
  83. package/dist/schemas/observedSessionV1.d.ts +27 -26
  84. package/dist/schemas/observedSessionV1.js +6 -4
  85. package/dist/schemas/quota.d.ts +2 -2
  86. package/dist/schemas/quota.js +3 -6
  87. package/dist/schemas/quotaHistory.d.ts +2 -2
  88. package/dist/schemas/tokenEstimation.d.ts +1 -1
  89. package/dist/sessionMonitor.d.ts +15 -0
  90. package/dist/sessionMonitor.js +142 -0
  91. package/dist/sessionPreviews.d.ts +85 -0
  92. package/dist/sessionPreviews.js +549 -0
  93. package/dist/sessionTranscripts.js +10 -4
  94. package/dist/types/observedSessionV1.d.ts +6 -2
  95. package/dist/types/observedSessionV1.js +14 -8
  96. package/dist/writers/atomic.d.ts +20 -0
  97. package/dist/writers/atomic.js +190 -19
  98. package/package.json +22 -1
@@ -1,34 +1,200 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.ObservedSessionCollector = void 0;
4
37
  exports.observedSessionSourceFromProvider = observedSessionSourceFromProvider;
38
+ exports.fileFingerprintParts = fileFingerprintParts;
5
39
  exports.fileFingerprint = fileFingerprint;
40
+ const fs = __importStar(require("node:fs"));
6
41
  const node_fs_1 = require("node:fs");
7
42
  const observedSessionV1_1 = require("./types/observedSessionV1");
43
+ const sessionActivityDetector_1 = require("./parsers/sessionActivityDetector");
44
+ const DEFAULT_MAX_CONCURRENT_READS = 4;
45
+ const MAX_CONCURRENT_READS = 16;
46
+ const DEFAULT_DEBOUNCE_MS = 100;
47
+ const DEFAULT_POLL_INTERVAL_MS = 30_000;
8
48
  /**
9
- * One-shot, host-scheduled observed-session collection with independent
10
- * provider/session isolation and bounded retry state.
49
+ * Fail-soft observed-session collection with fingerprint-keyed parse caching,
50
+ * cooperative reads, and optional provider-level subscriptions.
11
51
  */
12
52
  class ObservedSessionCollector {
13
53
  options;
14
54
  failures = new Map();
55
+ cache = new Map();
56
+ subscriptions = new Set();
15
57
  initialBackoffMs;
16
58
  maxBackoffMs;
17
59
  clock;
60
+ maxConcurrentReads;
61
+ yieldBetweenReads;
18
62
  constructor(options) {
19
63
  this.options = options;
20
64
  this.initialBackoffMs = Math.max(1, options.initialBackoffMs ?? 30_000);
21
65
  this.maxBackoffMs = Math.max(this.initialBackoffMs, options.maxBackoffMs ?? 5 * 60_000);
22
66
  this.clock = options.clock ?? { now: () => Date.now() };
67
+ this.maxConcurrentReads = Math.min(MAX_CONCURRENT_READS, Math.max(1, Math.floor(options.maxConcurrentReads ?? DEFAULT_MAX_CONCURRENT_READS)));
68
+ this.yieldBetweenReads = async () => {
69
+ if (options.yieldBetweenReads) {
70
+ await options.yieldBetweenReads();
71
+ return;
72
+ }
73
+ await new Promise((resolve) => setImmediate(resolve));
74
+ };
23
75
  }
24
76
  async collect() {
25
77
  const nested = await Promise.all(this.options.sources.map((source) => this.collectSource(source)));
26
78
  return nested.flat();
27
79
  }
80
+ /**
81
+ * Subscribe to debounced provider changes. The listener receives fingerprints
82
+ * only; call collect() to consume changed sessions through the parse cache.
83
+ */
84
+ subscribe(listener, options = {}) {
85
+ const debounceMs = Math.max(0, options.debounceMs ?? DEFAULT_DEBOUNCE_MS);
86
+ const pollIntervalMs = Math.max(0, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
87
+ const known = new Map();
88
+ for (const item of options.knownFingerprints ?? []) {
89
+ known.set(this.key(item.providerId, item.sessionId), {
90
+ fingerprint: item.fingerprint,
91
+ parts: item.fingerprintParts ?? null,
92
+ });
93
+ }
94
+ for (const [key, entry] of this.cache) {
95
+ known.set(key, { fingerprint: entry.fingerprint, parts: entry.fingerprintParts });
96
+ }
97
+ let disposed = false;
98
+ let debounceTimer = null;
99
+ let reconciling = false;
100
+ let pending = false;
101
+ const sourceDisposables = [];
102
+ const reconcile = async () => {
103
+ if (disposed)
104
+ return;
105
+ if (reconciling) {
106
+ pending = true;
107
+ return;
108
+ }
109
+ reconciling = true;
110
+ try {
111
+ do {
112
+ pending = false;
113
+ for (const source of this.options.sources) {
114
+ if (disposed)
115
+ break;
116
+ const batch = await this.reconcileSource(source, known);
117
+ // Re-check after the await: disposal during the reconcile must
118
+ // not deliver the pre-disposal batch to the listener.
119
+ if (disposed)
120
+ break;
121
+ if (batch && batch.changes.length > 0) {
122
+ try {
123
+ listener(batch);
124
+ }
125
+ catch {
126
+ // Subscriber failures cannot stop reconciliation.
127
+ }
128
+ }
129
+ }
130
+ } while (pending && !disposed);
131
+ }
132
+ finally {
133
+ reconciling = false;
134
+ }
135
+ };
136
+ const schedule = () => {
137
+ if (disposed)
138
+ return;
139
+ if (debounceTimer)
140
+ clearTimeout(debounceTimer);
141
+ debounceTimer = setTimeout(() => {
142
+ debounceTimer = null;
143
+ void reconcile();
144
+ }, debounceMs);
145
+ debounceTimer.unref?.();
146
+ };
147
+ let needsCollectorPoll = false;
148
+ for (const source of this.options.sources) {
149
+ if (source.subscribe) {
150
+ try {
151
+ sourceDisposables.push(source.subscribe(schedule, { pollIntervalMs }));
152
+ }
153
+ catch {
154
+ // The collector-level poll below remains the correctness fallback.
155
+ needsCollectorPoll = true;
156
+ }
157
+ }
158
+ else {
159
+ needsCollectorPoll = true;
160
+ }
161
+ }
162
+ let pollTimer = null;
163
+ if (needsCollectorPoll && pollIntervalMs > 0) {
164
+ pollTimer = setInterval(schedule, pollIntervalMs);
165
+ pollTimer.unref?.();
166
+ }
167
+ schedule();
168
+ const disposable = {
169
+ dispose: () => {
170
+ if (disposed)
171
+ return;
172
+ disposed = true;
173
+ if (debounceTimer)
174
+ clearTimeout(debounceTimer);
175
+ if (pollTimer)
176
+ clearInterval(pollTimer);
177
+ for (const item of sourceDisposables) {
178
+ try {
179
+ item.dispose();
180
+ }
181
+ catch {
182
+ // Disposal is best effort.
183
+ }
184
+ }
185
+ this.subscriptions.delete(disposable);
186
+ },
187
+ };
188
+ this.subscriptions.add(disposable);
189
+ return disposable;
190
+ }
28
191
  reset() {
29
192
  this.failures.clear();
193
+ this.cache.clear();
30
194
  }
31
195
  dispose() {
196
+ for (const subscription of [...this.subscriptions])
197
+ subscription.dispose();
32
198
  for (const source of this.options.sources) {
33
199
  try {
34
200
  source.dispose?.();
@@ -54,49 +220,170 @@ class ObservedSessionCollector {
54
220
  return [];
55
221
  }
56
222
  const activeKeys = new Set(references.map((ref) => this.key(source.providerId, ref.sessionId)));
57
- for (const key of this.failures.keys()) {
58
- if (key.startsWith(`${source.providerId}\0`) &&
59
- !key.endsWith('$discovery') &&
60
- !activeKeys.has(key)) {
61
- this.failures.delete(key);
62
- }
63
- }
64
- const rows = await Promise.all(references.map((reference) => this.collectSession(source, reference)));
223
+ this.evictMissing(source.providerId, activeKeys);
224
+ const rows = await this.mapWithConcurrency(references, async (reference) => {
225
+ const result = await this.collectSession(source, reference);
226
+ await this.yieldBetweenReads();
227
+ return result;
228
+ });
65
229
  return rows.filter((row) => row !== null);
66
230
  }
67
231
  async collectSession(source, reference) {
68
232
  const key = this.key(source.providerId, reference.sessionId);
69
233
  const fingerprint = await this.fingerprint(source, reference);
70
- const prior = this.failures.get(key);
71
- if (prior && prior.fingerprint === fingerprint && this.clock.now() < prior.retryAt)
234
+ const priorFailure = this.failures.get(key);
235
+ if (priorFailure &&
236
+ priorFailure.fingerprint === fingerprint.value &&
237
+ this.clock.now() < priorFailure.retryAt) {
72
238
  return null;
239
+ }
240
+ const observedAt = new Date(this.clock.now()).toISOString();
241
+ const cached = fingerprint.value === null ? undefined : this.cache.get(key);
242
+ if (cached && cached.fingerprint === fingerprint.value) {
243
+ let value = cached.value;
244
+ try {
245
+ if (source.refreshCached) {
246
+ value = await source.refreshCached(reference, cached.value, observedAt);
247
+ }
248
+ }
249
+ catch {
250
+ value = cached.value;
251
+ }
252
+ cached.value = value;
253
+ cached.fingerprintParts = fingerprint.parts;
254
+ const observation = this.makeObservation(source, reference, value, fingerprint, true, observedAt, cached.contentObservedAt);
255
+ this.emitObservation(observation);
256
+ return observation;
257
+ }
73
258
  let value;
74
259
  try {
75
260
  value = await source.read(reference);
76
261
  }
77
262
  catch (error) {
78
- this.fail(key, 'session-read-failed', source.providerId, reference.sessionId, fingerprint, error);
263
+ this.fail(key, 'session-read-failed', source.providerId, reference.sessionId, fingerprint.value, error);
79
264
  return null;
80
265
  }
81
266
  this.recover(key, 'session-recovered', source.providerId, reference.sessionId);
82
- const observation = { providerId: source.providerId, sessionId: reference.sessionId, value };
267
+ const contentObservedAt = contentTimestamp(value) ?? observedAt;
268
+ if (fingerprint.value !== null) {
269
+ this.cache.set(key, {
270
+ value,
271
+ fingerprint: fingerprint.value,
272
+ fingerprintParts: fingerprint.parts,
273
+ contentObservedAt,
274
+ });
275
+ }
276
+ const observation = this.makeObservation(source, reference, value, fingerprint, false, observedAt, contentObservedAt);
277
+ this.emitObservation(observation);
278
+ return observation;
279
+ }
280
+ makeObservation(source, reference, value, fingerprint, cacheHit, observedAt, contentObservedAt) {
281
+ return {
282
+ providerId: source.providerId,
283
+ sessionId: reference.sessionId,
284
+ value,
285
+ fingerprint: fingerprint.value,
286
+ fingerprintParts: fingerprint.parts,
287
+ cacheHit,
288
+ observedAt,
289
+ contentObservedAt,
290
+ };
291
+ }
292
+ emitObservation(observation) {
83
293
  try {
84
294
  this.options.onObservation?.(observation);
85
295
  }
86
296
  catch {
87
297
  // Host callbacks cannot turn a healthy provider read into a collection failure.
88
298
  }
89
- return observation;
299
+ }
300
+ async reconcileSource(source, known) {
301
+ let references;
302
+ try {
303
+ references = await source.discover();
304
+ }
305
+ catch {
306
+ return null;
307
+ }
308
+ const changes = [];
309
+ const seen = new Set();
310
+ for (const reference of references) {
311
+ const key = this.key(source.providerId, reference.sessionId);
312
+ seen.add(key);
313
+ const current = await this.fingerprint(source, reference);
314
+ if (current.value === null)
315
+ continue;
316
+ const previous = known.get(key);
317
+ if (!previous || previous.fingerprint !== current.value) {
318
+ changes.push({
319
+ type: previous ? 'changed' : 'added',
320
+ reference,
321
+ previousFingerprint: previous?.fingerprint ?? null,
322
+ previousFingerprintParts: previous?.parts ?? null,
323
+ fingerprint: current.value,
324
+ fingerprintParts: current.parts,
325
+ });
326
+ }
327
+ known.set(key, { fingerprint: current.value, parts: current.parts });
328
+ }
329
+ for (const [key, previous] of [...known]) {
330
+ if (!key.startsWith(`${source.providerId}\0`) || seen.has(key))
331
+ continue;
332
+ const sessionId = key.slice(source.providerId.length + 1);
333
+ changes.push({
334
+ type: 'removed',
335
+ reference: { sessionId },
336
+ previousFingerprint: previous.fingerprint,
337
+ previousFingerprintParts: previous.parts,
338
+ fingerprint: null,
339
+ fingerprintParts: null,
340
+ });
341
+ known.delete(key);
342
+ }
343
+ return {
344
+ providerId: source.providerId,
345
+ changes,
346
+ observedAt: new Date(this.clock.now()).toISOString(),
347
+ };
90
348
  }
91
349
  async fingerprint(source, reference) {
350
+ let value = reference.fingerprintHint ?? null;
92
351
  try {
93
- return this.options.fingerprint
94
- ? await this.options.fingerprint(source, reference)
95
- : (reference.fingerprintHint ?? null);
352
+ if (this.options.fingerprint)
353
+ value = await this.options.fingerprint(source, reference);
96
354
  }
97
355
  catch {
98
- return reference.fingerprintHint ?? null;
356
+ // Retain the reference hint.
99
357
  }
358
+ const parts = reference.fingerprintParts ?? parseFingerprintParts(value);
359
+ return { value, parts };
360
+ }
361
+ evictMissing(providerId, activeKeys) {
362
+ for (const key of this.failures.keys()) {
363
+ if (key.startsWith(`${providerId}\0`) &&
364
+ !key.endsWith('$discovery') &&
365
+ !activeKeys.has(key)) {
366
+ this.failures.delete(key);
367
+ }
368
+ }
369
+ for (const key of this.cache.keys()) {
370
+ if (key.startsWith(`${providerId}\0`) && !activeKeys.has(key))
371
+ this.cache.delete(key);
372
+ }
373
+ }
374
+ async mapWithConcurrency(values, mapper) {
375
+ const results = new Array(values.length);
376
+ let cursor = 0;
377
+ const workers = Array.from({ length: Math.min(this.maxConcurrentReads, values.length) }, async () => {
378
+ while (true) {
379
+ const index = cursor++;
380
+ if (index >= values.length)
381
+ return;
382
+ results[index] = await mapper(values[index]);
383
+ }
384
+ });
385
+ await Promise.all(workers);
386
+ return results;
100
387
  }
101
388
  fail(key, kind, providerId, sessionId, fingerprint, error) {
102
389
  const previous = this.failures.get(key);
@@ -106,14 +393,17 @@ class ObservedSessionCollector {
106
393
  const delay = Math.min(this.maxBackoffMs, this.initialBackoffMs * 2 ** (attempt - 1));
107
394
  const retryAt = this.clock.now() + delay;
108
395
  const reportedSignature = previous?.reportedSignature ?? '';
109
- this.failures.set(key, {
110
- attempt,
111
- retryAt,
112
- fingerprint,
113
- reportedSignature: signature,
114
- });
396
+ this.failures.set(key, { attempt, retryAt, fingerprint, reportedSignature: signature });
115
397
  if (signature !== reportedSignature || fingerprintChanged) {
116
- this.emitDiagnostic({ kind, providerId, sessionId, attempt, retryAt });
398
+ this.emitDiagnostic({
399
+ kind,
400
+ severity: 'error',
401
+ phase: kind === 'provider-discovery-failed' ? 'discover' : 'read',
402
+ providerId,
403
+ sessionId,
404
+ attempt,
405
+ retryAt,
406
+ });
117
407
  }
118
408
  }
119
409
  recover(key, kind, providerId, sessionId) {
@@ -121,7 +411,14 @@ class ObservedSessionCollector {
121
411
  if (!prior)
122
412
  return;
123
413
  this.failures.delete(key);
124
- this.emitDiagnostic({ kind, providerId, sessionId, attempt: prior.attempt });
414
+ this.emitDiagnostic({
415
+ kind,
416
+ severity: 'info',
417
+ phase: 'recover',
418
+ providerId,
419
+ sessionId,
420
+ attempt: prior.attempt,
421
+ });
125
422
  }
126
423
  emitDiagnostic(diagnostic) {
127
424
  try {
@@ -140,38 +437,151 @@ exports.ObservedSessionCollector = ObservedSessionCollector;
140
437
  * Adapt a provider using path-only discovery and independent adapter reads, so
141
438
  * one malformed session cannot reject discovery of its healthy siblings.
142
439
  */
143
- function observedSessionSourceFromProvider(provider, cwd) {
144
- const adapter = (0, observedSessionV1_1.createProviderSessionAdapterV1)(provider);
440
+ function observedSessionSourceFromProvider(provider, cwd, options = {}) {
441
+ const adapter = options.observationOnly
442
+ ? (0, observedSessionV1_1.createProviderSessionAdapterV1)(provider, { observationOnly: true })
443
+ : (0, observedSessionV1_1.createProviderSessionAdapterV1)(provider);
444
+ const referenceFromPath = (sessionPath, sessionId = provider.getSessionId(sessionPath), suppliedParts) => {
445
+ const metadata = suppliedParts ? null : provider.getSessionMetadata?.(sessionPath);
446
+ const parts = suppliedParts ??
447
+ fileFingerprintParts(sessionPath) ??
448
+ (metadata ? { sizeBytes: 0, mtimeMs: metadata.mtime.getTime() } : null);
449
+ return {
450
+ sessionId,
451
+ sourceKey: sessionPath,
452
+ fingerprintHint: parts ? fingerprintString(parts) : undefined,
453
+ fingerprintParts: parts ?? undefined,
454
+ };
455
+ };
456
+ const discover = async () => {
457
+ if (provider.listSessionFilesAsync) {
458
+ try {
459
+ const files = await provider.listSessionFilesAsync(cwd);
460
+ return files.map((info) => {
461
+ const mtimeMs = info.mtime.getTime();
462
+ const suppliedParts = info.sizeBytes !== undefined && Number.isFinite(mtimeMs)
463
+ ? { sizeBytes: info.sizeBytes, mtimeMs }
464
+ : undefined;
465
+ return referenceFromPath(info.path, info.sessionId ?? provider.getSessionId(info.path), suppliedParts);
466
+ });
467
+ }
468
+ catch {
469
+ // Retain compatibility with third-party providers whose optional async
470
+ // enumerator is temporarily unavailable.
471
+ }
472
+ }
473
+ return provider.findAllSessions(cwd).map((sessionPath) => {
474
+ const metadata = provider.getSessionMetadata?.(sessionPath);
475
+ const parts = fileFingerprintParts(sessionPath) ??
476
+ (metadata ? { sizeBytes: 0, mtimeMs: metadata.mtime.getTime() } : null);
477
+ return {
478
+ sessionId: provider.getSessionId(sessionPath),
479
+ sourceKey: sessionPath,
480
+ fingerprintHint: parts ? fingerprintString(parts) : undefined,
481
+ fingerprintParts: parts ?? undefined,
482
+ };
483
+ });
484
+ };
145
485
  return {
146
486
  providerId: provider.id,
147
487
  capabilities: adapter.capabilities,
148
- discover() {
149
- return provider.findAllSessions(cwd).map((sessionPath) => ({
150
- sessionId: provider.getSessionId(sessionPath),
151
- sourceKey: sessionPath,
152
- fingerprintHint: provider.getSessionMetadata?.(sessionPath)?.mtime.getTime().toString() ??
153
- fileFingerprint(sessionPath) ??
154
- undefined,
155
- }));
156
- },
157
- read(reference) {
488
+ discover,
489
+ async read(reference) {
158
490
  if (!reference.sourceKey)
159
491
  throw new Error('session source unavailable');
160
492
  return adapter.read(reference.sourceKey, cwd);
161
493
  },
494
+ refreshCached(reference, cached, observedAt) {
495
+ const mtimeMs = reference.fingerprintParts?.mtimeMs ?? Date.parse(cached.observedAt);
496
+ const activity = (0, sessionActivityDetector_1.refreshSessionActivityState)(cached.activity.value, mtimeMs);
497
+ return {
498
+ ...cached,
499
+ activity: { ...cached.activity, value: activity },
500
+ observedAt,
501
+ };
502
+ },
503
+ subscribe(listener, subscribeOptions = {}) {
504
+ return subscribeToProviderRoot(provider, listener, subscribeOptions.pollIntervalMs ?? 0);
505
+ },
162
506
  dispose: () => adapter.dispose(),
163
507
  };
164
508
  }
165
- /** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
166
- function fileFingerprint(sourcePath) {
509
+ /** Structured size/mtime fingerprint; returns null for inaccessible or synthetic paths. */
510
+ function fileFingerprintParts(sourcePath) {
167
511
  try {
168
512
  const stat = (0, node_fs_1.statSync)(sourcePath);
169
- return `${stat.size}:${stat.mtimeMs}`;
513
+ return { sizeBytes: stat.size, mtimeMs: stat.mtimeMs };
170
514
  }
171
515
  catch {
172
516
  return null;
173
517
  }
174
518
  }
519
+ /** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
520
+ function fileFingerprint(sourcePath) {
521
+ const parts = fileFingerprintParts(sourcePath);
522
+ return parts ? fingerprintString(parts) : null;
523
+ }
524
+ function fingerprintString(parts) {
525
+ return `${parts.sizeBytes}:${parts.mtimeMs}`;
526
+ }
527
+ function parseFingerprintParts(value) {
528
+ if (!value)
529
+ return null;
530
+ const match = value.match(/^([0-9]+):([0-9]+(?:\.[0-9]+)?)$/);
531
+ if (!match)
532
+ return null;
533
+ const sizeBytes = Number(match[1]);
534
+ const mtimeMs = Number(match[2]);
535
+ return Number.isFinite(sizeBytes) && Number.isFinite(mtimeMs) ? { sizeBytes, mtimeMs } : null;
536
+ }
537
+ function contentTimestamp(value) {
538
+ if (!value || typeof value !== 'object')
539
+ return null;
540
+ const candidate = value.contentObservedAt;
541
+ return typeof candidate === 'string' ? candidate : null;
542
+ }
543
+ function subscribeToProviderRoot(provider, listener, pollIntervalMs) {
544
+ const watchers = new Set();
545
+ let pollTimer = null;
546
+ let roots;
547
+ try {
548
+ roots = provider.getWatchRoots?.() ?? [provider.getProjectsBaseDir()];
549
+ }
550
+ catch {
551
+ roots = [];
552
+ }
553
+ for (const root of new Set(roots)) {
554
+ try {
555
+ const watcher = fs.watch(root, { persistent: false, recursive: true }, listener);
556
+ watchers.add(watcher);
557
+ watcher.on('error', () => {
558
+ watchers.delete(watcher);
559
+ try {
560
+ watcher.close();
561
+ }
562
+ catch {
563
+ // Catch-up polling remains active.
564
+ }
565
+ });
566
+ }
567
+ catch {
568
+ // Polling remains the documented fallback.
569
+ }
570
+ }
571
+ if (pollIntervalMs > 0) {
572
+ pollTimer = setInterval(listener, pollIntervalMs);
573
+ pollTimer.unref?.();
574
+ }
575
+ return {
576
+ dispose: () => {
577
+ for (const watcher of watchers)
578
+ watcher.close();
579
+ watchers.clear();
580
+ if (pollTimer)
581
+ clearInterval(pollTimer);
582
+ },
583
+ };
584
+ }
175
585
  function failureSignature(error) {
176
586
  if (error instanceof Error)
177
587
  return `${error.name}\0${error.message}`;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Reader for Codex's append-only `~/.codex/history.jsonl` (Node-only).
3
+ *
4
+ * Each line is `{"session_id","ts","text"}` — one entry per user prompt. The
5
+ * file grows without bound, so this reader takes a bounded tail instead of the
6
+ * whole file, drops the leading partial line, and parses tolerantly. Pair an
7
+ * entry with {@link findCodexRolloutFile} (providers/codex) to jump from a
8
+ * history row to its full rollout transcript.
9
+ *
10
+ * @module codexHistory
11
+ */
12
+ export interface CodexHistoryEntry {
13
+ sessionId: string;
14
+ /** Timestamp exactly as Codex recorded it (epoch seconds). */
15
+ ts: number;
16
+ /** Normalized epoch milliseconds, ready for Date construction. */
17
+ tsMs: number;
18
+ text: string;
19
+ }
20
+ export interface ReadCodexHistoryOptions {
21
+ /** Codex home containing `history.jsonl` (default: `$CODEX_HOME` or `~/.codex`). */
22
+ codexHome?: string;
23
+ /** Maximum entries returned, newest first (default 100). */
24
+ limit?: number;
25
+ /** Bytes read from the end. Defaults to max(512 KiB, limit × 4096). */
26
+ maxTailBytes?: number;
27
+ }
28
+ /**
29
+ * Returns the newest history entries, newest first. A missing or unreadable
30
+ * file yields `[]`; malformed lines are skipped. Never throws.
31
+ */
32
+ export declare function readCodexHistory(options?: ReadCodexHistoryOptions): CodexHistoryEntry[];