sidekick-shared 0.24.5 → 0.26.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 (157) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +205 -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 +170 -0
  7. package/dist/accountRegistry.d.ts +25 -2
  8. package/dist/accountRegistry.js +41 -13
  9. package/dist/accountStatus.d.ts +9 -1
  10. package/dist/accountStatus.js +3 -3
  11. package/dist/accounts.d.ts +3 -1
  12. package/dist/accounts.js +84 -64
  13. package/dist/aggregation/EventAggregator.d.ts +15 -2
  14. package/dist/aggregation/EventAggregator.js +54 -9
  15. package/dist/aggregation/costProvenance.d.ts +14 -0
  16. package/dist/aggregation/costProvenance.js +35 -0
  17. package/dist/aggregation/snapshot.d.ts +13 -2
  18. package/dist/aggregation/snapshot.js +57 -6
  19. package/dist/aggregation/types.d.ts +30 -1
  20. package/dist/analytics/compactionLedger.js +15 -5
  21. package/dist/analytics/qualityScore.js +2 -1
  22. package/dist/browser.d.ts +19 -4
  23. package/dist/browser.js +43 -1
  24. package/dist/codexProfiles.d.ts +3 -1
  25. package/dist/codexProfiles.js +4 -2
  26. package/dist/codexQuota.d.ts +2 -0
  27. package/dist/codexQuota.js +11 -66
  28. package/dist/codexQuotaWatcher.d.ts +6 -0
  29. package/dist/codexQuotaWatcher.js +101 -47
  30. package/dist/failingTools.d.ts +37 -0
  31. package/dist/failingTools.js +70 -0
  32. package/dist/formatters/sessionDump.js +13 -7
  33. package/dist/formatting.d.ts +13 -0
  34. package/dist/formatting.js +35 -0
  35. package/dist/historicalStore.d.ts +47 -0
  36. package/dist/historicalStore.js +312 -0
  37. package/dist/index.d.ts +67 -23
  38. package/dist/index.js +138 -26
  39. package/dist/modelAliases.d.ts +13 -0
  40. package/dist/modelAliases.js +58 -0
  41. package/dist/modelCatalog.d.ts +32 -0
  42. package/dist/modelCatalog.js +145 -0
  43. package/dist/modelContext.d.ts +36 -3
  44. package/dist/modelContext.js +109 -34
  45. package/dist/modelInfo.d.ts +30 -1
  46. package/dist/modelInfo.js +116 -26
  47. package/dist/multiProviderQuotaService.d.ts +8 -0
  48. package/dist/multiProviderQuotaService.js +64 -3
  49. package/dist/node.d.ts +23 -4
  50. package/dist/node.js +50 -1
  51. package/dist/observedContextWindows.d.ts +7 -0
  52. package/dist/observedContextWindows.js +17 -2
  53. package/dist/observedSessionCollector.d.ts +72 -6
  54. package/dist/observedSessionCollector.js +440 -47
  55. package/dist/parsers/codexHistory.d.ts +4 -2
  56. package/dist/parsers/codexHistory.js +9 -2
  57. package/dist/parsers/sessionActivityDetector.d.ts +28 -0
  58. package/dist/parsers/sessionActivityDetector.js +110 -15
  59. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  60. package/dist/parsers/sessionPathResolver.js +13 -10
  61. package/dist/peakHours.d.ts +34 -3
  62. package/dist/peakHours.js +92 -7
  63. package/dist/pricingCatalog.d.ts +7 -0
  64. package/dist/pricingCatalog.js +8 -5
  65. package/dist/providerIds.d.ts +11 -0
  66. package/dist/providerIds.js +9 -0
  67. package/dist/providerQuota.d.ts +3 -1
  68. package/dist/providerQuota.js +3 -0
  69. package/dist/providers/claudeCode.d.ts +10 -1
  70. package/dist/providers/claudeCode.js +211 -131
  71. package/dist/providers/codex.d.ts +20 -1
  72. package/dist/providers/codex.js +308 -244
  73. package/dist/providers/codexDatabase.d.ts +9 -0
  74. package/dist/providers/codexDatabase.js +107 -2
  75. package/dist/providers/detect.d.ts +2 -0
  76. package/dist/providers/detect.js +29 -24
  77. package/dist/providers/diagnostics.d.ts +13 -0
  78. package/dist/providers/diagnostics.js +88 -0
  79. package/dist/providers/factory.d.ts +20 -0
  80. package/dist/providers/factory.js +57 -0
  81. package/dist/providers/openCode.d.ts +22 -1
  82. package/dist/providers/openCode.js +460 -126
  83. package/dist/providers/openCodeDatabase.d.ts +10 -0
  84. package/dist/providers/openCodeDatabase.js +69 -0
  85. package/dist/providers/rolloutWalker.d.ts +53 -0
  86. package/dist/providers/rolloutWalker.js +209 -0
  87. package/dist/providers/types.d.ts +82 -2
  88. package/dist/providers/types.js +4 -0
  89. package/dist/quota.d.ts +34 -4
  90. package/dist/quota.js +37 -9
  91. package/dist/quotaHistory.d.ts +3 -2
  92. package/dist/quotaHistory.js +36 -35
  93. package/dist/quotaPoller.d.ts +13 -1
  94. package/dist/quotaPoller.js +65 -3
  95. package/dist/quotaResolver.d.ts +81 -0
  96. package/dist/quotaResolver.js +213 -0
  97. package/dist/quotaSnapshots.d.ts +15 -2
  98. package/dist/quotaSnapshots.js +25 -3
  99. package/dist/quotaThresholds.d.ts +60 -0
  100. package/dist/quotaThresholds.js +90 -0
  101. package/dist/report/htmlReportGenerator.js +10 -4
  102. package/dist/report/sessionReportInputs.d.ts +23 -0
  103. package/dist/report/sessionReportInputs.js +34 -0
  104. package/dist/schemas/accountManager.d.ts +2 -2
  105. package/dist/schemas/accountManager.js +2 -4
  106. package/dist/schemas/accountStatus.d.ts +3 -0
  107. package/dist/schemas/accountStatus.js +1 -0
  108. package/dist/schemas/index.d.ts +3 -0
  109. package/dist/schemas/index.js +16 -2
  110. package/dist/schemas/observedSessionV1.d.ts +27 -26
  111. package/dist/schemas/observedSessionV1.js +6 -4
  112. package/dist/schemas/quota.d.ts +141 -2
  113. package/dist/schemas/quota.js +20 -8
  114. package/dist/schemas/quotaHistory.d.ts +3 -2
  115. package/dist/schemas/quotaHistory.js +1 -1
  116. package/dist/schemas/sessionEvent.d.ts +6 -0
  117. package/dist/schemas/sessionEvent.js +1 -0
  118. package/dist/schemas/stateFile.d.ts +198 -0
  119. package/dist/schemas/stateFile.js +77 -0
  120. package/dist/schemas/tokenEstimation.d.ts +1 -1
  121. package/dist/schemas/transcript.js +2 -0
  122. package/dist/sessionFingerprint.d.ts +25 -0
  123. package/dist/sessionFingerprint.js +46 -0
  124. package/dist/sessionMonitor.d.ts +15 -0
  125. package/dist/sessionMonitor.js +142 -0
  126. package/dist/sessionPreviews.d.ts +33 -2
  127. package/dist/sessionPreviews.js +333 -6
  128. package/dist/sessionStats.d.ts +51 -0
  129. package/dist/sessionStats.js +166 -0
  130. package/dist/stateFile.d.ts +113 -0
  131. package/dist/stateFile.js +160 -0
  132. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  133. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  134. package/dist/statusline/formatter.d.ts +13 -0
  135. package/dist/statusline/formatter.js +48 -6
  136. package/dist/statusline/index.d.ts +10 -2
  137. package/dist/statusline/index.js +19 -1
  138. package/dist/tokenSummary.d.ts +62 -0
  139. package/dist/tokenSummary.js +72 -0
  140. package/dist/transcript.d.ts +5 -0
  141. package/dist/transcript.js +2 -0
  142. package/dist/types/observedSessionV1.d.ts +8 -2
  143. package/dist/types/observedSessionV1.js +88 -25
  144. package/dist/types/sessionEvent.d.ts +2 -0
  145. package/dist/usage/billingBlocks.d.ts +85 -0
  146. package/dist/usage/billingBlocks.js +153 -0
  147. package/dist/usage/importSessionHistory.d.ts +53 -0
  148. package/dist/usage/importSessionHistory.js +88 -0
  149. package/dist/usage/usageEvents.d.ts +92 -0
  150. package/dist/usage/usageEvents.js +322 -0
  151. package/dist/usage/usageReports.d.ts +76 -0
  152. package/dist/usage/usageReports.js +208 -0
  153. package/dist/watchers/factory.d.ts +6 -0
  154. package/dist/watchers/factory.js +30 -25
  155. package/dist/zaiQuotaApi.d.ts +2 -0
  156. package/dist/zaiQuotaApi.js +2 -0
  157. package/package.json +22 -1
