sidekick-shared 0.24.5 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +159 -52
- package/dist/accountChangeSignal.d.ts +3 -0
- package/dist/accountChangeSignal.js +19 -0
- package/dist/accountChanges.d.ts +21 -0
- package/dist/accountChanges.js +151 -0
- package/dist/accountRegistry.d.ts +3 -1
- package/dist/accountRegistry.js +5 -0
- package/dist/browser.d.ts +8 -3
- package/dist/browser.js +15 -1
- package/dist/codexQuotaWatcher.d.ts +6 -0
- package/dist/codexQuotaWatcher.js +101 -47
- package/dist/index.d.ts +23 -13
- package/dist/index.js +34 -10
- package/dist/modelAliases.d.ts +13 -0
- package/dist/modelAliases.js +58 -0
- package/dist/modelCatalog.d.ts +32 -0
- package/dist/modelCatalog.js +145 -0
- package/dist/modelContext.d.ts +36 -3
- package/dist/modelContext.js +109 -34
- package/dist/modelInfo.d.ts +30 -1
- package/dist/modelInfo.js +116 -26
- package/dist/multiProviderQuotaService.d.ts +8 -0
- package/dist/multiProviderQuotaService.js +64 -3
- package/dist/node.d.ts +4 -4
- package/dist/node.js +4 -1
- package/dist/observedSessionCollector.d.ts +75 -4
- package/dist/observedSessionCollector.js +452 -42
- package/dist/parsers/codexHistory.d.ts +4 -2
- package/dist/parsers/codexHistory.js +9 -2
- package/dist/parsers/sessionActivityDetector.d.ts +6 -0
- package/dist/parsers/sessionActivityDetector.js +17 -0
- package/dist/providerIds.d.ts +11 -0
- package/dist/providerIds.js +9 -0
- package/dist/providerQuota.d.ts +3 -1
- package/dist/providerQuota.js +3 -0
- package/dist/providers/claudeCode.d.ts +10 -1
- package/dist/providers/claudeCode.js +204 -39
- package/dist/providers/codex.d.ts +18 -1
- package/dist/providers/codex.js +269 -40
- package/dist/providers/codexDatabase.d.ts +9 -0
- package/dist/providers/codexDatabase.js +107 -2
- package/dist/providers/diagnostics.d.ts +13 -0
- package/dist/providers/diagnostics.js +88 -0
- package/dist/providers/factory.d.ts +14 -0
- package/dist/providers/factory.js +57 -0
- package/dist/providers/openCode.d.ts +20 -1
- package/dist/providers/openCode.js +361 -11
- package/dist/providers/openCodeDatabase.d.ts +10 -0
- package/dist/providers/openCodeDatabase.js +69 -0
- package/dist/providers/types.d.ts +52 -2
- package/dist/providers/types.js +4 -0
- package/dist/quota.d.ts +4 -1
- package/dist/quota.js +3 -7
- package/dist/quotaHistory.d.ts +2 -1
- package/dist/quotaPoller.d.ts +13 -1
- package/dist/quotaPoller.js +65 -3
- package/dist/schemas/accountManager.d.ts +2 -2
- package/dist/schemas/accountManager.js +2 -4
- package/dist/schemas/accountStatus.d.ts +3 -0
- package/dist/schemas/accountStatus.js +1 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/observedSessionV1.d.ts +27 -26
- package/dist/schemas/observedSessionV1.js +6 -4
- package/dist/schemas/quota.d.ts +2 -2
- package/dist/schemas/quota.js +3 -6
- package/dist/schemas/quotaHistory.d.ts +2 -2
- package/dist/schemas/tokenEstimation.d.ts +1 -1
- package/dist/sessionMonitor.d.ts +15 -0
- package/dist/sessionMonitor.js +142 -0
- package/dist/sessionPreviews.d.ts +26 -1
- package/dist/sessionPreviews.js +319 -4
- package/dist/types/observedSessionV1.d.ts +6 -2
- package/dist/types/observedSessionV1.js +14 -8
- package/package.json +22 -1
package/dist/providers/codex.js
CHANGED
|
@@ -53,6 +53,7 @@ const sessionContext_1 = require("../context/sessionContext");
|
|
|
53
53
|
const codexParser_1 = require("../parsers/codexParser");
|
|
54
54
|
const codexDatabase_1 = require("./codexDatabase");
|
|
55
55
|
const codexProfiles_1 = require("../codexProfiles");
|
|
56
|
+
const diagnostics_1 = require("./diagnostics");
|
|
56
57
|
const modelContext_1 = require("../modelContext");
|
|
57
58
|
const observedContextWindows_1 = require("../observedContextWindows");
|
|
58
59
|
const usageNormalization_1 = require("../usageNormalization");
|
|
@@ -165,6 +166,40 @@ function findRolloutFilesInConfiguredHomes() {
|
|
|
165
166
|
}
|
|
166
167
|
return results;
|
|
167
168
|
}
|
|
169
|
+
async function findRolloutFilesAsync(dir) {
|
|
170
|
+
const results = [];
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const fullPath = path.join(dir, entry.name);
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
results.push(...(await findRolloutFilesAsync(fullPath)));
|
|
182
|
+
}
|
|
183
|
+
else if (entry.isFile() && isRolloutFile(entry.name)) {
|
|
184
|
+
try {
|
|
185
|
+
const stats = await fs.promises.stat(fullPath);
|
|
186
|
+
if (stats.size > 0) {
|
|
187
|
+
results.push({
|
|
188
|
+
path: fullPath,
|
|
189
|
+
mtime: stats.mtime,
|
|
190
|
+
sizeBytes: stats.size,
|
|
191
|
+
sessionId: extractSessionId(entry.name),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Skip inaccessible files.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
200
|
+
}
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
168
203
|
/**
|
|
169
204
|
* Locates the rollout file for a Codex session id by walking
|
|
170
205
|
* `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`.
|
|
@@ -298,6 +333,41 @@ function readSessionMeta(rolloutPath) {
|
|
|
298
333
|
}
|
|
299
334
|
return null;
|
|
300
335
|
}
|
|
336
|
+
async function readSessionMetaAsync(rolloutPath) {
|
|
337
|
+
// Mirrors readSessionMeta()/readFirstLines(): chunk-read until the first
|
|
338
|
+
// line is complete under the same 256 KiB cap — session_meta carries
|
|
339
|
+
// `instructions` and can far exceed a single chunk.
|
|
340
|
+
const maxBytes = 256 * 1024;
|
|
341
|
+
const chunkSize = 64 * 1024;
|
|
342
|
+
let handle = null;
|
|
343
|
+
try {
|
|
344
|
+
handle = await fs.promises.open(rolloutPath, 'r');
|
|
345
|
+
const chunks = [];
|
|
346
|
+
let totalRead = 0;
|
|
347
|
+
let newlineFound = false;
|
|
348
|
+
while (totalRead < maxBytes && !newlineFound) {
|
|
349
|
+
const buffer = Buffer.alloc(Math.min(chunkSize, maxBytes - totalRead));
|
|
350
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, totalRead);
|
|
351
|
+
if (bytesRead === 0)
|
|
352
|
+
break;
|
|
353
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
354
|
+
chunks.push(chunk);
|
|
355
|
+
totalRead += bytesRead;
|
|
356
|
+
newlineFound = chunk.includes(0x0a);
|
|
357
|
+
}
|
|
358
|
+
const firstLine = Buffer.concat(chunks).toString('utf8').split('\n', 1)[0]?.trim();
|
|
359
|
+
if (!firstLine)
|
|
360
|
+
return null;
|
|
361
|
+
const parsed = JSON.parse(firstLine);
|
|
362
|
+
return parsed.type === 'session_meta' ? parsed.payload : null;
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
await handle?.close().catch(() => undefined);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
301
371
|
/** Read the CWD from a rollout file's session_meta. Cached. */
|
|
302
372
|
const cwdCache = new Map();
|
|
303
373
|
function readSessionCwd(rolloutPath) {
|
|
@@ -309,6 +379,16 @@ function readSessionCwd(rolloutPath) {
|
|
|
309
379
|
cwdCache.set(rolloutPath, cwd);
|
|
310
380
|
return cwd;
|
|
311
381
|
}
|
|
382
|
+
/** Async twin of readSessionCwd, sharing its cache so repeat listings do no content reads. */
|
|
383
|
+
async function readSessionCwdAsync(rolloutPath) {
|
|
384
|
+
const cached = cwdCache.get(rolloutPath);
|
|
385
|
+
if (cached !== undefined)
|
|
386
|
+
return cached;
|
|
387
|
+
const meta = await readSessionMetaAsync(rolloutPath);
|
|
388
|
+
const cwd = meta?.cwd || null;
|
|
389
|
+
cwdCache.set(rolloutPath, cwd);
|
|
390
|
+
return cwd;
|
|
391
|
+
}
|
|
312
392
|
/** Check if a session's CWD matches a workspace path. */
|
|
313
393
|
function cwdMatches(sessionCwd, workspacePath) {
|
|
314
394
|
const normalizedSession = normalizePath(sessionCwd);
|
|
@@ -333,48 +413,65 @@ function normalizePath(input) {
|
|
|
333
413
|
*/
|
|
334
414
|
function extractFirstUserMessage(rolloutPath) {
|
|
335
415
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
416
|
+
return extractFirstUserMessageFromLines(readFirstLines(rolloutPath, 20));
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function extractFirstUserMessageFromLines(lines) {
|
|
423
|
+
for (const line of lines) {
|
|
424
|
+
const trimmed = line.trim();
|
|
425
|
+
if (!trimmed)
|
|
426
|
+
continue;
|
|
427
|
+
try {
|
|
428
|
+
const parsed = JSON.parse(trimmed);
|
|
429
|
+
// Check response_item with user message
|
|
430
|
+
if (parsed.type === 'response_item') {
|
|
431
|
+
const payload = parsed.payload;
|
|
432
|
+
if (payload.role === 'user') {
|
|
433
|
+
const content = payload.content;
|
|
434
|
+
if (typeof content === 'string' && content.trim() && !isSystemInjection(content)) {
|
|
435
|
+
return truncate(content, 60);
|
|
436
|
+
}
|
|
437
|
+
if (Array.isArray(content)) {
|
|
438
|
+
for (const part of content) {
|
|
439
|
+
const p = part;
|
|
440
|
+
if (p.text?.trim() && !isSystemInjection(p.text)) {
|
|
441
|
+
return truncate(p.text, 60);
|
|
357
442
|
}
|
|
358
443
|
}
|
|
359
444
|
}
|
|
360
445
|
}
|
|
361
|
-
// Check event_msg with user_message
|
|
362
|
-
if (parsed.type === 'event_msg') {
|
|
363
|
-
const payload = parsed.payload;
|
|
364
|
-
if (payload.type === 'user_message' && payload.message?.trim()) {
|
|
365
|
-
return truncate(payload.message, 60);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
446
|
}
|
|
369
|
-
|
|
370
|
-
|
|
447
|
+
// Check event_msg with user_message
|
|
448
|
+
if (parsed.type === 'event_msg') {
|
|
449
|
+
const payload = parsed.payload;
|
|
450
|
+
if (payload.type === 'user_message' && payload.message?.trim()) {
|
|
451
|
+
return truncate(payload.message, 60);
|
|
452
|
+
}
|
|
371
453
|
}
|
|
372
454
|
}
|
|
373
|
-
|
|
455
|
+
catch {
|
|
456
|
+
// Skip malformed lines
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
async function extractFirstUserMessageAsync(rolloutPath) {
|
|
462
|
+
let handle = null;
|
|
463
|
+
try {
|
|
464
|
+
handle = await fs.promises.open(rolloutPath, 'r');
|
|
465
|
+
const buffer = Buffer.alloc(256 * 1024);
|
|
466
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
467
|
+
return extractFirstUserMessageFromLines(buffer.toString('utf8', 0, bytesRead).split('\n').slice(0, 20));
|
|
374
468
|
}
|
|
375
469
|
catch {
|
|
376
470
|
return null;
|
|
377
471
|
}
|
|
472
|
+
finally {
|
|
473
|
+
await handle?.close().catch(() => undefined);
|
|
474
|
+
}
|
|
378
475
|
}
|
|
379
476
|
/** Extract searchable text from a rollout line. */
|
|
380
477
|
function extractSearchableText(line) {
|
|
@@ -661,9 +758,14 @@ class CodexProvider {
|
|
|
661
758
|
displayName = 'Codex CLI';
|
|
662
759
|
db = null;
|
|
663
760
|
dbInitialized = false;
|
|
761
|
+
dbStatus = { available: false, kind: 'db_missing' };
|
|
762
|
+
diagnostics;
|
|
664
763
|
dynamicContextWindowLimit = null;
|
|
665
764
|
lastRateLimitsData = null;
|
|
666
765
|
lastTokenUsageData = null;
|
|
766
|
+
constructor(options = {}) {
|
|
767
|
+
this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
|
|
768
|
+
}
|
|
667
769
|
// --- Database ---
|
|
668
770
|
/** Lazy-initialize database connection. */
|
|
669
771
|
ensureDb() {
|
|
@@ -675,8 +777,34 @@ class CodexProvider {
|
|
|
675
777
|
if (db.isAvailable() && db.open()) {
|
|
676
778
|
this.db = db;
|
|
677
779
|
}
|
|
780
|
+
if (typeof db.getRuntimeStatus === 'function') {
|
|
781
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
782
|
+
}
|
|
678
783
|
return this.db;
|
|
679
784
|
}
|
|
785
|
+
async ensureDbAsync() {
|
|
786
|
+
if (this.db)
|
|
787
|
+
return this.db;
|
|
788
|
+
return new codexDatabase_1.CodexDatabase(getCodexHome());
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Cache an async-created database only after an operation proved it usable.
|
|
792
|
+
* Sync paths treat a cached `db` as "open() succeeded", so adopting an
|
|
793
|
+
* unproven instance would silently disable their file fallbacks.
|
|
794
|
+
*/
|
|
795
|
+
adoptDbIfAvailable(db) {
|
|
796
|
+
if (this.db || !db.getRuntimeStatus().available)
|
|
797
|
+
return;
|
|
798
|
+
this.db = db;
|
|
799
|
+
this.dbInitialized = true;
|
|
800
|
+
}
|
|
801
|
+
getRuntimeStatus() {
|
|
802
|
+
const db = this.ensureDb();
|
|
803
|
+
return db && typeof db.getRuntimeStatus === 'function' ? db.getRuntimeStatus() : this.dbStatus;
|
|
804
|
+
}
|
|
805
|
+
getLastOperationStatus() {
|
|
806
|
+
return this.diagnostics.getLastOperationStatus();
|
|
807
|
+
}
|
|
680
808
|
// --- Path resolution ---
|
|
681
809
|
getSessionDirectory(workspacePath) {
|
|
682
810
|
// Codex doesn't use workspace-encoded directories like Claude Code.
|
|
@@ -686,10 +814,13 @@ class CodexProvider {
|
|
|
686
814
|
if (db) {
|
|
687
815
|
const thread = db.getMostRecentThread(workspacePath);
|
|
688
816
|
if (thread?.rollout_path) {
|
|
817
|
+
this.recordDatabaseFallback('getSessionDirectory', true);
|
|
689
818
|
return path.dirname(thread.rollout_path);
|
|
690
819
|
}
|
|
691
820
|
}
|
|
692
|
-
|
|
821
|
+
const directory = getSessionsDir();
|
|
822
|
+
this.recordDatabaseFallback('getSessionDirectory', true);
|
|
823
|
+
return directory;
|
|
693
824
|
}
|
|
694
825
|
discoverSessionDirectory(workspacePath) {
|
|
695
826
|
// DB: find latest rollout directory for this workspace
|
|
@@ -697,23 +828,29 @@ class CodexProvider {
|
|
|
697
828
|
if (db) {
|
|
698
829
|
const thread = db.getMostRecentThread(workspacePath);
|
|
699
830
|
if (thread?.rollout_path && fs.existsSync(thread.rollout_path)) {
|
|
831
|
+
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
700
832
|
return path.dirname(thread.rollout_path);
|
|
701
833
|
}
|
|
702
834
|
}
|
|
703
835
|
// File scan: look for any rollout file whose session_meta.cwd matches
|
|
704
836
|
const files = findRolloutFilesInConfiguredHomes();
|
|
705
|
-
if (files.length === 0)
|
|
837
|
+
if (files.length === 0) {
|
|
838
|
+
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
706
839
|
return null;
|
|
840
|
+
}
|
|
707
841
|
// Sort by mtime descending
|
|
708
842
|
files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
709
843
|
for (const file of files) {
|
|
710
844
|
const meta = readSessionMeta(file.path);
|
|
711
845
|
if (meta && cwdMatches(meta.cwd, workspacePath)) {
|
|
846
|
+
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
712
847
|
return path.dirname(file.path);
|
|
713
848
|
}
|
|
714
849
|
}
|
|
715
850
|
// Fallback: return the most relevant base sessions dir if one exists
|
|
716
|
-
|
|
851
|
+
const directory = getSessionsDir();
|
|
852
|
+
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
853
|
+
return directory;
|
|
717
854
|
}
|
|
718
855
|
// --- Session discovery ---
|
|
719
856
|
findActiveSession(workspacePath) {
|
|
@@ -722,6 +859,7 @@ class CodexProvider {
|
|
|
722
859
|
if (db) {
|
|
723
860
|
const thread = db.getMostRecentThread(workspacePath);
|
|
724
861
|
if (thread?.rollout_path && fs.existsSync(thread.rollout_path)) {
|
|
862
|
+
this.recordDatabaseFallback('findActiveSession', true);
|
|
725
863
|
return thread.rollout_path;
|
|
726
864
|
}
|
|
727
865
|
}
|
|
@@ -731,9 +869,11 @@ class CodexProvider {
|
|
|
731
869
|
for (const file of files) {
|
|
732
870
|
const meta = readSessionMeta(file.path);
|
|
733
871
|
if (meta && cwdMatches(meta.cwd, workspacePath)) {
|
|
872
|
+
this.recordDatabaseFallback('findActiveSession', true);
|
|
734
873
|
return file.path;
|
|
735
874
|
}
|
|
736
875
|
}
|
|
876
|
+
this.recordDatabaseFallback('findActiveSession', true);
|
|
737
877
|
return null;
|
|
738
878
|
}
|
|
739
879
|
findAllSessions(workspacePath) {
|
|
@@ -744,18 +884,33 @@ class CodexProvider {
|
|
|
744
884
|
const dbPaths = threads
|
|
745
885
|
.filter((t) => t.rollout_path && fs.existsSync(t.rollout_path))
|
|
746
886
|
.map((t) => t.rollout_path);
|
|
747
|
-
if (dbPaths.length > 0)
|
|
887
|
+
if (dbPaths.length > 0) {
|
|
888
|
+
this.recordDatabaseFallback('findAllSessions', true);
|
|
748
889
|
return dbPaths;
|
|
890
|
+
}
|
|
749
891
|
}
|
|
750
892
|
// File scan
|
|
751
893
|
const files = findRolloutFilesInConfiguredHomes();
|
|
752
894
|
files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
753
|
-
|
|
895
|
+
const sessions = files
|
|
754
896
|
.filter((f) => {
|
|
755
897
|
const meta = readSessionMeta(f.path);
|
|
756
898
|
return meta && cwdMatches(meta.cwd, workspacePath);
|
|
757
899
|
})
|
|
758
900
|
.map((f) => f.path);
|
|
901
|
+
this.recordDatabaseFallback('findAllSessions', true);
|
|
902
|
+
return sessions;
|
|
903
|
+
}
|
|
904
|
+
findSessionById(_workspacePath, sessionId) {
|
|
905
|
+
try {
|
|
906
|
+
const found = findCodexRolloutFile(sessionId);
|
|
907
|
+
this.recordDatabaseFallback('findSessionById', true);
|
|
908
|
+
return found;
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
this.recordDatabaseFallback('findSessionById', false);
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
759
914
|
}
|
|
760
915
|
/** Backward-compatible alias for findAllSessions. */
|
|
761
916
|
findSessionFiles(workspacePath) {
|
|
@@ -767,7 +922,28 @@ class CodexProvider {
|
|
|
767
922
|
return files.map((f) => f.path);
|
|
768
923
|
}
|
|
769
924
|
listAllSessionFiles() {
|
|
770
|
-
|
|
925
|
+
const files = findRolloutFilesInConfiguredHomes();
|
|
926
|
+
this.recordDatabaseFallback('listAllSessionFiles', true);
|
|
927
|
+
return files;
|
|
928
|
+
}
|
|
929
|
+
async listSessionFilesAsync(workspacePath) {
|
|
930
|
+
const seen = new Set();
|
|
931
|
+
const results = [];
|
|
932
|
+
for (const sessionsDir of getSessionsDirs()) {
|
|
933
|
+
for (const file of await findRolloutFilesAsync(sessionsDir)) {
|
|
934
|
+
if (seen.has(file.path))
|
|
935
|
+
continue;
|
|
936
|
+
seen.add(file.path);
|
|
937
|
+
if (workspacePath) {
|
|
938
|
+
const cwd = await readSessionCwdAsync(file.path);
|
|
939
|
+
if (!cwd || !cwdMatches(cwd, workspacePath))
|
|
940
|
+
continue;
|
|
941
|
+
file.workspacePath = cwd;
|
|
942
|
+
}
|
|
943
|
+
results.push(file);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
771
947
|
}
|
|
772
948
|
getAllProjectFolders(workspacePath) {
|
|
773
949
|
const folders = [];
|
|
@@ -823,6 +999,7 @@ class CodexProvider {
|
|
|
823
999
|
}
|
|
824
1000
|
return b.lastModified.getTime() - a.lastModified.getTime();
|
|
825
1001
|
});
|
|
1002
|
+
this.recordDatabaseFallback('getAllProjectFolders', true);
|
|
826
1003
|
return folders;
|
|
827
1004
|
}
|
|
828
1005
|
// --- File identification ---
|
|
@@ -842,13 +1019,43 @@ class CodexProvider {
|
|
|
842
1019
|
if (db) {
|
|
843
1020
|
const sessionId = this.getSessionId(sessionPath);
|
|
844
1021
|
const thread = db.getThread(sessionId);
|
|
845
|
-
if (thread?.title)
|
|
1022
|
+
if (thread?.title) {
|
|
1023
|
+
this.recordDatabaseFallback('extractSessionLabel', true);
|
|
846
1024
|
return truncate(thread.title, 60);
|
|
847
|
-
|
|
1025
|
+
}
|
|
1026
|
+
if (thread?.first_user_message) {
|
|
1027
|
+
this.recordDatabaseFallback('extractSessionLabel', true);
|
|
848
1028
|
return truncate(thread.first_user_message, 60);
|
|
1029
|
+
}
|
|
849
1030
|
}
|
|
850
1031
|
// File fallback: parse first user message
|
|
851
|
-
|
|
1032
|
+
const label = extractFirstUserMessage(sessionPath);
|
|
1033
|
+
this.recordDatabaseFallback('extractSessionLabel', true);
|
|
1034
|
+
return label;
|
|
1035
|
+
}
|
|
1036
|
+
async extractSessionLabelsAsync(sessionPaths) {
|
|
1037
|
+
const results = new Map();
|
|
1038
|
+
const byId = new Map(sessionPaths.map((sessionPath) => [this.getSessionId(sessionPath), sessionPath]));
|
|
1039
|
+
const db = await this.ensureDbAsync();
|
|
1040
|
+
const threads = await db.getThreadsByIdsAsync([...byId.keys()]);
|
|
1041
|
+
for (const thread of threads) {
|
|
1042
|
+
const sessionPath = byId.get(thread.id);
|
|
1043
|
+
if (!sessionPath)
|
|
1044
|
+
continue;
|
|
1045
|
+
const label = thread.title ?? thread.first_user_message;
|
|
1046
|
+
if (label)
|
|
1047
|
+
results.set(sessionPath, truncate(label, 60));
|
|
1048
|
+
}
|
|
1049
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
1050
|
+
this.adoptDbIfAvailable(db);
|
|
1051
|
+
for (const sessionPath of sessionPaths) {
|
|
1052
|
+
if (!results.has(sessionPath)) {
|
|
1053
|
+
results.set(sessionPath, await extractFirstUserMessageAsync(sessionPath));
|
|
1054
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
this.recordDatabaseFallback('extractSessionLabelsAsync', true);
|
|
1058
|
+
return results;
|
|
852
1059
|
}
|
|
853
1060
|
// --- Data reading ---
|
|
854
1061
|
createReader(sessionPath) {
|
|
@@ -1015,6 +1222,9 @@ class CodexProvider {
|
|
|
1015
1222
|
getProjectsBaseDir() {
|
|
1016
1223
|
return getSessionsDir();
|
|
1017
1224
|
}
|
|
1225
|
+
getWatchRoots() {
|
|
1226
|
+
return getSessionsDirs();
|
|
1227
|
+
}
|
|
1018
1228
|
// --- Stats ---
|
|
1019
1229
|
readSessionStats(sessionPath) {
|
|
1020
1230
|
const sessionId = extractSessionId(path.basename(sessionPath));
|
|
@@ -1094,9 +1304,28 @@ class CodexProvider {
|
|
|
1094
1304
|
this.db?.close();
|
|
1095
1305
|
this.db = null;
|
|
1096
1306
|
this.dbInitialized = false;
|
|
1307
|
+
this.dbStatus = { available: false, kind: 'db_missing' };
|
|
1097
1308
|
this.dynamicContextWindowLimit = null;
|
|
1098
1309
|
this.lastRateLimitsData = null;
|
|
1099
1310
|
this.lastTokenUsageData = null;
|
|
1100
1311
|
}
|
|
1312
|
+
recordDatabaseFallback(operation, usable) {
|
|
1313
|
+
if (!this.dbInitialized) {
|
|
1314
|
+
this.diagnostics.available(operation);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const status = this.getRuntimeStatus();
|
|
1318
|
+
const diagnostic = (0, diagnostics_1.diagnosticFromRuntimeStatus)(this.id, 'query', status);
|
|
1319
|
+
if (!diagnostic) {
|
|
1320
|
+
this.diagnostics.available(operation);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
this.diagnostics.degraded(operation, status, {
|
|
1324
|
+
kind: diagnostic.kind,
|
|
1325
|
+
severity: diagnostic.severity,
|
|
1326
|
+
phase: diagnostic.phase,
|
|
1327
|
+
message: diagnostic.message,
|
|
1328
|
+
}, usable);
|
|
1329
|
+
}
|
|
1101
1330
|
}
|
|
1102
1331
|
exports.CodexProvider = CodexProvider;
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
* Ported from sidekick-vscode/src/services/providers/CodexDatabase.ts
|
|
4
4
|
*/
|
|
5
5
|
import type { CodexDbThread } from '../types/codex';
|
|
6
|
+
import type { ProviderRuntimeStatus } from './types';
|
|
6
7
|
export declare class CodexDatabase {
|
|
7
8
|
private readonly dbPath;
|
|
8
9
|
private sqlite3Available;
|
|
10
|
+
private runtimeStatus;
|
|
9
11
|
constructor(codexHome: string);
|
|
10
12
|
isAvailable(): boolean;
|
|
11
13
|
open(): boolean;
|
|
14
|
+
getRuntimeStatus(): ProviderRuntimeStatus;
|
|
12
15
|
close(): void;
|
|
13
16
|
private query;
|
|
17
|
+
private queryAsync;
|
|
14
18
|
private queryOne;
|
|
15
19
|
getThreadsByCwd(cwd: string): CodexDbThread[];
|
|
16
20
|
getMostRecentThread(cwd: string): CodexDbThread | null;
|
|
@@ -20,6 +24,11 @@ export declare class CodexDatabase {
|
|
|
20
24
|
lastUpdated: number;
|
|
21
25
|
}>;
|
|
22
26
|
getThread(id: string): CodexDbThread | null;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve many thread ids with non-blocking sqlite3 subprocesses — one per
|
|
29
|
+
* chunk of ids, so the inlined query stays clear of argv length limits.
|
|
30
|
+
*/
|
|
31
|
+
getThreadsByIdsAsync(ids: readonly string[]): Promise<CodexDbThread[]>;
|
|
23
32
|
/** Get all threads forked from a given session ID. */
|
|
24
33
|
getThreadsByForkedFromId(parentId: string): CodexDbThread[];
|
|
25
34
|
/** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
|
|
@@ -41,9 +41,15 @@ exports.CodexDatabase = void 0;
|
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const child_process_1 = require("child_process");
|
|
44
|
+
/**
|
|
45
|
+
* Ids per inlined `IN (…)` query. Values are passed inside a single argv
|
|
46
|
+
* element to sqlite3, so an unbounded list would hit E2BIG on large stores.
|
|
47
|
+
*/
|
|
48
|
+
const SQL_ID_CHUNK_SIZE = 400;
|
|
44
49
|
class CodexDatabase {
|
|
45
50
|
dbPath;
|
|
46
51
|
sqlite3Available = null;
|
|
52
|
+
runtimeStatus = null;
|
|
47
53
|
constructor(codexHome) {
|
|
48
54
|
this.dbPath = findLatestStateDatabase(codexHome) ?? path.join(codexHome, 'state.sqlite');
|
|
49
55
|
}
|
|
@@ -58,6 +64,11 @@ class CodexDatabase {
|
|
|
58
64
|
open() {
|
|
59
65
|
if (this.sqlite3Available !== null)
|
|
60
66
|
return this.sqlite3Available;
|
|
67
|
+
if (!this.isAvailable()) {
|
|
68
|
+
this.sqlite3Available = false;
|
|
69
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
61
72
|
try {
|
|
62
73
|
(0, child_process_1.execFileSync)('sqlite3', ['--version'], {
|
|
63
74
|
encoding: 'utf-8',
|
|
@@ -66,13 +77,26 @@ class CodexDatabase {
|
|
|
66
77
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
67
78
|
});
|
|
68
79
|
this.sqlite3Available = true;
|
|
80
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
69
81
|
return true;
|
|
70
82
|
}
|
|
71
|
-
catch {
|
|
83
|
+
catch (error) {
|
|
72
84
|
this.sqlite3Available = false;
|
|
85
|
+
this.runtimeStatus = classifySqliteError(error);
|
|
73
86
|
return false;
|
|
74
87
|
}
|
|
75
88
|
}
|
|
89
|
+
getRuntimeStatus() {
|
|
90
|
+
if (this.runtimeStatus)
|
|
91
|
+
return this.runtimeStatus;
|
|
92
|
+
if (!this.isAvailable())
|
|
93
|
+
return { available: false, kind: 'db_missing' };
|
|
94
|
+
return {
|
|
95
|
+
available: false,
|
|
96
|
+
kind: 'query_failed',
|
|
97
|
+
message: 'Codex database has not been initialized yet.',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
76
100
|
close() { }
|
|
77
101
|
query(sql, params = []) {
|
|
78
102
|
if (!this.sqlite3Available)
|
|
@@ -103,7 +127,34 @@ class CodexDatabase {
|
|
|
103
127
|
return [];
|
|
104
128
|
return JSON.parse(trimmed);
|
|
105
129
|
}
|
|
106
|
-
catch {
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.runtimeStatus = {
|
|
132
|
+
...classifySqliteError(error, 'query_failed'),
|
|
133
|
+
available: this.sqlite3Available === true,
|
|
134
|
+
};
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async queryAsync(sql, params = []) {
|
|
139
|
+
if (!this.isAvailable()) {
|
|
140
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const query = bindSqlParams(sql, params);
|
|
144
|
+
try {
|
|
145
|
+
const result = await execSqlite(this.dbPath, query, 10 * 1024 * 1024);
|
|
146
|
+
this.sqlite3Available = true;
|
|
147
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
148
|
+
const trimmed = result.trim();
|
|
149
|
+
return trimmed ? JSON.parse(trimmed) : [];
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const status = classifySqliteError(error, 'query_failed');
|
|
153
|
+
// Only a missing sqlite3 binary gates future attempts. A failed query
|
|
154
|
+
// is transient and must not latch the database off (mirrors query()).
|
|
155
|
+
if (status.kind === 'sqlite_missing')
|
|
156
|
+
this.sqlite3Available = false;
|
|
157
|
+
this.runtimeStatus = status;
|
|
107
158
|
return [];
|
|
108
159
|
}
|
|
109
160
|
}
|
|
@@ -132,6 +183,19 @@ class CodexDatabase {
|
|
|
132
183
|
getThread(id) {
|
|
133
184
|
return this.queryOne('SELECT * FROM threads WHERE id = ?', [id]);
|
|
134
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Resolve many thread ids with non-blocking sqlite3 subprocesses — one per
|
|
188
|
+
* chunk of ids, so the inlined query stays clear of argv length limits.
|
|
189
|
+
*/
|
|
190
|
+
async getThreadsByIdsAsync(ids) {
|
|
191
|
+
const results = [];
|
|
192
|
+
for (let i = 0; i < ids.length; i += SQL_ID_CHUNK_SIZE) {
|
|
193
|
+
const chunk = ids.slice(i, i + SQL_ID_CHUNK_SIZE);
|
|
194
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
195
|
+
results.push(...(await this.queryAsync(`SELECT * FROM threads WHERE id IN (${placeholders})`, [...chunk])));
|
|
196
|
+
}
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
135
199
|
/** Get all threads forked from a given session ID. */
|
|
136
200
|
getThreadsByForkedFromId(parentId) {
|
|
137
201
|
return this.query('SELECT * FROM threads WHERE forked_from_id = ? ORDER BY created_at ASC', [parentId]);
|
|
@@ -147,6 +211,47 @@ class CodexDatabase {
|
|
|
147
211
|
}
|
|
148
212
|
}
|
|
149
213
|
exports.CodexDatabase = CodexDatabase;
|
|
214
|
+
function classifySqliteError(error, fallback = 'sqlite_blocked') {
|
|
215
|
+
const code = error?.code;
|
|
216
|
+
if (code === 'ENOENT') {
|
|
217
|
+
return {
|
|
218
|
+
available: false,
|
|
219
|
+
kind: 'sqlite_missing',
|
|
220
|
+
message: 'The sqlite3 executable was not found.',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
available: false,
|
|
225
|
+
kind: fallback,
|
|
226
|
+
message: error instanceof Error ? error.message : 'sqlite3 could not be executed.',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function bindSqlParams(sql, params) {
|
|
230
|
+
let paramIndex = 0;
|
|
231
|
+
return sql.replace(/\?/g, () => {
|
|
232
|
+
if (paramIndex >= params.length)
|
|
233
|
+
return '?';
|
|
234
|
+
const param = params[paramIndex++];
|
|
235
|
+
if (typeof param === 'number')
|
|
236
|
+
return Number.isFinite(param) ? String(param) : '0';
|
|
237
|
+
return `'${String(param).replace(/'/g, "''")}'`;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function execSqlite(dbPath, query, maxBuffer) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
(0, child_process_1.execFile)('sqlite3', ['-json', '-readonly', dbPath, query], {
|
|
243
|
+
encoding: 'utf8',
|
|
244
|
+
timeout: 4_000,
|
|
245
|
+
killSignal: 'SIGKILL',
|
|
246
|
+
maxBuffer,
|
|
247
|
+
}, (error, stdout) => {
|
|
248
|
+
if (error)
|
|
249
|
+
reject(error);
|
|
250
|
+
else
|
|
251
|
+
resolve(stdout);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
150
255
|
function findLatestStateDatabase(codexHome) {
|
|
151
256
|
try {
|
|
152
257
|
const entries = fs.readdirSync(codexHome, { withFileTypes: true });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderDiagnostic, SessionProviderOptions } from './types';
|
|
2
|
+
/** Small per-provider diagnostic state; it never stores transcript or environment contents. */
|
|
3
|
+
export declare class ProviderDiagnosticTracker {
|
|
4
|
+
private readonly providerId;
|
|
5
|
+
private readonly options;
|
|
6
|
+
private lastEmittedSignature;
|
|
7
|
+
private lastStatus;
|
|
8
|
+
constructor(providerId: ProviderId, options?: SessionProviderOptions);
|
|
9
|
+
available(operation: string): void;
|
|
10
|
+
degraded(operation: string, runtimeStatus: ProviderRuntimeStatus, diagnostic: Omit<SessionProviderDiagnostic, 'providerId'>, usable?: boolean): void;
|
|
11
|
+
getLastOperationStatus(): ProviderOperationStatus;
|
|
12
|
+
}
|
|
13
|
+
export declare function diagnosticFromRuntimeStatus(providerId: ProviderId, phase: SessionProviderDiagnostic['phase'], status: ProviderRuntimeStatus): SessionProviderDiagnostic | null;
|