sidekick-shared 0.24.4 → 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.
Files changed (98) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +200 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +151 -0
  7. package/dist/accountManager.d.ts +13 -0
  8. package/dist/accountManager.js +60 -32
  9. package/dist/accountRegistry.d.ts +3 -1
  10. package/dist/accountRegistry.js +64 -41
  11. package/dist/accounts.js +40 -19
  12. package/dist/autoSwitch.d.ts +2 -1
  13. package/dist/autoSwitch.js +32 -7
  14. package/dist/browser.d.ts +10 -3
  15. package/dist/browser.js +17 -1
  16. package/dist/codexProfiles.d.ts +14 -0
  17. package/dist/codexProfiles.js +268 -116
  18. package/dist/codexQuotaWatcher.d.ts +6 -0
  19. package/dist/codexQuotaWatcher.js +101 -47
  20. package/dist/context/sessionContext.js +2 -4
  21. package/dist/ensureDefaultAccounts.js +4 -4
  22. package/dist/index.d.ts +32 -15
  23. package/dist/index.js +54 -9
  24. package/dist/modelAliases.d.ts +13 -0
  25. package/dist/modelAliases.js +58 -0
  26. package/dist/modelCatalog.d.ts +32 -0
  27. package/dist/modelCatalog.js +145 -0
  28. package/dist/modelContext.d.ts +36 -3
  29. package/dist/modelContext.js +109 -34
  30. package/dist/modelInfo.d.ts +30 -1
  31. package/dist/modelInfo.js +116 -26
  32. package/dist/multiProviderQuotaService.d.ts +8 -0
  33. package/dist/multiProviderQuotaService.js +64 -3
  34. package/dist/node.d.ts +8 -2
  35. package/dist/node.js +11 -1
  36. package/dist/observedContextWindows.d.ts +13 -0
  37. package/dist/observedContextWindows.js +13 -0
  38. package/dist/observedSessionCollector.d.ts +75 -4
  39. package/dist/observedSessionCollector.js +452 -42
  40. package/dist/parsers/codexHistory.d.ts +32 -0
  41. package/dist/parsers/codexHistory.js +125 -0
  42. package/dist/parsers/mcpToolName.d.ts +26 -0
  43. package/dist/parsers/mcpToolName.js +31 -0
  44. package/dist/parsers/sessionActivityDetector.d.ts +6 -0
  45. package/dist/parsers/sessionActivityDetector.js +17 -0
  46. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  47. package/dist/parsers/sessionPathResolver.js +11 -4
  48. package/dist/paths.d.ts +7 -3
  49. package/dist/paths.js +7 -3
  50. package/dist/providerIds.d.ts +11 -0
  51. package/dist/providerIds.js +9 -0
  52. package/dist/providerQuota.d.ts +3 -1
  53. package/dist/providerQuota.js +3 -0
  54. package/dist/providers/claudeCode.d.ts +11 -1
  55. package/dist/providers/claudeCode.js +222 -39
  56. package/dist/providers/codex.d.ts +39 -1
  57. package/dist/providers/codex.js +326 -39
  58. package/dist/providers/codexDatabase.d.ts +9 -0
  59. package/dist/providers/codexDatabase.js +107 -2
  60. package/dist/providers/diagnostics.d.ts +13 -0
  61. package/dist/providers/diagnostics.js +88 -0
  62. package/dist/providers/factory.d.ts +14 -0
  63. package/dist/providers/factory.js +57 -0
  64. package/dist/providers/openCode.d.ts +20 -1
  65. package/dist/providers/openCode.js +361 -11
  66. package/dist/providers/openCodeDatabase.d.ts +10 -0
  67. package/dist/providers/openCodeDatabase.js +69 -0
  68. package/dist/providers/types.d.ts +57 -2
  69. package/dist/providers/types.js +4 -0
  70. package/dist/quota.d.ts +4 -1
  71. package/dist/quota.js +3 -7
  72. package/dist/quotaHistory.d.ts +3 -2
  73. package/dist/quotaHistory.js +2 -2
  74. package/dist/quotaPoller.d.ts +13 -1
  75. package/dist/quotaPoller.js +65 -3
  76. package/dist/quotaSnapshots.js +8 -77
  77. package/dist/schemas/accountManager.d.ts +2 -2
  78. package/dist/schemas/accountManager.js +2 -4
  79. package/dist/schemas/accountStatus.d.ts +3 -0
  80. package/dist/schemas/accountStatus.js +1 -0
  81. package/dist/schemas/index.d.ts +1 -0
  82. package/dist/schemas/index.js +7 -2
  83. package/dist/schemas/observedSessionV1.d.ts +27 -26
  84. package/dist/schemas/observedSessionV1.js +6 -4
  85. package/dist/schemas/quota.d.ts +2 -2
  86. package/dist/schemas/quota.js +3 -6
  87. package/dist/schemas/quotaHistory.d.ts +2 -2
  88. package/dist/schemas/tokenEstimation.d.ts +1 -1
  89. package/dist/sessionMonitor.d.ts +15 -0
  90. package/dist/sessionMonitor.js +142 -0
  91. package/dist/sessionPreviews.d.ts +85 -0
  92. package/dist/sessionPreviews.js +549 -0
  93. package/dist/sessionTranscripts.js +10 -4
  94. package/dist/types/observedSessionV1.d.ts +6 -2
  95. package/dist/types/observedSessionV1.js +14 -8
  96. package/dist/writers/atomic.d.ts +20 -0
  97. package/dist/writers/atomic.js +190 -19
  98. package/package.json +22 -1
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.CodexProvider = void 0;
47
+ exports.findCodexRolloutFile = findCodexRolloutFile;
47
48
  const fs = __importStar(require("fs"));
