sidekick-shared 0.24.5 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +205 -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 +170 -0
  7. package/dist/accountRegistry.d.ts +25 -2
  8. package/dist/accountRegistry.js +41 -13
  9. package/dist/accountStatus.d.ts +9 -1
  10. package/dist/accountStatus.js +3 -3
  11. package/dist/accounts.d.ts +3 -1
  12. package/dist/accounts.js +84 -64
  13. package/dist/aggregation/EventAggregator.d.ts +15 -2
  14. package/dist/aggregation/EventAggregator.js +54 -9
  15. package/dist/aggregation/costProvenance.d.ts +14 -0
  16. package/dist/aggregation/costProvenance.js +35 -0
  17. package/dist/aggregation/snapshot.d.ts +13 -2
  18. package/dist/aggregation/snapshot.js +57 -6
  19. package/dist/aggregation/types.d.ts +30 -1
  20. package/dist/analytics/compactionLedger.js +15 -5
  21. package/dist/analytics/qualityScore.js +2 -1
  22. package/dist/browser.d.ts +19 -4
  23. package/dist/browser.js +43 -1
  24. package/dist/codexProfiles.d.ts +3 -1
  25. package/dist/codexProfiles.js +4 -2
  26. package/dist/codexQuota.d.ts +2 -0
  27. package/dist/codexQuota.js +11 -66
  28. package/dist/codexQuotaWatcher.d.ts +6 -0
  29. package/dist/codexQuotaWatcher.js +101 -47
  30. package/dist/failingTools.d.ts +37 -0
  31. package/dist/failingTools.js +70 -0
  32. package/dist/formatters/sessionDump.js +13 -7
  33. package/dist/formatting.d.ts +13 -0
  34. package/dist/formatting.js +35 -0
  35. package/dist/historicalStore.d.ts +47 -0
  36. package/dist/historicalStore.js +312 -0
  37. package/dist/index.d.ts +67 -23
  38. package/dist/index.js +138 -26
  39. package/dist/modelAliases.d.ts +13 -0
  40. package/dist/modelAliases.js +58 -0
  41. package/dist/modelCatalog.d.ts +32 -0
  42. package/dist/modelCatalog.js +145 -0
  43. package/dist/modelContext.d.ts +36 -3
  44. package/dist/modelContext.js +109 -34
  45. package/dist/modelInfo.d.ts +30 -1
  46. package/dist/modelInfo.js +116 -26
  47. package/dist/multiProviderQuotaService.d.ts +8 -0
  48. package/dist/multiProviderQuotaService.js +64 -3
  49. package/dist/node.d.ts +23 -4
  50. package/dist/node.js +50 -1
  51. package/dist/observedContextWindows.d.ts +7 -0
  52. package/dist/observedContextWindows.js +17 -2
  53. package/dist/observedSessionCollector.d.ts +72 -6
  54. package/dist/observedSessionCollector.js +440 -47
  55. package/dist/parsers/codexHistory.d.ts +4 -2
  56. package/dist/parsers/codexHistory.js +9 -2
  57. package/dist/parsers/sessionActivityDetector.d.ts +28 -0
  58. package/dist/parsers/sessionActivityDetector.js +110 -15
  59. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  60. package/dist/parsers/sessionPathResolver.js +13 -10
  61. package/dist/peakHours.d.ts +34 -3
  62. package/dist/peakHours.js +92 -7
  63. package/dist/pricingCatalog.d.ts +7 -0
  64. package/dist/pricingCatalog.js +8 -5
  65. package/dist/providerIds.d.ts +11 -0
  66. package/dist/providerIds.js +9 -0
  67. package/dist/providerQuota.d.ts +3 -1
  68. package/dist/providerQuota.js +3 -0
  69. package/dist/providers/claudeCode.d.ts +10 -1
  70. package/dist/providers/claudeCode.js +211 -131
  71. package/dist/providers/codex.d.ts +20 -1
  72. package/dist/providers/codex.js +308 -244
  73. package/dist/providers/codexDatabase.d.ts +9 -0
  74. package/dist/providers/codexDatabase.js +107 -2
  75. package/dist/providers/detect.d.ts +2 -0
  76. package/dist/providers/detect.js +29 -24
  77. package/dist/providers/diagnostics.d.ts +13 -0
  78. package/dist/providers/diagnostics.js +88 -0
  79. package/dist/providers/factory.d.ts +20 -0
  80. package/dist/providers/factory.js +57 -0
  81. package/dist/providers/openCode.d.ts +22 -1
  82. package/dist/providers/openCode.js +460 -126
  83. package/dist/providers/openCodeDatabase.d.ts +10 -0
  84. package/dist/providers/openCodeDatabase.js +69 -0
  85. package/dist/providers/rolloutWalker.d.ts +53 -0
  86. package/dist/providers/rolloutWalker.js +209 -0
  87. package/dist/providers/types.d.ts +82 -2
  88. package/dist/providers/types.js +4 -0
  89. package/dist/quota.d.ts +34 -4
  90. package/dist/quota.js +37 -9
  91. package/dist/quotaHistory.d.ts +3 -2
  92. package/dist/quotaHistory.js +36 -35
  93. package/dist/quotaPoller.d.ts +13 -1
  94. package/dist/quotaPoller.js +65 -3
  95. package/dist/quotaResolver.d.ts +81 -0
  96. package/dist/quotaResolver.js +213 -0
  97. package/dist/quotaSnapshots.d.ts +15 -2
  98. package/dist/quotaSnapshots.js +25 -3
  99. package/dist/quotaThresholds.d.ts +60 -0
  100. package/dist/quotaThresholds.js +90 -0
  101. package/dist/report/htmlReportGenerator.js +10 -4
  102. package/dist/report/sessionReportInputs.d.ts +23 -0
  103. package/dist/report/sessionReportInputs.js +34 -0
  104. package/dist/schemas/accountManager.d.ts +2 -2
  105. package/dist/schemas/accountManager.js +2 -4
  106. package/dist/schemas/accountStatus.d.ts +3 -0
  107. package/dist/schemas/accountStatus.js +1 -0
  108. package/dist/schemas/index.d.ts +3 -0
  109. package/dist/schemas/index.js +16 -2
  110. package/dist/schemas/observedSessionV1.d.ts +27 -26
  111. package/dist/schemas/observedSessionV1.js +6 -4
  112. package/dist/schemas/quota.d.ts +141 -2
  113. package/dist/schemas/quota.js +20 -8
  114. package/dist/schemas/quotaHistory.d.ts +3 -2
  115. package/dist/schemas/quotaHistory.js +1 -1
  116. package/dist/schemas/sessionEvent.d.ts +6 -0
  117. package/dist/schemas/sessionEvent.js +1 -0
  118. package/dist/schemas/stateFile.d.ts +198 -0
  119. package/dist/schemas/stateFile.js +77 -0
  120. package/dist/schemas/tokenEstimation.d.ts +1 -1
  121. package/dist/schemas/transcript.js +2 -0
  122. package/dist/sessionFingerprint.d.ts +25 -0
  123. package/dist/sessionFingerprint.js +46 -0
  124. package/dist/sessionMonitor.d.ts +15 -0
  125. package/dist/sessionMonitor.js +142 -0
  126. package/dist/sessionPreviews.d.ts +33 -2
  127. package/dist/sessionPreviews.js +333 -6
  128. package/dist/sessionStats.d.ts +51 -0
  129. package/dist/sessionStats.js +166 -0
  130. package/dist/stateFile.d.ts +113 -0
  131. package/dist/stateFile.js +160 -0
  132. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  133. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  134. package/dist/statusline/formatter.d.ts +13 -0
  135. package/dist/statusline/formatter.js +48 -6
  136. package/dist/statusline/index.d.ts +10 -2
  137. package/dist/statusline/index.js +19 -1
  138. package/dist/tokenSummary.d.ts +62 -0
  139. package/dist/tokenSummary.js +72 -0
  140. package/dist/transcript.d.ts +5 -0
  141. package/dist/transcript.js +2 -0
  142. package/dist/types/observedSessionV1.d.ts +8 -2
  143. package/dist/types/observedSessionV1.js +88 -25
  144. package/dist/types/sessionEvent.d.ts +2 -0
  145. package/dist/usage/billingBlocks.d.ts +85 -0
  146. package/dist/usage/billingBlocks.js +153 -0
  147. package/dist/usage/importSessionHistory.d.ts +53 -0
  148. package/dist/usage/importSessionHistory.js +88 -0
  149. package/dist/usage/usageEvents.d.ts +92 -0
  150. package/dist/usage/usageEvents.js +322 -0
  151. package/dist/usage/usageReports.d.ts +76 -0
  152. package/dist/usage/usageReports.js +208 -0
  153. package/dist/watchers/factory.d.ts +6 -0
  154. package/dist/watchers/factory.js +30 -25
  155. package/dist/zaiQuotaApi.d.ts +2 -0
  156. package/dist/zaiQuotaApi.js +2 -0
  157. package/package.json +22 -1
