sidekick-shared 0.25.0 → 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 +57 -0
- package/README.md +49 -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/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/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/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
package/dist/providers/detect.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
};
|
|
39
39
|
})();
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports._resetProviderDetectionCache = _resetProviderDetectionCache;
|
|
41
42
|
exports.getAllDetectedProviders = getAllDetectedProviders;
|
|
42
43
|
exports.detectProvider = detectProvider;
|
|
43
44
|
const fs = __importStar(require("fs"));
|
|
@@ -169,11 +170,25 @@ function getProviderPaths() {
|
|
|
169
170
|
codexHomes,
|
|
170
171
|
};
|
|
171
172
|
}
|
|
173
|
+
/** Ranked detection results are reused within a process for this long. */
|
|
174
|
+
const DETECTION_CACHE_TTL_MS = 5_000;
|
|
175
|
+
let detectionCache = null;
|
|
176
|
+
/** Forget the memoised ranking. Test-only. */
|
|
177
|
+
function _resetProviderDetectionCache() {
|
|
178
|
+
detectionCache = null;
|
|
179
|
+
}
|
|
172
180
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
181
|
+
* Providers with data on disk, most recently active first.
|
|
182
|
+
*
|
|
183
|
+
* Detection walks every provider's session corpus (a recursive stat over
|
|
184
|
+
* `~/.claude/projects`, every Codex home, and the OpenCode data dir), so the
|
|
185
|
+
* ranking is memoised briefly: the CLI resolves the provider several times per
|
|
186
|
+
* command and the extension asks again on every settings-driven re-detect.
|
|
175
187
|
*/
|
|
176
|
-
function
|
|
188
|
+
function rankDetectedProviders(now = Date.now()) {
|
|
189
|
+
if (detectionCache && now - detectionCache.at < DETECTION_CACHE_TTL_MS) {
|
|
190
|
+
return detectionCache.ranked;
|
|
191
|
+
}
|
|
177
192
|
const { claudeBase, openCodeDbPath, openCodeStorageDir, codexHomes } = getProviderPaths();
|
|
178
193
|
const hasClaude = fs.existsSync(claudeBase);
|
|
179
194
|
const hasOpenCode = fs.existsSync(openCodeStorageDir) || fs.existsSync(openCodeDbPath);
|
|
@@ -186,7 +201,16 @@ function getAllDetectedProviders() {
|
|
|
186
201
|
if (hasCodex)
|
|
187
202
|
available.push({ id: 'codex', mtime: getCodexActivityMtime() });
|
|
188
203
|
available.sort((a, b) => b.mtime - a.mtime);
|
|
189
|
-
|
|
204
|
+
const ranked = available.map((a) => a.id);
|
|
205
|
+
detectionCache = { at: now, ranked };
|
|
206
|
+
return ranked;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Returns all provider IDs whose data directories exist on the filesystem.
|
|
210
|
+
* Ordered by most-recent activity first.
|
|
211
|
+
*/
|
|
212
|
+
function getAllDetectedProviders() {
|
|
213
|
+
return [...rankDetectedProviders()];
|
|
190
214
|
}
|
|
191
215
|
/**
|
|
192
216
|
* Detects the most appropriate provider based on filesystem presence and recency.
|
|
@@ -195,24 +219,5 @@ function getAllDetectedProviders() {
|
|
|
195
219
|
function detectProvider(override) {
|
|
196
220
|
if (override && override !== 'auto')
|
|
197
221
|
return override;
|
|
198
|
-
|
|
199
|
-
const hasClaude = fs.existsSync(claudeBase);
|
|
200
|
-
const hasOpenCode = fs.existsSync(openCodeStorageDir) || fs.existsSync(openCodeDbPath);
|
|
201
|
-
const hasCodex = codexHomes.some((codexHome) => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
|
|
202
|
-
const available = [];
|
|
203
|
-
if (hasClaude) {
|
|
204
|
-
available.push({ id: 'claude-code', mtime: getClaudeActivityMtime(claudeBase) });
|
|
205
|
-
}
|
|
206
|
-
if (hasOpenCode) {
|
|
207
|
-
available.push({ id: 'opencode', mtime: getOpenCodeActivityMtime() });
|
|
208
|
-
}
|
|
209
|
-
if (hasCodex) {
|
|
210
|
-
available.push({ id: 'codex', mtime: getCodexActivityMtime() });
|
|
211
|
-
}
|
|
212
|
-
if (available.length === 0)
|
|
213
|
-
return 'claude-code';
|
|
214
|
-
if (available.length === 1)
|
|
215
|
-
return available[0].id;
|
|
216
|
-
available.sort((a, b) => b.mtime - a.mtime);
|
|
217
|
-
return available[0].id;
|
|
222
|
+
return rankDetectedProviders()[0] ?? 'claude-code';
|
|
218
223
|
}
|
|
@@ -5,6 +5,12 @@ export interface CreateSessionProvidersOptions {
|
|
|
5
5
|
}
|
|
6
6
|
export interface CreateSessionProvidersResult {
|
|
7
7
|
providers: SessionProviderBase[];
|
|
8
|
+
/**
|
|
9
|
+
* Live, not a snapshot: the providers keep the emit closure and update
|
|
10
|
+
* entries in place (one per provider/kind/phase) as they run, so this array
|
|
11
|
+
* reflects the latest diagnostics whenever it is read. Copy it to freeze a
|
|
12
|
+
* point in time; use `onDiagnostic` to observe each emission.
|
|
13
|
+
*/
|
|
8
14
|
diagnostics: SessionProviderDiagnostic[];
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
@@ -78,6 +78,8 @@ export declare class OpenCodeProvider implements SessionProviderBase {
|
|
|
78
78
|
isSessionFile(filename: string): boolean;
|
|
79
79
|
getSessionId(sessionPath: string): string;
|
|
80
80
|
encodeWorkspacePath(workspacePath: string): string;
|
|
81
|
+
/** Session title from the metadata cache or the database; null without either. */
|
|
82
|
+
private extractSessionLabelFromMetadata;
|
|
81
83
|
extractSessionLabel(sessionPath: string): string | null;
|
|
82
84
|
extractSessionLabelsAsync(sessionPaths: readonly string[]): Promise<Map<string, string | null>>;
|
|
83
85
|
private extractSessionLabelFromFiles;
|
|
@@ -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[]>;
|