48
49
  const path = __importStar(require("path"));
49
50
  const string_decoder_1 = require("string_decoder");
@@ -52,6 +53,7 @@ const sessionContext_1 = require("../context/sessionContext");
52
53
  const codexParser_1 = require("../parsers/codexParser");
53
54
  const codexDatabase_1 = require("./codexDatabase");
54
55
  const codexProfiles_1 = require("../codexProfiles");
56
+ const diagnostics_1 = require("./diagnostics");
55
57
  const modelContext_1 = require("../modelContext");
56
58
  const observedContextWindows_1 = require("../observedContextWindows");
57
59
  const usageNormalization_1 = require("../usageNormalization");
@@ -164,6 +166,94 @@ function findRolloutFilesInConfiguredHomes() {
164
166
  }
165
167
  return results;
166
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
+ }
203
+ /**
204
+ * Locates the rollout file for a Codex session id by walking
205
+ * `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`.
206
+ *
207
+ * Filename-only match, case-insensitive on the id — no file contents are read
208
+ * and files are only stat'ed once their name matches, so the walk stays cheap
209
+ * even over a large history. When the same id exists in more than one home,
210
+ * the most recently modified copy wins. Returns `null` for an unknown, empty,
211
+ * or whitespace id. Consumers who want the database's answer instead can query
212
+ * the already-public `CodexDatabase` (requires the external `sqlite3` binary).
213
+ */
214
+ function findCodexRolloutFile(sessionId, options = {}) {
215
+ const normalizedId = sessionId.trim().toLowerCase();
216
+ if (!normalizedId)
217
+ return null;
218
+ const sessionsDirs = options.codexHome
219
+ ? [path.join(options.codexHome, 'sessions')]
220
+ : getSessionsDirs();
221
+ let best = null;
222
+ for (const dir of sessionsDirs) {
223
+ for (const candidate of findRolloutFilesById(dir, normalizedId)) {
224
+ if (!best || candidate.mtimeMs > best.mtimeMs)
225
+ best = candidate;
226
+ }
227
+ }
228
+ return best?.path ?? null;
229
+ }
230
+ function findRolloutFilesById(dir, normalizedId) {
231
+ const results = [];
232
+ try {
233
+ if (!fs.existsSync(dir))
234
+ return results;
235
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
236
+ const fullPath = path.join(dir, entry.name);
237
+ if (entry.isDirectory()) {
238
+ results.push(...findRolloutFilesById(fullPath, normalizedId));
239
+ }
240
+ else if (entry.isFile() &&
241
+ isRolloutFile(entry.name) &&
242
+ extractSessionId(entry.name).toLowerCase() === normalizedId) {
243
+ try {
244
+ results.push({ path: fullPath, mtimeMs: fs.statSync(fullPath).mtimeMs });
245
+ }
246
+ catch {
247
+ // Skip files that vanish between readdir and stat.
248
+ }
249
+ }
250
+ }
251
+ }
252
+ catch {
253
+ // Skip inaccessible directories.
254
+ }
255
+ return results;
256
+ }
167
257
  /** Pick the best sessions directory for UI/search entrypoints. */
168
258
  function getSessionsDir() {
169
259
  const dirs = getSessionsDirs();
@@ -243,6 +333,41 @@ function readSessionMeta(rolloutPath) {
243
333
  }
244
334
  return null;
245
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
+ }
246
371
  /** Read the CWD from a rollout file's session_meta. Cached. */
247
372
  const cwdCache = new Map();
