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
@@ -60,6 +60,7 @@ const sessionContext_1 = require("../context/sessionContext");
60
60
  const openCodeParser_1 = require("../parsers/openCodeParser");
61
61
  const openCodeDatabase_1 = require("./openCodeDatabase");
62
62
  const zaiQuota_1 = require("../zaiQuota");
63
+ const diagnostics_1 = require("./diagnostics");
63
64
  const modelContext_1 = require("../modelContext");
64
65
  // ---------------------------------------------------------------------------
65
66
  // Helper functions
@@ -169,6 +170,17 @@ function makeDbSessionPath(dataDir, projectId, sessionId) {
169
170
  function isDbSessionPath(sessionPath) {
170
171
  return sessionPath.includes(path.sep + DB_SESSION_PREFIX + path.sep);
171
172
  }
173
+ function workspaceMatches(left, right) {
174
+ const a = path
175
+ .resolve(left)
176
+ .replace(/[\\/]+$/, '')
177
+ .toLowerCase();
178
+ const b = path
179
+ .resolve(right)
180
+ .replace(/[\\/]+$/, '')
181
+ .toLowerCase();
182
+ return a === b || a.startsWith(b + path.sep) || b.startsWith(a + path.sep);
183
+ }
172
184
  /** Extract project ID from a synthetic DB session path. */
173
185
  function extractProjectIdFromDbPath(sessionPath) {
174
186
  const prefix = path.sep + DB_SESSION_PREFIX + path.sep;
@@ -650,10 +662,14 @@ class OpenCodeProvider {
650
662
  db = null;
651
663
  dbInitialized = false;
652
664
  dbStatus = { available: false, kind: 'db_missing' };
665
+ diagnostics;
653
666
  /** Cache of session metadata populated during listing */
654
667
  sessionMetaCache = new Map();
655
668
  /** Runtime-reported context window limit (overrides static map when set). */
656
669
  dynamicContextWindowLimit = null;
670
+ constructor(options = {}) {
671
+ this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
672
+ }
657
673
  /** Lazy-initialize the database connection. */
658
674
  ensureDb() {
659
675
  if (this.dbInitialized)
@@ -667,12 +683,31 @@ class OpenCodeProvider {
667
683
  this.dbStatus = db.getRuntimeStatus();
668
684
  return this.db;
669
685
  }
686
+ async ensureDbAsync() {
687
+ if (this.db)
688
+ return this.db;
689
+ return new openCodeDatabase_1.OpenCodeDatabase(getOpenCodeDataDir());
690
+ }
691
+ /**
692
+ * Cache an async-created database only after an operation proved it usable.
693
+ * Sync paths treat a cached `db` as "open() succeeded", so adopting an
694
+ * unproven instance would cut them off from the file-storage fallback.
695
+ */
696
+ adoptDbIfAvailable(db) {
697
+ if (this.db || !db.getRuntimeStatus().available)
698
+ return;
699
+ this.db = db;
700
+ this.dbInitialized = true;
701
+ }
670
702
  getRuntimeStatus() {
671
703
  const db = this.ensureDb();
672
704
  if (db)
673
705
  return db.getRuntimeStatus();
674
706
  return this.dbStatus;
675
707
  }
708
+ getLastOperationStatus() {
709
+ return this.diagnostics.getLastOperationStatus();
710
+ }
676
711
  canMonitorDirectory(dir) {
677
712
  if (fs.existsSync(dir))
678
713
  return true;
@@ -738,8 +773,10 @@ class OpenCodeProvider {
738
773
  const db = this.ensureDb();
739
774
  const dbStatus = this.getRuntimeStatus();
740
775
  const projectId = resolveProjectId(workspacePath, db, dbStatus);
741
- if (!projectId)
776
+ if (!projectId) {
777
+ this.recordDatabaseStatus('findActiveSession', dbStatus.kind === 'db_missing');
742
778
  return null;
779
+ }
743
780
  // DB primary
744
781
  if (db) {
745
782
  const session = db.getMostRecentSession(projectId);
@@ -749,14 +786,18 @@ class OpenCodeProvider {
749
786
  title: session.title,
750
787
  timeUpdated: session.time_updated,
751
788
  });
789
+ this.recordDatabaseStatus('findActiveSession', true);
752
790
  return syntheticPath;
753
791
  }
754
792
  }
755
793
  if (dbStatus.kind !== 'db_missing') {
794
+ this.recordDatabaseStatus('findActiveSession', dbStatus.available);
756
795
  return null;
757
796
  }
758
797
  // File fallback
759
- return this.findActiveSessionFromFiles(projectId);
798
+ const result = this.findActiveSessionFromFiles(projectId);
799
+ this.recordDatabaseStatus('findActiveSession', true);
800
+ return result;
760
801
  }
761
802
  findActiveSessionFromFiles(projectId) {
762
803
  const sessionDir = path.join(getStorageDir(), 'session', projectId);
@@ -817,18 +858,64 @@ class OpenCodeProvider {
817
858
  const db = this.ensureDb();
818
859
  const dbStatus = this.getRuntimeStatus();
819
860
  const projectId = resolveProjectId(workspacePath, db, dbStatus);
820
- if (!projectId)
861
+ if (!projectId) {
862
+ this.recordDatabaseStatus('findAllSessions', dbStatus.kind === 'db_missing');
821
863
  return [];
864
+ }
822
865
  // DB primary
823
866
  if (db) {
824
- return this.mapDbSessions(projectId);
867
+ const sessions = this.mapDbSessions(projectId);
868
+ this.recordDatabaseStatus('findAllSessions', true);
869
+ return sessions;
825
870
  }
826
871
  if (dbStatus.kind !== 'db_missing') {
872
+ this.recordDatabaseStatus('findAllSessions', dbStatus.available);
827
873
  return [];
828
874
  }
829
875
  // File fallback
830
876
  const sessionDir = path.join(getStorageDir(), 'session', projectId);
831
- return this.findSessionsInDirectoryFromFiles(sessionDir);
877
+ const sessions = this.findSessionsInDirectoryFromFiles(sessionDir);
878
+ this.recordDatabaseStatus('findAllSessions', true);
879
+ return sessions;
880
+ }
881
+ findSessionById(workspacePath, sessionId) {
882
+ const normalizedId = sessionId.trim();
883
+ if (!normalizedId || path.basename(normalizedId) !== normalizedId)
884
+ return null;
885
+ try {
886
+ const db = this.ensureDb();
887
+ if (db) {
888
+ const session = db.getSession(normalizedId);
889
+ if (session && workspaceMatches(session.directory, workspacePath)) {
890
+ const syntheticPath = makeDbSessionPath(getOpenCodeDataDir(), session.project_id, session.id);
891
+ this.sessionMetaCache.set(syntheticPath, {
892
+ title: session.title,
893
+ timeUpdated: session.time_updated,
894
+ });
895
+ this.recordDatabaseStatus('findSessionById', true);
896
+ return syntheticPath;
897
+ }
898
+ }
899
+ const projectId = resolveProjectId(workspacePath, db, this.getRuntimeStatus());
900
+ if (projectId) {
901
+ const candidate = path.join(getStorageDir(), 'session', projectId, `${normalizedId}.json`);
902
+ try {
903
+ if (fs.statSync(candidate).isFile()) {
904
+ this.recordDatabaseStatus('findSessionById', true);
905
+ return candidate;
906
+ }
907
+ }
908
+ catch {
909
+ // Missing legacy session is a normal miss.
910
+ }
911
+ }
912
+ this.recordDatabaseStatus('findSessionById', true);
913
+ return null;
914
+ }
915
+ catch {
916
+ this.recordDatabaseStatus('findSessionById', false);
917
+ return null;
918
+ }
832
919
  }
833
920
  findSessionsInDirectory(dir) {
834
921
  const db = this.ensureDb();
@@ -837,7 +924,9 @@ class OpenCodeProvider {
837
924
  if (db && dir.includes(path.sep + DB_SESSION_PREFIX + path.sep)) {
838
925
  const projectId = extractProjectIdFromDbPath(dir + path.sep + 'dummy.json');
839
926
  if (projectId) {
840
- return this.mapDbSessions(projectId);
927
+ const sessions = this.mapDbSessions(projectId);
928
+ this.recordDatabaseStatus('findSessionsInDirectory', true);
929
+ return sessions;
841
930
  }
842
931
  }
843
932
  // For a DB-backed directory path ending with the project ID,
@@ -845,14 +934,19 @@ class OpenCodeProvider {
845
934
  if (db) {
846
935
  const dirName = path.basename(dir);
847
936
  if (db.hasProject(dirName)) {
848
- return this.mapDbSessions(dirName);
937
+ const sessions = this.mapDbSessions(dirName);
938
+ this.recordDatabaseStatus('findSessionsInDirectory', true);
939
+ return sessions;
849
940
  }
850
941
  }
851
942
  if (dbStatus.kind !== 'db_missing') {
943
+ this.recordDatabaseStatus('findSessionsInDirectory', dbStatus.available);
852
944
  return [];
853
945
  }
854
946
  // File fallback
855
- return this.findSessionsInDirectoryFromFiles(dir);
947
+ const sessions = this.findSessionsInDirectoryFromFiles(dir);
948
+ this.recordDatabaseStatus('findSessionsInDirectory', true);
949
+ return sessions;
856
950
  }
857
951
  findSessionsInDirectoryFromFiles(dir) {
858
952
  try {
@@ -879,6 +973,159 @@ class OpenCodeProvider {
879
973
  return [];
880
974
  }
881
975
  }
976
+ listAllSessionFiles() {
977
+ try {
978
+ const db = this.ensureDb();
979
+ if (db) {
980
+ const dataDir = getOpenCodeDataDir();
981
+ const files = db.getAllSessions().map((session) => {
982
+ const syntheticPath = makeDbSessionPath(dataDir, session.project_id, session.id);
983
+ this.sessionMetaCache.set(syntheticPath, {
984
+ title: session.title,
985
+ timeUpdated: session.time_updated,
986
+ });
987
+ return {
988
+ path: syntheticPath,
989
+ mtime: new Date(session.time_updated),
990
+ label: session.title ?? undefined,
991
+ sizeBytes: 0,
992
+ sessionId: session.id,
993
+ workspacePath: session.directory,
994
+ createdAt: new Date(session.time_created),
995
+ };
996
+ });
997
+ this.recordDatabaseStatus('listAllSessionFiles', true);
998
+ return files;
999
+ }
1000
+ if (this.getRuntimeStatus().kind !== 'db_missing') {
1001
+ this.recordDatabaseStatus('listAllSessionFiles', true);
1002
+ return [];
1003
+ }
1004
+ const root = path.join(getStorageDir(), 'session');
1005
+ const results = [];
1006
+ let projectDirs = [];
1007
+ try {
1008
+ projectDirs = fs.readdirSync(root, { withFileTypes: true });
1009
+ }
1010
+ catch {
1011
+ this.recordDatabaseStatus('listAllSessionFiles', true);
1012
+ return [];
1013
+ }
1014
+ for (const entry of projectDirs) {
1015
+ if (!entry.isDirectory())
1016
+ continue;
1017
+ for (const sessionPath of this.findSessionsInDirectoryFromFiles(path.join(root, entry.name))) {
1018
+ try {
1019
+ const stats = fs.statSync(sessionPath);
1020
+ results.push({
1021
+ path: sessionPath,
1022
+ mtime: stats.mtime,
1023
+ sizeBytes: stats.size,
1024
+ sessionId: path.basename(sessionPath, '.json'),
1025
+ });
1026
+ }
1027
+ catch {
1028
+ // Session vanished between listing and stat.
1029
+ }
1030
+ }
1031
+ }
1032
+ this.recordDatabaseStatus('listAllSessionFiles', true);
1033
+ return results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1034
+ }
1035
+ catch {
1036
+ this.recordDatabaseStatus('listAllSessionFiles', false);
1037
+ return [];
1038
+ }
1039
+ }
1040
+ async listSessionFilesAsync(workspacePath) {
1041
+ const db = await this.ensureDbAsync();
1042
+ const sessions = await db.getAllSessionsAsync();
1043
+ this.dbStatus = db.getRuntimeStatus();
1044
+ this.adoptDbIfAvailable(db);
1045
+ if (this.dbStatus.available) {
1046
+ const dataDir = getOpenCodeDataDir();
1047
+ const files = [];
1048
+ for (const session of sessions) {
1049
+ if (workspacePath && !workspaceMatches(session.directory, workspacePath))
1050
+ continue;
1051
+ const syntheticPath = makeDbSessionPath(dataDir, session.project_id, session.id);
1052
+ this.sessionMetaCache.set(syntheticPath, {
1053
+ title: session.title,
1054
+ timeUpdated: session.time_updated,
1055
+ });
1056
+ files.push({
1057
+ path: syntheticPath,
1058
+ mtime: new Date(session.time_updated),
1059
+ label: session.title ?? undefined,
1060
+ sizeBytes: 0,
1061
+ sessionId: session.id,
1062
+ workspacePath: session.directory,
1063
+ createdAt: new Date(session.time_created),
1064
+ });
1065
+ }
1066
+ this.recordDatabaseStatus('listSessionFilesAsync', true);
1067
+ return files;
1068
+ }
1069
+ const root = path.join(getStorageDir(), 'session');
1070
+ const results = [];
1071
+ let projectDirs = [];
1072
+ try {
1073
+ projectDirs = await fs.promises.readdir(root, { withFileTypes: true });
1074
+ }
1075
+ catch {
1076
+ this.recordDatabaseStatus('listSessionFilesAsync', true);
1077
+ return [];
1078
+ }
1079
+ for (const project of projectDirs) {
1080
+ if (!project.isDirectory())
1081
+ continue;
1082
+ let entries = [];
1083
+ try {
1084
+ entries = await fs.promises.readdir(path.join(root, project.name), {
1085
+ withFileTypes: true,
1086
+ });
1087
+ }
1088
+ catch {
1089
+ continue;
1090
+ }
1091
+ for (const entry of entries) {
1092
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
1093
+ continue;
1094
+ const sessionPath = path.join(root, project.name, entry.name);
1095
+ try {
1096
+ const [stat, raw] = await Promise.all([
1097
+ fs.promises.stat(sessionPath),
1098
+ workspacePath ? fs.promises.readFile(sessionPath, 'utf8') : Promise.resolve(null),
1099
+ ]);
1100
+ let session = null;
1101
+ if (raw !== null) {
1102
+ try {
1103
+ session = JSON.parse(raw);
1104
+ }
1105
+ catch {
1106
+ continue;
1107
+ }
1108
+ const directory = session.directory;
1109
+ if (directory && !workspaceMatches(directory, workspacePath))
1110
+ continue;
1111
+ }
1112
+ results.push({
1113
+ path: sessionPath,
1114
+ mtime: stat.mtime,
1115
+ sizeBytes: stat.size,
1116
+ sessionId: path.basename(entry.name, '.json'),
1117
+ workspacePath: session?.directory,
1118
+ });
1119
+ }
1120
+ catch {
1121
+ // Skip inaccessible sessions.
1122
+ }
1123
+ await new Promise((resolve) => setImmediate(resolve));
1124
+ }
1125
+ }
1126
+ this.recordDatabaseStatus('listSessionFilesAsync', true);
1127
+ return results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1128
+ }
882
1129
  getAllProjectFolders(workspacePath) {
883
1130
  const db = this.ensureDb();
884
1131
  const folders = [];
@@ -917,14 +1164,19 @@ class OpenCodeProvider {
917
1164
  }
918
1165
  return b.lastModified.getTime() - a.lastModified.getTime();
919
1166
  });
920
- if (folders.length > 0)
1167
+ if (folders.length > 0) {
1168
+ this.recordDatabaseStatus('getAllProjectFolders', true);
921
1169
  return folders;
1170
+ }
922
1171
  }
923
1172
  if (dbStatus.kind !== 'db_missing') {
1173
+ this.recordDatabaseStatus('getAllProjectFolders', dbStatus.available);
924
1174
  return [];
925
1175
  }
926
1176
  // File fallback
927
- return this.getAllProjectFoldersFromFiles(workspacePath);
1177
+ const fallback = this.getAllProjectFoldersFromFiles(workspacePath);
1178
+ this.recordDatabaseStatus('getAllProjectFolders', true);
1179
+ return fallback;
928
1180
  }
929
1181
  getAllProjectFoldersFromFiles(workspacePath) {
930
1182
  const folders = [];
@@ -1031,11 +1283,60 @@ class OpenCodeProvider {
1031
1283
  if (db) {
1032
1284
  const session = db.getSession(sessionId);
1033
1285
  if (session?.title) {
1286
+ this.recordDatabaseStatus('extractSessionLabel', true);
1034
1287
  return truncateTitle(session.title);
1035
1288
  }
1036
1289
  }
1037
1290
  // File fallback
1038
- return this.extractSessionLabelFromFiles(sessionPath, sessionId);
1291
+ const label = this.extractSessionLabelFromFiles(sessionPath, sessionId);
1292
+ this.recordDatabaseStatus('extractSessionLabel', label !== null || !isDbSessionPath(sessionPath));
1293
+ return label;
1294
+ }
1295
+ async extractSessionLabelsAsync(sessionPaths) {
1296
+ const results = new Map();
1297
+ const unresolved = [];
1298
+ for (const sessionPath of sessionPaths) {
1299
+ const cached = this.sessionMetaCache.get(sessionPath);
1300
+ if (cached)
1301
+ results.set(sessionPath, cached.title ? truncateTitle(cached.title) : null);
1302
+ else
1303
+ unresolved.push(sessionPath);
1304
+ }
1305
+ if (unresolved.length > 0 && (!this.dbInitialized || this.dbStatus.available)) {
1306
+ const db = await this.ensureDbAsync();
1307
+ const byId = new Map(unresolved.map((item) => [this.getSessionId(item), item]));
1308
+ const sessions = await db.getSessionsByIdsAsync([...byId.keys()]);
1309
+ this.dbStatus = db.getRuntimeStatus();
1310
+ this.adoptDbIfAvailable(db);
1311
+ for (const session of sessions) {
1312
+ const sessionPath = byId.get(session.id);
1313
+ if (!sessionPath)
1314
+ continue;
1315
+ this.sessionMetaCache.set(sessionPath, {
1316
+ title: session.title,
1317
+ timeUpdated: session.time_updated,
1318
+ });
1319
+ results.set(sessionPath, session.title ? truncateTitle(session.title) : null);
1320
+ }
1321
+ }
1322
+ for (const sessionPath of unresolved) {
1323
+ if (results.has(sessionPath))
1324
+ continue;
1325
+ if (isDbSessionPath(sessionPath)) {
1326
+ results.set(sessionPath, null);
1327
+ continue;
1328
+ }
1329
+ try {
1330
+ const session = JSON.parse(await fs.promises.readFile(sessionPath, 'utf8'));
1331
+ results.set(sessionPath, session.title ? truncateTitle(session.title) : null);
1332
+ }
1333
+ catch {
1334
+ results.set(sessionPath, null);
1335
+ }
1336
+ await new Promise((resolve) => setImmediate(resolve));
1337
+ }
1338
+ this.recordDatabaseStatus('extractSessionLabelsAsync', true);
1339
+ return results;
1039
1340
  }
1040
1341
  extractSessionLabelFromFiles(sessionPath, sessionId) {
1041
1342
  if (isDbSessionPath(sessionPath))
@@ -1343,6 +1644,9 @@ class OpenCodeProvider {
1343
1644
  getProjectsBaseDir() {
1344
1645
  return path.join(getStorageDir(), 'session');
1345
1646
  }
1647
+ getWatchRoots() {
1648
+ return [getOpenCodeDataDir(), this.getProjectsBaseDir()];
1649
+ }
1346
1650
  // --- Stats ---
1347
1651
  readSessionStats(sessionPath) {
1348
1652
  const sessionId = path.basename(sessionPath, '.json');
@@ -1453,6 +1757,37 @@ class OpenCodeProvider {
1453
1757
  }
1454
1758
  return null;
1455
1759
  }
1760
+ async getSessionMetadataAsync(sessionPath) {
1761
+ if (!isDbSessionPath(sessionPath)) {
1762
+ try {
1763
+ return { mtime: (await fs.promises.stat(sessionPath)).mtime };
1764
+ }
1765
+ catch {
1766
+ return null;
1767
+ }
1768
+ }
1769
+ const cached = this.sessionMetaCache.get(sessionPath);
1770
+ try {
1771
+ const db = await this.ensureDbAsync();
1772
+ const sessionId = this.getSessionId(sessionPath);
1773
+ const session = (await db.getSessionsByIdsAsync([sessionId]))[0];
1774
+ this.dbStatus = db.getRuntimeStatus();
1775
+ this.adoptDbIfAvailable(db);
1776
+ if (session) {
1777
+ this.sessionMetaCache.set(sessionPath, {
1778
+ title: session.title,
1779
+ timeUpdated: session.time_updated,
1780
+ });
1781
+ this.recordDatabaseStatus('getSessionMetadataAsync', true);
1782
+ return { mtime: new Date(session.time_updated) };
1783
+ }
1784
+ this.recordDatabaseStatus('getSessionMetadataAsync', cached !== undefined);
1785
+ }
1786
+ catch {
1787
+ this.recordDatabaseStatus('getSessionMetadataAsync', cached !== undefined);
1788
+ }
1789
+ return cached ? { mtime: new Date(cached.timeUpdated) } : null;
1790
+ }
1456
1791
  /**
1457
1792
  * Returns the latest assistant message's token snapshot for context window sizing.
1458
1793
  * Cumulative session totals are derived from readNew() events processed by SessionMonitor.
@@ -1602,6 +1937,21 @@ class OpenCodeProvider {
1602
1937
  this.sessionMetaCache.clear();
1603
1938
  this.dynamicContextWindowLimit = null;
1604
1939
  }
1940
+ recordDatabaseStatus(operation, usable) {
1941
+ const status = this.db?.getRuntimeStatus() ?? this.dbStatus;
1942
+ const diagnostic = (0, diagnostics_1.diagnosticFromRuntimeStatus)(this.id, 'query', status);
1943
+ if (!diagnostic || status.kind === 'db_missing') {
1944
+ // A genuinely absent OpenCode DB is expected for legacy file storage.
1945
+ this.diagnostics.available(operation);
1946
+ return;
1947
+ }
1948
+ this.diagnostics.degraded(operation, status, {
1949
+ kind: diagnostic.kind,
1950
+ severity: diagnostic.severity,
1951
+ phase: diagnostic.phase,
1952
+ message: diagnostic.message,
1953
+ }, usable);
1954
+ }
1605
1955
  // --- z.ai (GLM Coding Plan) compatibility quota derivation ---
1606
1956
  /**
1607
1957
  * Derives a z.ai coding-plan `QuotaState` from OpenCode assistant turns
@@ -17,12 +17,22 @@ export declare class OpenCodeDatabase {
17
17
  open(): boolean;
18
18
  close(): void;
19
19
  private query;
20
+ private queryAsync;
20
21
  private queryOne;
21
22
  findProjectByWorktree(workspacePath: string): DbProject | null;
22
23
  findProjectBySessionDirectory(workspacePath: string): DbProject | null;
23
24
  getAllProjects(): DbProject[];
24
25
  hasProject(projectId: string): boolean;
25
26
  getSessionsForProject(projectId: string): DbSession[];
27
+ /** Get every root session in one query for global preview enumeration. */
28
+ getAllSessions(): DbSession[];
29
+ /** Enumerate root sessions with one non-blocking sqlite3 subprocess. */
30
+ getAllSessionsAsync(): Promise<DbSession[]>;
31
+ /**
32
+ * Resolve many sessions with non-blocking sqlite3 subprocesses — one per
33
+ * chunk of ids, so the inlined query stays clear of argv length limits.
34
+ */
35
+ getSessionsByIdsAsync(sessionIds: readonly string[]): Promise<DbSession[]>;
26
36
  /** Get the most recently updated session for a project (excludes subagent child sessions). */
27
37
  getMostRecentSession(projectId: string): DbSession | null;
28
38
  /** Get child sessions (subagents) whose parent_id matches the given session ID. */
@@ -41,6 +41,11 @@ exports.OpenCodeDatabase = 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 OpenCodeDatabase {
45
50
  dbPath;
46
51
  runtimeStatus = null;
@@ -127,6 +132,23 @@ class OpenCodeDatabase {
127
132
  return [];
128
133
  }
129
134
  }
135
+ async queryAsync(sql, params = []) {
136
+ if (!this.isAvailable()) {
137
+ this.runtimeStatus = { available: false, kind: 'db_missing' };
138
+ return [];
139
+ }
140
+ const query = bindSqlParams(sql, params);
141
+ try {
142
+ const result = await execSqlite(this.dbPath, query);
143
+ this.runtimeStatus = { available: true, kind: 'available' };
144
+ const trimmed = result.trim();
145
+ return trimmed ? JSON.parse(trimmed) : [];
146
+ }
147
+ catch (error) {
148
+ this.runtimeStatus = toRuntimeStatus(error, 'query_failed');
149
+ return [];
150
+ }
151
+ }
130
152
  queryOne(sql, params = []) {
131
153
  const results = this.query(sql, params);
132
154
  return results[0] ?? null;
@@ -181,6 +203,27 @@ class OpenCodeDatabase {
181
203
  getSessionsForProject(projectId) {
182
204
  return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC', [projectId]);
183
205
  }
206
+ /** Get every root session in one query for global preview enumeration. */
207
+ getAllSessions() {
208
+ return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
209
+ }
210
+ /** Enumerate root sessions with one non-blocking sqlite3 subprocess. */
211
+ getAllSessionsAsync() {
212
+ return this.queryAsync('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
213
+ }
214
+ /**
215
+ * Resolve many sessions with non-blocking sqlite3 subprocesses — one per
216
+ * chunk of ids, so the inlined query stays clear of argv length limits.
217
+ */
218
+ async getSessionsByIdsAsync(sessionIds) {
219
+ const results = [];
220
+ for (let i = 0; i < sessionIds.length; i += SQL_ID_CHUNK_SIZE) {
221
+ const chunk = sessionIds.slice(i, i + SQL_ID_CHUNK_SIZE);
222
+ const placeholders = chunk.map(() => '?').join(', ');
223
+ results.push(...(await this.queryAsync(`SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE id IN (${placeholders})`, [...chunk])));
224
+ }
225
+ return results;
226
+ }
184
227
  /** Get the most recently updated session for a project (excludes subagent child sessions). */
185
228
  getMostRecentSession(projectId) {
186
229
  return this.queryOne('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC LIMIT 1', [projectId]);
@@ -315,6 +358,32 @@ class OpenCodeDatabase {
315
358
  }
316
359
  }
317
360
  exports.OpenCodeDatabase = OpenCodeDatabase;
361
+ function bindSqlParams(sql, params) {
362
+ let paramIndex = 0;
363
+ return sql.replace(/\?/g, () => {
364
+ if (paramIndex >= params.length)
365
+ return '?';
366
+ const param = params[paramIndex++];
367
+ if (typeof param === 'number')
368
+ return Number.isFinite(param) ? String(param) : '0';
369
+ return `'${String(param).replace(/'/g, "''")}'`;
370
+ });
371
+ }
372
+ function execSqlite(dbPath, query) {
373
+ return new Promise((resolve, reject) => {
374
+ (0, child_process_1.execFile)('sqlite3', ['-json', '-readonly', dbPath, query], {
375
+ encoding: 'utf8',
376
+ timeout: 4_000,
377
+ killSignal: 'SIGKILL',
378
+ maxBuffer: 50 * 1024 * 1024,
379
+ }, (error, stdout) => {
380
+ if (error)
381
+ reject(error);
382
+ else
383
+ resolve(stdout);
384
+ });
385
+ });
386
+ }
318
387
  function normalizePath(input) {
319
388
  try {
320
389
  return fs.realpathSync(input);