sidekick-shared 0.25.0 → 0.26.1
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 +65 -0
- package/README.md +50 -3
- package/dist/accountChanges.js +22 -3
- package/dist/accountRegistry.d.ts +22 -1
- package/dist/accountRegistry.js +37 -14
- 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 +11 -1
- package/dist/browser.js +29 -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/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 +46 -12
- package/dist/index.js +112 -24
- package/dist/modelContext.js +8 -3
- package/dist/modelInfo.js +45 -27
- package/dist/node.d.ts +19 -0
- package/dist/node.js +47 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +3 -8
- package/dist/observedSessionCollector.js +10 -27
- package/dist/parsers/sessionActivityDetector.d.ts +24 -2
- package/dist/parsers/sessionActivityDetector.js +95 -17
- 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/providers/claudeCode.js +11 -96
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +94 -259
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/factory.d.ts +6 -0
- package/dist/providers/openCode.d.ts +2 -0
- package/dist/providers/openCode.js +105 -121
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +30 -0
- package/dist/quota.d.ts +30 -3
- package/dist/quota.js +35 -3
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaPresentation.js +17 -5
- package/dist/quotaResolver.d.ts +83 -0
- package/dist/quotaResolver.js +234 -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/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/quota.d.ts +139 -0
- package/dist/schemas/quota.js +17 -2
- package/dist/schemas/quotaHistory.d.ts +1 -0
- 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/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionPreviews.d.ts +7 -1
- package/dist/sessionPreviews.js +15 -3
- 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 +2 -0
- package/dist/types/observedSessionV1.js +75 -18
- 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 +1 -1
|
@@ -57,6 +57,7 @@ const os = __importStar(require("os"));
|
|
|
57
57
|
const path = __importStar(require("path"));
|
|
58
58
|
const child_process_1 = require("child_process");
|
|
59
59
|
const sessionContext_1 = require("../context/sessionContext");
|
|
60
|
+
const sessionStats_1 = require("../sessionStats");
|
|
60
61
|
const openCodeParser_1 = require("../parsers/openCodeParser");
|
|
61
62
|
const openCodeDatabase_1 = require("./openCodeDatabase");
|
|
62
63
|
const zaiQuota_1 = require("../zaiQuota");
|
|
@@ -170,16 +171,24 @@ function makeDbSessionPath(dataDir, projectId, sessionId) {
|
|
|
170
171
|
function isDbSessionPath(sessionPath) {
|
|
171
172
|
return sessionPath.includes(path.sep + DB_SESSION_PREFIX + path.sep);
|
|
172
173
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Whether a session recorded in `sessionDirectory` belongs to `workspacePath`.
|
|
176
|
+
*
|
|
177
|
+
* A session started inside the workspace (the workspace itself or any
|
|
178
|
+
* subdirectory, e.g. a monorepo package) matches. A session started in a
|
|
179
|
+
* *parent* of the workspace does not: a sub-package workspace must not list
|
|
180
|
+
* every session of the enclosing repo as its own.
|
|
181
|
+
*/
|
|
182
|
+
function workspaceMatches(sessionDirectory, workspacePath) {
|
|
183
|
+
const session = path
|
|
184
|
+
.resolve(sessionDirectory)
|
|
176
185
|
.replace(/[\\/]+$/, '')
|
|
177
186
|
.toLowerCase();
|
|
178
|
-
const
|
|
179
|
-
.resolve(
|
|
187
|
+
const workspace = path
|
|
188
|
+
.resolve(workspacePath)
|
|
180
189
|
.replace(/[\\/]+$/, '')
|
|
181
190
|
.toLowerCase();
|
|
182
|
-
return
|
|
191
|
+
return session === workspace || session.startsWith(workspace + path.sep);
|
|
183
192
|
}
|
|
184
193
|
/** Extract project ID from a synthetic DB session path. */
|
|
185
194
|
function extractProjectIdFromDbPath(sessionPath) {
|
|
@@ -322,18 +331,15 @@ function normalizePath(input) {
|
|
|
322
331
|
return path.resolve(input);
|
|
323
332
|
}
|
|
324
333
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
*
|
|
331
|
-
* Tracks which message IDs have been seen and reads new messages
|
|
332
|
-
* plus their parts on each readNew() call.
|
|
333
|
-
*/
|
|
334
|
+
function messageCreatedMs(message) {
|
|
335
|
+
const created = message.time?.created;
|
|
336
|
+
const ms = typeof created === 'number' ? created : Date.parse(String(created ?? ''));
|
|
337
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
338
|
+
}
|
|
334
339
|
class OpenCodeFileReader {
|
|
335
340
|
sessionId;
|
|
336
341
|
seenMessages = new Map();
|
|
342
|
+
pendingSkip = 0;
|
|
337
343
|
storageBase;
|
|
338
344
|
constructor(sessionId) {
|
|
339
345
|
this.sessionId = sessionId;
|
|
@@ -350,47 +356,47 @@ class OpenCodeFileReader {
|
|
|
350
356
|
catch {
|
|
351
357
|
return [];
|
|
352
358
|
}
|
|
353
|
-
const
|
|
359
|
+
const entries = [];
|
|
354
360
|
for (const file of messageFiles) {
|
|
355
361
|
const msgId = path.basename(file, '.json');
|
|
356
362
|
const messagePath = path.join(messageDir, file);
|
|
357
363
|
const message = readJsonSafe(messagePath);
|
|
358
364
|
if (!message)
|
|
359
365
|
continue;
|
|
360
|
-
let
|
|
366
|
+
let mtimeMs = 0;
|
|
361
367
|
try {
|
|
362
|
-
|
|
368
|
+
mtimeMs = fs.statSync(messagePath).mtimeMs;
|
|
363
369
|
}
|
|
364
370
|
catch {
|
|
365
|
-
|
|
371
|
+
mtimeMs = 0;
|
|
366
372
|
}
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
// Skip unreadable part directories
|
|
379
|
-
}
|
|
373
|
+
const parts = this.readParts(msgId);
|
|
374
|
+
entries.push({ msgId, message, mtimeMs, parts, partIds: new Set(parts.map((p) => p.id)) });
|
|
375
|
+
}
|
|
376
|
+
if (this.pendingSkip > 0) {
|
|
377
|
+
// A position for this reader is a message count (see getPosition()), so a
|
|
378
|
+
// seek marks the oldest N messages as already seen without emitting them.
|
|
379
|
+
const ordered = [...entries].sort((a, b) => messageCreatedMs(a.message) - messageCreatedMs(b.message) ||
|
|
380
|
+
a.msgId.localeCompare(b.msgId));
|
|
381
|
+
for (const entry of ordered.slice(0, this.pendingSkip)) {
|
|
382
|
+
this.seenMessages.set(entry.msgId, { partIds: entry.partIds, mtimeMs: entry.mtimeMs });
|
|
380
383
|
}
|
|
381
|
-
|
|
382
|
-
|
|
384
|
+
this.pendingSkip = 0;
|
|
385
|
+
}
|
|
386
|
+
const newEvents = [];
|
|
387
|
+
for (const entry of entries) {
|
|
388
|
+
const previous = this.seenMessages.get(entry.msgId);
|
|
383
389
|
const isNewMessage = !previous;
|
|
384
390
|
let hasNewParts = false;
|
|
385
391
|
if (previous) {
|
|
386
|
-
if (previous.mtimeMs !==
|
|
392
|
+
if (previous.mtimeMs !== entry.mtimeMs) {
|
|
387
393
|
hasNewParts = true;
|
|
388
394
|
}
|
|
389
|
-
else if (partIds.size !== previous.partIds.size) {
|
|
395
|
+
else if (entry.partIds.size !== previous.partIds.size) {
|
|
390
396
|
hasNewParts = true;
|
|
391
397
|
}
|
|
392
398
|
else {
|
|
393
|
-
for (const partId of partIds) {
|
|
399
|
+
for (const partId of entry.partIds) {
|
|
394
400
|
if (!previous.partIds.has(partId)) {
|
|
395
401
|
hasNewParts = true;
|
|
396
402
|
break;
|
|
@@ -399,19 +405,36 @@ class OpenCodeFileReader {
|
|
|
399
405
|
}
|
|
400
406
|
}
|
|
401
407
|
if (isNewMessage || hasNewParts) {
|
|
402
|
-
newEvents.push(...(0, openCodeParser_1.convertOpenCodeMessage)(message, parts));
|
|
403
|
-
this.seenMessages.set(msgId, { partIds, mtimeMs:
|
|
408
|
+
newEvents.push(...(0, openCodeParser_1.convertOpenCodeMessage)(entry.message, entry.parts));
|
|
409
|
+
this.seenMessages.set(entry.msgId, { partIds: entry.partIds, mtimeMs: entry.mtimeMs });
|
|
404
410
|
}
|
|
405
411
|
}
|
|
406
412
|
// Sort by timestamp
|
|
407
413
|
return newEvents.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
408
414
|
}
|
|
415
|
+
readParts(msgId) {
|
|
416
|
+
const partDir = path.join(this.storageBase, 'part', msgId);
|
|
417
|
+
if (!fs.existsSync(partDir))
|
|
418
|
+
return [];
|
|
419
|
+
try {
|
|
420
|
+
return fs
|
|
421
|
+
.readdirSync(partDir)
|
|
422
|
+
.filter((f) => f.endsWith('.json'))
|
|
423
|
+
.map((f) => readJsonSafe(path.join(partDir, f)))
|
|
424
|
+
.filter((p) => p !== null);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Skip unreadable part directories
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
409
431
|
readAll() {
|
|
410
432
|
this.reset();
|
|
411
433
|
return this.readNew();
|
|
412
434
|
}
|
|
413
435
|
reset() {
|
|
414
436
|
this.seenMessages.clear();
|
|
437
|
+
this.pendingSkip = 0;
|
|
415
438
|
}
|
|
416
439
|
exists() {
|
|
417
440
|
return fs.existsSync(path.join(this.storageBase, 'message', this.sessionId));
|
|
@@ -422,8 +445,10 @@ class OpenCodeFileReader {
|
|
|
422
445
|
getPosition() {
|
|
423
446
|
return this.seenMessages.size;
|
|
424
447
|
}
|
|
425
|
-
seekTo(
|
|
426
|
-
//
|
|
448
|
+
seekTo(position) {
|
|
449
|
+
// Positions are message counts (getPosition()), so the next readNew() marks
|
|
450
|
+
// the oldest `position` messages as seen and emits only what follows.
|
|
451
|
+
this.pendingSkip = Number.isFinite(position) && position > 0 ? Math.floor(position) : 0;
|
|
427
452
|
}
|
|
428
453
|
wasTruncated() {
|
|
429
454
|
return false; // JSON files don't truncate
|
|
@@ -438,11 +463,15 @@ class OpenCodeFileReader {
|
|
|
438
463
|
* Uses time_updated timestamps to track which data has been seen,
|
|
439
464
|
* querying only for newer messages/parts on each readNew() call.
|
|
440
465
|
*/
|
|
466
|
+
/** How long a database-backed reader trusts its last session-row check. */
|
|
467
|
+
const DB_EXISTS_MEMO_MS = 30_000;
|
|
441
468
|
class OpenCodeDbReader {
|
|
442
469
|
sessionId;
|
|
443
470
|
db;
|
|
444
471
|
lastTimeUpdated = 0;
|
|
445
472
|
hasReadOnce = false;
|
|
473
|
+
existsCheckedAtMs = 0;
|
|
474
|
+
existsResult = true;
|
|
446
475
|
constructor(sessionId, db) {
|
|
447
476
|
this.sessionId = sessionId;
|
|
448
477
|
this.db = db;
|
|
@@ -628,10 +657,22 @@ class OpenCodeDbReader {
|
|
|
628
657
|
this.hasReadOnce = false;
|
|
629
658
|
}
|
|
630
659
|
exists() {
|
|
631
|
-
// DB-backed sessions are durable rows rather than ephemeral files.
|
|
632
|
-
//
|
|
633
|
-
// SessionMonitor does not flap
|
|
634
|
-
|
|
660
|
+
// DB-backed sessions are durable rows rather than ephemeral files. The row
|
|
661
|
+
// check costs a sqlite3 subprocess, so it is memoised, and a transient
|
|
662
|
+
// query failure still reports "exists" so SessionMonitor does not flap
|
|
663
|
+
// into discovery mode.
|
|
664
|
+
const now = Date.now();
|
|
665
|
+
if (now - this.existsCheckedAtMs < DB_EXISTS_MEMO_MS)
|
|
666
|
+
return this.existsResult;
|
|
667
|
+
this.existsCheckedAtMs = now;
|
|
668
|
+
try {
|
|
669
|
+
const row = this.db.getSession(this.sessionId);
|
|
670
|
+
this.existsResult = row !== null || this.db.getRuntimeStatus().kind === 'query_failed';
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
this.existsResult = true;
|
|
674
|
+
}
|
|
675
|
+
return this.existsResult;
|
|
635
676
|
}
|
|
636
677
|
flush() {
|
|
637
678
|
// No-op for DB reading
|
|
@@ -1271,23 +1312,30 @@ class OpenCodeProvider {
|
|
|
1271
1312
|
const db = this.ensureDb();
|
|
1272
1313
|
return resolveProjectId(workspacePath, db, this.getRuntimeStatus()) || workspacePath;
|
|
1273
1314
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const sessionId = this.getSessionId(sessionPath);
|
|
1315
|
+
/** Session title from the metadata cache or the database; null without either. */
|
|
1316
|
+
extractSessionLabelFromMetadata(sessionPath) {
|
|
1277
1317
|
// Check metadata cache first
|
|
1278
1318
|
const cached = this.sessionMetaCache.get(sessionPath);
|
|
1279
1319
|
if (cached?.title) {
|
|
1280
1320
|
return truncateTitle(cached.title);
|
|
1281
1321
|
}
|
|
1282
1322
|
// DB lookup
|
|
1323
|
+
const db = this.ensureDb();
|
|
1283
1324
|
if (db) {
|
|
1284
|
-
const session = db.getSession(
|
|
1325
|
+
const session = db.getSession(this.getSessionId(sessionPath));
|
|
1285
1326
|
if (session?.title) {
|
|
1286
1327
|
this.recordDatabaseStatus('extractSessionLabel', true);
|
|
1287
1328
|
return truncateTitle(session.title);
|
|
1288
1329
|
}
|
|
1289
1330
|
}
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
extractSessionLabel(sessionPath) {
|
|
1334
|
+
const fromMetadata = this.extractSessionLabelFromMetadata(sessionPath);
|
|
1335
|
+
if (fromMetadata)
|
|
1336
|
+
return fromMetadata;
|
|
1290
1337
|
// File fallback
|
|
1338
|
+
const sessionId = this.getSessionId(sessionPath);
|
|
1291
1339
|
const label = this.extractSessionLabelFromFiles(sessionPath, sessionId);
|
|
1292
1340
|
this.recordDatabaseStatus('extractSessionLabel', label !== null || !isDbSessionPath(sessionPath));
|
|
1293
1341
|
return label;
|
|
@@ -1649,78 +1697,13 @@ class OpenCodeProvider {
|
|
|
1649
1697
|
}
|
|
1650
1698
|
// --- Stats ---
|
|
1651
1699
|
readSessionStats(sessionPath) {
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const toolUsage = {};
|
|
1660
|
-
let reportedCost = 0;
|
|
1661
|
-
if (db) {
|
|
1662
|
-
const messages = db.getMessagesForSession(sessionId);
|
|
1663
|
-
const parts = db.getPartsForSession(sessionId);
|
|
1664
|
-
for (const msg of messages) {
|
|
1665
|
-
try {
|
|
1666
|
-
const data = JSON.parse(msg.data);
|
|
1667
|
-
const role = data.role;
|
|
1668
|
-
if (role === 'assistant' || role === 'user')
|
|
1669
|
-
messageCount++;
|
|
1670
|
-
const msgTokens = data.tokens;
|
|
1671
|
-
const cache = msgTokens?.cache;
|
|
1672
|
-
if (msgTokens) {
|
|
1673
|
-
tokens.input += msgTokens.input || 0;
|
|
1674
|
-
tokens.output += msgTokens.output || 0;
|
|
1675
|
-
tokens.cacheRead += cache?.read || 0;
|
|
1676
|
-
tokens.cacheWrite += cache?.write || 0;
|
|
1677
|
-
}
|
|
1678
|
-
if (data.cost)
|
|
1679
|
-
reportedCost += data.cost || 0;
|
|
1680
|
-
const modelId = data.modelID || 'unknown';
|
|
1681
|
-
if (role === 'assistant' && msgTokens) {
|
|
1682
|
-
if (!modelUsage[modelId])
|
|
1683
|
-
modelUsage[modelId] = { calls: 0, tokens: 0 };
|
|
1684
|
-
modelUsage[modelId].calls++;
|
|
1685
|
-
modelUsage[modelId].tokens +=
|
|
1686
|
-
(msgTokens.input || 0) + (msgTokens.output || 0);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
catch {
|
|
1690
|
-
/* skip */
|
|
1691
|
-
}
|
|
1692
|
-
if (!startTime)
|
|
1693
|
-
startTime = new Date(msg.time_created).toISOString();
|
|
1694
|
-
endTime = new Date(msg.time_updated || msg.time_created).toISOString();
|
|
1695
|
-
}
|
|
1696
|
-
for (const part of parts) {
|
|
1697
|
-
try {
|
|
1698
|
-
const data = JSON.parse(part.data);
|
|
1699
|
-
if (data.type === 'tool' || data.type === 'tool-invocation') {
|
|
1700
|
-
const toolName = data.tool || 'unknown';
|
|
1701
|
-
toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
catch {
|
|
1705
|
-
/* skip */
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
return {
|
|
1710
|
-
providerId: 'opencode',
|
|
1711
|
-
sessionId,
|
|
1712
|
-
filePath: sessionPath,
|
|
1713
|
-
label: this.extractSessionLabel(sessionPath),
|
|
1714
|
-
startTime,
|
|
1715
|
-
endTime,
|
|
1716
|
-
messageCount,
|
|
1717
|
-
tokens,
|
|
1718
|
-
modelUsage,
|
|
1719
|
-
toolUsage,
|
|
1720
|
-
compactionEstimate: 0,
|
|
1721
|
-
truncationCount: 0,
|
|
1722
|
-
reportedCost,
|
|
1723
|
-
};
|
|
1700
|
+
// One reader pass (database-backed or file-backed) feeds the shared
|
|
1701
|
+
// aggregator, so compaction and truncation counts are real instead of the
|
|
1702
|
+
// hardcoded zeros the direct SQLite read returned, and a session that
|
|
1703
|
+
// cannot be read reports `availability: 'unavailable'` instead of zeros.
|
|
1704
|
+
return (0, sessionStats_1.readSessionFileStats)(this, sessionPath, {
|
|
1705
|
+
resolveLabel: () => this.extractSessionLabelFromMetadata(sessionPath),
|
|
1706
|
+
});
|
|
1724
1707
|
}
|
|
1725
1708
|
readSessionContextSnapshot(sessionPath, options = {}) {
|
|
1726
1709
|
return (0, sessionContext_1.readSessionContextSnapshot)(this, sessionPath, options);
|
|
@@ -1934,6 +1917,7 @@ class OpenCodeProvider {
|
|
|
1934
1917
|
this.db?.close();
|
|
1935
1918
|
this.db = null;
|
|
1936
1919
|
this.dbInitialized = false;
|
|
1920
|
+
this.dbStatus = { available: false, kind: 'db_missing' };
|
|
1937
1921
|
this.sessionMetaCache.clear();
|
|
1938
1922
|
this.dynamicContextWindowLimit = null;
|
|
1939
1923
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One capped walker for Codex rollout files.
|
|
3
|
+
*
|
|
4
|
+
* Codex writes `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`.
|
|
5
|
+
* Six recursive, uncapped walkers used to exist across the provider and the
|
|
6
|
+
* quota resolver; this module replaces them with a single walk that caps
|
|
7
|
+
* depth and file count, visits the newest-dated directories first so a
|
|
8
|
+
* `limit` can stop early, stats each file exactly once, and returns results
|
|
9
|
+
* newest first with the size and session id already attached.
|
|
10
|
+
*
|
|
11
|
+
* With `limit`, the early exit is by directory date: a rollout that was
|
|
12
|
+
* created days ago but is still being written lands in its creation day's
|
|
13
|
+
* directory, so a very small limit can miss it in favour of newer-dated,
|
|
14
|
+
* older-modified files. Callers that need exact mtime order over the whole
|
|
15
|
+
* history omit `limit` (the `maxFiles` cap still applies).
|
|
16
|
+
*
|
|
17
|
+
* @module providers/rolloutWalker
|
|
18
|
+
*/
|
|
19
|
+
export interface RolloutFileInfo {
|
|
20
|
+
path: string;
|
|
21
|
+
mtime: Date;
|
|
22
|
+
sizeBytes: number;
|
|
23
|
+
/** Session id parsed from the file name. */
|
|
24
|
+
sessionId: string;
|
|
25
|
+
}
|
|
26
|
+
export interface WalkRolloutFilesOptions {
|
|
27
|
+
/** Directory depth below each root to descend into (default 6; the dated tree is 3). */
|
|
28
|
+
maxDepth?: number;
|
|
29
|
+
/** Upper bound on files returned across all roots (default 20 000). */
|
|
30
|
+
maxFiles?: number;
|
|
31
|
+
/** Stop once this many files are collected, visiting newest-dated directories first. */
|
|
32
|
+
limit?: number;
|
|
33
|
+
/** Only files whose name carries this session id (case-insensitive). */
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
/** Include zero-byte files (default false). */
|
|
36
|
+
includeEmpty?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare const DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
|
|
39
|
+
export declare const DEFAULT_ROLLOUT_WALK_MAX_FILES = 20000;
|
|
40
|
+
/** Test if a filename is a Codex rollout file. */
|
|
41
|
+
export declare function isRolloutFile(filename: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Extract the session UUID from a rollout filename.
|
|
44
|
+
* Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractRolloutSessionId(filename: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Walk one or more `sessions` roots for rollout files, newest first.
|
|
49
|
+
* Missing or unreadable directories are skipped; paths are deduplicated.
|
|
50
|
+
*/
|
|
51
|
+
export declare function walkRolloutFiles(roots: readonly string[], options?: WalkRolloutFilesOptions): RolloutFileInfo[];
|
|
52
|
+
/** Async twin of {@link walkRolloutFiles} that yields between directories. */
|
|
53
|
+
export declare function walkRolloutFilesAsync(roots: readonly string[], options?: WalkRolloutFilesOptions): Promise<RolloutFileInfo[]>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* One capped walker for Codex rollout files.
|
|
4
|
+
*
|
|
5
|
+
* Codex writes `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`.
|
|
6
|
+
* Six recursive, uncapped walkers used to exist across the provider and the
|
|
7
|
+
* quota resolver; this module replaces them with a single walk that caps
|
|
8
|
+
* depth and file count, visits the newest-dated directories first so a
|
|
9
|
+
* `limit` can stop early, stats each file exactly once, and returns results
|
|
10
|
+
* newest first with the size and session id already attached.
|
|
11
|
+
*
|
|
12
|
+
* With `limit`, the early exit is by directory date: a rollout that was
|
|
13
|
+
* created days ago but is still being written lands in its creation day's
|
|
14
|
+
* directory, so a very small limit can miss it in favour of newer-dated,
|
|
15
|
+
* older-modified files. Callers that need exact mtime order over the whole
|
|
16
|
+
* history omit `limit` (the `maxFiles` cap still applies).
|
|
17
|
+
*
|
|
18
|
+
* @module providers/rolloutWalker
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.DEFAULT_ROLLOUT_WALK_MAX_FILES = exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH = void 0;
|
|
55
|
+
exports.isRolloutFile = isRolloutFile;
|
|
56
|
+
exports.extractRolloutSessionId = extractRolloutSessionId;
|
|
57
|
+
exports.walkRolloutFiles = walkRolloutFiles;
|
|
58
|
+
exports.walkRolloutFilesAsync = walkRolloutFilesAsync;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
|
|
62
|
+
exports.DEFAULT_ROLLOUT_WALK_MAX_FILES = 20_000;
|
|
63
|
+
/** Test if a filename is a Codex rollout file. */
|
|
64
|
+
function isRolloutFile(filename) {
|
|
65
|
+
return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Extract the session UUID from a rollout filename.
|
|
69
|
+
* Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
|
|
70
|
+
*/
|
|
71
|
+
function extractRolloutSessionId(filename) {
|
|
72
|
+
const base = path.basename(filename, '.jsonl');
|
|
73
|
+
// rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
|
|
74
|
+
const parts = base.split('-');
|
|
75
|
+
// The UUID is typically the last 5 segments (8-4-4-4-12)
|
|
76
|
+
if (parts.length >= 6) {
|
|
77
|
+
const possibleUuid = parts.slice(-5).join('-');
|
|
78
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(possibleUuid)) {
|
|
79
|
+
return possibleUuid;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Fallback: use everything after "rollout-"
|
|
83
|
+
return base.replace(/^rollout-/, '');
|
|
84
|
+
}
|
|
85
|
+
function walkState(options) {
|
|
86
|
+
const maxFiles = Math.max(0, options.maxFiles ?? exports.DEFAULT_ROLLOUT_WALK_MAX_FILES);
|
|
87
|
+
const limit = options.limit === undefined ? Infinity : Math.max(0, options.limit);
|
|
88
|
+
return {
|
|
89
|
+
results: [],
|
|
90
|
+
seen: new Set(),
|
|
91
|
+
cap: Math.min(maxFiles, limit),
|
|
92
|
+
maxDepth: options.maxDepth ?? exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH,
|
|
93
|
+
sessionId: options.sessionId ? options.sessionId.trim().toLowerCase() : null,
|
|
94
|
+
includeEmpty: options.includeEmpty ?? false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Newest-dated first: directory and file names sort descending. */
|
|
98
|
+
function byNameDescending(a, b) {
|
|
99
|
+
return b.name.localeCompare(a.name);
|
|
100
|
+
}
|
|
101
|
+
function matchesFilter(state, name) {
|
|
102
|
+
if (!isRolloutFile(name))
|
|
103
|
+
return false;
|
|
104
|
+
return (state.sessionId === null || extractRolloutSessionId(name).toLowerCase() === state.sessionId);
|
|
105
|
+
}
|
|
106
|
+
function record(state, fullPath, name, stat) {
|
|
107
|
+
if (!state.includeEmpty && stat.size <= 0)
|
|
108
|
+
return;
|
|
109
|
+
if (state.seen.has(fullPath))
|
|
110
|
+
return;
|
|
111
|
+
state.seen.add(fullPath);
|
|
112
|
+
state.results.push({
|
|
113
|
+
path: fullPath,
|
|
114
|
+
mtime: stat.mtime,
|
|
115
|
+
sizeBytes: stat.size,
|
|
116
|
+
sessionId: extractRolloutSessionId(name),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function finish(state) {
|
|
120
|
+
return state.results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
121
|
+
}
|
|
122
|
+
function visitSync(state, dir, depth) {
|
|
123
|
+
if (depth > state.maxDepth || state.results.length >= state.cap)
|
|
124
|
+
return;
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
entries.sort(byNameDescending);
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (state.results.length >= state.cap)
|
|
135
|
+
return;
|
|
136
|
+
if (!entry.isFile() || !matchesFilter(state, entry.name))
|
|
137
|
+
continue;
|
|
138
|
+
const fullPath = path.join(dir, entry.name);
|
|
139
|
+
try {
|
|
140
|
+
record(state, fullPath, entry.name, fs.statSync(fullPath));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Skip files that vanish between readdir and stat.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (state.results.length >= state.cap)
|
|
148
|
+
return;
|
|
149
|
+
if (entry.isDirectory())
|
|
150
|
+
visitSync(state, path.join(dir, entry.name), depth + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Walk one or more `sessions` roots for rollout files, newest first.
|
|
155
|
+
* Missing or unreadable directories are skipped; paths are deduplicated.
|
|
156
|
+
*/
|
|
157
|
+
function walkRolloutFiles(roots, options = {}) {
|
|
158
|
+
const state = walkState(options);
|
|
159
|
+
for (const root of roots)
|
|
160
|
+
visitSync(state, root, 0);
|
|
161
|
+
return finish(state);
|
|
162
|
+
}
|
|
163
|
+
function yieldToEventLoop() {
|
|
164
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
165
|
+
}
|
|
166
|
+
async function visitAsync(state, dir, depth) {
|
|
167
|
+
if (depth > state.maxDepth || state.results.length >= state.cap)
|
|
168
|
+
return;
|
|
169
|
+
let entries;
|
|
170
|
+
try {
|
|
171
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
entries.sort(byNameDescending);
|
|
177
|
+
let sinceYield = 0;
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (state.results.length >= state.cap)
|
|
180
|
+
return;
|
|
181
|
+
if (!entry.isFile() || !matchesFilter(state, entry.name))
|
|
182
|
+
continue;
|
|
183
|
+
const fullPath = path.join(dir, entry.name);
|
|
184
|
+
try {
|
|
185
|
+
record(state, fullPath, entry.name, await fs.promises.stat(fullPath));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Skip files that vanish between readdir and stat.
|
|
189
|
+
}
|
|
190
|
+
if (++sinceYield >= 200) {
|
|
191
|
+
sinceYield = 0;
|
|
192
|
+
await yieldToEventLoop();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
await yieldToEventLoop();
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (state.results.length >= state.cap)
|
|
198
|
+
return;
|
|
199
|
+
if (entry.isDirectory())
|
|
200
|
+
await visitAsync(state, path.join(dir, entry.name), depth + 1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Async twin of {@link walkRolloutFiles} that yields between directories. */
|
|
204
|
+
async function walkRolloutFilesAsync(roots, options = {}) {
|
|
205
|
+
const state = walkState(options);
|
|
206
|
+
for (const root of roots)
|
|
207
|
+
await visitAsync(state, root, 0);
|
|
208
|
+
return finish(state);
|
|
209
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
|
|
12
12
|
import type { SessionEvent, SubagentStats, TokenUsage, ContextAttribution } from '../types/sessionEvent';
|
|
13
13
|
import { SESSION_PROVIDER_IDS } from '../providerIds';
|
|
14
|
+
import type { AggregatedCostProvenance } from '../aggregation/types';
|
|
14
15
|
/** Stable runtime list for consumers that must enumerate session providers. */
|
|
15
16
|
export { SESSION_PROVIDER_IDS } from '../providerIds';
|
|
16
17
|
export type ProviderId = (typeof SESSION_PROVIDER_IDS)[number];
|
|
@@ -58,6 +59,12 @@ export interface SessionFileInfo {
|
|
|
58
59
|
/** Optional provider-recorded creation timestamp. */
|
|
59
60
|
createdAt?: Date;
|
|
60
61
|
}
|
|
62
|
+
/** How much of a session `readSessionStats()` could read. */
|
|
63
|
+
export type SessionFileStatsAvailability = 'full' | 'partial' | 'unavailable';
|
|
64
|
+
/**
|
|
65
|
+
* Aggregated stats for one session file, computed by `computeSessionFileStats()`
|
|
66
|
+
* over the shared `EventAggregator` for every provider.
|
|
67
|
+
*/
|
|
61
68
|
export interface SessionFileStats {
|
|
62
69
|
providerId: ProviderId;
|
|
63
70
|
sessionId: string;
|
|
@@ -66,19 +73,42 @@ export interface SessionFileStats {
|
|
|
66
73
|
startTime: string;
|
|
67
74
|
endTime: string;
|
|
68
75
|
messageCount: number;
|
|
76
|
+
/** Token buckets; `summarizeTokens()` turns them into the cache-inclusive total. */
|
|
69
77
|
tokens: {
|
|
70
78
|
input: number;
|
|
71
79
|
output: number;
|
|
72
80
|
cacheWrite: number;
|
|
73
81
|
cacheRead: number;
|
|
74
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Per-model calls, cache-inclusive token total (the `summarizeTokens().total`
|
|
85
|
+
* vocabulary), priced cost, and whether every call could be priced.
|
|
86
|
+
*/
|
|
75
87
|
modelUsage: Record<string, {
|
|
76
88
|
calls: number;
|
|
77
89
|
tokens: number;
|
|
90
|
+
costUsd: number;
|
|
91
|
+
priced: boolean;
|
|
78
92
|
}>;
|
|
93
|
+
/** Calls per tool name: successful, failed, and still pending. */
|
|
79
94
|
toolUsage: Record<string, number>;
|
|
95
|
+
/** Failed calls per tool name; a subset of `toolUsage`. */
|
|
96
|
+
toolFailures: Record<string, number>;
|
|
80
97
|
compactionEstimate: number;
|
|
81
98
|
truncationCount: number;
|
|
99
|
+
/** Cost of the priced portion of the session; read `costProvenance` before labelling it. */
|
|
100
|
+
costUsd: number;
|
|
101
|
+
costProvenance: AggregatedCostProvenance;
|
|
102
|
+
/** Usage events with neither a provider-reported cost nor a catalog price. */
|
|
103
|
+
unpricedCalls: number;
|
|
104
|
+
/**
|
|
105
|
+
* `full` when every event was read, `partial` when the reader reported a
|
|
106
|
+
* truncated source, `unavailable` when the session could not be read at all
|
|
107
|
+
* (`unavailableReason` says why and every number is zero).
|
|
108
|
+
*/
|
|
109
|
+
availability: SessionFileStatsAvailability;
|
|
110
|
+
unavailableReason?: string;
|
|
111
|
+
/** @deprecated Same value as `costUsd`; use it together with `costProvenance`. */
|
|
82
112
|
reportedCost: number;
|
|
83
113
|
}
|
|
84
114
|
export interface ProjectFolderInfo {
|