@@ -1,34 +1,201 @@
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
- exports.ObservedSessionCollector = void 0;
36
+ exports.ObservedSessionCollector = exports.fileFingerprintParts = exports.fileFingerprint = void 0;
4
37
  exports.observedSessionSourceFromProvider = observedSessionSourceFromProvider;
5
- exports.fileFingerprint = fileFingerprint;
6
- const node_fs_1 = require("node:fs");
38
+ const fs = __importStar(require("node:fs"));
7
39
  const observedSessionV1_1 = require("./types/observedSessionV1");
40
+ const sessionActivityDetector_1 = require("./parsers/sessionActivityDetector");
41
+ const sessionFingerprint_1 = require("./sessionFingerprint");
42
+ var sessionFingerprint_2 = require("./sessionFingerprint");
43
+ Object.defineProperty(exports, "fileFingerprint", { enumerable: true, get: function () { return sessionFingerprint_2.fileFingerprint; } });
44
+ Object.defineProperty(exports, "fileFingerprintParts", { enumerable: true, get: function () { return sessionFingerprint_2.fileFingerprintParts; } });
45
+ const DEFAULT_MAX_CONCURRENT_READS = 4;
46
+ const MAX_CONCURRENT_READS = 16;
47
+ const DEFAULT_DEBOUNCE_MS = 100;
48
+ const DEFAULT_POLL_INTERVAL_MS = 30_000;
8
49
  /**
9
- * One-shot, host-scheduled observed-session collection with independent
10
- * provider/session isolation and bounded retry state.
50
+ * Fail-soft observed-session collection with fingerprint-keyed parse caching,
51
+ * cooperative reads, and optional provider-level subscriptions.
11
52
  */
