sidekick-shared 0.24.5 → 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.
- package/CHANGELOG.md +35 -0
- package/README.md +159 -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 +151 -0
- package/dist/accountRegistry.d.ts +3 -1
- package/dist/accountRegistry.js +5 -0
- package/dist/browser.d.ts +8 -3
- package/dist/browser.js +15 -1
- package/dist/codexQuotaWatcher.d.ts +6 -0
- package/dist/codexQuotaWatcher.js +101 -47
- package/dist/index.d.ts +23 -13
- package/dist/index.js +34 -10
- 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 +4 -4
- package/dist/node.js +4 -1
- package/dist/observedSessionCollector.d.ts +75 -4
- package/dist/observedSessionCollector.js +452 -42
- package/dist/parsers/codexHistory.d.ts +4 -2
- package/dist/parsers/codexHistory.js +9 -2
- package/dist/parsers/sessionActivityDetector.d.ts +6 -0
- package/dist/parsers/sessionActivityDetector.js +17 -0
- 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 +204 -39
- package/dist/providers/codex.d.ts +18 -1
- package/dist/providers/codex.js +269 -40
- package/dist/providers/codexDatabase.d.ts +9 -0
- package/dist/providers/codexDatabase.js +107 -2
- package/dist/providers/diagnostics.d.ts +13 -0
- package/dist/providers/diagnostics.js +88 -0
- package/dist/providers/factory.d.ts +14 -0
- package/dist/providers/factory.js +57 -0
- package/dist/providers/openCode.d.ts +20 -1
- package/dist/providers/openCode.js +361 -11
- package/dist/providers/openCodeDatabase.d.ts +10 -0
- package/dist/providers/openCodeDatabase.js +69 -0
- package/dist/providers/types.d.ts +52 -2
- package/dist/providers/types.js +4 -0
- package/dist/quota.d.ts +4 -1
- package/dist/quota.js +3 -7
- package/dist/quotaHistory.d.ts +2 -1
- package/dist/quotaPoller.d.ts +13 -1
- package/dist/quotaPoller.js +65 -3
- 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 +1 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/observedSessionV1.d.ts +27 -26
- package/dist/schemas/observedSessionV1.js +6 -4
- package/dist/schemas/quota.d.ts +2 -2
- package/dist/schemas/quota.js +3 -6
- package/dist/schemas/quotaHistory.d.ts +2 -2
- package/dist/schemas/tokenEstimation.d.ts +1 -1
- package/dist/sessionMonitor.d.ts +15 -0
- package/dist/sessionMonitor.js +142 -0
- package/dist/sessionPreviews.d.ts +26 -1
- package/dist/sessionPreviews.js +319 -4
- package/dist/types/observedSessionV1.d.ts +6 -2
- package/dist/types/observedSessionV1.js +14 -8
- 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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
71
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
: (reference.fingerprintHint ?? null);
|
|
352
|
+
if (this.options.fingerprint)
|
|
353
|
+
value = await this.options.fingerprint(source, reference);
|
|
96
354
|
}
|
|
97
355
|
catch {
|
|
98
|
-
|
|
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({
|
|
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({
|
|
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 =
|
|
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
|
-
|
|
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
|
-
/**
|
|
166
|
-
function
|
|
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
|
|
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}`;
|
|
@@ -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();
|
|
@@ -29,3 +29,9 @@ export interface SessionActivityResult {
|
|
|
29
29
|
* @returns Activity state with classification reason
|
|
30
30
|
*/
|
|
31
31
|
export declare function detectSessionActivity(sessionPath: string): SessionActivityResult;
|
|
32
|
+
/**
|
|
33
|
+
* Refresh a cached classification from its unchanged fingerprint without
|
|
34
|
+
* re-reading the transcript. An active file remains active until it crosses
|
|
35
|
+
* the same five-minute staleness boundary used by detectSessionActivity.
|
|
36
|
+
*/
|
|
37
|
+
export declare function refreshSessionActivityState(previous: 'active' | 'idle' | 'ended' | 'unknown', mtimeMs: number, nowMs?: number): 'active' | 'idle' | 'ended' | 'unknown';
|
|
@@ -45,6 +45,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
45
|
})();
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
47
|
exports.detectSessionActivity = detectSessionActivity;
|
|
48
|
+
exports.refreshSessionActivityState = refreshSessionActivityState;
|
|
48
49
|
const fs = __importStar(require("fs"));
|
|
49
50
|
// ── Constants ──
|
|
50
51
|
/** How many bytes to read from the end of the file for analysis */
|
|
@@ -161,6 +162,22 @@ function detectSessionActivity(sessionPath) {
|
|
|
161
162
|
}
|
|
162
163
|
return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'no-activity-signal' };
|
|
163
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Refresh a cached classification from its unchanged fingerprint without
|
|
167
|
+
* re-reading the transcript. An active file remains active until it crosses
|
|
168
|
+
* the same five-minute staleness boundary used by detectSessionActivity.
|
|
169
|
+
*/
|
|
170
|
+
function refreshSessionActivityState(previous, mtimeMs, nowMs = Date.now()) {
|
|
171
|
+
if (!Number.isFinite(mtimeMs))
|
|
172
|
+
return previous;
|
|
173
|
+
if (nowMs - mtimeMs > STALENESS_THRESHOLD_MS)
|
|
174
|
+
return 'idle';
|
|
175
|
+
if (previous === 'ended')
|
|
176
|
+
return 'ended';
|
|
177
|
+
if (previous === 'idle')
|
|
178
|
+
return 'idle';
|
|
179
|
+
return previous === 'unknown' ? 'unknown' : 'active';
|
|
180
|
+
}
|
|
164
181
|
// ── Helpers ──
|
|
165
182
|
function readTail(filePath, fileSize) {
|
|
166
183
|
const bytesToRead = Math.min(TAIL_BYTES, fileSize);
|