@@ -48,11 +48,13 @@ exports.findCodexRolloutFile = findCodexRolloutFile;
48
48
  const fs = __importStar(require("fs"));
49
49
  const path = __importStar(require("path"));
50
50
  const string_decoder_1 = require("string_decoder");
51
- const EventAggregator_1 = require("../aggregation/EventAggregator");
52
51
  const sessionContext_1 = require("../context/sessionContext");
52
+ const sessionStats_1 = require("../sessionStats");
53
53
  const codexParser_1 = require("../parsers/codexParser");
54
54
  const codexDatabase_1 = require("./codexDatabase");
55
55
  const codexProfiles_1 = require("../codexProfiles");
56
+ const rolloutWalker_1 = require("./rolloutWalker");
57
+ const diagnostics_1 = require("./diagnostics");
56
58
  const modelContext_1 = require("../modelContext");
57
59
  const observedContextWindows_1 = require("../observedContextWindows");
58
60
  const usageNormalization_1 = require("../usageNormalization");
@@ -72,28 +74,6 @@ function getCodexHomes() {
72
74
  function getSessionsDirs() {
73
75
  return getCodexHomes().map((home) => path.join(home, 'sessions'));
74
76
  }
75
- /** Test if a filename is a Codex rollout file. */
76
- function isRolloutFile(filename) {
77
- return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
78
- }
79
- /**
80
- * Extract session UUID from a rollout filename.
81
- * Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
82
- */
83
- function extractSessionId(filename) {
84
- const base = path.basename(filename, '.jsonl');
85
- // rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
86
- const parts = base.split('-');
87
- // The UUID is typically the last 5 segments (8-4-4-4-12)
88
- if (parts.length >= 6) {
89
- const possibleUuid = parts.slice(-5).join('-');
90
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(possibleUuid)) {
91
- return possibleUuid;
92
- }
93
- }
94
- // Fallback: use everything after "rollout-"
95
- return base.replace(/^rollout-/, '');
96
- }
97
77
  /** Truncate a string to maxLen with ellipsis. */