12
53
  class ObservedSessionCollector {
13
54
  options;
14
55
  failures = new Map();
56
+ cache = new Map();
57
+ subscriptions = new Set();
15
58
  initialBackoffMs;
16
59
  maxBackoffMs;
17
60
  clock;
61
+ maxConcurrentReads;
62
+ yieldBetweenReads;
18
63
  constructor(options) {
19
64
  this.options = options;
20
65
  this.initialBackoffMs = Math.max(1, options.initialBackoffMs ?? 30_000);
21
66
  this.maxBackoffMs = Math.max(this.initialBackoffMs, options.maxBackoffMs ?? 5 * 60_000);
22
67
  this.clock = options.clock ?? { now: () => Date.now() };
68
+ this.maxConcurrentReads = Math.min(MAX_CONCURRENT_READS, Math.max(1, Math.floor(options.maxConcurrentReads ?? DEFAULT_MAX_CONCURRENT_READS)));
69
+ this.yieldBetweenReads = async () => {
70
+ if (options.yieldBetweenReads) {
71
+ await options.yieldBetweenReads();
72
+ return;
73
+ }
74
+ await new Promise((resolve) => setImmediate(resolve));
75
+ };
23
76
  }
24
77
  async collect() {
25
78
  const nested = await Promise.all(this.options.sources.map((source) => this.collectSource(source)));
26
79
  return nested.flat();
27
80
  }
81
+ /**
82
+ * Subscribe to debounced provider changes. The listener receives fingerprints
83
+ * only; call collect() to consume changed sessions through the parse cache.
84
+ */
85
+ subscribe(listener, options = {}) {
86
+ const debounceMs = Math.max(0, options.debounceMs ?? DEFAULT_DEBOUNCE_MS);
87
+ const pollIntervalMs = Math.max(0, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
88
+ const known = new Map();
89
+ for (const item of options.knownFingerprints ?? []) {
90
+ known.set(this.key(item.providerId, item.sessionId), {
91
+ fingerprint: item.fingerprint,
92
+ parts: item.fingerprintParts ?? null,
93
+ });
94
+ }
95
+ for (const [key, entry] of this.cache) {
96
+ known.set(key, { fingerprint: entry.fingerprint, parts: entry.fingerprintParts });
97
+ }
98
+ let disposed = false;
99
+ let debounceTimer = null;
100
+ let reconciling = false;
101
+ let pending = false;
102
+ const sourceDisposables = [];
103
+ const reconcile = async () => {
104
+ if (disposed)
105
+ return;
106
+ if (reconciling) {
107
+ pending = true;
108
+ return;
109
+ }
110
+ reconciling = true;
111
+ try {
112
+ do {
113
+ pending = false;
114
+ for (const source of this.options.sources) {
115
+ if (disposed)
116
+ break;
117
+ const batch = await this.reconcileSource(source, known);
118
+ // Re-check after the await: disposal during the reconcile must
119
+ // not deliver the pre-disposal batch to the listener.
120
+ if (disposed)
121
+ break;
122
+ if (batch && batch.changes.length > 0) {
123
+ try {
124
+ listener(batch);
125
+ }
126
+ catch {
127
+ // Subscriber failures cannot stop reconciliation.
128
+ }
129
+ }
130
+ }
131
+ } while (pending && !disposed);
132
+ }
133
+ finally {
134
+ reconciling = false;
135
+ }
136
+ };
137
+ const schedule = () => {
138
+ if (disposed)
139
+ return;
140
+ if (debounceTimer)
141
+ clearTimeout(debounceTimer);
142
+ debounceTimer = setTimeout(() => {
143
+ debounceTimer = null;
144
+ void reconcile();
145
+ }, debounceMs);
146
+ debounceTimer.unref?.();
147
+ };
148
+ let needsCollectorPoll = false;
149
+ for (const source of this.options.sources) {
150
+ if (source.subscribe) {
151
+ try {
152
+ sourceDisposables.push(source.subscribe(schedule, { pollIntervalMs }));
153
+ }
154
+ catch {
155
+ // The collector-level poll below remains the correctness fallback.
156
+ needsCollectorPoll = true;
157
+ }
158
+ }
159
+ else {
160
+ needsCollectorPoll = true;
161
+ }
162
+ }
163
+ let pollTimer = null;
164
+ if (needsCollectorPoll && pollIntervalMs > 0) {
165
+ pollTimer = setInterval(schedule, pollIntervalMs);
166
+ pollTimer.unref?.();
167
+ }
168
+ schedule();
169
+ const disposable = {
170
+ dispose: () => {
171
+ if (disposed)
172
+ return;
173
+ disposed = true;
174
+ if (debounceTimer)
175
+ clearTimeout(debounceTimer);
176
+ if (pollTimer)
177
+ clearInterval(pollTimer);
178
+ for (const item of sourceDisposables) {
179
+ try {
180
+ item.dispose();
181
+ }
182
+ catch {
183
+ // Disposal is best effort.
184
+ }
185
+ }
186
+ this.subscriptions.delete(disposable);
187
+ },
188
+ };
189
+ this.subscriptions.add(disposable);
190
+ return disposable;
191
+ }
28
192
  reset() {
29
193
  this.failures.clear();
194
+ this.cache.clear();
30
195
  }
31
196
  dispose() {
197
+ for (const subscription of [...this.subscriptions])
198
+ subscription.dispose();
32
199
  for (const source of this.options.sources) {
33
200
  try {
34
201
  source.dispose?.();
@@ -54,49 +221,170 @@ class ObservedSessionCollector {
54
221
  return [];
55
222
  }
56
223
  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)));
224
+ this.evictMissing(source.providerId, activeKeys);
225
+ const rows = await this.mapWithConcurrency(references, async (reference) => {
226
+ const result = await this.collectSession(source, reference);
227
+ await this.yieldBetweenReads();
228
+ return result;
229
+ });
65
230
  return rows.filter((row) => row !== null);
66
231
  }
67
232
  async collectSession(source, reference) {
68
233
  const key = this.key(source.providerId, reference.sessionId);
69
234
  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)
235
+ const priorFailure = this.failures.get(key);
236
+ if (priorFailure &&
237
+ priorFailure.fingerprint === fingerprint.value &&
238
+ this.clock.now() < priorFailure.retryAt) {
72
239
  return null;
240
+ }
241
+ const observedAt = new Date(this.clock.now()).toISOString();
242
+ const cached = fingerprint.value === null ? undefined : this.cache.get(key);
243
+ if (cached && cached.fingerprint === fingerprint.value) {
244
+ let value = cached.value;
245
+ try {
246
+ if (source.refreshCached) {
247
+ value = await source.refreshCached(reference, cached.value, observedAt);
248
+ }
249
+ }
250
+ catch {
251
+ value = cached.value;
252
+ }
253
+ cached.value = value;
254
+ cached.fingerprintParts = fingerprint.parts;
255
+ const observation = this.makeObservation(source, reference, value, fingerprint, true, observedAt, cached.contentObservedAt);
256
+ this.emitObservation(observation);
257
+ return observation;
258
+ }
73
259
  let value;
74
260
  try {
75
261
  value = await source.read(reference);
76
262
  }
77
263
  catch (error) {
78
- this.fail(key, 'session-read-failed', source.providerId, reference.sessionId, fingerprint, error);
264
+ this.fail(key, 'session-read-failed', source.providerId, reference.sessionId, fingerprint.value, error);
79
265
  return null;
80
266
  }
81
267
  this.recover(key, 'session-recovered', source.providerId, reference.sessionId);
82
- const observation = { providerId: source.providerId, sessionId: reference.sessionId, value };
268
+ const contentObservedAt = contentTimestamp(value) ?? observedAt;
269
+ if (fingerprint.value !== null) {
270
+ this.cache.set(key, {
271
+ value,
272
+ fingerprint: fingerprint.value,
273
+ fingerprintParts: fingerprint.parts,
274
+ contentObservedAt,
275
+ });
276
+ }
277
+ const observation = this.makeObservation(source, reference, value, fingerprint, false, observedAt, contentObservedAt);
278
+ this.emitObservation(observation);
279
+ return observation;
280
+ }
281
+ makeObservation(source, reference, value, fingerprint, cacheHit, observedAt, contentObservedAt) {
282
+ return {
283
+ providerId: source.providerId,
284
+ sessionId: reference.sessionId,
285
+ value,
286
+ fingerprint: fingerprint.value,
287
+ fingerprintParts: fingerprint.parts,
288
+ cacheHit,
289
+ observedAt,
290
+ contentObservedAt,
291
+ };
292
+ }
293
+ emitObservation(observation) {
83
294
  try {
84
295
  this.options.onObservation?.(observation);
85
296
  }
86
297
  catch {
87
298
  // Host callbacks cannot turn a healthy provider read into a collection failure.
88
299
  }
89
- return observation;
300
+ }
301
+ async reconcileSource(source, known) {
302
+ let references;
303
+ try {
304
+ references = await source.discover();
305
+ }
306
+ catch {
307
+ return null;
308
+ }
309
+ const changes = [];
310
+ const seen = new Set();
311
+ for (const reference of references) {
312
+ const key = this.key(source.providerId, reference.sessionId);
313
+ seen.add(key);
314
+ const current = await this.fingerprint(source, reference);
315
+ if (current.value === null)
316
+ continue;
317
+ const previous = known.get(key);
318
+ if (!previous || previous.fingerprint !== current.value) {
319
+ changes.push({
320
+ type: previous ? 'changed' : 'added',
321
+ reference,
322
+ previousFingerprint: previous?.fingerprint ?? null,
323
+ previousFingerprintParts: previous?.parts ?? null,
324
+ fingerprint: current.value,
325
+ fingerprintParts: current.parts,
326
+ });
327
+ }
328
+ known.set(key, { fingerprint: current.value, parts: current.parts });
329
+ }
330
+ for (const [key, previous] of [...known]) {
331
+ if (!key.startsWith(`${source.providerId}\0`) || seen.has(key))
332
+ continue;
333
+ const sessionId = key.slice(source.providerId.length + 1);
334
+ changes.push({
335
+ type: 'removed',
336
+ reference: { sessionId },
337
+ previousFingerprint: previous.fingerprint,
338
+ previousFingerprintParts: previous.parts,
339
+ fingerprint: null,
340
+ fingerprintParts: null,
341
+ });
342
+ known.delete(key);
343
+ }
344
+ return {
345
+ providerId: source.providerId,
346
+ changes,
347
+ observedAt: new Date(this.clock.now()).toISOString(),
348
+ };
90
349
  }
