sidekick-shared 0.27.6 → 0.28.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 +28 -0
- package/README.md +58 -18
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +2 -9
- package/dist/claudeProfiles.js +10 -25
- package/dist/codexPaths.d.ts +22 -0
- package/dist/codexPaths.js +67 -0
- package/dist/codexProfiles.d.ts +1 -4
- package/dist/codexProfiles.js +5 -43
- package/dist/doctor.js +2 -2
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -15
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only SQLite wrapper for OpenCode's database.
|
|
3
|
+
* Ported from sidekick-vscode/src/services/providers/OpenCodeDatabase.ts
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { execFile, execFileSync } from 'child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Ids per inlined `IN (…)` query. Values are passed inside a single argv
|
|
10
|
+
* element to sqlite3, so an unbounded list would hit E2BIG on large stores.
|
|
11
|
+
*/
|
|
12
|
+
const SQL_ID_CHUNK_SIZE = 400;
|
|
13
|
+
export class OpenCodeDatabase {
|
|
14
|
+
dbPath;
|
|
15
|
+
runtimeStatus = null;
|
|
16
|
+
constructor(dataDir) {
|
|
17
|
+
this.dbPath = path.join(dataDir, 'opencode.db');
|
|
18
|
+
}
|
|
19
|
+
isAvailable() {
|
|
20
|
+
return fs.existsSync(this.dbPath);
|
|
21
|
+
}
|
|
22
|
+
getRuntimeStatus() {
|
|
23
|
+
if (this.runtimeStatus)
|
|
24
|
+
return this.runtimeStatus;
|
|
25
|
+
if (!this.isAvailable()) {
|
|
26
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
27
|
+
return this.runtimeStatus;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
available: false,
|
|
31
|
+
kind: 'query_failed',
|
|
32
|
+
message: 'OpenCode database has not been initialized yet.',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
open() {
|
|
36
|
+
if (this.runtimeStatus?.kind === 'available')
|
|
37
|
+
return true;
|
|
38
|
+
if (this.runtimeStatus &&
|
|
39
|
+
['db_missing', 'sqlite_missing', 'sqlite_blocked'].includes(this.runtimeStatus.kind)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (!this.isAvailable()) {
|
|
43
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
execFileSync('sqlite3', ['--version'], {
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
timeout: 4000,
|
|
50
|
+
killSignal: 'SIGKILL',
|
|
51
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
+
});
|
|
53
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.runtimeStatus = toRuntimeStatus(error);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
close() { }
|
|
62
|
+
query(sql, params = []) {
|
|
63
|
+
if (!this.open())
|
|
64
|
+
return [];
|
|
65
|
+
let paramIndex = 0;
|
|
66
|
+
const query = sql.replace(/\?/g, () => {
|
|
67
|
+
if (paramIndex >= params.length)
|
|
68
|
+
return '?';
|
|
69
|
+
const param = params[paramIndex++];
|
|
70
|
+
if (typeof param === 'number')
|
|
71
|
+
return Number.isFinite(param) ? String(param) : '0';
|
|
72
|
+
const escaped = String(param).replace(/'/g, "''");
|
|
73
|
+
return `'${escaped}'`;
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
const result = execFileSync('sqlite3', ['-json', '-readonly', this.dbPath, query], {
|
|
77
|
+
encoding: 'utf-8',
|
|
78
|
+
timeout: 4000,
|
|
79
|
+
killSignal: 'SIGKILL',
|
|
80
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
82
|
+
});
|
|
83
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
84
|
+
const trimmed = result.trim();
|
|
85
|
+
if (!trimmed)
|
|
86
|
+
return [];
|
|
87
|
+
return JSON.parse(trimmed);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
this.runtimeStatus = {
|
|
91
|
+
...toRuntimeStatus(error, 'query_failed'),
|
|
92
|
+
// The database and sqlite binary were successfully opened. A failed
|
|
93
|
+
// query is transient and must not gate the next attempt.
|
|
94
|
+
available: true,
|
|
95
|
+
};
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async queryAsync(sql, params = []) {
|
|
100
|
+
if (!this.isAvailable()) {
|
|
101
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const query = bindSqlParams(sql, params);
|
|
105
|
+
try {
|
|
106
|
+
const result = await execSqlite(this.dbPath, query);
|
|
107
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
108
|
+
const trimmed = result.trim();
|
|
109
|
+
return trimmed ? JSON.parse(trimmed) : [];
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.runtimeStatus = toRuntimeStatus(error, 'query_failed');
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
queryOne(sql, params = []) {
|
|
117
|
+
const results = this.query(sql, params);
|
|
118
|
+
return results[0] ?? null;
|
|
119
|
+
}
|
|
120
|
+
findProjectByWorktree(workspacePath) {
|
|
121
|
+
const normalized = normalizePath(workspacePath);
|
|
122
|
+
const all = this.getAllProjects();
|
|
123
|
+
let best = null;
|
|
124
|
+
let bestScore = -1;
|
|
125
|
+
for (const project of all) {
|
|
126
|
+
const worktreeScore = pathMatchScore(project.worktree, normalized);
|
|
127
|
+
if (worktreeScore > bestScore) {
|
|
128
|
+
best = project;
|
|
129
|
+
bestScore = worktreeScore;
|
|
130
|
+
}
|
|
131
|
+
for (const sandbox of project.sandboxes) {
|
|
132
|
+
const sandboxScore = pathMatchScore(sandbox, normalized);
|
|
133
|
+
if (sandboxScore > bestScore) {
|
|
134
|
+
best = project;
|
|
135
|
+
bestScore = sandboxScore;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return bestScore >= 0 ? best : null;
|
|
140
|
+
}
|
|
141
|
+
findProjectBySessionDirectory(workspacePath) {
|
|
142
|
+
const normalized = normalizePath(workspacePath);
|
|
143
|
+
const sessions = this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
|
|
144
|
+
let bestProjectId = null;
|
|
145
|
+
let bestScore = -1;
|
|
146
|
+
for (const session of sessions) {
|
|
147
|
+
const score = pathMatchScore(session.directory, normalized);
|
|
148
|
+
if (score > bestScore) {
|
|
149
|
+
bestProjectId = session.project_id;
|
|
150
|
+
bestScore = score;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!bestProjectId)
|
|
154
|
+
return null;
|
|
155
|
+
return this.getAllProjects().find((project) => project.id === bestProjectId) ?? null;
|
|
156
|
+
}
|
|
157
|
+
getAllProjects() {
|
|
158
|
+
return this.query('SELECT id, worktree, name, sandboxes, time_created, time_updated FROM project').map((project) => ({
|
|
159
|
+
...project,
|
|
160
|
+
sandboxes: parseStringArray(project.sandboxes),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
hasProject(projectId) {
|
|
164
|
+
return (this.queryOne('SELECT id FROM project WHERE id = ? LIMIT 1', [projectId]) !==
|
|
165
|
+
null);
|
|
166
|
+
}
|
|
167
|
+
getSessionsForProject(projectId) {
|
|
168
|
+
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]);
|
|
169
|
+
}
|
|
170
|
+
/** Get every root session in one query for global preview enumeration. */
|
|
171
|
+
getAllSessions() {
|
|
172
|
+
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');
|
|
173
|
+
}
|
|
174
|
+
/** Enumerate root sessions with one non-blocking sqlite3 subprocess. */
|
|
175
|
+
getAllSessionsAsync() {
|
|
176
|
+
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');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Resolve many sessions with non-blocking sqlite3 subprocesses — one per
|
|
180
|
+
* chunk of ids, so the inlined query stays clear of argv length limits.
|
|
181
|
+
*/
|
|
182
|
+
async getSessionsByIdsAsync(sessionIds) {
|
|
183
|
+
const results = [];
|
|
184
|
+
for (let i = 0; i < sessionIds.length; i += SQL_ID_CHUNK_SIZE) {
|
|
185
|
+
const chunk = sessionIds.slice(i, i + SQL_ID_CHUNK_SIZE);
|
|
186
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
187
|
+
results.push(...(await this.queryAsync(`SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE id IN (${placeholders})`, [...chunk])));
|
|
188
|
+
}
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
/** Get the most recently updated session for a project (excludes subagent child sessions). */
|
|
192
|
+
getMostRecentSession(projectId) {
|
|
193
|
+
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]);
|
|
194
|
+
}
|
|
195
|
+
/** Get child sessions (subagents) whose parent_id matches the given session ID. */
|
|
196
|
+
getChildSessions(parentSessionId) {
|
|
197
|
+
return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id = ? ORDER BY time_created ASC', [parentSessionId]);
|
|
198
|
+
}
|
|
199
|
+
getSession(sessionId) {
|
|
200
|
+
return this.queryOne('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE id = ?', [sessionId]);
|
|
201
|
+
}
|
|
202
|
+
getMessagesForSession(sessionId) {
|
|
203
|
+
return this.query('SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ? ORDER BY time_created ASC', [sessionId]);
|
|
204
|
+
}
|
|
205
|
+
/** Get specific messages for a session by message IDs. */
|
|
206
|
+
getMessagesByIds(sessionId, messageIds) {
|
|
207
|
+
if (messageIds.length === 0)
|
|
208
|
+
return [];
|
|
209
|
+
const placeholders = messageIds.map(() => '?').join(', ');
|
|
210
|
+
return this.query(`SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ? AND id IN (${placeholders}) ORDER BY time_created ASC`, [sessionId, ...messageIds]);
|
|
211
|
+
}
|
|
212
|
+
/** Get messages newer than a given time_updated timestamp (ms epoch). */
|
|
213
|
+
getMessagesNewerThan(sessionId, afterTimeUpdated) {
|
|
214
|
+
return this.query('SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ? AND time_updated > ? ORDER BY time_created ASC', [sessionId, afterTimeUpdated]);
|
|
215
|
+
}
|
|
216
|
+
/** Get the latest message time_updated for a session. */
|
|
217
|
+
getLatestMessageTimeUpdated(sessionId) {
|
|
218
|
+
const row = this.queryOne('SELECT COALESCE(MAX(time_updated), 0) AS maxTimeUpdated FROM message WHERE session_id = ?', [sessionId]);
|
|
219
|
+
return row?.maxTimeUpdated ?? 0;
|
|
220
|
+
}
|
|
221
|
+
/** Get user message IDs that already have an assistant child message. */
|
|
222
|
+
getProcessedUserMessageIds(sessionId, userMessageIds) {
|
|
223
|
+
if (userMessageIds.length === 0)
|
|
224
|
+
return [];
|
|
225
|
+
const placeholders = userMessageIds.map(() => '?').join(', ');
|
|
226
|
+
const rows = this.query(`SELECT DISTINCT json_extract(data, '$.parentID') AS parentId
|
|
227
|
+
FROM message
|
|
228
|
+
WHERE session_id = ?
|
|
229
|
+
AND json_extract(data, '$.role') = 'assistant'
|
|
230
|
+
AND json_extract(data, '$.parentID') IN (${placeholders})`, [sessionId, ...userMessageIds]);
|
|
231
|
+
return rows
|
|
232
|
+
.map((r) => r.parentId)
|
|
233
|
+
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
234
|
+
}
|
|
235
|
+
/** Get the latest assistant token usage row with non-zero context signal. */
|
|
236
|
+
getLatestAssistantContextUsage(sessionId) {
|
|
237
|
+
return this.queryOne(`SELECT
|
|
238
|
+
time_created AS timeCreated,
|
|
239
|
+
COALESCE(json_extract(data, '$.modelID'), 'unknown') AS modelId,
|
|
240
|
+
COALESCE(json_extract(data, '$.tokens.input'), 0) AS inputTokens,
|
|
241
|
+
COALESCE(json_extract(data, '$.tokens.output'), 0) AS outputTokens,
|
|
242
|
+
COALESCE(json_extract(data, '$.tokens.cache.read'), 0) AS cacheReadTokens,
|
|
243
|
+
COALESCE(json_extract(data, '$.tokens.cache.write'), 0) AS cacheWriteTokens,
|
|
244
|
+
COALESCE(json_extract(data, '$.tokens.reasoning'), 0) AS reasoningTokens
|
|
245
|
+
FROM message
|
|
246
|
+
WHERE session_id = ?
|
|
247
|
+
AND json_extract(data, '$.role') = 'assistant'
|
|
248
|
+
AND (
|
|
249
|
+
COALESCE(json_extract(data, '$.tokens.input'), 0) > 0
|
|
250
|
+
OR COALESCE(json_extract(data, '$.tokens.output'), 0) > 0
|
|
251
|
+
OR COALESCE(json_extract(data, '$.tokens.cache.read'), 0) > 0
|
|
252
|
+
OR COALESCE(json_extract(data, '$.tokens.cache.write'), 0) > 0
|
|
253
|
+
OR COALESCE(json_extract(data, '$.tokens.reasoning'), 0) > 0
|
|
254
|
+
)
|
|
255
|
+
ORDER BY time_created DESC
|
|
256
|
+
LIMIT 1`, [sessionId]);
|
|
257
|
+
}
|
|
258
|
+
/** Get all parts for a message. */
|
|
259
|
+
getPartsForMessage(messageId) {
|
|
260
|
+
return this.query('SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE message_id = ? ORDER BY time_created ASC', [messageId]);
|
|
261
|
+
}
|
|
262
|
+
/** Get all parts for a set of messages in one query. */
|
|
263
|
+
getPartsForMessages(sessionId, messageIds) {
|
|
264
|
+
if (messageIds.length === 0)
|
|
265
|
+
return [];
|
|
266
|
+
const placeholders = messageIds.map(() => '?').join(', ');
|
|
267
|
+
return this.query(`SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ? AND message_id IN (${placeholders}) ORDER BY time_created ASC`, [sessionId, ...messageIds]);
|
|
268
|
+
}
|
|
269
|
+
getPartsForSession(sessionId) {
|
|
270
|
+
return this.query('SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ? ORDER BY time_created ASC', [sessionId]);
|
|
271
|
+
}
|
|
272
|
+
/** Get parts newer than a given time_updated timestamp (ms epoch). */
|
|
273
|
+
getPartsNewerThan(sessionId, afterTimeUpdated) {
|
|
274
|
+
return this.query('SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ? AND time_updated > ? ORDER BY time_created ASC', [sessionId, afterTimeUpdated]);
|
|
275
|
+
}
|
|
276
|
+
/** Get the latest part time_updated for a session. */
|
|
277
|
+
getLatestPartTimeUpdated(sessionId) {
|
|
278
|
+
const row = this.queryOne('SELECT COALESCE(MAX(time_updated), 0) AS maxTimeUpdated FROM part WHERE session_id = ?', [sessionId]);
|
|
279
|
+
return row?.maxTimeUpdated ?? 0;
|
|
280
|
+
}
|
|
281
|
+
getProjectSessionStats() {
|
|
282
|
+
return this.query('SELECT project_id AS projectId, COUNT(*) AS sessionCount, MAX(time_updated) AS maxTimeUpdated FROM session GROUP BY project_id');
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Returns assistant messages tagged with the given providerID(s), completed
|
|
286
|
+
* within `sinceMs`. Used by the z.ai quota accumulator to walk OpenCode's
|
|
287
|
+
* per-turn token records without re-deriving the session filter.
|
|
288
|
+
*
|
|
289
|
+
* Results are ordered oldest-first by `time_created`.
|
|
290
|
+
*/
|
|
291
|
+
getAssistantMessagesByProviderId(providerIds, sinceMs) {
|
|
292
|
+
if (providerIds.length === 0)
|
|
293
|
+
return [];
|
|
294
|
+
const providerClauses = providerIds
|
|
295
|
+
.map(() => "json_extract(data, '$.providerID') = ?")
|
|
296
|
+
.join(' OR ');
|
|
297
|
+
return this.query(`SELECT
|
|
298
|
+
time_created AS timeCreated,
|
|
299
|
+
COALESCE(json_extract(data, '$.modelID'), 'unknown') AS modelId,
|
|
300
|
+
COALESCE(json_extract(data, '$.providerID'), '') AS providerId,
|
|
301
|
+
COALESCE(json_extract(data, '$.tokens.input'), 0) AS inputTokens,
|
|
302
|
+
COALESCE(json_extract(data, '$.tokens.output'), 0) AS outputTokens,
|
|
303
|
+
COALESCE(json_extract(data, '$.tokens.cache.read'), 0) AS cacheReadTokens,
|
|
304
|
+
COALESCE(json_extract(data, '$.tokens.cache.write'), 0) AS cacheWriteTokens,
|
|
305
|
+
COALESCE(json_extract(data, '$.tokens.reasoning'), 0) AS reasoningTokens,
|
|
306
|
+
json_extract(data, '$.error.message') AS errorMessage,
|
|
307
|
+
json_extract(data, '$.error.code') AS errorCode
|
|
308
|
+
FROM message
|
|
309
|
+
WHERE time_created >= ?
|
|
310
|
+
AND json_extract(data, '$.role') = 'assistant'
|
|
311
|
+
AND (${providerClauses})
|
|
312
|
+
ORDER BY time_created ASC`, [sinceMs, ...providerIds]);
|
|
313
|
+
}
|
|
314
|
+
/** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
|
|
315
|
+
getDbMtime() {
|
|
316
|
+
try {
|
|
317
|
+
return fs.statSync(this.dbPath).mtime.getTime();
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function bindSqlParams(sql, params) {
|
|
325
|
+
let paramIndex = 0;
|
|
326
|
+
return sql.replace(/\?/g, () => {
|
|
327
|
+
if (paramIndex >= params.length)
|
|
328
|
+
return '?';
|
|
329
|
+
const param = params[paramIndex++];
|
|
330
|
+
if (typeof param === 'number')
|
|
331
|
+
return Number.isFinite(param) ? String(param) : '0';
|
|
332
|
+
return `'${String(param).replace(/'/g, "''")}'`;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function execSqlite(dbPath, query) {
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
execFile('sqlite3', ['-json', '-readonly', dbPath, query], {
|
|
338
|
+
encoding: 'utf8',
|
|
339
|
+
timeout: 4_000,
|
|
340
|
+
killSignal: 'SIGKILL',
|
|
341
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
342
|
+
}, (error, stdout) => {
|
|
343
|
+
if (error)
|
|
344
|
+
reject(error);
|
|
345
|
+
else
|
|
346
|
+
resolve(stdout);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function normalizePath(input) {
|
|
351
|
+
try {
|
|
352
|
+
return fs.realpathSync(input);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return path.resolve(input);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function parseStringArray(value) {
|
|
359
|
+
if (!value)
|
|
360
|
+
return [];
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(value);
|
|
363
|
+
return Array.isArray(parsed)
|
|
364
|
+
? parsed
|
|
365
|
+
.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
366
|
+
.map(normalizePath)
|
|
367
|
+
: [];
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function normalizeForCompare(input) {
|
|
374
|
+
const normalized = normalizePath(input);
|
|
375
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
376
|
+
}
|
|
377
|
+
function pathMatchScore(candidate, workspacePath) {
|
|
378
|
+
if (!candidate)
|
|
379
|
+
return -1;
|
|
380
|
+
const normalizedCandidate = normalizeForCompare(candidate);
|
|
381
|
+
const normalizedWorkspace = normalizeForCompare(workspacePath);
|
|
382
|
+
if (normalizedCandidate === normalizedWorkspace)
|
|
383
|
+
return 10_000 + normalizedCandidate.length;
|
|
384
|
+
const candidatePrefix = normalizedCandidate.endsWith(path.sep)
|
|
385
|
+
? normalizedCandidate
|
|
386
|
+
: normalizedCandidate + path.sep;
|
|
387
|
+
const workspacePrefix = normalizedWorkspace.endsWith(path.sep)
|
|
388
|
+
? normalizedWorkspace
|
|
389
|
+
: normalizedWorkspace + path.sep;
|
|
390
|
+
if (normalizedWorkspace.startsWith(candidatePrefix))
|
|
391
|
+
return 5_000 + normalizedCandidate.length;
|
|
392
|
+
if (normalizedCandidate.startsWith(workspacePrefix))
|
|
393
|
+
return 1_000 + normalizedCandidate.length;
|
|
394
|
+
return -1;
|
|
395
|
+
}
|
|
396
|
+
function toRuntimeStatus(error, fallback = 'sqlite_missing') {
|
|
397
|
+
if (isErrno(error, 'ENOENT')) {
|
|
398
|
+
return {
|
|
399
|
+
available: false,
|
|
400
|
+
kind: 'sqlite_missing',
|
|
401
|
+
message: 'sqlite3 executable not found in PATH.',
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (isErrno(error, 'EPERM') || isErrno(error, 'EACCES')) {
|
|
405
|
+
return {
|
|
406
|
+
available: false,
|
|
407
|
+
kind: 'sqlite_blocked',
|
|
408
|
+
message: 'sqlite3 exists but could not be executed.',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
412
|
+
return { available: false, kind: fallback, message };
|
|
413
|
+
}
|
|
414
|
+
function isErrno(error, code) {
|
|
415
|
+
return (typeof error === 'object' &&
|
|
416
|
+
error !== null &&
|
|
417
|
+
'code' in error &&
|
|
418
|
+
error.code === code);
|
|
419
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One capped walker for Codex rollout files.
|
|
3
|
+
*
|
|
4
|
+
* Codex writes `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`.
|
|
5
|
+
* Six recursive, uncapped walkers used to exist across the provider and the
|
|
6
|
+
* quota resolver; this module replaces them with a single walk that caps
|
|
7
|
+
* depth and file count, visits the newest-dated directories first so a
|
|
8
|
+
* `limit` can stop early, stats each file exactly once, and returns results
|
|
9
|
+
* newest first with the size and session id already attached.
|
|
10
|
+
*
|
|
11
|
+
* With `limit` or `since`, the early exit is by directory date: a rollout
|
|
12
|
+
* that was created days ago but is still being written lands in its creation
|
|
13
|
+
* day's directory, so a very small limit (or a recent `since`) can miss it in
|
|
14
|
+
* favour of newer-dated, older-modified files. Callers that need exact mtime
|
|
15
|
+
* order over the whole history omit both (the `maxFiles` cap still applies);
|
|
16
|
+
* a live session missed this way is picked up by its next watch event.
|
|
17
|
+
*
|
|
18
|
+
* The async walker accepts a {@link DirectoryListingCache}: unchanged
|
|
19
|
+
* directories are then one stat each instead of a readdir plus a stat per
|
|
20
|
+
* file, which is what makes repeated discovery over a large history cheap.
|
|
21
|
+
*
|
|
22
|
+
* @module providers/rolloutWalker
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'fs';
|
|
25
|
+
import * as path from 'path';
|
|
26
|
+
export const DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
|
|
27
|
+
export const DEFAULT_ROLLOUT_WALK_MAX_FILES = 20_000;
|
|
28
|
+
/** Test if a filename is a Codex rollout file. */
|
|
29
|
+
export function isRolloutFile(filename) {
|
|
30
|
+
return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extract the session UUID from a rollout filename.
|
|
34
|
+
* Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
|
|
35
|
+
*/
|
|
36
|
+
export function extractRolloutSessionId(filename) {
|
|
37
|
+
const base = path.basename(filename, '.jsonl');
|
|
38
|
+
// rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
|
|
39
|
+
const parts = base.split('-');
|
|
40
|
+
// The UUID is typically the last 5 segments (8-4-4-4-12)
|
|
41
|
+
if (parts.length >= 6) {
|
|
42
|
+
const possibleUuid = parts.slice(-5).join('-');
|
|
43
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(possibleUuid)) {
|
|
44
|
+
return possibleUuid;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Fallback: use everything after "rollout-"
|
|
48
|
+
return base.replace(/^rollout-/, '');
|
|
49
|
+
}
|
|
50
|
+
function walkState(options) {
|
|
51
|
+
const maxFiles = Math.max(0, options.maxFiles ?? DEFAULT_ROLLOUT_WALK_MAX_FILES);
|
|
52
|
+
const limit = options.limit === undefined ? Infinity : Math.max(0, options.limit);
|
|
53
|
+
return {
|
|
54
|
+
results: [],
|
|
55
|
+
seen: new Set(),
|
|
56
|
+
cap: Math.min(maxFiles, limit),
|
|
57
|
+
maxDepth: options.maxDepth ?? DEFAULT_ROLLOUT_WALK_MAX_DEPTH,
|
|
58
|
+
since: options.since !== undefined && Number.isFinite(options.since) ? options.since : null,
|
|
59
|
+
sessionId: options.sessionId ? options.sessionId.trim().toLowerCase() : null,
|
|
60
|
+
includeEmpty: options.includeEmpty ?? false,
|
|
61
|
+
cache: options.cache ?? null,
|
|
62
|
+
revalidateRecent: options.revalidateRecent ?? false,
|
|
63
|
+
stats: options.stats,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Newest-dated first: directory and file names sort descending. */
|
|
67
|
+
function byNameDescending(a, b) {
|
|
68
|
+
return b.name.localeCompare(a.name);
|
|
69
|
+
}
|
|
70
|
+
function matchesFilter(state, name) {
|
|
71
|
+
if (!isRolloutFile(name))
|
|
72
|
+
return false;
|
|
73
|
+
return (state.sessionId === null || extractRolloutSessionId(name).toLowerCase() === state.sessionId);
|
|
74
|
+
}
|
|
75
|
+
function record(state, fullPath, name, stat) {
|
|
76
|
+
if (!state.includeEmpty && stat.size <= 0)
|
|
77
|
+
return;
|
|
78
|
+
if (state.since !== null && stat.mtimeMs < state.since)
|
|
79
|
+
return;
|
|
80
|
+
if (state.seen.has(fullPath))
|
|
81
|
+
return;
|
|
82
|
+
state.seen.add(fullPath);
|
|
83
|
+
state.results.push({
|
|
84
|
+
path: fullPath,
|
|
85
|
+
mtime: new Date(stat.mtimeMs),
|
|
86
|
+
sizeBytes: stat.size,
|
|
87
|
+
sessionId: extractRolloutSessionId(name),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function finish(state) {
|
|
91
|
+
return state.results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
92
|
+
}
|
|
93
|
+
/** Slack added to a dated directory's range so a timezone skew cannot prune a live day. */
|
|
94
|
+
const DATE_DIR_SLACK_MS = 24 * 60 * 60_000;
|
|
95
|
+
/**
|
|
96
|
+
* True when `segments` (the directory names below the root, e.g. `['2026',
|
|
97
|
+
* '09', '04']`) are all numeric and describe a year, month, or day that ended
|
|
98
|
+
* more than a day before `since`. Non-numeric names are never pruned.
|
|
99
|
+
*/
|
|
100
|
+
export function isDatedDirectoryBefore(segments, since) {
|
|
101
|
+
if (segments.length === 0 || segments.length > 3)
|
|
102
|
+
return false;
|
|
103
|
+
const numbers = segments.map((segment) => (/^\d+$/.test(segment) ? Number(segment) : NaN));
|
|
104
|
+
if (numbers.some((value) => !Number.isFinite(value)))
|
|
105
|
+
return false;
|
|
106
|
+
const [year, month, day] = numbers;
|
|
107
|
+
if (year < 1970 || year > 9999)
|
|
108
|
+
return false;
|
|
109
|
+
if (segments.length >= 2 && (month < 1 || month > 12))
|
|
110
|
+
return false;
|
|
111
|
+
if (segments.length === 3 && (day < 1 || day > 31))
|
|
112
|
+
return false;
|
|
113
|
+
const rangeEnd = segments.length === 1
|
|
114
|
+
? new Date(year + 1, 0, 1).getTime()
|
|
115
|
+
: segments.length === 2
|
|
116
|
+
? new Date(year, month, 1).getTime()
|
|
117
|
+
: new Date(year, month - 1, day + 1).getTime();
|
|
118
|
+
return rangeEnd + DATE_DIR_SLACK_MS < since;
|
|
119
|
+
}
|
|
120
|
+
function visitSync(state, dir, depth, segments) {
|
|
121
|
+
if (depth > state.maxDepth || state.results.length >= state.cap)
|
|
122
|
+
return;
|
|
123
|
+
if (state.since !== null && isDatedDirectoryBefore(segments, state.since))
|
|
124
|
+
return;
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
entries.sort(byNameDescending);
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (state.results.length >= state.cap)
|
|
135
|
+
return;
|
|
136
|
+
if (!entry.isFile() || !matchesFilter(state, entry.name))
|
|
137
|
+
continue;
|
|
138
|
+
const fullPath = path.join(dir, entry.name);
|
|
139
|
+
try {
|
|
140
|
+
record(state, fullPath, entry.name, fs.statSync(fullPath));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Skip files that vanish between readdir and stat.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (state.results.length >= state.cap)
|
|
148
|
+
return;
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
visitSync(state, path.join(dir, entry.name), depth + 1, [...segments, entry.name]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Walk one or more `sessions` roots for rollout files, newest first.
|
|
156
|
+
* Missing or unreadable directories are skipped; paths are deduplicated.
|
|
157
|
+
*/
|
|
158
|
+
export function walkRolloutFiles(roots, options = {}) {
|
|
159
|
+
const state = walkState(options);
|
|
160
|
+
for (const root of roots)
|
|
161
|
+
visitSync(state, root, 0, []);
|
|
162
|
+
return finish(state);
|
|
163
|
+
}
|
|
164
|
+
function yieldToEventLoop() {
|
|
165
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
166
|
+
}
|
|
167
|
+
function byStringDescending(a, b) {
|
|
168
|
+
return b.localeCompare(a);
|
|
169
|
+
}
|
|
170
|
+
async function visitCached(state, cache, dir, depth, segments) {
|
|
171
|
+
if (depth > state.maxDepth || state.results.length >= state.cap)
|
|
172
|
+
return;
|
|
173
|
+
if (state.since !== null && isDatedDirectoryBefore(segments, state.since))
|
|
174
|
+
return;
|
|
175
|
+
const listing = await cache.listDirectory(dir, (name) => matchesFilter(state, name), {
|
|
176
|
+
revalidateRecent: state.revalidateRecent,
|
|
177
|
+
stats: state.stats,
|
|
178
|
+
});
|
|
179
|
+
if (!listing)
|
|
180
|
+
return;
|
|
181
|
+
const names = [...listing.files.keys()].sort(byStringDescending);
|
|
182
|
+
for (const name of names) {
|
|
183
|
+
if (state.results.length >= state.cap)
|
|
184
|
+
return;
|
|
185
|
+
const stat = listing.files.get(name);
|
|
186
|
+
if (!stat)
|
|
187
|
+
continue;
|
|
188
|
+
record(state, path.join(dir, name), name, { size: stat.sizeBytes, mtimeMs: stat.mtimeMs });
|
|
189
|
+
}
|
|
190
|
+
await yieldToEventLoop();
|
|
191
|
+
for (const name of [...listing.subdirectories].sort(byStringDescending)) {
|
|
192
|
+
if (state.results.length >= state.cap)
|
|
193
|
+
return;
|
|
194
|
+
await visitCached(state, cache, path.join(dir, name), depth + 1, [...segments, name]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function visitAsync(state, dir, depth, segments) {
|
|
198
|
+
if (depth > state.maxDepth || state.results.length >= state.cap)
|
|
199
|
+
return;
|
|
200
|
+
if (state.since !== null && isDatedDirectoryBefore(segments, state.since))
|
|
201
|
+
return;
|
|
202
|
+
let entries;
|
|
203
|
+
try {
|
|
204
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
205
|
+
if (state.stats)
|
|
206
|
+
state.stats.directoriesListed = (state.stats.directoriesListed ?? 0) + 1;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
entries.sort(byNameDescending);
|
|
212
|
+
let sinceYield = 0;
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
if (state.results.length >= state.cap)
|
|
215
|
+
return;
|
|
216
|
+
if (!entry.isFile() || !matchesFilter(state, entry.name))
|
|
217
|
+
continue;
|
|
218
|
+
const fullPath = path.join(dir, entry.name);
|
|
219
|
+
try {
|
|
220
|
+
const stat = await fs.promises.stat(fullPath);
|
|
221
|
+
if (state.stats)
|
|
222
|
+
state.stats.filesStatted = (state.stats.filesStatted ?? 0) + 1;
|
|
223
|
+
record(state, fullPath, entry.name, stat);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Skip files that vanish between readdir and stat.
|
|
227
|
+
}
|
|
228
|
+
if (++sinceYield >= 200) {
|
|
229
|
+
sinceYield = 0;
|
|
230
|
+
await yieldToEventLoop();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
await yieldToEventLoop();
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
if (state.results.length >= state.cap)
|
|
236
|
+
return;
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
await visitAsync(state, path.join(dir, entry.name), depth + 1, [...segments, entry.name]);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Async twin of {@link walkRolloutFiles} that yields between directories. */
|
|
243
|
+
export async function walkRolloutFilesAsync(roots, options = {}) {
|
|
244
|
+
const state = walkState(options);
|
|
245
|
+
for (const root of roots) {
|
|
246
|
+
if (state.cache)
|
|
247
|
+
await visitCached(state, state.cache, root, 0, []);
|
|
248
|
+
else
|
|
249
|
+
await visitAsync(state, root, 0, []);
|
|
250
|
+
}
|
|
251
|
+
return finish(state);
|
|
252
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared provider interface and types for session providers.
|
|
3
|
+
*
|
|
4
|
+
* SessionProviderBase is the expanded interface that both the VS Code extension
|
|
5
|
+
* and CLI can use. The VS Code extension extends it with vscode.Disposable and
|
|
6
|
+
* optional VS Code-specific methods.
|
|
7
|
+
*
|
|
8
|
+
* SessionProvider is the original simplified interface maintained for backward
|
|
9
|
+
* compatibility with existing CLI code.
|
|
10
|
+
*/
|
|
11
|
+
/** Stable runtime list for consumers that must enumerate session providers. */
|
|
12
|
+
export { SESSION_PROVIDER_IDS } from '../providerIds';
|