sidekick-shared 0.25.0 → 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.
- package/CHANGELOG.md +57 -0
- package/README.md +49 -3
- package/dist/accountChanges.js +22 -3
- package/dist/accountRegistry.d.ts +22 -1
- package/dist/accountRegistry.js +37 -14
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +11 -1
- package/dist/browser.js +29 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +46 -12
- package/dist/index.js +112 -24
- package/dist/node.d.ts +19 -0
- package/dist/node.js +47 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +3 -8
- package/dist/observedSessionCollector.js +10 -27
- package/dist/parsers/sessionActivityDetector.d.ts +24 -2
- package/dist/parsers/sessionActivityDetector.js +95 -17
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providers/claudeCode.js +11 -96
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +94 -259
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/factory.d.ts +6 -0
- package/dist/providers/openCode.d.ts +2 -0
- package/dist/providers/openCode.js +105 -121
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +30 -0
- package/dist/quota.d.ts +30 -3
- package/dist/quota.js +35 -3
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaResolver.d.ts +81 -0
- package/dist/quotaResolver.js +213 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/quota.d.ts +139 -0
- package/dist/schemas/quota.js +17 -2
- package/dist/schemas/quotaHistory.d.ts +1 -0
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionPreviews.d.ts +7 -1
- package/dist/sessionPreviews.js +15 -3
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +2 -0
- package/dist/types/observedSessionV1.js +75 -18
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- package/package.json +1 -1
|
@@ -48,6 +48,7 @@ const os = __importStar(require("os"));
|
|
|
48
48
|
const path = __importStar(require("path"));
|
|
49
49
|
const string_decoder_1 = require("string_decoder");
|
|
50
50
|
const sessionContext_1 = require("../context/sessionContext");
|
|
51
|
+
const sessionStats_1 = require("../sessionStats");
|
|
51
52
|
const jsonl_1 = require("../parsers/jsonl");
|
|
52
53
|
const diagnostics_1 = require("./diagnostics");
|
|
53
54
|
const sessionPathResolver_1 = require("../parsers/sessionPathResolver");
|
|
@@ -342,16 +343,13 @@ class ClaudeCodeProvider {
|
|
|
342
343
|
const results = [];
|
|
343
344
|
const seen = new Set();
|
|
344
345
|
for (const folder of this.getAllProjectFolders()) {
|
|
345
|
-
|
|
346
|
-
|
|
346
|
+
// The directory listing already stat'ed each file; reuse it instead of
|
|
347
|
+
// stat'ing every session a second time.
|
|
348
|
+
for (const file of (0, sessionPathResolver_1.findSessionFilesWithStats)(folder.dir)) {
|
|
349
|
+
if (seen.has(file.path))
|
|
347
350
|
continue;
|
|
348
|
-
seen.add(
|
|
349
|
-
|
|
350
|
-
results.push({ path: sessionPath, mtime: fs.statSync(sessionPath).mtime });
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
// Skip files that vanish between listing and stat.
|
|
354
|
-
}
|
|
351
|
+
seen.add(file.path);
|
|
352
|
+
results.push({ path: file.path, mtime: file.mtime, sizeBytes: file.sizeBytes });
|
|
355
353
|
}
|
|
356
354
|
}
|
|
357
355
|
this.recordHomeStatus('listAllSessionFiles');
|
|
@@ -535,93 +533,10 @@ class ClaudeCodeProvider {
|
|
|
535
533
|
}
|
|
536
534
|
// --- Stats ---
|
|
537
535
|
readSessionStats(sessionPath) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const tokens = { input: 0, output: 0, cacheWrite: 0, cacheRead: 0 };
|
|
543
|
-
const modelUsage = {};
|
|
544
|
-
const toolUsage = {};
|
|
545
|
-
let compactionEstimate = 0;
|
|
546
|
-
let truncationCount = 0;
|
|
547
|
-
let reportedCost = 0;
|
|
548
|
-
try {
|
|
549
|
-
const content = fs.readFileSync(sessionPath, 'utf8');
|
|
550
|
-
const lines = content.split('\n');
|
|
551
|
-
for (const line of lines) {
|
|
552
|
-
const trimmed = line.trim();
|
|
553
|
-
if (!trimmed || !trimmed.startsWith('{'))
|
|
554
|
-
continue;
|
|
555
|
-
try {
|
|
556
|
-
const event = JSON.parse(trimmed);
|
|
557
|
-
if (!startTime && event.timestamp)
|
|
558
|
-
startTime = event.timestamp;
|
|
559
|
-
if (event.timestamp)
|
|
560
|
-
endTime = event.timestamp;
|
|
561
|
-
if (event.type === 'assistant' && event.message?.usage) {
|
|
562
|
-
messageCount++;
|
|
563
|
-
const u = event.message.usage;
|
|
564
|
-
tokens.input += u.input_tokens || 0;
|
|
565
|
-
tokens.output += u.output_tokens || 0;
|
|
566
|
-
tokens.cacheWrite += u.cache_creation_input_tokens || 0;
|
|
567
|
-
tokens.cacheRead += u.cache_read_input_tokens || 0;
|
|
568
|
-
if (u.reported_cost)
|
|
569
|
-
reportedCost += u.reported_cost;
|
|
570
|
-
const model = event.message.model || 'unknown';
|
|
571
|
-
if (!modelUsage[model])
|
|
572
|
-
modelUsage[model] = { calls: 0, tokens: 0 };
|
|
573
|
-
modelUsage[model].calls++;
|
|
574
|
-
modelUsage[model].tokens += (u.input_tokens || 0) + (u.output_tokens || 0);
|
|
575
|
-
// Check content for tool_use blocks
|
|
576
|
-
if (Array.isArray(event.message.content)) {
|
|
577
|
-
for (const block of event.message.content) {
|
|
578
|
-
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
579
|
-
toolUsage[block.name] = (toolUsage[block.name] || 0) + 1;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
if (event.type === 'user')
|
|
585
|
-
messageCount++;
|
|
586
|
-
if (event.type === 'summary')
|
|
587
|
-
compactionEstimate++;
|
|
588
|
-
// Check for truncation in tool results
|
|
589
|
-
if (event.type === 'user' && Array.isArray(event.message?.content)) {
|
|
590
|
-
for (const block of event.message.content) {
|
|
591
|
-
if (block.type === 'tool_result' && typeof block.content === 'string') {
|
|
592
|
-
for (const pattern of jsonl_1.TRUNCATION_PATTERNS) {
|
|
593
|
-
if (pattern.regex.test(block.content)) {
|
|
594
|
-
truncationCount++;
|
|
595
|
-
break;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
catch {
|
|
603
|
-
// Skip malformed lines
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
catch {
|
|
608
|
-
// Skip unreadable files
|
|
609
|
-
}
|
|
610
|
-
return {
|
|
611
|
-
providerId: 'claude-code',
|
|
612
|
-
sessionId,
|
|
613
|
-
filePath: sessionPath,
|
|
614
|
-
label: this.extractSessionLabel(sessionPath),
|
|
615
|
-
startTime,
|
|
616
|
-
endTime,
|
|
617
|
-
messageCount,
|
|
618
|
-
tokens,
|
|
619
|
-
modelUsage,
|
|
620
|
-
toolUsage,
|
|
621
|
-
compactionEstimate,
|
|
622
|
-
truncationCount,
|
|
623
|
-
reportedCost,
|
|
624
|
-
};
|
|
536
|
+
// One reader pass through the shared aggregator: cache-inclusive per-model
|
|
537
|
+
// totals, cost with provenance, a tool success/failure split, and the label
|
|
538
|
+
// from the first user prompt in the events already read.
|
|
539
|
+
return (0, sessionStats_1.readSessionFileStats)(this, sessionPath);
|
|
625
540
|
}
|
|
626
541
|
readSessionContextSnapshot(sessionPath, options = {}) {
|
|
627
542
|
return (0, sessionContext_1.readSessionContextSnapshot)(this, sessionPath, options);
|
|
@@ -74,6 +74,8 @@ export declare class CodexProvider implements SessionProviderBase {
|
|
|
74
74
|
isSessionFile(filename: string): boolean;
|
|
75
75
|
getSessionId(sessionPath: string): string;
|
|
76
76
|
encodeWorkspacePath(workspacePath: string): string;
|
|
77
|
+
/** Thread title or first user message from the SQLite index; null without it. */
|
|
78
|
+
private extractSessionLabelFromDb;
|
|
77
79
|
extractSessionLabel(sessionPath: string): string | null;
|
|
78
80
|
extractSessionLabelsAsync(sessionPaths: readonly string[]): Promise<Map<string, string | null>>;
|
|
79
81
|
createReader(sessionPath: string): SessionReader;
|
package/dist/providers/codex.js
CHANGED
|
@@ -48,11 +48,12 @@ 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");
|
|
56
57
|
const diagnostics_1 = require("./diagnostics");
|
|
57
58
|
const modelContext_1 = require("../modelContext");
|
|
58
59
|
const observedContextWindows_1 = require("../observedContextWindows");
|
|
@@ -73,28 +74,6 @@ function getCodexHomes() {
|
|
|
73
74
|
function getSessionsDirs() {
|
|
74
75
|
return getCodexHomes().map((home) => path.join(home, 'sessions'));
|
|
75
76
|
}
|
|
76
|
-
/** Test if a filename is a Codex rollout file. */
|
|
77
|
-
function isRolloutFile(filename) {
|
|
78
|
-
return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Extract session UUID from a rollout filename.
|
|
82
|
-
* Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
|
|
83
|
-
*/
|
|
84
|
-
function extractSessionId(filename) {
|
|
85
|
-
const base = path.basename(filename, '.jsonl');
|
|
86
|
-
// rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
|
|
87
|
-
const parts = base.split('-');
|
|
88
|
-
// The UUID is typically the last 5 segments (8-4-4-4-12)
|
|
89
|
-
if (parts.length >= 6) {
|
|
90
|
-
const possibleUuid = parts.slice(-5).join('-');
|
|
91
|
-
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(possibleUuid)) {
|
|
92
|
-
return possibleUuid;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Fallback: use everything after "rollout-"
|
|
96
|
-
return base.replace(/^rollout-/, '');
|
|
97
|
-
}
|
|
98
77
|
/** Truncate a string to maxLen with ellipsis. */
|
|
99
78
|
function truncate(text, maxLen) {
|
|
100
79
|
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
@@ -103,102 +82,16 @@ function truncate(text, maxLen) {
|
|
|
103
82
|
}
|
|
104
83
|
return trimmed;
|
|
105
84
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Recursively find all rollout files under a directory.
|
|
108
|
-
* Handles the YYYY/MM/DD subdirectory structure.
|
|
109
|
-
*/
|
|
85
|
+
/** Every rollout under a directory, newest first (one capped walk). */
|
|
110
86
|
function findRolloutFiles(dir) {
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
if (!fs.existsSync(dir))
|
|
114
|
-
return results;
|
|
115
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
116
|
-
for (const entry of entries) {
|
|
117
|
-
const fullPath = path.join(dir, entry.name);
|
|
118
|
-
if (entry.isDirectory()) {
|
|
119
|
-
results.push(...findRolloutFiles(fullPath));
|
|
120
|
-
}
|
|
121
|
-
else if (entry.isFile() && isRolloutFile(entry.name)) {
|
|
122
|
-
try {
|
|
123
|
-
const stats = fs.statSync(fullPath);
|
|
124
|
-
if (stats.size > 0) {
|
|
125
|
-
results.push({ path: fullPath, mtime: stats.mtime });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Skip inaccessible files
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// Skip inaccessible directories
|
|
136
|
-
}
|
|
137
|
-
return results;
|
|
87
|
+
return (0, rolloutWalker_1.walkRolloutFiles)([dir]);
|
|
138
88
|
}
|
|
139
89
|
function hasRolloutFile(dir) {
|
|
140
|
-
|
|
141
|
-
if (!fs.existsSync(dir))
|
|
142
|
-
return false;
|
|
143
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
144
|
-
const fullPath = path.join(dir, entry.name);
|
|
145
|
-
if (entry.isDirectory() && hasRolloutFile(fullPath))
|
|
146
|
-
return true;
|
|
147
|
-
if (entry.isFile() && isRolloutFile(entry.name))
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
return false;
|
|
90
|
+
return (0, rolloutWalker_1.walkRolloutFiles)([dir], { limit: 1 }).length > 0;
|
|
155
91
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for (const sessionsDir of getSessionsDirs()) {
|
|
160
|
-
for (const file of findRolloutFiles(sessionsDir)) {
|
|
161
|
-
if (seen.has(file.path))
|
|
162
|
-
continue;
|
|
163
|
-
seen.add(file.path);
|
|
164
|
-
results.push(file);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return results;
|
|
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;
|
|
92
|
+
/** Every rollout across the monitored homes, newest first, deduplicated by path. */
|
|
93
|
+
function findRolloutFilesInConfiguredHomes(options = {}) {
|
|
94
|
+
return (0, rolloutWalker_1.walkRolloutFiles)(getSessionsDirs(), options);
|
|
202
95
|
}
|
|
203
96
|
/**
|
|
204
97
|
* Locates the rollout file for a Codex session id by walking
|
|
@@ -218,41 +111,8 @@ function findCodexRolloutFile(sessionId, options = {}) {
|
|
|
218
111
|
const sessionsDirs = options.codexHome
|
|
219
112
|
? [path.join(options.codexHome, 'sessions')]
|
|
220
113
|
: getSessionsDirs();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
for (const candidate of findRolloutFilesById(dir, normalizedId)) {
|
|
224
|
-
if (!best || candidate.mtimeMs > best.mtimeMs)
|
|
225
|
-
best = candidate;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return best?.path ?? null;
|
|
229
|
-
}
|
|
230
|
-
function findRolloutFilesById(dir, normalizedId) {
|
|
231
|
-
const results = [];
|
|
232
|
-
try {
|
|
233
|
-
if (!fs.existsSync(dir))
|
|
234
|
-
return results;
|
|
235
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
236
|
-
const fullPath = path.join(dir, entry.name);
|
|
237
|
-
if (entry.isDirectory()) {
|
|
238
|
-
results.push(...findRolloutFilesById(fullPath, normalizedId));
|
|
239
|
-
}
|
|
240
|
-
else if (entry.isFile() &&
|
|
241
|
-
isRolloutFile(entry.name) &&
|
|
242
|
-
extractSessionId(entry.name).toLowerCase() === normalizedId) {
|
|
243
|
-
try {
|
|
244
|
-
results.push({ path: fullPath, mtimeMs: fs.statSync(fullPath).mtimeMs });
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
// Skip files that vanish between readdir and stat.
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
// Skip inaccessible directories.
|
|
254
|
-
}
|
|
255
|
-
return results;
|
|
114
|
+
// Name-only filter, one walk across the homes, newest modification first.
|
|
115
|
+
return (0, rolloutWalker_1.walkRolloutFiles)(sessionsDirs, { sessionId: normalizedId })[0]?.path ?? null;
|
|
256
116
|
}
|
|
257
117
|
/** Pick the best sessions directory for UI/search entrypoints. */
|
|
258
118
|
function getSessionsDir() {
|
|
@@ -368,15 +228,28 @@ async function readSessionMetaAsync(rolloutPath) {
|
|
|
368
228
|
await handle?.close().catch(() => undefined);
|
|
369
229
|
}
|
|
370
230
|
}
|
|
371
|
-
/**
|
|
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;
|
|
372
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
|
+
}
|
|
373
246
|
function readSessionCwd(rolloutPath) {
|
|
374
247
|
const cached = cwdCache.get(rolloutPath);
|
|
375
248
|
if (cached !== undefined)
|
|
376
249
|
return cached;
|
|
377
250
|
const meta = readSessionMeta(rolloutPath);
|
|
378
251
|
const cwd = meta?.cwd || null;
|
|
379
|
-
|
|
252
|
+
rememberCwd(rolloutPath, cwd);
|
|
380
253
|
return cwd;
|
|
381
254
|
}
|
|
382
255
|
/** Async twin of readSessionCwd, sharing its cache so repeat listings do no content reads. */
|
|
@@ -386,7 +259,7 @@ async function readSessionCwdAsync(rolloutPath) {
|
|
|
386
259
|
return cached;
|
|
387
260
|
const meta = await readSessionMetaAsync(rolloutPath);
|
|
388
261
|
const cwd = meta?.cwd || null;
|
|
389
|
-
|
|
262
|
+
rememberCwd(rolloutPath, cwd);
|
|
390
263
|
return cwd;
|
|
391
264
|
}
|
|
392
265
|
/** Check if a session's CWD matches a workspace path. */
|
|
@@ -833,16 +706,15 @@ class CodexProvider {
|
|
|
833
706
|
}
|
|
834
707
|
}
|
|
835
708
|
// File scan: look for any rollout file whose session_meta.cwd matches
|
|
709
|
+
// (newest first; the cwd read is cached per path).
|
|
836
710
|
const files = findRolloutFilesInConfiguredHomes();
|
|
837
711
|
if (files.length === 0) {
|
|
838
712
|
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
839
713
|
return null;
|
|
840
714
|
}
|
|
841
|
-
// Sort by mtime descending
|
|
842
|
-
files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
843
715
|
for (const file of files) {
|
|
844
|
-
const
|
|
845
|
-
if (
|
|
716
|
+
const cwd = readSessionCwd(file.path);
|
|
717
|
+
if (cwd && cwdMatches(cwd, workspacePath)) {
|
|
846
718
|
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
847
719
|
return path.dirname(file.path);
|
|
848
720
|
}
|
|
@@ -863,12 +735,10 @@ class CodexProvider {
|
|
|
863
735
|
return thread.rollout_path;
|
|
864
736
|
}
|
|
865
737
|
}
|
|
866
|
-
// File scan fallback
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const meta = readSessionMeta(file.path);
|
|
871
|
-
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)) {
|
|
872
742
|
this.recordDatabaseFallback('findActiveSession', true);
|
|
873
743
|
return file.path;
|
|
874
744
|
}
|
|
@@ -889,13 +759,11 @@ class CodexProvider {
|
|
|
889
759
|
return dbPaths;
|
|
890
760
|
}
|
|
891
761
|
}
|
|
892
|
-
// File scan
|
|
893
|
-
const
|
|
894
|
-
files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
895
|
-
const sessions = files
|
|
762
|
+
// File scan (newest first; cwd reads are cached per path)
|
|
763
|
+
const sessions = findRolloutFilesInConfiguredHomes()
|
|
896
764
|
.filter((f) => {
|
|
897
|
-
const
|
|
898
|
-
return
|
|
765
|
+
const cwd = readSessionCwd(f.path);
|
|
766
|
+
return cwd !== null && cwdMatches(cwd, workspacePath);
|
|
899
767
|
})
|
|
900
768
|
.map((f) => f.path);
|
|
901
769
|
this.recordDatabaseFallback('findAllSessions', true);
|
|
@@ -917,9 +785,7 @@ class CodexProvider {
|
|
|
917
785
|
return this.findAllSessions(workspacePath);
|
|
918
786
|
}
|
|
919
787
|
findSessionsInDirectory(dir) {
|
|
920
|
-
|
|
921
|
-
files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
922
|
-
return files.map((f) => f.path);
|
|
788
|
+
return findRolloutFiles(dir).map((f) => f.path);
|
|
923
789
|
}
|
|
924
790
|
listAllSessionFiles() {
|
|
925
791
|
const files = findRolloutFilesInConfiguredHomes();
|
|
@@ -927,23 +793,23 @@ class CodexProvider {
|
|
|
927
793
|
return files;
|
|
928
794
|
}
|
|
929
795
|
async listSessionFilesAsync(workspacePath) {
|
|
930
|
-
const seen = new Set();
|
|
931
796
|
const results = [];
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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))
|
|
935
803
|
continue;
|
|
936
|
-
|
|
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);
|
|
804
|
+
info.workspacePath = cwd;
|
|
944
805
|
}
|
|
806
|
+
results.push(info);
|
|
945
807
|
}
|
|
946
|
-
|
|
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;
|
|
947
813
|
}
|
|
948
814
|
getAllProjectFolders(workspacePath) {
|
|
949
815
|
const folders = [];
|
|
@@ -963,12 +829,17 @@ class CodexProvider {
|
|
|
963
829
|
});
|
|
964
830
|
}
|
|
965
831
|
}
|
|
966
|
-
// 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;
|
|
967
836
|
for (const file of findRolloutFilesInConfiguredHomes()) {
|
|
968
|
-
|
|
969
|
-
|
|
837
|
+
if (db && indexedUpTo > 0 && file.mtime.getTime() <= indexedUpTo)
|
|
838
|
+
continue;
|
|
839
|
+
const cwd = readSessionCwd(file.path);
|
|
840
|
+
if (!cwd)
|
|
970
841
|
continue;
|
|
971
|
-
const existing = seenCwds.get(
|
|
842
|
+
const existing = seenCwds.get(cwd);
|
|
972
843
|
if (existing) {
|
|
973
844
|
// Update mtime if newer
|
|
974
845
|
if (file.mtime > existing.lastModified) {
|
|
@@ -976,10 +847,10 @@ class CodexProvider {
|
|
|
976
847
|
}
|
|
977
848
|
}
|
|
978
849
|
else {
|
|
979
|
-
seenCwds.set(
|
|
850
|
+
seenCwds.set(cwd, {
|
|
980
851
|
dir: path.dirname(file.path),
|
|
981
|
-
name:
|
|
982
|
-
encodedName:
|
|
852
|
+
name: cwd,
|
|
853
|
+
encodedName: cwd,
|
|
983
854
|
sessionCount: 1,
|
|
984
855
|
lastModified: file.mtime,
|
|
985
856
|
});
|
|
@@ -1004,30 +875,36 @@ class CodexProvider {
|
|
|
1004
875
|
}
|
|
1005
876
|
// --- File identification ---
|
|
1006
877
|
isSessionFile(filename) {
|
|
1007
|
-
return isRolloutFile(filename);
|
|
878
|
+
return (0, rolloutWalker_1.isRolloutFile)(filename);
|
|
1008
879
|
}
|
|
1009
880
|
getSessionId(sessionPath) {
|
|
1010
|
-
return
|
|
881
|
+
return (0, rolloutWalker_1.extractRolloutSessionId)(path.basename(sessionPath));
|
|
1011
882
|
}
|
|
1012
883
|
encodeWorkspacePath(workspacePath) {
|
|
1013
884
|
// Codex doesn't encode workspace paths in directory names
|
|
1014
885
|
return workspacePath;
|
|
1015
886
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
887
|
+
/** Thread title or first user message from the SQLite index; null without it. */
|
|
888
|
+
extractSessionLabelFromDb(sessionPath) {
|
|
1018
889
|
const db = this.ensureDb();
|
|
1019
|
-
if (db)
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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);
|
|
1030
901
|
}
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
extractSessionLabel(sessionPath) {
|
|
905
|
+
const fromDb = this.extractSessionLabelFromDb(sessionPath);
|
|
906
|
+
if (fromDb)
|
|
907
|
+
return fromDb;
|
|
1031
908
|
// File fallback: parse first user message
|
|
1032
909
|
const label = extractFirstUserMessage(sessionPath);
|
|
1033
910
|
this.recordDatabaseFallback('extractSessionLabel', true);
|
|
@@ -1091,15 +968,13 @@ class CodexProvider {
|
|
|
1091
968
|
if (forkedThreads.length > 0)
|
|
1092
969
|
return subagents;
|
|
1093
970
|
}
|
|
1094
|
-
// Filesystem fallback: scan
|
|
1095
|
-
//
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const filesToCheck = files.slice(0, 50);
|
|
1099
|
-
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 })) {
|
|
1100
975
|
const meta = readSessionMeta(file.path);
|
|
1101
976
|
if (meta?.forked_from_id === sessionId) {
|
|
1102
|
-
const childId =
|
|
977
|
+
const childId = (0, rolloutWalker_1.extractRolloutSessionId)(path.basename(file.path));
|
|
1103
978
|
const stats = this.buildSubagentStats(childId, file.path, meta.source);
|
|
1104
979
|
if (stats)
|
|
1105
980
|
subagents.push(stats);
|
|
@@ -1227,52 +1102,12 @@ class CodexProvider {
|
|
|
1227
1102
|
}
|
|
1228
1103
|
// --- Stats ---
|
|
1229
1104
|
readSessionStats(sessionPath) {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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),
|
|
1234
1110
|
});
|
|
1235
|
-
try {
|
|
1236
|
-
const reader = this.createReader(sessionPath);
|
|
1237
|
-
for (const event of reader.readAll()) {
|
|
1238
|
-
aggregator.processEvent(event);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
catch (err) {
|
|
1242
|
-
// Return an empty stats shell below. Surface the cause under DEBUG so a
|
|
1243
|
-
// malformed rollout doesn't fail silently during dashboard refreshes.
|
|
1244
|
-
if (process.env.DEBUG)
|
|
1245
|
-
console.error(`CodexProvider.readSessionStats: ${err}`);
|
|
1246
|
-
}
|
|
1247
|
-
const metrics = aggregator.getMetrics();
|
|
1248
|
-
const modelUsage = {};
|
|
1249
|
-
for (const model of metrics.modelStats) {
|
|
1250
|
-
modelUsage[model.model] = { calls: model.calls, tokens: model.tokens };
|
|
1251
|
-
}
|
|
1252
|
-
const toolUsage = {};
|
|
1253
|
-
for (const tool of metrics.toolStats) {
|
|
1254
|
-
toolUsage[tool.name] = tool.successCount + tool.failureCount + tool.pendingCount;
|
|
1255
|
-
}
|
|
1256
|
-
return {
|
|
1257
|
-
providerId: 'codex',
|
|
1258
|
-
sessionId,
|
|
1259
|
-
filePath: sessionPath,
|
|
1260
|
-
label: this.extractSessionLabel(sessionPath),
|
|
1261
|
-
startTime: metrics.sessionStartTime || '',
|
|
1262
|
-
endTime: metrics.lastEventTime || '',
|
|
1263
|
-
messageCount: metrics.messageCount,
|
|
1264
|
-
tokens: {
|
|
1265
|
-
input: metrics.tokens.inputTokens,
|
|
1266
|
-
output: metrics.tokens.outputTokens,
|
|
1267
|
-
cacheWrite: metrics.tokens.cacheWriteTokens,
|
|
1268
|
-
cacheRead: metrics.tokens.cacheReadTokens,
|
|
1269
|
-
},
|
|
1270
|
-
modelUsage,
|
|
1271
|
-
toolUsage,
|
|
1272
|
-
compactionEstimate: metrics.compactionCount,
|
|
1273
|
-
truncationCount: metrics.truncationCount,
|
|
1274
|
-
reportedCost: metrics.tokens.reportedCost,
|
|
1275
|
-
};
|
|
1276
1111
|
}
|
|
1277
1112
|
readSessionContextSnapshot(sessionPath, options = {}) {
|
|
1278
1113
|
return (0, sessionContext_1.readSessionContextSnapshot)(this, sessionPath, options);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* without VS Code dependency (no vscode.workspace.getConfiguration).
|
|
5
5
|
*/
|
|
6
6
|
import type { ProviderId } from './types';
|
|
7
|
+
/** Forget the memoised ranking. Test-only. */
|
|
8
|
+
export declare function _resetProviderDetectionCache(): void;
|
|
7
9
|
/**
|
|
8
10
|
* Returns all provider IDs whose data directories exist on the filesystem.
|
|
9
11
|
* Ordered by most-recent activity first.
|