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.
Files changed (76) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +159 -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/accountRegistry.d.ts +3 -1
  8. package/dist/accountRegistry.js +5 -0
  9. package/dist/browser.d.ts +8 -3
  10. package/dist/browser.js +15 -1
  11. package/dist/codexQuotaWatcher.d.ts +6 -0
  12. package/dist/codexQuotaWatcher.js +101 -47
  13. package/dist/index.d.ts +23 -13
  14. package/dist/index.js +34 -10
  15. package/dist/modelAliases.d.ts +13 -0
  16. package/dist/modelAliases.js +58 -0
  17. package/dist/modelCatalog.d.ts +32 -0
  18. package/dist/modelCatalog.js +145 -0
  19. package/dist/modelContext.d.ts +36 -3
  20. package/dist/modelContext.js +109 -34
  21. package/dist/modelInfo.d.ts +30 -1
  22. package/dist/modelInfo.js +116 -26
  23. package/dist/multiProviderQuotaService.d.ts +8 -0
  24. package/dist/multiProviderQuotaService.js +64 -3
  25. package/dist/node.d.ts +4 -4
  26. package/dist/node.js +4 -1
  27. package/dist/observedSessionCollector.d.ts +75 -4
  28. package/dist/observedSessionCollector.js +452 -42
  29. package/dist/parsers/codexHistory.d.ts +4 -2
  30. package/dist/parsers/codexHistory.js +9 -2
  31. package/dist/parsers/sessionActivityDetector.d.ts +6 -0
  32. package/dist/parsers/sessionActivityDetector.js +17 -0
  33. package/dist/providerIds.d.ts +11 -0
  34. package/dist/providerIds.js +9 -0
  35. package/dist/providerQuota.d.ts +3 -1
  36. package/dist/providerQuota.js +3 -0
  37. package/dist/providers/claudeCode.d.ts +10 -1
  38. package/dist/providers/claudeCode.js +204 -39
  39. package/dist/providers/codex.d.ts +18 -1
  40. package/dist/providers/codex.js +269 -40
  41. package/dist/providers/codexDatabase.d.ts +9 -0
  42. package/dist/providers/codexDatabase.js +107 -2
  43. package/dist/providers/diagnostics.d.ts +13 -0
  44. package/dist/providers/diagnostics.js +88 -0
  45. package/dist/providers/factory.d.ts +14 -0
  46. package/dist/providers/factory.js +57 -0
  47. package/dist/providers/openCode.d.ts +20 -1
  48. package/dist/providers/openCode.js +361 -11
  49. package/dist/providers/openCodeDatabase.d.ts +10 -0
  50. package/dist/providers/openCodeDatabase.js +69 -0
  51. package/dist/providers/types.d.ts +52 -2
  52. package/dist/providers/types.js +4 -0
  53. package/dist/quota.d.ts +4 -1
  54. package/dist/quota.js +3 -7
  55. package/dist/quotaHistory.d.ts +2 -1
  56. package/dist/quotaPoller.d.ts +13 -1
  57. package/dist/quotaPoller.js +65 -3
  58. package/dist/schemas/accountManager.d.ts +2 -2
  59. package/dist/schemas/accountManager.js +2 -4
  60. package/dist/schemas/accountStatus.d.ts +3 -0
  61. package/dist/schemas/accountStatus.js +1 -0
  62. package/dist/schemas/index.d.ts +1 -0
  63. package/dist/schemas/index.js +7 -2
  64. package/dist/schemas/observedSessionV1.d.ts +27 -26
  65. package/dist/schemas/observedSessionV1.js +6 -4
  66. package/dist/schemas/quota.d.ts +2 -2
  67. package/dist/schemas/quota.js +3 -6
  68. package/dist/schemas/quotaHistory.d.ts +2 -2
  69. package/dist/schemas/tokenEstimation.d.ts +1 -1
  70. package/dist/sessionMonitor.d.ts +15 -0
  71. package/dist/sessionMonitor.js +142 -0
  72. package/dist/sessionPreviews.d.ts +26 -1
  73. package/dist/sessionPreviews.js +319 -4
  74. package/dist/types/observedSessionV1.d.ts +6 -2
  75. package/dist/types/observedSessionV1.js +14 -8
  76. package/package.json +22 -1
@@ -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
- const lines = readFirstLines(rolloutPath, 20);
337
- for (const line of lines) {
338
- const trimmed = line.trim();
339
- if (!trimmed)
340
- continue;
341
- try {
342
- const parsed = JSON.parse(trimmed);
343
- // Check response_item with user message
344
- if (parsed.type === 'response_item') {
345
- const payload = parsed.payload;
346
- if (payload.role === 'user') {
347
- const content = payload.content;
348
- if (typeof content === 'string' && content.trim() && !isSystemInjection(content)) {
349
- return truncate(content, 60);
350
- }
351
- if (Array.isArray(content)) {
352
- for (const part of content) {
353
- const p = part;
354
- if (p.text?.trim() && !isSystemInjection(p.text)) {
355
- return truncate(p.text, 60);
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
- catch {
370
- // 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
+ }
371
453
  }
372
454
  }
373
- 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));
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
- return getSessionsDir();
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
- return getSessionsDir();
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
- return files
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
- return findRolloutFilesInConfiguredHomes();
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
- if (thread?.first_user_message)
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
- 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;
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;