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.
- package/CHANGELOG.md +92 -0
- package/README.md +205 -52
- package/dist/accountChangeSignal.d.ts +3 -0
- package/dist/accountChangeSignal.js +19 -0
- package/dist/accountChanges.d.ts +21 -0
- package/dist/accountChanges.js +170 -0
- package/dist/accountRegistry.d.ts +25 -2
- package/dist/accountRegistry.js +41 -13
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +19 -4
- package/dist/browser.js +43 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/codexQuotaWatcher.d.ts +6 -0
- package/dist/codexQuotaWatcher.js +101 -47
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +67 -23
- package/dist/index.js +138 -26
- package/dist/modelAliases.d.ts +13 -0
- package/dist/modelAliases.js +58 -0
- package/dist/modelCatalog.d.ts +32 -0
- package/dist/modelCatalog.js +145 -0
- package/dist/modelContext.d.ts +36 -3
- package/dist/modelContext.js +109 -34
- package/dist/modelInfo.d.ts +30 -1
- package/dist/modelInfo.js +116 -26
- package/dist/multiProviderQuotaService.d.ts +8 -0
- package/dist/multiProviderQuotaService.js +64 -3
- package/dist/node.d.ts +23 -4
- package/dist/node.js +50 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +72 -6
- package/dist/observedSessionCollector.js +440 -47
- package/dist/parsers/codexHistory.d.ts +4 -2
- package/dist/parsers/codexHistory.js +9 -2
- package/dist/parsers/sessionActivityDetector.d.ts +28 -0
- package/dist/parsers/sessionActivityDetector.js +110 -15
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providerIds.d.ts +11 -0
- package/dist/providerIds.js +9 -0
- package/dist/providerQuota.d.ts +3 -1
- package/dist/providerQuota.js +3 -0
- package/dist/providers/claudeCode.d.ts +10 -1
- package/dist/providers/claudeCode.js +211 -131
- package/dist/providers/codex.d.ts +20 -1
- package/dist/providers/codex.js +308 -244
- package/dist/providers/codexDatabase.d.ts +9 -0
- package/dist/providers/codexDatabase.js +107 -2
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/diagnostics.d.ts +13 -0
- package/dist/providers/diagnostics.js +88 -0
- package/dist/providers/factory.d.ts +20 -0
- package/dist/providers/factory.js +57 -0
- package/dist/providers/openCode.d.ts +22 -1
- package/dist/providers/openCode.js +460 -126
- package/dist/providers/openCodeDatabase.d.ts +10 -0
- package/dist/providers/openCodeDatabase.js +69 -0
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +82 -2
- package/dist/providers/types.js +4 -0
- package/dist/quota.d.ts +34 -4
- package/dist/quota.js +37 -9
- package/dist/quotaHistory.d.ts +3 -2
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaPoller.d.ts +13 -1
- package/dist/quotaPoller.js +65 -3
- package/dist/quotaResolver.d.ts +81 -0
- package/dist/quotaResolver.js +213 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/accountManager.d.ts +2 -2
- package/dist/schemas/accountManager.js +2 -4
- package/dist/schemas/accountStatus.d.ts +3 -0
- package/dist/schemas/accountStatus.js +1 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +16 -2
- package/dist/schemas/observedSessionV1.d.ts +27 -26
- package/dist/schemas/observedSessionV1.js +6 -4
- package/dist/schemas/quota.d.ts +141 -2
- package/dist/schemas/quota.js +20 -8
- package/dist/schemas/quotaHistory.d.ts +3 -2
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/tokenEstimation.d.ts +1 -1
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionMonitor.d.ts +15 -0
- package/dist/sessionMonitor.js +142 -0
- package/dist/sessionPreviews.d.ts +33 -2
- package/dist/sessionPreviews.js +333 -6
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +8 -2
- package/dist/types/observedSessionV1.js +88 -25
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- 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
|
-
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
71
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
: (reference.fingerprintHint ?? null);
|
|
353
|
+
if (this.options.fingerprint)
|
|
354
|
+
value = await this.options.fingerprint(source, reference);
|
|
96
355
|
}
|
|
97
356
|
catch {
|
|
98
|
-
|
|
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({
|
|
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({
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
return `${stat.size}:${stat.mtimeMs}`;
|
|
531
|
+
roots = provider.getWatchRoots?.() ?? [provider.getProjectsBaseDir()];
|
|
170
532
|
}
|
|
171
533
|
catch {
|
|
172
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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({
|
|
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';
|