98
78
  function truncate(text, maxLen) {
99
79
  const trimmed = text.trim().replace(/\s+/g, ' ');
@@ -102,68 +82,16 @@ function truncate(text, maxLen) {
102
82
  }
103
83
  return trimmed;
104
84
  }
105
- /**
106
- * Recursively find all rollout files under a directory.
107
- * Handles the YYYY/MM/DD subdirectory structure.
108
- */
85
+ /** Every rollout under a directory, newest first (one capped walk). */
109
86
  function findRolloutFiles(dir) {
110
- const results = [];
111
- try {
112
- if (!fs.existsSync(dir))
113
- return results;
114
- const entries = fs.readdirSync(dir, { withFileTypes: true });
115
- for (const entry of entries) {
116
- const fullPath = path.join(dir, entry.name);
117
- if (entry.isDirectory()) {
118
- results.push(...findRolloutFiles(fullPath));
119
- }
120
- else if (entry.isFile() && isRolloutFile(entry.name)) {
121
- try {
122
- const stats = fs.statSync(fullPath);
123
- if (stats.size > 0) {
124
- results.push({ path: fullPath, mtime: stats.mtime });
125
- }
126
- }
127
- catch {
128
- // Skip inaccessible files
129
- }
130
- }
131
- }
132
- }
133
- catch {
134
- // Skip inaccessible directories
135
- }
136
- return results;
87
+ return (0, rolloutWalker_1.walkRolloutFiles)([dir]);
137
88
  }
138
89
  function hasRolloutFile(dir) {
139
- try {
140
- if (!fs.existsSync(dir))
141
- return false;
142
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
143
- const fullPath = path.join(dir, entry.name);
144
- if (entry.isDirectory() && hasRolloutFile(fullPath))
145
- return true;
146
- if (entry.isFile() && isRolloutFile(entry.name))
147
- return true;
148
- }
149
- }
150
- catch {
151
- return false;
152
- }
153
- return false;
90
+ return (0, rolloutWalker_1.walkRolloutFiles)([dir], { limit: 1 }).length > 0;
154
91
  }
155
- function findRolloutFilesInConfiguredHomes() {
156
- const results = [];
157
- const seen = new Set();
158
- for (const sessionsDir of getSessionsDirs()) {
159
- for (const file of findRolloutFiles(sessionsDir)) {
160
- if (seen.has(file.path))
161
- continue;
162
- seen.add(file.path);
163
- results.push(file);
164
- }
165
- }
166
- return results;
92
+ /** Every rollout across the monitored homes, newest first, deduplicated by path. */
93
+ function findRolloutFilesInConfiguredHomes(options = {}) {
94
+ return (0, rolloutWalker_1.walkRolloutFiles)(getSessionsDirs(), options);
167
95
  }
168
96
  /**
169
97
  * Locates the rollout file for a Codex session id by walking
@@ -183,41 +111,8 @@ function findCodexRolloutFile(sessionId, options = {}) {
183
111
  const sessionsDirs = options.codexHome
184
112
  ? [path.join(options.codexHome, 'sessions')]
185
113
  : getSessionsDirs();
186
- let best = null;
187
- for (const dir of sessionsDirs) {
188
- for (const candidate of findRolloutFilesById(dir, normalizedId)) {
189
- if (!best || candidate.mtimeMs > best.mtimeMs)
190
- best = candidate;
191
- }
192
- }
193
- return best?.path ?? null;
194
- }
195
- function findRolloutFilesById(dir, normalizedId) {
196
- const results = [];
197
- try {
198
- if (!fs.existsSync(dir))
199
- return results;
200
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
201
- const fullPath = path.join(dir, entry.name);
202
- if (entry.isDirectory()) {
203
- results.push(...findRolloutFilesById(fullPath, normalizedId));
204
- }
205
- else if (entry.isFile() &&
206
- isRolloutFile(entry.name) &&
207
- extractSessionId(entry.name).toLowerCase() === normalizedId) {
208
- try {
209
- results.push({ path: fullPath, mtimeMs: fs.statSync(fullPath).mtimeMs });
210
- }
211
- catch {
212
- // Skip files that vanish between readdir and stat.
213
- }
214
- }
215
- }
216
- }
217
- catch {
218
- // Skip inaccessible directories.
219
- }
220
- return results;
114
+ // Name-only filter, one walk across the homes, newest modification first.
115
+ return (0, rolloutWalker_1.walkRolloutFiles)(sessionsDirs, { sessionId: normalizedId })[0]?.path ?? null;
221
116
  }
222
117
  /** Pick the best sessions directory for UI/search entrypoints. */
223
118
  function getSessionsDir() {
@@ -298,15 +193,73 @@ function readSessionMeta(rolloutPath) {
298
193
  }
299
194
  return null;
300
195
  }
301
- /** Read the CWD from a rollout file's session_meta. Cached. */
196
+ async function readSessionMetaAsync(rolloutPath) {
197
+ // Mirrors readSessionMeta()/readFirstLines(): chunk-read until the first
198
+ // line is complete under the same 256 KiB cap — session_meta carries
199
+ // `instructions` and can far exceed a single chunk.
200
+ const maxBytes = 256 * 1024;
201
+ const chunkSize = 64 * 1024;
202
+ let handle = null;
203
+ try {
204
+ handle = await fs.promises.open(rolloutPath, 'r');
205
+ const chunks = [];
206
+ let totalRead = 0;
207
+ let newlineFound = false;
208
+ while (totalRead < maxBytes && !newlineFound) {
209
+ const buffer = Buffer.alloc(Math.min(chunkSize, maxBytes - totalRead));
210
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, totalRead);
211
+ if (bytesRead === 0)
212
+ break;
213
+ const chunk = buffer.subarray(0, bytesRead);
214
+ chunks.push(chunk);
215
+ totalRead += bytesRead;
216
+ newlineFound = chunk.includes(0x0a);
217
+ }
218
+ const firstLine = Buffer.concat(chunks).toString('utf8').split('\n', 1)[0]?.trim();
219
+ if (!firstLine)
220
+ return null;
221
+ const parsed = JSON.parse(firstLine);
222
+ return parsed.type === 'session_meta' ? parsed.payload : null;
223
+ }
224
+ catch {
225
+ return null;
226
+ }
227
+ finally {
228
+ await handle?.close().catch(() => undefined);
229
+ }
230
+ }
231
+ /**
232
+ * Read the CWD from a rollout file's session_meta. Cached per path, bounded
233
+ * so a long-lived host watching a large history cannot grow it without limit
234
+ * (insertion order doubles as an eviction order).
235
+ */
236
+ const CWD_CACHE_LIMIT = 5000;
302
237
  const cwdCache = new Map();