91
350
  async fingerprint(source, reference) {
351
+ let value = reference.fingerprintHint ?? null;
92
352
  try {
93
- return this.options.fingerprint
94
- ? await this.options.fingerprint(source, reference)
95
- : (reference.fingerprintHint ?? null);
353
+ if (this.options.fingerprint)
354
+ value = await this.options.fingerprint(source, reference);
96
355
  }
97
356
  catch {
98
- return reference.fingerprintHint ?? null;
357
+ // Retain the reference hint.
99
358
  }
359
+ const parts = reference.fingerprintParts ?? parseFingerprintParts(value);
360
+ return { value, parts };
361
+ }
362
+ evictMissing(providerId, activeKeys) {
363
+ for (const key of this.failures.keys()) {
364
+ if (key.startsWith(`${providerId}\0`) &&
365
+ !key.endsWith('$discovery') &&
366
+ !activeKeys.has(key)) {
367
+ this.failures.delete(key);
368
+ }
369
+ }
370
+ for (const key of this.cache.keys()) {
371
+ if (key.startsWith(`${providerId}\0`) && !activeKeys.has(key))
372
+ this.cache.delete(key);
373
+ }
374
+ }
375
+ async mapWithConcurrency(values, mapper) {
376
+ const results = new Array(values.length);
377
+ let cursor = 0;
378
+ const workers = Array.from({ length: Math.min(this.maxConcurrentReads, values.length) }, async () => {
379
+ while (true) {
380
+ const index = cursor++;
381
+ if (index >= values.length)
382
+ return;
383
+ results[index] = await mapper(values[index]);
384
+ }
385
+ });
386
+ await Promise.all(workers);
387
+ return results;
100
388
  }
