sidekick-shared 0.24.5 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +205 -52
- package/dist/accountChangeSignal.d.ts +3 -0
- package/dist/accountChangeSignal.js +19 -0
- package/dist/accountChanges.d.ts +21 -0
- package/dist/accountChanges.js +170 -0
- package/dist/accountRegistry.d.ts +25 -2
- package/dist/accountRegistry.js +41 -13
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +19 -4
- package/dist/browser.js +43 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/codexQuotaWatcher.d.ts +6 -0
- package/dist/codexQuotaWatcher.js +101 -47
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +67 -23
- package/dist/index.js +138 -26
- package/dist/modelAliases.d.ts +13 -0
- package/dist/modelAliases.js +58 -0
- package/dist/modelCatalog.d.ts +32 -0
- package/dist/modelCatalog.js +145 -0
- package/dist/modelContext.d.ts +36 -3
- package/dist/modelContext.js +109 -34
- package/dist/modelInfo.d.ts +30 -1
- package/dist/modelInfo.js +116 -26
- package/dist/multiProviderQuotaService.d.ts +8 -0
- package/dist/multiProviderQuotaService.js +64 -3
- package/dist/node.d.ts +23 -4
- package/dist/node.js +50 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +72 -6
- package/dist/observedSessionCollector.js +440 -47
- package/dist/parsers/codexHistory.d.ts +4 -2
- package/dist/parsers/codexHistory.js +9 -2
- package/dist/parsers/sessionActivityDetector.d.ts +28 -0
- package/dist/parsers/sessionActivityDetector.js +110 -15
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providerIds.d.ts +11 -0
- package/dist/providerIds.js +9 -0
- package/dist/providerQuota.d.ts +3 -1
- package/dist/providerQuota.js +3 -0
- package/dist/providers/claudeCode.d.ts +10 -1
- package/dist/providers/claudeCode.js +211 -131
- package/dist/providers/codex.d.ts +20 -1
- package/dist/providers/codex.js +308 -244
- package/dist/providers/codexDatabase.d.ts +9 -0
- package/dist/providers/codexDatabase.js +107 -2
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/diagnostics.d.ts +13 -0
- package/dist/providers/diagnostics.js +88 -0
- package/dist/providers/factory.d.ts +20 -0
- package/dist/providers/factory.js +57 -0
- package/dist/providers/openCode.d.ts +22 -1
- package/dist/providers/openCode.js +460 -126
- package/dist/providers/openCodeDatabase.d.ts +10 -0
- package/dist/providers/openCodeDatabase.js +69 -0
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +82 -2
- package/dist/providers/types.js +4 -0
- package/dist/quota.d.ts +34 -4
- package/dist/quota.js +37 -9
- package/dist/quotaHistory.d.ts +3 -2
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaPoller.d.ts +13 -1
- package/dist/quotaPoller.js +65 -3
- package/dist/quotaResolver.d.ts +81 -0
- package/dist/quotaResolver.js +213 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/accountManager.d.ts +2 -2
- package/dist/schemas/accountManager.js +2 -4
- package/dist/schemas/accountStatus.d.ts +3 -0
- package/dist/schemas/accountStatus.js +1 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +16 -2
- package/dist/schemas/observedSessionV1.d.ts +27 -26
- package/dist/schemas/observedSessionV1.js +6 -4
- package/dist/schemas/quota.d.ts +141 -2
- package/dist/schemas/quota.js +20 -8
- package/dist/schemas/quotaHistory.d.ts +3 -2
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/tokenEstimation.d.ts +1 -1
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionMonitor.d.ts +15 -0
- package/dist/sessionMonitor.js +142 -0
- package/dist/sessionPreviews.d.ts +33 -2
- package/dist/sessionPreviews.js +333 -6
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +8 -2
- package/dist/types/observedSessionV1.js +88 -25
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- package/package.json +22 -1
|
@@ -57,9 +57,11 @@ 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");
|
|
64
|
+
const diagnostics_1 = require("./diagnostics");
|
|
63
65
|
const modelContext_1 = require("../modelContext");
|
|
64
66
|
// ---------------------------------------------------------------------------
|
|
65
67
|
// Helper functions
|
|
@@ -169,6 +171,25 @@ function makeDbSessionPath(dataDir, projectId, sessionId) {
|
|
|
169
171
|
function isDbSessionPath(sessionPath) {
|
|
170
172
|
return sessionPath.includes(path.sep + DB_SESSION_PREFIX + path.sep);
|
|
171
173
|
}
|
|
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)
|
|
185
|
+
.replace(/[\\/]+$/, '')
|
|
186
|
+
.toLowerCase();
|
|
187
|
+
const workspace = path
|
|
188
|
+
.resolve(workspacePath)
|
|
189
|
+
.replace(/[\\/]+$/, '')
|
|
190
|
+
.toLowerCase();
|
|
191
|
+
return session === workspace || session.startsWith(workspace + path.sep);
|
|
192
|
+
}
|
|
172
193
|
/** Extract project ID from a synthetic DB session path. */
|
|
173
194
|
function extractProjectIdFromDbPath(sessionPath) {
|
|
174
195
|
const prefix = path.sep + DB_SESSION_PREFIX + path.sep;
|
|
@@ -310,18 +331,15 @@ function normalizePath(input) {
|
|
|
310
331
|
return path.resolve(input);
|
|
311
332
|
}
|
|
312
333
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
*
|
|
319
|
-
* Tracks which message IDs have been seen and reads new messages
|
|
320
|
-
* plus their parts on each readNew() call.
|
|
321
|
-
*/
|
|
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
|
+
}
|
|
322
339
|
class OpenCodeFileReader {
|
|
323
340
|
sessionId;
|
|
324
341
|
seenMessages = new Map();
|
|
342
|
+
pendingSkip = 0;
|
|
325
343
|
storageBase;
|
|
326
344
|
constructor(sessionId) {
|
|
327
345
|
this.sessionId = sessionId;
|
|
@@ -338,47 +356,47 @@ class OpenCodeFileReader {
|
|
|
338
356
|
catch {
|
|
339
357
|
return [];
|
|
340
358
|
}
|
|
341
|
-
const
|
|
359
|
+
const entries = [];
|
|
342
360
|
for (const file of messageFiles) {
|
|
343
361
|
const msgId = path.basename(file, '.json');
|
|
344
362
|
const messagePath = path.join(messageDir, file);
|
|
345
363
|
const message = readJsonSafe(messagePath);
|
|
346
364
|
if (!message)
|
|
347
365
|
continue;
|
|
348
|
-
let
|
|
366
|
+
let mtimeMs = 0;
|
|
349
367
|
try {
|
|
350
|
-
|
|
368
|
+
mtimeMs = fs.statSync(messagePath).mtimeMs;
|
|
351
369
|
}
|
|
352
370
|
catch {
|
|
353
|
-
|
|
371
|
+
mtimeMs = 0;
|
|
354
372
|
}
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
// Skip unreadable part directories
|
|
367
|
-
}
|
|
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 });
|
|
368
383
|
}
|
|
369
|
-
|
|
370
|
-
|
|
384
|
+
this.pendingSkip = 0;
|
|
385
|
+
}
|
|
386
|
+
const newEvents = [];
|
|
387
|
+
for (const entry of entries) {
|
|
388
|
+
const previous = this.seenMessages.get(entry.msgId);
|
|
371
389
|
const isNewMessage = !previous;
|
|
372
390
|
let hasNewParts = false;
|
|
373
391
|
if (previous) {
|
|
374
|
-
if (previous.mtimeMs !==
|
|
392
|
+
if (previous.mtimeMs !== entry.mtimeMs) {
|
|
375
393
|
hasNewParts = true;
|
|
376
394
|
}
|
|
377
|
-
else if (partIds.size !== previous.partIds.size) {
|
|
395
|
+
else if (entry.partIds.size !== previous.partIds.size) {
|
|
378
396
|
hasNewParts = true;
|
|
379
397
|
}
|
|
380
398
|
else {
|
|
381
|
-
for (const partId of partIds) {
|
|
399
|
+
for (const partId of entry.partIds) {
|
|
382
400
|
if (!previous.partIds.has(partId)) {
|
|
383
401
|
hasNewParts = true;
|
|
384
402
|
break;
|
|
@@ -387,19 +405,36 @@ class OpenCodeFileReader {
|
|
|
387
405
|
}
|
|
388
406
|
}
|
|
389
407
|
if (isNewMessage || hasNewParts) {
|
|
390
|
-
newEvents.push(...(0, openCodeParser_1.convertOpenCodeMessage)(message, parts));
|
|
391
|
-
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 });
|
|
392
410
|
}
|
|
393
411
|
}
|
|
394
412
|
// Sort by timestamp
|
|
395
413
|
return newEvents.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
396
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
|
+
}
|
|
397
431
|
readAll() {
|
|
398
432
|
this.reset();
|
|
399
433
|
return this.readNew();
|
|
400
434
|
}
|
|
401
435
|
reset() {
|
|
402
436
|
this.seenMessages.clear();
|
|
437
|
+
this.pendingSkip = 0;
|
|
403
438
|
}
|
|
404
439
|
exists() {
|
|
405
440
|
return fs.existsSync(path.join(this.storageBase, 'message', this.sessionId));
|
|
@@ -410,8 +445,10 @@ class OpenCodeFileReader {
|
|
|
410
445
|
getPosition() {
|
|
411
446
|
return this.seenMessages.size;
|
|
412
447
|
}
|
|
413
|
-
seekTo(
|
|
414
|
-
//
|
|
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;
|
|
415
452
|
}
|
|
416
453
|
wasTruncated() {
|
|
417
454
|
return false; // JSON files don't truncate
|
|
@@ -426,11 +463,15 @@ class OpenCodeFileReader {
|
|
|
426
463
|
* Uses time_updated timestamps to track which data has been seen,
|
|
427
464
|
* querying only for newer messages/parts on each readNew() call.
|
|
428
465
|
*/
|
|
466
|
+
/** How long a database-backed reader trusts its last session-row check. */
|
|
467
|
+
const DB_EXISTS_MEMO_MS = 30_000;
|
|
429
468
|
class OpenCodeDbReader {
|
|
430
469
|
sessionId;
|
|
431
470
|
db;
|
|
432
471
|
lastTimeUpdated = 0;
|
|
433
472
|
hasReadOnce = false;
|
|
473
|
+
existsCheckedAtMs = 0;
|
|
474
|
+
existsResult = true;
|
|
434
475
|
constructor(sessionId, db) {
|
|
435
476
|
this.sessionId = sessionId;
|
|
436
477
|
this.db = db;
|
|
@@ -616,10 +657,22 @@ class OpenCodeDbReader {
|
|
|
616
657
|
this.hasReadOnce = false;
|
|
617
658
|
}
|
|
618
659
|
exists() {
|
|
619
|
-
// DB-backed sessions are durable rows rather than ephemeral files.
|
|
620
|
-
//
|
|
621
|
-
// SessionMonitor does not flap
|
|
622
|
-
|
|
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;
|
|
623
676
|
}
|
|
624
677
|
flush() {
|
|
625
678
|
// No-op for DB reading
|
|
@@ -650,10 +703,14 @@ class OpenCodeProvider {
|
|
|
650
703
|
db = null;
|
|
651
704
|
dbInitialized = false;
|
|
652
705
|
dbStatus = { available: false, kind: 'db_missing' };
|
|
706
|
+
diagnostics;
|
|
653
707
|
/** Cache of session metadata populated during listing */
|
|
654
708
|
sessionMetaCache = new Map();
|
|
655
709
|
/** Runtime-reported context window limit (overrides static map when set). */
|
|
656
710
|
dynamicContextWindowLimit = null;
|
|
711
|
+
constructor(options = {}) {
|
|
712
|
+
this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
|
|
713
|
+
}
|
|
657
714
|
/** Lazy-initialize the database connection. */
|
|
658
715
|
ensureDb() {
|
|
659
716
|
if (this.dbInitialized)
|
|
@@ -667,12 +724,31 @@ class OpenCodeProvider {
|
|
|
667
724
|
this.dbStatus = db.getRuntimeStatus();
|
|
668
725
|
return this.db;
|
|
669
726
|
}
|
|
727
|
+
async ensureDbAsync() {
|
|
728
|
+
if (this.db)
|
|
729
|
+
return this.db;
|
|
730
|
+
return new openCodeDatabase_1.OpenCodeDatabase(getOpenCodeDataDir());
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Cache an async-created database only after an operation proved it usable.
|
|
734
|
+
* Sync paths treat a cached `db` as "open() succeeded", so adopting an
|
|
735
|
+
* unproven instance would cut them off from the file-storage fallback.
|
|
736
|
+
*/
|
|
737
|
+
adoptDbIfAvailable(db) {
|
|
738
|
+
if (this.db || !db.getRuntimeStatus().available)
|
|
739
|
+
return;
|
|
740
|
+
this.db = db;
|
|
741
|
+
this.dbInitialized = true;
|
|
742
|
+
}
|
|
670
743
|
getRuntimeStatus() {
|
|
671
744
|
const db = this.ensureDb();
|
|
672
745
|
if (db)
|
|
673
746
|
return db.getRuntimeStatus();
|
|
674
747
|
return this.dbStatus;
|
|
675
748
|
}
|
|
749
|
+
getLastOperationStatus() {
|
|
750
|
+
return this.diagnostics.getLastOperationStatus();
|
|
751
|
+
}
|
|
676
752
|
canMonitorDirectory(dir) {
|
|
677
753
|
if (fs.existsSync(dir))
|
|
678
754
|
return true;
|
|
@@ -738,8 +814,10 @@ class OpenCodeProvider {
|
|
|
738
814
|
const db = this.ensureDb();
|
|
739
815
|
const dbStatus = this.getRuntimeStatus();
|
|
740
816
|
const projectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
741
|
-
if (!projectId)
|
|
817
|
+
if (!projectId) {
|
|
818
|
+
this.recordDatabaseStatus('findActiveSession', dbStatus.kind === 'db_missing');
|
|
742
819
|
return null;
|
|
820
|
+
}
|
|
743
821
|
// DB primary
|
|
744
822
|
if (db) {
|
|
745
823
|
const session = db.getMostRecentSession(projectId);
|
|
@@ -749,14 +827,18 @@ class OpenCodeProvider {
|
|
|
749
827
|
title: session.title,
|
|
750
828
|
timeUpdated: session.time_updated,
|
|
751
829
|
});
|
|
830
|
+
this.recordDatabaseStatus('findActiveSession', true);
|
|
752
831
|
return syntheticPath;
|
|
753
832
|
}
|
|
754
833
|
}
|
|
755
834
|
if (dbStatus.kind !== 'db_missing') {
|
|
835
|
+
this.recordDatabaseStatus('findActiveSession', dbStatus.available);
|
|
756
836
|
return null;
|
|
757
837
|
}
|
|
758
838
|
// File fallback
|
|
759
|
-
|
|
839
|
+
const result = this.findActiveSessionFromFiles(projectId);
|
|
840
|
+
this.recordDatabaseStatus('findActiveSession', true);
|
|
841
|
+
return result;
|
|
760
842
|
}
|
|
761
843
|
findActiveSessionFromFiles(projectId) {
|
|
762
844
|
const sessionDir = path.join(getStorageDir(), 'session', projectId);
|
|
@@ -817,18 +899,64 @@ class OpenCodeProvider {
|
|
|
817
899
|
const db = this.ensureDb();
|
|
818
900
|
const dbStatus = this.getRuntimeStatus();
|
|
819
901
|
const projectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
820
|
-
if (!projectId)
|
|
902
|
+
if (!projectId) {
|
|
903
|
+
this.recordDatabaseStatus('findAllSessions', dbStatus.kind === 'db_missing');
|
|
821
904
|
return [];
|
|
905
|
+
}
|
|
822
906
|
// DB primary
|
|
823
907
|
if (db) {
|
|
824
|
-
|
|
908
|
+
const sessions = this.mapDbSessions(projectId);
|
|
909
|
+
this.recordDatabaseStatus('findAllSessions', true);
|
|
910
|
+
return sessions;
|
|
825
911
|
}
|
|
826
912
|
if (dbStatus.kind !== 'db_missing') {
|
|
913
|
+
this.recordDatabaseStatus('findAllSessions', dbStatus.available);
|
|
827
914
|
return [];
|
|
828
915
|
}
|
|
829
916
|
// File fallback
|
|
830
917
|
const sessionDir = path.join(getStorageDir(), 'session', projectId);
|
|
831
|
-
|
|
918
|
+
const sessions = this.findSessionsInDirectoryFromFiles(sessionDir);
|
|
919
|
+
this.recordDatabaseStatus('findAllSessions', true);
|
|
920
|
+
return sessions;
|
|
921
|
+
}
|
|
922
|
+
findSessionById(workspacePath, sessionId) {
|
|
923
|
+
const normalizedId = sessionId.trim();
|
|
924
|
+
if (!normalizedId || path.basename(normalizedId) !== normalizedId)
|
|
925
|
+
return null;
|
|
926
|
+
try {
|
|
927
|
+
const db = this.ensureDb();
|
|
928
|
+
if (db) {
|
|
929
|
+
const session = db.getSession(normalizedId);
|
|
930
|
+
if (session && workspaceMatches(session.directory, workspacePath)) {
|
|
931
|
+
const syntheticPath = makeDbSessionPath(getOpenCodeDataDir(), session.project_id, session.id);
|
|
932
|
+
this.sessionMetaCache.set(syntheticPath, {
|
|
933
|
+
title: session.title,
|
|
934
|
+
timeUpdated: session.time_updated,
|
|
935
|
+
});
|
|
936
|
+
this.recordDatabaseStatus('findSessionById', true);
|
|
937
|
+
return syntheticPath;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const projectId = resolveProjectId(workspacePath, db, this.getRuntimeStatus());
|
|
941
|
+
if (projectId) {
|
|
942
|
+
const candidate = path.join(getStorageDir(), 'session', projectId, `${normalizedId}.json`);
|
|
943
|
+
try {
|
|
944
|
+
if (fs.statSync(candidate).isFile()) {
|
|
945
|
+
this.recordDatabaseStatus('findSessionById', true);
|
|
946
|
+
return candidate;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
// Missing legacy session is a normal miss.
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
this.recordDatabaseStatus('findSessionById', true);
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
this.recordDatabaseStatus('findSessionById', false);
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
832
960
|
}
|
|
833
961
|
findSessionsInDirectory(dir) {
|
|
834
962
|
const db = this.ensureDb();
|
|
@@ -837,7 +965,9 @@ class OpenCodeProvider {
|
|
|
837
965
|
if (db && dir.includes(path.sep + DB_SESSION_PREFIX + path.sep)) {
|
|
838
966
|
const projectId = extractProjectIdFromDbPath(dir + path.sep + 'dummy.json');
|
|
839
967
|
if (projectId) {
|
|
840
|
-
|
|
968
|
+
const sessions = this.mapDbSessions(projectId);
|
|
969
|
+
this.recordDatabaseStatus('findSessionsInDirectory', true);
|
|
970
|
+
return sessions;
|
|
841
971
|
}
|
|
842
972
|
}
|
|
843
973
|
// For a DB-backed directory path ending with the project ID,
|
|
@@ -845,14 +975,19 @@ class OpenCodeProvider {
|
|
|
845
975
|
if (db) {
|
|
846
976
|
const dirName = path.basename(dir);
|
|
847
977
|
if (db.hasProject(dirName)) {
|
|
848
|
-
|
|
978
|
+
const sessions = this.mapDbSessions(dirName);
|
|
979
|
+
this.recordDatabaseStatus('findSessionsInDirectory', true);
|
|
980
|
+
return sessions;
|
|
849
981
|
}
|
|
850
982
|
}
|
|
851
983
|
if (dbStatus.kind !== 'db_missing') {
|
|
984
|
+
this.recordDatabaseStatus('findSessionsInDirectory', dbStatus.available);
|
|
852
985
|
return [];
|
|
853
986
|
}
|
|
854
987
|
// File fallback
|
|
855
|
-
|
|
988
|
+
const sessions = this.findSessionsInDirectoryFromFiles(dir);
|
|
989
|
+
this.recordDatabaseStatus('findSessionsInDirectory', true);
|
|
990
|
+
return sessions;
|
|
856
991
|
}
|
|
857
992
|
findSessionsInDirectoryFromFiles(dir) {
|
|
858
993
|
try {
|
|
@@ -879,6 +1014,159 @@ class OpenCodeProvider {
|
|
|
879
1014
|
return [];
|
|
880
1015
|
}
|
|
881
1016
|
}
|
|
1017
|
+
listAllSessionFiles() {
|
|
1018
|
+
try {
|
|
1019
|
+
const db = this.ensureDb();
|
|
1020
|
+
if (db) {
|
|
1021
|
+
const dataDir = getOpenCodeDataDir();
|
|
1022
|
+
const files = db.getAllSessions().map((session) => {
|
|
1023
|
+
const syntheticPath = makeDbSessionPath(dataDir, session.project_id, session.id);
|
|
1024
|
+
this.sessionMetaCache.set(syntheticPath, {
|
|
1025
|
+
title: session.title,
|
|
1026
|
+
timeUpdated: session.time_updated,
|
|
1027
|
+
});
|
|
1028
|
+
return {
|
|
1029
|
+
path: syntheticPath,
|
|
1030
|
+
mtime: new Date(session.time_updated),
|
|
1031
|
+
label: session.title ?? undefined,
|
|
1032
|
+
sizeBytes: 0,
|
|
1033
|
+
sessionId: session.id,
|
|
1034
|
+
workspacePath: session.directory,
|
|
1035
|
+
createdAt: new Date(session.time_created),
|
|
1036
|
+
};
|
|
1037
|
+
});
|
|
1038
|
+
this.recordDatabaseStatus('listAllSessionFiles', true);
|
|
1039
|
+
return files;
|
|
1040
|
+
}
|
|
1041
|
+
if (this.getRuntimeStatus().kind !== 'db_missing') {
|
|
1042
|
+
this.recordDatabaseStatus('listAllSessionFiles', true);
|
|
1043
|
+
return [];
|
|
1044
|
+
}
|
|
1045
|
+
const root = path.join(getStorageDir(), 'session');
|
|
1046
|
+
const results = [];
|
|
1047
|
+
let projectDirs = [];
|
|
1048
|
+
try {
|
|
1049
|
+
projectDirs = fs.readdirSync(root, { withFileTypes: true });
|
|
1050
|
+
}
|
|
1051
|
+
catch {
|
|
1052
|
+
this.recordDatabaseStatus('listAllSessionFiles', true);
|
|
1053
|
+
return [];
|
|
1054
|
+
}
|
|
1055
|
+
for (const entry of projectDirs) {
|
|
1056
|
+
if (!entry.isDirectory())
|
|
1057
|
+
continue;
|
|
1058
|
+
for (const sessionPath of this.findSessionsInDirectoryFromFiles(path.join(root, entry.name))) {
|
|
1059
|
+
try {
|
|
1060
|
+
const stats = fs.statSync(sessionPath);
|
|
1061
|
+
results.push({
|
|
1062
|
+
path: sessionPath,
|
|
1063
|
+
mtime: stats.mtime,
|
|
1064
|
+
sizeBytes: stats.size,
|
|
1065
|
+
sessionId: path.basename(sessionPath, '.json'),
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
// Session vanished between listing and stat.
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
this.recordDatabaseStatus('listAllSessionFiles', true);
|
|
1074
|
+
return results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
this.recordDatabaseStatus('listAllSessionFiles', false);
|
|
1078
|
+
return [];
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async listSessionFilesAsync(workspacePath) {
|
|
1082
|
+
const db = await this.ensureDbAsync();
|
|
1083
|
+
const sessions = await db.getAllSessionsAsync();
|
|
1084
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
1085
|
+
this.adoptDbIfAvailable(db);
|
|
1086
|
+
if (this.dbStatus.available) {
|
|
1087
|
+
const dataDir = getOpenCodeDataDir();
|
|
1088
|
+
const files = [];
|
|
1089
|
+
for (const session of sessions) {
|
|
1090
|
+
if (workspacePath && !workspaceMatches(session.directory, workspacePath))
|
|
1091
|
+
continue;
|
|
1092
|
+
const syntheticPath = makeDbSessionPath(dataDir, session.project_id, session.id);
|
|
1093
|
+
this.sessionMetaCache.set(syntheticPath, {
|
|
1094
|
+
title: session.title,
|
|
1095
|
+
timeUpdated: session.time_updated,
|
|
1096
|
+
});
|
|
1097
|
+
files.push({
|
|
1098
|
+
path: syntheticPath,
|
|
1099
|
+
mtime: new Date(session.time_updated),
|
|
1100
|
+
label: session.title ?? undefined,
|
|
1101
|
+
sizeBytes: 0,
|
|
1102
|
+
sessionId: session.id,
|
|
1103
|
+
workspacePath: session.directory,
|
|
1104
|
+
createdAt: new Date(session.time_created),
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
this.recordDatabaseStatus('listSessionFilesAsync', true);
|
|
1108
|
+
return files;
|
|
1109
|
+
}
|
|
1110
|
+
const root = path.join(getStorageDir(), 'session');
|
|
1111
|
+
const results = [];
|
|
1112
|
+
let projectDirs = [];
|
|
1113
|
+
try {
|
|
1114
|
+
projectDirs = await fs.promises.readdir(root, { withFileTypes: true });
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
this.recordDatabaseStatus('listSessionFilesAsync', true);
|
|
1118
|
+
return [];
|
|
1119
|
+
}
|
|
1120
|
+
for (const project of projectDirs) {
|
|
1121
|
+
if (!project.isDirectory())
|
|
1122
|
+
continue;
|
|
1123
|
+
let entries = [];
|
|
1124
|
+
try {
|
|
1125
|
+
entries = await fs.promises.readdir(path.join(root, project.name), {
|
|
1126
|
+
withFileTypes: true,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
for (const entry of entries) {
|
|
1133
|
+
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
1134
|
+
continue;
|
|
1135
|
+
const sessionPath = path.join(root, project.name, entry.name);
|
|
1136
|
+
try {
|
|
1137
|
+
const [stat, raw] = await Promise.all([
|
|
1138
|
+
fs.promises.stat(sessionPath),
|
|
1139
|
+
workspacePath ? fs.promises.readFile(sessionPath, 'utf8') : Promise.resolve(null),
|
|
1140
|
+
]);
|
|
1141
|
+
let session = null;
|
|
1142
|
+
if (raw !== null) {
|
|
1143
|
+
try {
|
|
1144
|
+
session = JSON.parse(raw);
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
const directory = session.directory;
|
|
1150
|
+
if (directory && !workspaceMatches(directory, workspacePath))
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
results.push({
|
|
1154
|
+
path: sessionPath,
|
|
1155
|
+
mtime: stat.mtime,
|
|
1156
|
+
sizeBytes: stat.size,
|
|
1157
|
+
sessionId: path.basename(entry.name, '.json'),
|
|
1158
|
+
workspacePath: session?.directory,
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
// Skip inaccessible sessions.
|
|
1163
|
+
}
|
|
1164
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
this.recordDatabaseStatus('listSessionFilesAsync', true);
|
|
1168
|
+
return results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
1169
|
+
}
|
|
882
1170
|
getAllProjectFolders(workspacePath) {
|
|
883
1171
|
const db = this.ensureDb();
|
|
884
1172
|
const folders = [];
|
|
@@ -917,14 +1205,19 @@ class OpenCodeProvider {
|
|
|
917
1205
|
}
|
|
918
1206
|
return b.lastModified.getTime() - a.lastModified.getTime();
|
|
919
1207
|
});
|
|
920
|
-
if (folders.length > 0)
|
|
1208
|
+
if (folders.length > 0) {
|
|
1209
|
+
this.recordDatabaseStatus('getAllProjectFolders', true);
|
|
921
1210
|
return folders;
|
|
1211
|
+
}
|
|
922
1212
|
}
|
|
923
1213
|
if (dbStatus.kind !== 'db_missing') {
|
|
1214
|
+
this.recordDatabaseStatus('getAllProjectFolders', dbStatus.available);
|
|
924
1215
|
return [];
|
|
925
1216
|
}
|
|
926
1217
|
// File fallback
|
|
927
|
-
|
|
1218
|
+
const fallback = this.getAllProjectFoldersFromFiles(workspacePath);
|
|
1219
|
+
this.recordDatabaseStatus('getAllProjectFolders', true);
|
|
1220
|
+
return fallback;
|
|
928
1221
|
}
|
|
929
1222
|
getAllProjectFoldersFromFiles(workspacePath) {
|
|
930
1223
|
const folders = [];
|
|
@@ -1019,23 +1312,79 @@ class OpenCodeProvider {
|
|
|
1019
1312
|
const db = this.ensureDb();
|
|
1020
1313
|
return resolveProjectId(workspacePath, db, this.getRuntimeStatus()) || workspacePath;
|
|
1021
1314
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const sessionId = this.getSessionId(sessionPath);
|
|
1315
|
+
/** Session title from the metadata cache or the database; null without either. */
|
|
1316
|
+
extractSessionLabelFromMetadata(sessionPath) {
|
|
1025
1317
|
// Check metadata cache first
|
|
1026
1318
|
const cached = this.sessionMetaCache.get(sessionPath);
|
|
1027
1319
|
if (cached?.title) {
|
|
1028
1320
|
return truncateTitle(cached.title);
|
|
1029
1321
|
}
|
|
1030
1322
|
// DB lookup
|
|
1323
|
+
const db = this.ensureDb();
|
|
1031
1324
|
if (db) {
|
|
1032
|
-
const session = db.getSession(
|
|
1325
|
+
const session = db.getSession(this.getSessionId(sessionPath));
|
|
1033
1326
|
if (session?.title) {
|
|
1327
|
+
this.recordDatabaseStatus('extractSessionLabel', true);
|
|
1034
1328
|
return truncateTitle(session.title);
|
|
1035
1329
|
}
|
|
1036
1330
|
}
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
extractSessionLabel(sessionPath) {
|
|
1334
|
+
const fromMetadata = this.extractSessionLabelFromMetadata(sessionPath);
|
|
1335
|
+
if (fromMetadata)
|
|
1336
|
+
return fromMetadata;
|
|
1037
1337
|
// File fallback
|
|
1038
|
-
|
|
1338
|
+
const sessionId = this.getSessionId(sessionPath);
|
|
1339
|
+
const label = this.extractSessionLabelFromFiles(sessionPath, sessionId);
|
|
1340
|
+
this.recordDatabaseStatus('extractSessionLabel', label !== null || !isDbSessionPath(sessionPath));
|
|
1341
|
+
return label;
|
|
1342
|
+
}
|
|
1343
|
+
async extractSessionLabelsAsync(sessionPaths) {
|
|
1344
|
+
const results = new Map();
|
|
1345
|
+
const unresolved = [];
|
|
1346
|
+
for (const sessionPath of sessionPaths) {
|
|
1347
|
+
const cached = this.sessionMetaCache.get(sessionPath);
|
|
1348
|
+
if (cached)
|
|
1349
|
+
results.set(sessionPath, cached.title ? truncateTitle(cached.title) : null);
|
|
1350
|
+
else
|
|
1351
|
+
unresolved.push(sessionPath);
|
|
1352
|
+
}
|
|
1353
|
+
if (unresolved.length > 0 && (!this.dbInitialized || this.dbStatus.available)) {
|
|
1354
|
+
const db = await this.ensureDbAsync();
|
|
1355
|
+
const byId = new Map(unresolved.map((item) => [this.getSessionId(item), item]));
|
|
1356
|
+
const sessions = await db.getSessionsByIdsAsync([...byId.keys()]);
|
|
1357
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
1358
|
+
this.adoptDbIfAvailable(db);
|
|
1359
|
+
for (const session of sessions) {
|
|
1360
|
+
const sessionPath = byId.get(session.id);
|
|
1361
|
+
if (!sessionPath)
|
|
1362
|
+
continue;
|
|
1363
|
+
this.sessionMetaCache.set(sessionPath, {
|
|
1364
|
+
title: session.title,
|
|
1365
|
+
timeUpdated: session.time_updated,
|
|
1366
|
+
});
|
|
1367
|
+
results.set(sessionPath, session.title ? truncateTitle(session.title) : null);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
for (const sessionPath of unresolved) {
|
|
1371
|
+
if (results.has(sessionPath))
|
|
1372
|
+
continue;
|
|
1373
|
+
if (isDbSessionPath(sessionPath)) {
|
|
1374
|
+
results.set(sessionPath, null);
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
try {
|
|
1378
|
+
const session = JSON.parse(await fs.promises.readFile(sessionPath, 'utf8'));
|
|
1379
|
+
results.set(sessionPath, session.title ? truncateTitle(session.title) : null);
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
results.set(sessionPath, null);
|
|
1383
|
+
}
|
|
1384
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1385
|
+
}
|
|
1386
|
+
this.recordDatabaseStatus('extractSessionLabelsAsync', true);
|
|
1387
|
+
return results;
|
|
1039
1388
|
}
|
|
1040
1389
|
extractSessionLabelFromFiles(sessionPath, sessionId) {
|
|
1041
1390
|
if (isDbSessionPath(sessionPath))
|
|
@@ -1343,80 +1692,18 @@ class OpenCodeProvider {
|
|
|
1343
1692
|
getProjectsBaseDir() {
|
|
1344
1693
|
return path.join(getStorageDir(), 'session');
|
|
1345
1694
|
}
|
|
1695
|
+
getWatchRoots() {
|
|
1696
|
+
return [getOpenCodeDataDir(), this.getProjectsBaseDir()];
|
|
1697
|
+
}
|
|
1346
1698
|
// --- Stats ---
|
|
1347
1699
|
readSessionStats(sessionPath) {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const toolUsage = {};
|
|
1356
|
-
let reportedCost = 0;
|
|
1357
|
-
if (db) {
|
|
1358
|
-
const messages = db.getMessagesForSession(sessionId);
|
|
1359
|
-
const parts = db.getPartsForSession(sessionId);
|
|
1360
|
-
for (const msg of messages) {
|
|
1361
|
-
try {
|
|
1362
|
-
const data = JSON.parse(msg.data);
|
|
1363
|
-
const role = data.role;
|
|
1364
|
-
if (role === 'assistant' || role === 'user')
|
|
1365
|
-
messageCount++;
|
|
1366
|
-
const msgTokens = data.tokens;
|
|
1367
|
-
const cache = msgTokens?.cache;
|
|
1368
|
-
if (msgTokens) {
|
|
1369
|
-
tokens.input += msgTokens.input || 0;
|
|
1370
|
-
tokens.output += msgTokens.output || 0;
|
|
1371
|
-
tokens.cacheRead += cache?.read || 0;
|
|
1372
|
-
tokens.cacheWrite += cache?.write || 0;
|
|
1373
|
-
}
|
|
1374
|
-
if (data.cost)
|
|
1375
|
-
reportedCost += data.cost || 0;
|
|
1376
|
-
const modelId = data.modelID || 'unknown';
|
|
1377
|
-
if (role === 'assistant' && msgTokens) {
|
|
1378
|
-
if (!modelUsage[modelId])
|
|
1379
|
-
modelUsage[modelId] = { calls: 0, tokens: 0 };
|
|
1380
|
-
modelUsage[modelId].calls++;
|
|
1381
|
-
modelUsage[modelId].tokens +=
|
|
1382
|
-
(msgTokens.input || 0) + (msgTokens.output || 0);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
catch {
|
|
1386
|
-
/* skip */
|
|
1387
|
-
}
|
|
1388
|
-
if (!startTime)
|
|
1389
|
-
startTime = new Date(msg.time_created).toISOString();
|
|
1390
|
-
endTime = new Date(msg.time_updated || msg.time_created).toISOString();
|
|
1391
|
-
}
|
|
1392
|
-
for (const part of parts) {
|
|
1393
|
-
try {
|
|
1394
|
-
const data = JSON.parse(part.data);
|
|
1395
|
-
if (data.type === 'tool' || data.type === 'tool-invocation') {
|
|
1396
|
-
const toolName = data.tool || 'unknown';
|
|
1397
|
-
toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
catch {
|
|
1401
|
-
/* skip */
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
return {
|
|
1406
|
-
providerId: 'opencode',
|
|
1407
|
-
sessionId,
|
|
1408
|
-
filePath: sessionPath,
|
|
1409
|
-
label: this.extractSessionLabel(sessionPath),
|
|
1410
|
-
startTime,
|
|
1411
|
-
endTime,
|
|
1412
|
-
messageCount,
|
|
1413
|
-
tokens,
|
|
1414
|
-
modelUsage,
|
|
1415
|
-
toolUsage,
|
|
1416
|
-
compactionEstimate: 0,
|
|
1417
|
-
truncationCount: 0,
|
|
1418
|
-
reportedCost,
|
|
1419
|
-
};
|
|
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
|
+
});
|
|
1420
1707
|
}
|
|
1421
1708
|
readSessionContextSnapshot(sessionPath, options = {}) {
|
|
1422
1709
|
return (0, sessionContext_1.readSessionContextSnapshot)(this, sessionPath, options);
|
|
@@ -1453,6 +1740,37 @@ class OpenCodeProvider {
|
|
|
1453
1740
|
}
|
|
1454
1741
|
return null;
|
|
1455
1742
|
}
|
|
1743
|
+
async getSessionMetadataAsync(sessionPath) {
|
|
1744
|
+
if (!isDbSessionPath(sessionPath)) {
|
|
1745
|
+
try {
|
|
1746
|
+
return { mtime: (await fs.promises.stat(sessionPath)).mtime };
|
|
1747
|
+
}
|
|
1748
|
+
catch {
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const cached = this.sessionMetaCache.get(sessionPath);
|
|
1753
|
+
try {
|
|
1754
|
+
const db = await this.ensureDbAsync();
|
|
1755
|
+
const sessionId = this.getSessionId(sessionPath);
|
|
1756
|
+
const session = (await db.getSessionsByIdsAsync([sessionId]))[0];
|
|
1757
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
1758
|
+
this.adoptDbIfAvailable(db);
|
|
1759
|
+
if (session) {
|
|
1760
|
+
this.sessionMetaCache.set(sessionPath, {
|
|
1761
|
+
title: session.title,
|
|
1762
|
+
timeUpdated: session.time_updated,
|
|
1763
|
+
});
|
|
1764
|
+
this.recordDatabaseStatus('getSessionMetadataAsync', true);
|
|
1765
|
+
return { mtime: new Date(session.time_updated) };
|
|
1766
|
+
}
|
|
1767
|
+
this.recordDatabaseStatus('getSessionMetadataAsync', cached !== undefined);
|
|
1768
|
+
}
|
|
1769
|
+
catch {
|
|
1770
|
+
this.recordDatabaseStatus('getSessionMetadataAsync', cached !== undefined);
|
|
1771
|
+
}
|
|
1772
|
+
return cached ? { mtime: new Date(cached.timeUpdated) } : null;
|
|
1773
|
+
}
|
|
1456
1774
|
/**
|
|
1457
1775
|
* Returns the latest assistant message's token snapshot for context window sizing.
|
|
1458
1776
|
* Cumulative session totals are derived from readNew() events processed by SessionMonitor.
|
|
@@ -1599,9 +1917,25 @@ class OpenCodeProvider {
|
|
|
1599
1917
|
this.db?.close();
|
|
1600
1918
|
this.db = null;
|
|
1601
1919
|
this.dbInitialized = false;
|
|
1920
|
+
this.dbStatus = { available: false, kind: 'db_missing' };
|
|
1602
1921
|
this.sessionMetaCache.clear();
|
|
1603
1922
|
this.dynamicContextWindowLimit = null;
|
|
1604
1923
|
}
|
|
1924
|
+
recordDatabaseStatus(operation, usable) {
|
|
1925
|
+
const status = this.db?.getRuntimeStatus() ?? this.dbStatus;
|
|
1926
|
+
const diagnostic = (0, diagnostics_1.diagnosticFromRuntimeStatus)(this.id, 'query', status);
|
|
1927
|
+
if (!diagnostic || status.kind === 'db_missing') {
|
|
1928
|
+
// A genuinely absent OpenCode DB is expected for legacy file storage.
|
|
1929
|
+
this.diagnostics.available(operation);
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
this.diagnostics.degraded(operation, status, {
|
|
1933
|
+
kind: diagnostic.kind,
|
|
1934
|
+
severity: diagnostic.severity,
|
|
1935
|
+
phase: diagnostic.phase,
|
|
1936
|
+
message: diagnostic.message,
|
|
1937
|
+
}, usable);
|
|
1938
|
+
}
|
|
1605
1939
|
// --- z.ai (GLM Coding Plan) compatibility quota derivation ---
|
|
1606
1940
|
/**
|
|
1607
1941
|
* Derives a z.ai coding-plan `QuotaState` from OpenCode assistant turns
|