238
+ function rememberCwd(rolloutPath, cwd) {
239
+ if (!cwdCache.has(rolloutPath) && cwdCache.size >= CWD_CACHE_LIMIT) {
240
+ const oldest = cwdCache.keys().next().value;
241
+ if (oldest !== undefined)
242
+ cwdCache.delete(oldest);
243
+ }
244
+ cwdCache.set(rolloutPath, cwd);
245
+ }
303
246
  function readSessionCwd(rolloutPath) {
304
247
  const cached = cwdCache.get(rolloutPath);
305
248
  if (cached !== undefined)
306
249
  return cached;
307
250
  const meta = readSessionMeta(rolloutPath);
308
251
  const cwd = meta?.cwd || null;
309
- cwdCache.set(rolloutPath, cwd);
252
+ rememberCwd(rolloutPath, cwd);
253
+ return cwd;
254
+ }
255
+ /** Async twin of readSessionCwd, sharing its cache so repeat listings do no content reads. */
256
+ async function readSessionCwdAsync(rolloutPath) {
257
+ const cached = cwdCache.get(rolloutPath);
258
+ if (cached !== undefined)
259
+ return cached;
260
+ const meta = await readSessionMetaAsync(rolloutPath);
261
+ const cwd = meta?.cwd || null;
262
+ rememberCwd(rolloutPath, cwd);
310
263
  return cwd;
311
264
  }
312
265
  /** Check if a session's CWD matches a workspace path. */