101
389
  fail(key, kind, providerId, sessionId, fingerprint, error) {
102
390
  const previous = this.failures.get(key);
@@ -106,14 +394,17 @@ class ObservedSessionCollector {
106
394
  const delay = Math.min(this.maxBackoffMs, this.initialBackoffMs * 2 ** (attempt - 1));
107
395
  const retryAt = this.clock.now() + delay;
108
396
  const reportedSignature = previous?.reportedSignature ?? '';
109
- this.failures.set(key, {
110
- attempt,
111
- retryAt,
112
- fingerprint,
113
- reportedSignature: signature,
114
- });
397
+ this.failures.set(key, { attempt, retryAt, fingerprint, reportedSignature: signature });
115
398
  if (signature !== reportedSignature || fingerprintChanged) {
116
- this.emitDiagnostic({ kind, providerId, sessionId, attempt, retryAt });
399
+ this.emitDiagnostic({
400
+ kind,
401
+ severity: 'error',
402
+ phase: kind === 'provider-discovery-failed' ? 'discover' : 'read',
403
+ providerId,
404
+ sessionId,
405
+ attempt,
406
+ retryAt,
407
+ });
117
408
  }
118
409
  }
119
410
  recover(key, kind, providerId, sessionId) {
@@ -121,7 +412,14 @@ class ObservedSessionCollector {
121
412
  if (!prior)
122
413
  return;
123
414
  this.failures.delete(key);
124
- this.emitDiagnostic({ kind, providerId, sessionId, attempt: prior.attempt });
415
+ this.emitDiagnostic({
416
+ kind,
417
+ severity: 'info',
418
+ phase: 'recover',
419
+ providerId,
420
+ sessionId,
421
+ attempt: prior.attempt,
422
+ });
125
423
  }
126
424
  emitDiagnostic(diagnostic) {
127
425
  try {
@@ -140,37 +438,132 @@ exports.ObservedSessionCollector = ObservedSessionCollector;
140
438
  * Adapt a provider using path-only discovery and independent adapter reads, so
141
439
  * one malformed session cannot reject discovery of its healthy siblings.
142
440
  */
143
- function observedSessionSourceFromProvider(provider, cwd) {
144
- const adapter = (0, observedSessionV1_1.createProviderSessionAdapterV1)(provider);
441
+ function observedSessionSourceFromProvider(provider, cwd, options = {}) {
442
+ const adapter = options.observationOnly
443
+ ? (0, observedSessionV1_1.createProviderSessionAdapterV1)(provider, { observationOnly: true })
444
+ : (0, observedSessionV1_1.createProviderSessionAdapterV1)(provider);
445
+ const referenceFromPath = (sessionPath, sessionId = provider.getSessionId(sessionPath), suppliedParts) => {
446
+ const metadata = suppliedParts ? null : provider.getSessionMetadata?.(sessionPath);
447
+ const parts = suppliedParts ??
448
+ (0, sessionFingerprint_1.fileFingerprintParts)(sessionPath) ??
449
+ (metadata ? { sizeBytes: 0, mtimeMs: metadata.mtime.getTime() } : null);
450
+ return {
451
+ sessionId,
452
+ sourceKey: sessionPath,
453
+ fingerprintHint: parts ? (0, sessionFingerprint_1.fingerprintString)(parts) : undefined,
454
+ fingerprintParts: parts ?? undefined,
455
+ };
456
+ };
457
+ const discover = async () => {
458
+ if (provider.listSessionFilesAsync) {
459
+ try {
460
+ const files = await provider.listSessionFilesAsync(cwd);
461
+ return files.map((info) => {
462
+ const mtimeMs = info.mtime.getTime();
463
+ const suppliedParts = info.sizeBytes !== undefined && Number.isFinite(mtimeMs)
464
+ ? { sizeBytes: info.sizeBytes, mtimeMs }
465
+ : undefined;
466
+ return referenceFromPath(info.path, info.sessionId ?? provider.getSessionId(info.path), suppliedParts);
467
+ });
468
+ }
469
+ catch {
470
+ // Retain compatibility with third-party providers whose optional async
471
+ // enumerator is temporarily unavailable.
472
+ }
473
+ }
474
+ return provider.findAllSessions(cwd).map((sessionPath) => {
475
+ const metadata = provider.getSessionMetadata?.(sessionPath);
476
+ const parts = (0, sessionFingerprint_1.fileFingerprintParts)(sessionPath) ??
477
+ (metadata ? { sizeBytes: 0, mtimeMs: metadata.mtime.getTime() } : null);
478
+ return {
479
+ sessionId: provider.getSessionId(sessionPath),
480
+ sourceKey: sessionPath,
481
+ fingerprintHint: parts ? (0, sessionFingerprint_1.fingerprintString)(parts) : undefined,
482
+ fingerprintParts: parts ?? undefined,
483
+ };
484
+ });
485
+ };
145
486
  return {
146
487
  providerId: provider.id,
147
488
  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) {
489
+ discover,
490
+ async read(reference) {
158
491
  if (!reference.sourceKey)
159
492
  throw new Error('session source unavailable');
160
493
  return adapter.read(reference.sourceKey, cwd);
161
494
  },
495
+ refreshCached(reference, cached, observedAt) {
496
+ const mtimeMs = reference.fingerprintParts?.mtimeMs ?? Date.parse(cached.observedAt);
497
+ const activity = (0, sessionActivityDetector_1.refreshSessionActivityState)(cached.activity.value, mtimeMs, Date.now(), (0, observedSessionV1_1.getObservedActivityReason)(cached));
498
+ return {
499
+ ...cached,
500
+ activity: { ...cached.activity, value: activity },
501
+ observedAt,
502
+ };
503
+ },
504
+ subscribe(listener, subscribeOptions = {}) {
505
+ return subscribeToProviderRoot(provider, listener, subscribeOptions.pollIntervalMs ?? 0);
506
+ },
162
507
  dispose: () => adapter.dispose(),
163
508
  };
164
509
  }
165
- /** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
166
- function fileFingerprint(sourcePath) {
510
+ function parseFingerprintParts(value) {
511
+ if (!value)
512
+ return null;
513
+ const match = value.match(/^([0-9]+):([0-9]+(?:\.[0-9]+)?)$/);
514
+ if (!match)
515
+ return null;
516
+ const sizeBytes = Number(match[1]);
517
+ const mtimeMs = Number(match[2]);
518
+ return Number.isFinite(sizeBytes) && Number.isFinite(mtimeMs) ? { sizeBytes, mtimeMs } : null;
519
+ }
520
+ function contentTimestamp(value) {
521
+ if (!value || typeof value !== 'object')
522
+ return null;
523
+ const candidate = value.contentObservedAt;
524
+ return typeof candidate === 'string' ? candidate : null;
525
+ }
526
+ function subscribeToProviderRoot(provider, listener, pollIntervalMs) {
527
+ const watchers = new Set();
528
+ let pollTimer = null;
529
+ let roots;
167
530
  try {
168
- const stat = (0, node_fs_1.statSync)(sourcePath);
169
- return `${stat.size}:${stat.mtimeMs}`;
531
+ roots = provider.getWatchRoots?.() ?? [provider.getProjectsBaseDir()];
170
532
  }
171
533
  catch {
172
- return null;
534
+ roots = [];
173
535
  }
536
+ for (const root of new Set(roots)) {
537
+ try {
538
+ const watcher = fs.watch(root, { persistent: false, recursive: true }, listener);
539
+ watchers.add(watcher);
540
+ watcher.on('error', () => {
541
+ watchers.delete(watcher);
542
+ try {
543
+ watcher.close();
544
+ }
545
+ catch {
546
+ // Catch-up polling remains active.
547
+ }
548
+ });
549
+ }
550
+ catch {
551
+ // Polling remains the documented fallback.
552
+ }
553
+ }
554
+ if (pollIntervalMs > 0) {
555
+ pollTimer = setInterval(listener, pollIntervalMs);
556
+ pollTimer.unref?.();
557
+ }
558
+ return {
559
+ dispose: () => {
560
+ for (const watcher of watchers)
561
+ watcher.close();
562
+ watchers.clear();
563
+ if (pollTimer)
564
+ clearInterval(pollTimer);
565
+ },
566
+ };
174
567
  }
175
568
  function failureSignature(error) {
176
569
  if (error instanceof Error)
@@ -11,8 +11,10 @@
11
11
  */
12
12
  export interface CodexHistoryEntry {
13
13
  sessionId: string;
14
- /** Timestamp exactly as Codex recorded it (numeric epoch value). */
14
+ /** Timestamp exactly as Codex recorded it (epoch seconds). */
15
15
  ts: number;
16
+ /** Normalized epoch milliseconds, ready for Date construction. */
17
+ tsMs: number;
16
18
  text: string;
17
19
  }
18
20
  export interface ReadCodexHistoryOptions {
@@ -20,7 +22,7 @@ export interface ReadCodexHistoryOptions {
20
22
  codexHome?: string;
21
23
  /** Maximum entries returned, newest first (default 100). */
22
24
  limit?: number;
23
- /** Bytes read from the end of the file (default 512 KiB). */
25
+ /** Bytes read from the end. Defaults to max(512 KiB, limit × 4096). */
24
26
  maxTailBytes?: number;
25
27
  }
26
28
  /**
@@ -56,7 +56,9 @@ const DEFAULT_TAIL_BYTES = 512 * 1024;
56
56
  */
57
57
  function readCodexHistory(options = {}) {
58
58
  const limit = Math.max(0, options.limit ?? DEFAULT_LIMIT);
59
- const maxTailBytes = Math.max(1, options.maxTailBytes ?? DEFAULT_TAIL_BYTES);
59
+ if (limit === 0)
60
+ return [];
61
+ const maxTailBytes = Math.max(1, options.maxTailBytes ?? Math.max(DEFAULT_TAIL_BYTES, limit * 4096));
60
62
  const historyPath = path.join(options.codexHome ?? (0, codexProfiles_1.getSystemCodexHome)(), 'history.jsonl');
61
63
  let descriptor;
62
64
  try {
@@ -98,7 +100,12 @@ function readCodexHistory(options = {}) {
98
100
  typeof parsed.text !== 'string') {
99
101
  continue;
100
102
  }
101
- entries.push({ sessionId: parsed.session_id, ts: parsed.ts, text: parsed.text });
103
+ entries.push({
104
+ sessionId: parsed.session_id,
105
+ ts: parsed.ts,
106
+ tsMs: parsed.ts * 1000,
107
+ text: parsed.text,
108
+ });
102
109
  }
103
110
  // The file is append-only, so on-disk order is oldest first.
104
111
  entries.reverse();
@@ -9,6 +9,7 @@
9
9
  *
10
10
  * @module parsers/sessionActivityDetector
11
11
  */
12
+ import type { SessionEvent } from '../types/sessionEvent';
12
13
  export type SessionActivityState = 'ongoing' | 'ended' | 'stale';
13
14
  export interface SessionActivityResult {
14
15
  state: SessionActivityState;
@@ -17,6 +18,10 @@ export interface SessionActivityResult {
17
18
  /** Reason for the classification */
18
19
  reason: string;
19
20
  }
21
+ /** Grace period to prevent spinner flicker (ms) */
22
+ export declare const SESSION_ACTIVITY_GRACE_PERIOD_MS = 5000;
23
+ /** If mtime is older than this, session is stale regardless of content */
24
+ export declare const SESSION_ACTIVITY_STALENESS_MS: number;
20
25
  /**
21
26
  * Determines whether a session is ongoing, ended, or stale.
22
27
  *
@@ -29,3 +34,26 @@ export interface SessionActivityResult {
29
34
  * @returns Activity state with classification reason
30
35
  */
31
36
  export declare function detectSessionActivity(sessionPath: string): SessionActivityResult;
37
+ export interface ClassifySessionActivityOptions {
38
+ /** Canonical events already read for the session; only the tail is inspected. */
39
+ events: readonly SessionEvent[];
40
+ /** Source mtime; the last event's timestamp is used when unknown. */
41
+ mtimeMs: number | null | undefined;
42
+ /** Clock (default `Date.now()`). */
43
+ now?: number;
44
+ }
45
+ /**
46
+ * Classify a session from events already in memory, with the same states,
47
+ * reasons, grace period, and staleness rule as {@link detectSessionActivity},
48
+ * but without opening the file again. Works for every provider (the raw
49
+ * tail patterns only ever matched Claude Code JSONL) and for database-backed
50
+ * sessions that have no file to tail.
51
+ */
52
+ export declare function classifySessionActivity(options: ClassifySessionActivityOptions): SessionActivityResult;
53
+ /**
54
+ * Refresh a cached classification from its unchanged fingerprint without
55
+ * re-reading the transcript. An active file remains active until it crosses
56
+ * the same five-minute staleness boundary used by detectSessionActivity,
57
+ * unless it was active only by grace period (`previousReason`).
58
+ */
59
+ export declare function refreshSessionActivityState(previous: 'active' | 'idle' | 'ended' | 'unknown', mtimeMs: number, nowMs?: number, previousReason?: string): 'active' | 'idle' | 'ended' | 'unknown';