248
373
  function readSessionCwd(rolloutPath) {
@@ -254,6 +379,16 @@ function readSessionCwd(rolloutPath) {
254
379
  cwdCache.set(rolloutPath, cwd);
255
380
  return cwd;
256
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
+ }
257
392
  /** Check if a session's CWD matches a workspace path. */
258
393
  function cwdMatches(sessionCwd, workspacePath) {
259
394
  const normalizedSession = normalizePath(sessionCwd);
@@ -278,48 +413,65 @@ function normalizePath(input) {
278
413
  */
279
414
  function extractFirstUserMessage(rolloutPath) {
280
415
  try {
281
- const lines = readFirstLines(rolloutPath, 20);
282
- for (const line of lines) {
283
- const trimmed = line.trim();
284
- if (!trimmed)
285
- continue;
286
- try {
287
- const parsed = JSON.parse(trimmed);
288
- // Check response_item with user message
289
- if (parsed.type === 'response_item') {
290
- const payload = parsed.payload;
291
- if (payload.role === 'user') {
292
- const content = payload.content;
293
- if (typeof content === 'string' && content.trim() && !isSystemInjection(content)) {
294
- return truncate(content, 60);
295
- }
296
- if (Array.isArray(content)) {
297
- for (const part of content) {
298
- const p = part;
299
- if (p.text?.trim() && !isSystemInjection(p.text)) {
300
- return truncate(p.text, 60);
301
- }
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);
302
442
  }
303
443
  }
304
444
  }
305
445
  }
306
- // Check event_msg with user_message
307
- if (parsed.type === 'event_msg') {
308
- const payload = parsed.payload;
309
- if (payload.type === 'user_message' && payload.message?.trim()) {
310
- return truncate(payload.message, 60);
311
- }
312
- }
313
446
  }
314
- catch {
315
- // Skip malformed lines
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
+ }
316
453
  }
317
454
  }
318
- return null;
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));
319
468
  }
320
469
  catch {
321
470
  return null;
322
471
  }
472
+ finally {
473
+ await handle?.close().catch(() => undefined);
474
+ }
323
475
  }
324
476
  /** Extract searchable text from a rollout line. */