@@ -333,48 +286,65 @@ function normalizePath(input) {
333
286
  */
334
287
  function extractFirstUserMessage(rolloutPath) {
335
288
  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
- }
289
+ return extractFirstUserMessageFromLines(readFirstLines(rolloutPath, 20));
290
+ }
291
+ catch {
292
+ return null;
293
+ }
294
+ }
295
+ function extractFirstUserMessageFromLines(lines) {
296
+ for (const line of lines) {
297
+ const trimmed = line.trim();
298
+ if (!trimmed)
299
+ continue;
300
+ try {
301
+ const parsed = JSON.parse(trimmed);
302
+ // Check response_item with user message
303
+ if (parsed.type === 'response_item') {
304
+ const payload = parsed.payload;
305
+ if (payload.role === 'user') {
306
+ const content = payload.content;
307
+ if (typeof content === 'string' && content.trim() && !isSystemInjection(content)) {
308
+ return truncate(content, 60);
309
+ }
310
+ if (Array.isArray(content)) {
311
+ for (const part of content) {
312
+ const p = part;
313
+ if (p.text?.trim() && !isSystemInjection(p.text)) {
314
+ return truncate(p.text, 60);
357
315
  }
358
316
  }
359
317
  }
360
318
  }
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
319
  }
369
- catch {
370
- // Skip malformed lines
320
+ // Check event_msg with user_message
321
+ if (parsed.type === 'event_msg') {
322
+ const payload = parsed.payload;
323
+ if (payload.type === 'user_message' && payload.message?.trim()) {
324
+ return truncate(payload.message, 60);
325
+ }
371
326
  }
372
327
  }
373
- return null;
328
+ catch {
329
+ // Skip malformed lines
330
+ }
331
+ }
332
+ return null;
333
+ }
334
+ async function extractFirstUserMessageAsync(rolloutPath) {
335
+ let handle = null;
336
+ try {
337
+ handle = await fs.promises.open(rolloutPath, 'r');
338
+ const buffer = Buffer.alloc(256 * 1024);
339
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
340
+ return extractFirstUserMessageFromLines(buffer.toString('utf8', 0, bytesRead).split('\n').slice(0, 20));
374
341
  }
375
342
  catch {
376
343
  return null;
377
344
  }
345
+ finally {
346
+ await handle?.close().catch(() => undefined);
347
+ }
378
348
  }
379
349
  /** Extract searchable text from a rollout line. */
380
350
  function extractSearchableText(line) {
@@ -661,9 +631,14 @@ class CodexProvider {
661
631
  displayName = 'Codex CLI';
662
632
  db = null;
663
633
  dbInitialized = false;
634
+ dbStatus = { available: false, kind: 'db_missing' };
635
+ diagnostics;
664
636
  dynamicContextWindowLimit = null;
665
637
  lastRateLimitsData = null;
666
638
  lastTokenUsageData = null;
639
+ constructor(options = {}) {
640
+ this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
641
+ }
667
642
  // --- Database ---
668
643
  /** Lazy-initialize database connection. */
669
644
  ensureDb() {
@@ -675,8 +650,34 @@ class CodexProvider {
675
650
  if (db.isAvailable() && db.open()) {
676
651
  this.db = db;
677
652
  }
653
+ if (typeof db.getRuntimeStatus === 'function') {
654
+ this.dbStatus = db.getRuntimeStatus();
655
+ }
678
656
  return this.db;
679
657
  }
658
+ async ensureDbAsync() {
659
+ if (this.db)
660
+ return this.db;
661
+ return new codexDatabase_1.CodexDatabase(getCodexHome());
662
+ }
663
+ /**
664
+ * Cache an async-created database only after an operation proved it usable.
665
+ * Sync paths treat a cached `db` as "open() succeeded", so adopting an
666
+ * unproven instance would silently disable their file fallbacks.
667
+ */
668
+ adoptDbIfAvailable(db) {
669
+ if (this.db || !db.getRuntimeStatus().available)
670
+ return;
671
+ this.db = db;
672
+ this.dbInitialized = true;
673
+ }
674
+ getRuntimeStatus() {
675
+ const db = this.ensureDb();
676
+ return db && typeof db.getRuntimeStatus === 'function' ? db.getRuntimeStatus() : this.dbStatus;
677
+ }
678
+ getLastOperationStatus() {
679
+ return this.diagnostics.getLastOperationStatus();
680
+ }
680
681
  // --- Path resolution ---
681
682
  getSessionDirectory(workspacePath) {
682
683
  // Codex doesn't use workspace-encoded directories like Claude Code.
@@ -686,10 +687,13 @@ class CodexProvider {
686
687
  if (db) {
687
688
  const thread = db.getMostRecentThread(workspacePath);
688
689
  if (thread?.rollout_path) {
690
+ this.recordDatabaseFallback('getSessionDirectory', true);
689
691
  return path.dirname(thread.rollout_path);
690
692
  }
691
693
  }
692
- return getSessionsDir();
694
+ const directory = getSessionsDir();
695
+ this.recordDatabaseFallback('getSessionDirectory', true);
696
+ return directory;
693
697
  }
694
698
  discoverSessionDirectory(workspacePath) {
695
699
  // DB: find latest rollout directory for this workspace
@@ -697,23 +701,28 @@ class CodexProvider {
697
701
  if (db) {
698
702
  const thread = db.getMostRecentThread(workspacePath);
699
703
  if (thread?.rollout_path && fs.existsSync(thread.rollout_path)) {
704
+ this.recordDatabaseFallback('discoverSessionDirectory', true);
700
705
  return path.dirname(thread.rollout_path);
701
706
  }
702
707
  }
703
708
  // File scan: look for any rollout file whose session_meta.cwd matches
709
+ // (newest first; the cwd read is cached per path).
704
710
  const files = findRolloutFilesInConfiguredHomes();
705
- if (files.length === 0)
711
+ if (files.length === 0) {
712
+ this.recordDatabaseFallback('discoverSessionDirectory', true);
706
713
  return null;
707
- // Sort by mtime descending
708
- files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
714
+ }
709
715
  for (const file of files) {
710
- const meta = readSessionMeta(file.path);
711
- if (meta && cwdMatches(meta.cwd, workspacePath)) {
716
+ const cwd = readSessionCwd(file.path);
717
+ if (cwd && cwdMatches(cwd, workspacePath)) {
718
+ this.recordDatabaseFallback('discoverSessionDirectory', true);
712
719
  return path.dirname(file.path);
713
720
  }
714
721
  }
715
722
  // Fallback: return the most relevant base sessions dir if one exists
716
- return getSessionsDir();
723
+ const directory = getSessionsDir();
724
+ this.recordDatabaseFallback('discoverSessionDirectory', true);
725
+ return directory;
717
726
  }
718
727
  // --- Session discovery ---
719
728
  findActiveSession(workspacePath) {
@@ -722,18 +731,19 @@ class CodexProvider {
722
731
  if (db) {
723
732
  const thread = db.getMostRecentThread(workspacePath);
724
733
  if (thread?.rollout_path && fs.existsSync(thread.rollout_path)) {
734
+ this.recordDatabaseFallback('findActiveSession', true);
725
735
  return thread.rollout_path;
726
736
  }
727
737
  }
728
- // File scan fallback
729
- const files = findRolloutFilesInConfiguredHomes();
730
- files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
731
- for (const file of files) {
732
- const meta = readSessionMeta(file.path);
733
- if (meta && cwdMatches(meta.cwd, workspacePath)) {
738
+ // File scan fallback (newest first; cwd reads are cached per path)
739
+ for (const file of findRolloutFilesInConfiguredHomes()) {
740
+ const cwd = readSessionCwd(file.path);
741
+ if (cwd && cwdMatches(cwd, workspacePath)) {
742
+ this.recordDatabaseFallback('findActiveSession', true);
734
743
  return file.path;
735
744
  }
736
745
  }
746
+ this.recordDatabaseFallback('findActiveSession', true);
737
747
  return null;
738
748
  }
739
749
  findAllSessions(workspacePath) {
@@ -744,30 +754,62 @@ class CodexProvider {
744
754
  const dbPaths = threads
745
755
  .filter((t) => t.rollout_path && fs.existsSync(t.rollout_path))
746
756
  .map((t) => t.rollout_path);
747
- if (dbPaths.length > 0)
757
+ if (dbPaths.length > 0) {
758
+ this.recordDatabaseFallback('findAllSessions', true);
748
759
  return dbPaths;
760
+ }
749
761
  }
750
- // File scan
751
- const files = findRolloutFilesInConfiguredHomes();
752
- files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
753
- return files
762
+ // File scan (newest first; cwd reads are cached per path)
763
+ const sessions = findRolloutFilesInConfiguredHomes()
754
764
  .filter((f) => {
755
- const meta = readSessionMeta(f.path);
756
- return meta && cwdMatches(meta.cwd, workspacePath);
765
+ const cwd = readSessionCwd(f.path);
766
+ return cwd !== null && cwdMatches(cwd, workspacePath);
757
767
  })
758
768
  .map((f) => f.path);
769
+ this.recordDatabaseFallback('findAllSessions', true);
770
+ return sessions;
771
+ }
772
+ findSessionById(_workspacePath, sessionId) {
773
+ try {
774
+ const found = findCodexRolloutFile(sessionId);
775
+ this.recordDatabaseFallback('findSessionById', true);
776
+ return found;
777
+ }
778
+ catch {
779
+ this.recordDatabaseFallback('findSessionById', false);
780
+ return null;
781
+ }
759
782
  }
760
783
  /** Backward-compatible alias for findAllSessions. */
761
784
  findSessionFiles(workspacePath) {
762
785
  return this.findAllSessions(workspacePath);
763
786
  }
764
787
  findSessionsInDirectory(dir) {
765
- const files = findRolloutFiles(dir);
766
- files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
767
- return files.map((f) => f.path);
788
+ return findRolloutFiles(dir).map((f) => f.path);
768
789
  }
769
790
  listAllSessionFiles() {
770
- return findRolloutFilesInConfiguredHomes();
791
+ const files = findRolloutFilesInConfiguredHomes();
792
+ this.recordDatabaseFallback('listAllSessionFiles', true);
793
+ return files;
794
+ }
795
+ async listSessionFilesAsync(workspacePath) {
796
+ const results = [];
797
+ // One capped walk across the homes (deduplicated, newest first).
798
+ for (const file of await (0, rolloutWalker_1.walkRolloutFilesAsync)(getSessionsDirs())) {
799
+ const info = { ...file };
800
+ if (workspacePath) {
801
+ const cwd = await readSessionCwdAsync(file.path);
802
+ if (!cwd || !cwdMatches(cwd, workspacePath))
803
+ continue;
804
+ info.workspacePath = cwd;
805
+ }
806
+ results.push(info);
807
+ }
808
+ // Filesystem enumeration bypasses the database, so record that here as
809
+ // the sync listing paths do; otherwise `getLastOperationStatus()` keeps
810
+ // describing whichever operation ran before this one.
811
+ this.recordDatabaseFallback('listSessionFilesAsync', true);
812
+ return results;
771
813
  }
772
814
  getAllProjectFolders(workspacePath) {
773
815
  const folders = [];
@@ -787,12 +829,17 @@ class CodexProvider {
787
829
  });
788
830
  }
789
831
  }
790
- // File scan supplement
832
+ // File scan supplement. When the index answered, only rollouts written
833
+ // after its last update can be missing from it, so older files are not
834
+ // opened; the cwd read itself is cached per path either way.
835
+ const indexedUpTo = db ? db.getDbMtime() : 0;
791
836
  for (const file of findRolloutFilesInConfiguredHomes()) {
792
- const meta = readSessionMeta(file.path);
793
- if (!meta?.cwd)
837
+ if (db && indexedUpTo > 0 && file.mtime.getTime() <= indexedUpTo)
838
+ continue;
839
+ const cwd = readSessionCwd(file.path);
840
+ if (!cwd)
794
841
  continue;
795
- const existing = seenCwds.get(meta.cwd);
842
+ const existing = seenCwds.get(cwd);
796
843
  if (existing) {
797
844
  // Update mtime if newer
798
845
  if (file.mtime > existing.lastModified) {
@@ -800,10 +847,10 @@ class CodexProvider {
800
847
  }
801
848
  }
802
849
  else {
803
- seenCwds.set(meta.cwd, {
850
+ seenCwds.set(cwd, {
804
851
  dir: path.dirname(file.path),
805
- name: meta.cwd,
806
- encodedName: meta.cwd,
852
+ name: cwd,
853
+ encodedName: cwd,
807
854
  sessionCount: 1,
808
855
  lastModified: file.mtime,
809
856
  });
@@ -823,32 +870,69 @@ class CodexProvider {
823
870
  }
824
871
  return b.lastModified.getTime() - a.lastModified.getTime();
825
872
  });
873
+ this.recordDatabaseFallback('getAllProjectFolders', true);
826
874
  return folders;
827
875
  }
828
876
  // --- File identification ---
829
877
  isSessionFile(filename) {
830
- return isRolloutFile(filename);
878
+ return (0, rolloutWalker_1.isRolloutFile)(filename);
831
879
  }
832
880
  getSessionId(sessionPath) {
833
- return extractSessionId(path.basename(sessionPath));
881
+ return (0, rolloutWalker_1.extractRolloutSessionId)(path.basename(sessionPath));
834
882
  }
835
883
  encodeWorkspacePath(workspacePath) {
836
884
  // Codex doesn't encode workspace paths in directory names
837
885
  return workspacePath;
838
886
  }
839
- extractSessionLabel(sessionPath) {
840
- // DB first
887
+ /** Thread title or first user message from the SQLite index; null without it. */
888
+ extractSessionLabelFromDb(sessionPath) {
841
889
  const db = this.ensureDb();
842
- if (db) {
843
- const sessionId = this.getSessionId(sessionPath);
844
- const thread = db.getThread(sessionId);
845
- if (thread?.title)
846
- return truncate(thread.title, 60);
847
- if (thread?.first_user_message)
848
- return truncate(thread.first_user_message, 60);
890
+ if (!db)
891
+ return null;
892
+ const sessionId = this.getSessionId(sessionPath);
893
+ const thread = db.getThread(sessionId);
894
+ if (thread?.title) {
895
+ this.recordDatabaseFallback('extractSessionLabel', true);
896
+ return truncate(thread.title, 60);
897
+ }
898
+ if (thread?.first_user_message) {
899
+ this.recordDatabaseFallback('extractSessionLabel', true);
900
+ return truncate(thread.first_user_message, 60);
849
901
  }
902
+ return null;
903
+ }
904
+ extractSessionLabel(sessionPath) {
905
+ const fromDb = this.extractSessionLabelFromDb(sessionPath);
906
+ if (fromDb)
907
+ return fromDb;
850
908
  // File fallback: parse first user message
851
- return extractFirstUserMessage(sessionPath);
909
+ const label = extractFirstUserMessage(sessionPath);
910
+ this.recordDatabaseFallback('extractSessionLabel', true);
911
+ return label;
912
+ }
913
+ async extractSessionLabelsAsync(sessionPaths) {
914
+ const results = new Map();
915
+ const byId = new Map(sessionPaths.map((sessionPath) => [this.getSessionId(sessionPath), sessionPath]));
916
+ const db = await this.ensureDbAsync();
917
+ const threads = await db.getThreadsByIdsAsync([...byId.keys()]);
918
+ for (const thread of threads) {
919
+ const sessionPath = byId.get(thread.id);
920
+ if (!sessionPath)
921
+ continue;
922
+ const label = thread.title ?? thread.first_user_message;
923
+ if (label)
924
+ results.set(sessionPath, truncate(label, 60));
925
+ }
926
+ this.dbStatus = db.getRuntimeStatus();
927
+ this.adoptDbIfAvailable(db);
928
+ for (const sessionPath of sessionPaths) {
929
+ if (!results.has(sessionPath)) {
930
+ results.set(sessionPath, await extractFirstUserMessageAsync(sessionPath));
931
+ await new Promise((resolve) => setImmediate(resolve));
932
+ }
933
+ }
934
+ this.recordDatabaseFallback('extractSessionLabelsAsync', true);
935
+ return results;
852
936
  }
853
937
  // --- Data reading ---
854
938
  createReader(sessionPath) {
@@ -884,15 +968,13 @@ class CodexProvider {
884
968
  if (forkedThreads.length > 0)
885
969
  return subagents;
886
970
  }
887
- // Filesystem fallback: scan all rollout files for matching forked_from_id.
888
- // Limited to recent files (last 50) to avoid excessive I/O.
889
- const files = findRolloutFilesInConfiguredHomes();
890
- files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
891
- const filesToCheck = files.slice(0, 50);
892
- for (const file of filesToCheck) {
971
+ // Filesystem fallback: scan recent rollout files for a matching
972
+ // forked_from_id. The walk stops after the 50 newest-dated files instead
973
+ // of enumerating the whole history and slicing afterwards.
974
+ for (const file of findRolloutFilesInConfiguredHomes({ limit: 50 })) {
893
975
  const meta = readSessionMeta(file.path);
894
976
  if (meta?.forked_from_id === sessionId) {
895
- const childId = extractSessionId(path.basename(file.path));
977
+ const childId = (0, rolloutWalker_1.extractRolloutSessionId)(path.basename(file.path));
896
978
  const stats = this.buildSubagentStats(childId, file.path, meta.source);
897
979
  if (stats)
898
980
  subagents.push(stats);
@@ -1015,54 +1097,17 @@ class CodexProvider {
1015
1097
  getProjectsBaseDir() {
1016
1098
  return getSessionsDir();
1017
1099
  }
1100
+ getWatchRoots() {
1101
+ return getSessionsDirs();
1102
+ }
1018
1103
  // --- Stats ---
1019
1104
  readSessionStats(sessionPath) {
1020
- const sessionId = extractSessionId(path.basename(sessionPath));
1021
- const aggregator = new EventAggregator_1.EventAggregator({
1022
- providerId: 'codex',
1023
- computeContextSize: (usage) => usage.inputTokens,
1105
+ // One reader pass computes stats and the label: the SQLite thread title is
1106
+ // used when the index is available, otherwise the first user prompt comes
1107
+ // from the events already read, so the rollout is never opened twice.
1108
+ return (0, sessionStats_1.readSessionFileStats)(this, sessionPath, {
1109
+ resolveLabel: () => this.extractSessionLabelFromDb(sessionPath),
1024
1110
  });
1025
- try {
1026
- const reader = this.createReader(sessionPath);
1027
- for (const event of reader.readAll()) {
1028
- aggregator.processEvent(event);
1029
- }
1030
- }
1031
- catch (err) {
1032
- // Return an empty stats shell below. Surface the cause under DEBUG so a
1033
- // malformed rollout doesn't fail silently during dashboard refreshes.
1034
- if (process.env.DEBUG)
1035
- console.error(`CodexProvider.readSessionStats: ${err}`);
1036
- }
1037
- const metrics = aggregator.getMetrics();
1038
- const modelUsage = {};
1039
- for (const model of metrics.modelStats) {
1040
- modelUsage[model.model] = { calls: model.calls, tokens: model.tokens };
1041
- }
1042
- const toolUsage = {};
1043
- for (const tool of metrics.toolStats) {
1044
- toolUsage[tool.name] = tool.successCount + tool.failureCount + tool.pendingCount;
1045
- }
1046
- return {
1047
- providerId: 'codex',
1048
- sessionId,
1049
- filePath: sessionPath,
1050
- label: this.extractSessionLabel(sessionPath),
1051
- startTime: metrics.sessionStartTime || '',
1052
- endTime: metrics.lastEventTime || '',
1053
- messageCount: metrics.messageCount,
1054
- tokens: {
1055
- input: metrics.tokens.inputTokens,
1056
- output: metrics.tokens.outputTokens,
1057
- cacheWrite: metrics.tokens.cacheWriteTokens,
1058
- cacheRead: metrics.tokens.cacheReadTokens,
1059
- },
1060
- modelUsage,
1061
- toolUsage,
1062
- compactionEstimate: metrics.compactionCount,
1063
- truncationCount: metrics.truncationCount,
1064
- reportedCost: metrics.tokens.reportedCost,
1065
- };
1066
1111
  }
1067
1112
  readSessionContextSnapshot(sessionPath, options = {}) {
1068
1113
  return (0, sessionContext_1.readSessionContextSnapshot)(this, sessionPath, options);
@@ -1094,9 +1139,28 @@ class CodexProvider {
1094
1139
  this.db?.close();
1095
1140
  this.db = null;
1096
1141
  this.dbInitialized = false;
1142
+ this.dbStatus = { available: false, kind: 'db_missing' };
1097
1143
  this.dynamicContextWindowLimit = null;
1098
1144
  this.lastRateLimitsData = null;
1099
1145
  this.lastTokenUsageData = null;
1100
1146
  }
1147
+ recordDatabaseFallback(operation, usable) {
1148
+ if (!this.dbInitialized) {
1149
+ this.diagnostics.available(operation);
1150
+ return;
1151
+ }
1152
+ const status = this.getRuntimeStatus();
1153
+ const diagnostic = (0, diagnostics_1.diagnosticFromRuntimeStatus)(this.id, 'query', status);
1154
+ if (!diagnostic) {
1155
+ this.diagnostics.available(operation);
1156
+ return;
1157
+ }
1158
+ this.diagnostics.degraded(operation, status, {
1159
+ kind: diagnostic.kind,
1160
+ severity: diagnostic.severity,
1161
+ phase: diagnostic.phase,
1162
+ message: diagnostic.message,
1163
+ }, usable);
1164
+ }
1101
1165
  }
1102
1166
  exports.CodexProvider = CodexProvider;