325
477
  function extractSearchableText(line) {
@@ -606,9 +758,14 @@ class CodexProvider {
606
758
  displayName = 'Codex CLI';
607
759
  db = null;
608
760
  dbInitialized = false;
761
+ dbStatus = { available: false, kind: 'db_missing' };
762
+ diagnostics;
609
763
  dynamicContextWindowLimit = null;
610
764
  lastRateLimitsData = null;
611
765
  lastTokenUsageData = null;
766
+ constructor(options = {}) {
767
+ this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
768
+ }
612
769
  // --- Database ---
613
770
  /** Lazy-initialize database connection. */
614
771
  ensureDb() {
@@ -620,8 +777,34 @@ class CodexProvider {
620
777
  if (db.isAvailable() && db.open()) {
621
778
  this.db = db;
622
779
  }
780
+ if (typeof db.getRuntimeStatus === 'function') {
781
+ this.dbStatus = db.getRuntimeStatus();
782
+ }
623
783
  return this.db;
624
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
+ }
625
808
  // --- Path resolution ---
626
809
  getSessionDirectory(workspacePath) {
627
810
  // Codex doesn't use workspace-encoded directories like Claude Code.
@@ -631,10 +814,13 @@ class CodexProvider {
631
814
  if (db) {
632
815
  const thread = db.getMostRecentThread(workspacePath);
633
816
  if (thread?.rollout_path) {
817
+ this.recordDatabaseFallback('getSessionDirectory', true);
634
818
  return path.dirname(thread.rollout_path);
635
819
  }
636
820
  }
637
- return getSessionsDir();
821
+ const directory = getSessionsDir();
822
+ this.recordDatabaseFallback('getSessionDirectory', true);
823
+ return directory;
638
824
  }
639
825
  discoverSessionDirectory(workspacePath) {
640
826
  // DB: find latest rollout directory for this workspace
@@ -642,23 +828,29 @@ class CodexProvider {
642
828
  if (db) {
643
829
  const thread = db.getMostRecentThread(workspacePath);
644
830
  if (thread?.rollout_path && fs.existsSync(thread.rollout_path)) {
831
+ this.recordDatabaseFallback('discoverSessionDirectory', true);
645
832
  return path.dirname(thread.rollout_path);
646
833
  }
647
834
  }
648
835
  // File scan: look for any rollout file whose session_meta.cwd matches
649
836
  const files = findRolloutFilesInConfiguredHomes();
650
- if (files.length === 0)
837
+ if (files.length === 0) {
838
+ this.recordDatabaseFallback('discoverSessionDirectory', true);
651
839
  return null;
840
+ }
652
841
  // Sort by mtime descending
653
842
  files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
654
843
  for (const file of files) {
655
844
  const meta = readSessionMeta(file.path);
656
845
  if (meta && cwdMatches(meta.cwd, workspacePath)) {
846
+ this.recordDatabaseFallback('discoverSessionDirectory', true);
657
847
  return path.dirname(file.path);
658
848
  }
659
849
  }
660
850
  // Fallback: return the most relevant base sessions dir if one exists
661
- return getSessionsDir();
851
+ const directory = getSessionsDir();
852
+ this.recordDatabaseFallback('discoverSessionDirectory', true);
853
+ return directory;
662
854
  }
663
855
  // --- Session discovery ---
664
856
  findActiveSession(workspacePath) {
@@ -667,6 +859,7 @@ class CodexProvider {
667
859
  if (db) {
668
860
  const thread = db.getMostRecentThread(workspacePath);
669
861
  if (thread?.rollout_path && fs.existsSync(thread.rollout_path)) {
862
+ this.recordDatabaseFallback('findActiveSession', true);
670
863
  return thread.rollout_path;
671
864
  }
672
865
  }
@@ -676,9 +869,11 @@ class CodexProvider {
676
869
  for (const file of files) {
677
870
  const meta = readSessionMeta(file.path);
678
871
  if (meta && cwdMatches(meta.cwd, workspacePath)) {
872
+ this.recordDatabaseFallback('findActiveSession', true);
679
873
  return file.path;
680
874
  }
681
875
  }
876
+ this.recordDatabaseFallback('findActiveSession', true);
682
877
  return null;
683
878
  }
684
879
  findAllSessions(workspacePath) {
@@ -689,18 +884,33 @@ class CodexProvider {
689
884
  const dbPaths = threads
690
885
  .filter((t) => t.rollout_path && fs.existsSync(t.rollout_path))
691
886
  .map((t) => t.rollout_path);
692
- if (dbPaths.length > 0)
887
+ if (dbPaths.length > 0) {
888
+ this.recordDatabaseFallback('findAllSessions', true);
693
889
  return dbPaths;
890
+ }
694
891
  }
695
892
  // File scan
696
893
  const files = findRolloutFilesInConfiguredHomes();
697
894
  files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
698
- return files
895
+ const sessions = files
699
896
  .filter((f) => {
700
897
  const meta = readSessionMeta(f.path);
701
898
  return meta && cwdMatches(meta.cwd, workspacePath);
702
899
  })
703
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
+ }
704
914
  }
705
915
  /** Backward-compatible alias for findAllSessions. */
706
916
  findSessionFiles(workspacePath) {
@@ -711,6 +921,30 @@ class CodexProvider {
711
921
  files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
712
922
  return files.map((f) => f.path);
713
923
  }
924
+ listAllSessionFiles() {
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());
947
+ }
714
948
  getAllProjectFolders(workspacePath) {
715
949
  const folders = [];
716
950
  const seenCwds = new Map();
@@ -765,6 +999,7 @@ class CodexProvider {
765
999
  }
766
1000
  return b.lastModified.getTime() - a.lastModified.getTime();
767
1001
  });
1002
+ this.recordDatabaseFallback('getAllProjectFolders', true);
768
1003
  return folders;
769
1004
  }
770
1005
  // --- File identification ---
@@ -784,13 +1019,43 @@ class CodexProvider {
784
1019
  if (db) {
785
1020
  const sessionId = this.getSessionId(sessionPath);
786
1021
  const thread = db.getThread(sessionId);
787
- if (thread?.title)
1022
+ if (thread?.title) {
1023
+ this.recordDatabaseFallback('extractSessionLabel', true);
788
1024
  return truncate(thread.title, 60);
789
- if (thread?.first_user_message)
1025
+ }
1026
+ if (thread?.first_user_message) {
1027
+ this.recordDatabaseFallback('extractSessionLabel', true);
790
1028
  return truncate(thread.first_user_message, 60);
1029
+ }
791
1030
  }
792
1031
  // File fallback: parse first user message
793
- return extractFirstUserMessage(sessionPath);
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;
794
1059
  }
795
1060
  // --- Data reading ---
796
1061
  createReader(sessionPath) {
@@ -957,6 +1222,9 @@ class CodexProvider {
957
1222
  getProjectsBaseDir() {
958
1223
  return getSessionsDir();
959
1224
  }
1225
+ getWatchRoots() {
1226
+ return getSessionsDirs();
1227
+ }
960
1228
  // --- Stats ---
961
1229
  readSessionStats(sessionPath) {
962
1230
  const sessionId = extractSessionId(path.basename(sessionPath));
@@ -1036,9 +1304,28 @@ class CodexProvider {
1036
1304
  this.db?.close();
1037
1305
  this.db = null;
1038
1306
  this.dbInitialized = false;
1307
+ this.dbStatus = { available: false, kind: 'db_missing' };
1039
1308
  this.dynamicContextWindowLimit = null;
1040
1309
  this.lastRateLimitsData = null;
1041
1310
  this.lastTokenUsageData = null;
1042
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
+ }
1043
1330
  }
1044
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. */