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,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap, bounded previews of provider-native session files (Node-only).
|
|
3
|
+
*
|
|
4
|
+
* `listRecentSessions` answers "what happened in this workspace" but labels
|
|
5
|
+
* every discovered session before applying its limit, so its cost scales with
|
|
6
|
+
* total session count. This module inverts the order of operations: enumerate
|
|
7
|
+
* with stats only, sort by mtime, apply `since`/`limit`, and only then do
|
|
8
|
+
* bounded content reads for the survivors. That keeps a "recent sessions"
|
|
9
|
+
* surface cheap even against a machine with thousands of accumulated sessions.
|
|
10
|
+
*
|
|
11
|
+
* Prompt extraction delegates to each provider's `extractSessionLabel` (Claude
|
|
12
|
+
* Code reads only the first 8 KiB; Codex prefers the sqlite thread title, else
|
|
13
|
+
* a bounded first-user-message scan that skips injected context). The first
|
|
14
|
+
* timestamp and workspace path come from one generic prefix parse in this
|
|
15
|
+
* module. A session whose opening events are larger than the prefix budget
|
|
16
|
+
* yields `null` fields rather than an error.
|
|
17
|
+
*
|
|
18
|
+
* @module sessionPreviews
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
function emptyPrefixScan() {
|
|
22
|
+
return { firstTimestamp: null, workspacePath: null };
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_PREFIX_BYTES = 16 * 1024;
|
|
25
|
+
const DEFAULT_LIMIT = 50;
|
|
26
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
27
|
+
const MAX_CONCURRENCY = 16;
|
|
28
|
+
/**
|
|
29
|
+
* Reads a bounded preview of one session file. Returns `null` when the file is
|
|
30
|
+
* missing or unreadable; malformed content degrades to `null` fields instead
|
|
31
|
+
* of throwing.
|
|
32
|
+
*/
|
|
33
|
+
export function readSessionPreview(provider, sessionPath, options = {}) {
|
|
34
|
+
let sizeBytes;
|
|
35
|
+
let modifiedAt;
|
|
36
|
+
try {
|
|
37
|
+
const stats = fs.statSync(sessionPath);
|
|
38
|
+
sizeBytes = stats.size;
|
|
39
|
+
// Inside the try: an out-of-range mtime RangeErrors on serialization,
|
|
40
|
+
// and this function's contract is to degrade, not throw.
|
|
41
|
+
modifiedAt = stats.mtime.toISOString();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
let metadata;
|
|
45
|
+
try {
|
|
46
|
+
metadata = provider.getSessionMetadata?.(sessionPath);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (!metadata)
|
|
52
|
+
return null;
|
|
53
|
+
sizeBytes = 0;
|
|
54
|
+
try {
|
|
55
|
+
modifiedAt = metadata.mtime.toISOString();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let firstUserPrompt = null;
|
|
62
|
+
try {
|
|
63
|
+
firstUserPrompt = provider.extractSessionLabel(sessionPath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Label extraction is best-effort.
|
|
67
|
+
}
|
|
68
|
+
let sessionId = '';
|
|
69
|
+
try {
|
|
70
|
+
sessionId = provider.getSessionId(sessionPath);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// A provider that cannot identify the file still yields a usable preview.
|
|
74
|
+
}
|
|
75
|
+
const prefixBudget = options.maxPrefixBytes ?? DEFAULT_PREFIX_BYTES;
|
|
76
|
+
const prefix = prefixBudget > 0 ? scanPrefix(sessionPath, prefixBudget) : emptyPrefixScan();
|
|
77
|
+
return {
|
|
78
|
+
provider: provider.id,
|
|
79
|
+
sessionId,
|
|
80
|
+
filePath: sessionPath,
|
|
81
|
+
modifiedAt,
|
|
82
|
+
sizeBytes,
|
|
83
|
+
firstUserPrompt,
|
|
84
|
+
firstTimestamp: prefix.firstTimestamp,
|
|
85
|
+
workspacePath: prefix.workspacePath,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Event-loop-safe counterpart to readSessionPreview with per-call degradation status. */
|
|
89
|
+
export async function readSessionPreviewAsync(provider, sessionPath, options = {}) {
|
|
90
|
+
let labels;
|
|
91
|
+
const diagnostics = [];
|
|
92
|
+
try {
|
|
93
|
+
labels = await extractLabelsAsync(provider, [sessionPath], options);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
labels = new Map([[sessionPath, null]]);
|
|
97
|
+
diagnostics.push({
|
|
98
|
+
providerId: provider.id,
|
|
99
|
+
kind: 'read_failed',
|
|
100
|
+
severity: 'warning',
|
|
101
|
+
phase: 'read',
|
|
102
|
+
message: `${provider.id} session label was unavailable.`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const info = await statSessionFileAsync(provider, sessionPath);
|
|
106
|
+
if (!info) {
|
|
107
|
+
return {
|
|
108
|
+
preview: null,
|
|
109
|
+
diagnostics: dedupeDiagnostics([...diagnostics, ...providerDiagnostics(provider)]),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const preview = await readSessionPreviewFromInfoAsync(provider, info, labels.get(sessionPath) ?? null, options);
|
|
113
|
+
return {
|
|
114
|
+
preview,
|
|
115
|
+
diagnostics: dedupeDiagnostics([...diagnostics, ...providerDiagnostics(provider)]),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Lists previews across providers, newest first. Enumeration is stat-only;
|
|
120
|
+
* content is read only for the post-`limit` survivors.
|
|
121
|
+
*/
|
|
122
|
+
export function listSessionPreviews(providers, options = {}) {
|
|
123
|
+
const limit = Math.max(0, options.limit ?? DEFAULT_LIMIT);
|
|
124
|
+
const sinceMs = resolveSinceMs(options.since);
|
|
125
|
+
const candidates = [];
|
|
126
|
+
for (const provider of providers) {
|
|
127
|
+
for (const file of enumerateSessionFiles(provider, options.workspacePath)) {
|
|
128
|
+
const mtimeMs = file.mtime.getTime();
|
|
129
|
+
if (sinceMs !== null && mtimeMs <= sinceMs)
|
|
130
|
+
continue;
|
|
131
|
+
candidates.push({ provider, path: file.path, mtimeMs });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
135
|
+
const previews = [];
|
|
136
|
+
for (const candidate of candidates.slice(0, limit)) {
|
|
137
|
+
const preview = readSessionPreview(candidate.provider, candidate.path, options);
|
|
138
|
+
if (preview)
|
|
139
|
+
previews.push(preview);
|
|
140
|
+
}
|
|
141
|
+
return previews;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Lists previews without blocking the caller loop on corpus-wide synchronous
|
|
145
|
+
* work. SQLite-backed providers perform at most one subprocess per call.
|
|
146
|
+
*/
|
|
147
|
+
export async function listSessionPreviewsAsync(providers, options = {}) {
|
|
148
|
+
const limit = Math.max(0, options.limit ?? DEFAULT_LIMIT);
|
|
149
|
+
const sinceMs = resolveSinceMs(options.since);
|
|
150
|
+
const candidates = [];
|
|
151
|
+
const operationDiagnostics = [];
|
|
152
|
+
await Promise.all(providers.map(async (provider) => {
|
|
153
|
+
let files = [];
|
|
154
|
+
try {
|
|
155
|
+
files = provider.listSessionFilesAsync
|
|
156
|
+
? await provider.listSessionFilesAsync(options.workspacePath)
|
|
157
|
+
: await enumerateSessionFilesAsyncFallback(provider, options.workspacePath, options);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
files = [];
|
|
161
|
+
operationDiagnostics.push({
|
|
162
|
+
providerId: provider.id,
|
|
163
|
+
kind: 'read_failed',
|
|
164
|
+
severity: 'error',
|
|
165
|
+
phase: 'enumerate',
|
|
166
|
+
message: `${provider.id} session enumeration failed.`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
for (const info of files) {
|
|
170
|
+
const mtimeMs = info.mtime.getTime();
|
|
171
|
+
if (sinceMs !== null && mtimeMs <= sinceMs)
|
|
172
|
+
continue;
|
|
173
|
+
candidates.push({ provider, info });
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
176
|
+
candidates.sort((left, right) => right.info.mtime.getTime() - left.info.mtime.getTime());
|
|
177
|
+
const selected = candidates.slice(0, limit);
|
|
178
|
+
const labels = new Map();
|
|
179
|
+
await Promise.all(providers.map(async (provider) => {
|
|
180
|
+
const paths = selected
|
|
181
|
+
.filter((item) => item.provider === provider)
|
|
182
|
+
.map((item) => item.info.path);
|
|
183
|
+
if (paths.length === 0)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
labels.set(provider, await extractLabelsAsync(provider, paths, options));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
labels.set(provider, new Map(paths.map((sessionPath) => [sessionPath, null])));
|
|
190
|
+
operationDiagnostics.push({
|
|
191
|
+
providerId: provider.id,
|
|
192
|
+
kind: 'read_failed',
|
|
193
|
+
severity: 'warning',
|
|
194
|
+
phase: 'read',
|
|
195
|
+
message: `${provider.id} session labels were unavailable.`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}));
|
|
199
|
+
const previews = await mapWithConcurrency(selected, async ({ provider, info }) => {
|
|
200
|
+
const preview = await readSessionPreviewFromInfoAsync(provider, info, labels.get(provider)?.get(info.path) ?? info.label ?? null, options);
|
|
201
|
+
await yieldControl(options);
|
|
202
|
+
return preview;
|
|
203
|
+
}, concurrency(options));
|
|
204
|
+
return {
|
|
205
|
+
previews: previews.filter((preview) => preview !== null),
|
|
206
|
+
hasMore: candidates.length > limit,
|
|
207
|
+
diagnostics: dedupeDiagnostics([
|
|
208
|
+
...operationDiagnostics,
|
|
209
|
+
...providers.flatMap(providerDiagnostics),
|
|
210
|
+
]),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function resolveSinceMs(since) {
|
|
214
|
+
if (since === undefined)
|
|
215
|
+
return null;
|
|
216
|
+
const ms = since instanceof Date ? since.getTime() : Date.parse(since);
|
|
217
|
+
if (!Number.isFinite(ms)) {
|
|
218
|
+
// An unparseable cutoff used to mean "no cutoff", silently returning
|
|
219
|
+
// every session. Fail loudly instead so a typo cannot masquerade as a
|
|
220
|
+
// full listing.
|
|
221
|
+
throw new RangeError(`Invalid \`since\` value: ${String(since)}`);
|
|
222
|
+
}
|
|
223
|
+
return ms;
|
|
224
|
+
}
|
|
225
|
+
function enumerateSessionFiles(provider, workspacePath) {
|
|
226
|
+
try {
|
|
227
|
+
if (workspacePath) {
|
|
228
|
+
return provider
|
|
229
|
+
.findAllSessions(workspacePath)
|
|
230
|
+
.map(statSessionFile)
|
|
231
|
+
.filter((info) => info !== null);
|
|
232
|
+
}
|
|
233
|
+
if (provider.listAllSessionFiles)
|
|
234
|
+
return provider.listAllSessionFiles();
|
|
235
|
+
// Best-effort global fallback for providers without listAllSessionFiles:
|
|
236
|
+
// correct for project-folder-shaped providers, empty otherwise.
|
|
237
|
+
const results = [];
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
for (const folder of provider.getAllProjectFolders()) {
|
|
240
|
+
for (const sessionPath of provider.findSessionsInDirectory(folder.dir)) {
|
|
241
|
+
if (seen.has(sessionPath))
|
|
242
|
+
continue;
|
|
243
|
+
seen.add(sessionPath);
|
|
244
|
+
const info = statSessionFile(sessionPath);
|
|
245
|
+
if (info)
|
|
246
|
+
results.push(info);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return results;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function statSessionFile(sessionPath) {
|
|
256
|
+
try {
|
|
257
|
+
return { path: sessionPath, mtime: fs.statSync(sessionPath).mtime };
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function statSessionFileAsync(provider, sessionPath) {
|
|
264
|
+
try {
|
|
265
|
+
const stat = await fs.promises.stat(sessionPath);
|
|
266
|
+
return {
|
|
267
|
+
path: sessionPath,
|
|
268
|
+
mtime: stat.mtime,
|
|
269
|
+
sizeBytes: stat.size,
|
|
270
|
+
sessionId: safeSessionId(provider, sessionPath),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
let metadata = null;
|
|
275
|
+
try {
|
|
276
|
+
// Async first: for DB-backed synthetic paths the sync variant can spawn
|
|
277
|
+
// a blocking sqlite3 subprocess, defeating this API's event-loop safety.
|
|
278
|
+
if (provider.getSessionMetadataAsync) {
|
|
279
|
+
metadata = await provider.getSessionMetadataAsync(sessionPath);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
metadata = provider.getSessionMetadata?.(sessionPath) ?? null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
metadata = null;
|
|
287
|
+
}
|
|
288
|
+
if (!metadata)
|
|
289
|
+
return null;
|
|
290
|
+
return {
|
|
291
|
+
path: sessionPath,
|
|
292
|
+
mtime: metadata.mtime,
|
|
293
|
+
sizeBytes: 0,
|
|
294
|
+
sessionId: safeSessionId(provider, sessionPath),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function extractLabelsAsync(provider, sessionPaths, options) {
|
|
299
|
+
if (provider.extractSessionLabelsAsync) {
|
|
300
|
+
// Callers deliberately do not retry through the synchronous provider path
|
|
301
|
+
// if this batch fails: DB-backed providers could otherwise spawn once per
|
|
302
|
+
// session after their single async batch failed.
|
|
303
|
+
return provider.extractSessionLabelsAsync(sessionPaths);
|
|
304
|
+
}
|
|
305
|
+
const values = new Map();
|
|
306
|
+
for (const sessionPath of sessionPaths) {
|
|
307
|
+
values.set(sessionPath, await extractLabelWithYield(provider, sessionPath, options));
|
|
308
|
+
}
|
|
309
|
+
return values;
|
|
310
|
+
}
|
|
311
|
+
async function enumerateSessionFilesAsyncFallback(provider, workspacePath, options) {
|
|
312
|
+
await yieldControl(options);
|
|
313
|
+
const paths = workspacePath
|
|
314
|
+
? provider.findAllSessions(workspacePath)
|
|
315
|
+
: provider.listAllSessionFiles
|
|
316
|
+
? provider.listAllSessionFiles().map((item) => item.path)
|
|
317
|
+
: provider
|
|
318
|
+
.getAllProjectFolders()
|
|
319
|
+
.flatMap((folder) => provider.findSessionsInDirectory(folder.dir));
|
|
320
|
+
return (await mapWithConcurrency([...new Set(paths)], async (sessionPath) => {
|
|
321
|
+
const info = await statSessionFileAsync(provider, sessionPath);
|
|
322
|
+
await yieldControl(options);
|
|
323
|
+
return info;
|
|
324
|
+
}, concurrency(options))).filter((item) => item !== null);
|
|
325
|
+
}
|
|
326
|
+
async function readSessionPreviewFromInfoAsync(provider, info, firstUserPrompt, options) {
|
|
327
|
+
let modifiedAt;
|
|
328
|
+
try {
|
|
329
|
+
modifiedAt = info.mtime.toISOString();
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const prefixBudget = options.maxPrefixBytes ?? DEFAULT_PREFIX_BYTES;
|
|
335
|
+
const prefix = prefixBudget > 0 ? await scanPrefixAsync(info.path, prefixBudget) : emptyPrefixScan();
|
|
336
|
+
return {
|
|
337
|
+
provider: provider.id,
|
|
338
|
+
sessionId: info.sessionId ?? safeSessionId(provider, info.path),
|
|
339
|
+
filePath: info.path,
|
|
340
|
+
modifiedAt,
|
|
341
|
+
sizeBytes: info.sizeBytes ?? 0,
|
|
342
|
+
firstUserPrompt,
|
|
343
|
+
firstTimestamp: prefix.firstTimestamp ?? info.createdAt?.toISOString() ?? null,
|
|
344
|
+
workspacePath: prefix.workspacePath ?? info.workspacePath ?? null,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
async function extractLabelWithYield(provider, sessionPath, options) {
|
|
348
|
+
await yieldControl(options);
|
|
349
|
+
try {
|
|
350
|
+
return provider.extractSessionLabel(sessionPath);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function safeSessionId(provider, sessionPath) {
|
|
357
|
+
try {
|
|
358
|
+
return provider.getSessionId(sessionPath);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return '';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function concurrency(options) {
|
|
365
|
+
const requested = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
366
|
+
if (!Number.isFinite(requested))
|
|
367
|
+
return DEFAULT_CONCURRENCY;
|
|
368
|
+
return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(requested)));
|
|
369
|
+
}
|
|
370
|
+
async function yieldControl(options) {
|
|
371
|
+
if (options.yieldBetweenReads) {
|
|
372
|
+
await options.yieldBetweenReads();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
376
|
+
}
|
|
377
|
+
async function mapWithConcurrency(values, mapper, maxConcurrency) {
|
|
378
|
+
const results = new Array(values.length);
|
|
379
|
+
let cursor = 0;
|
|
380
|
+
await Promise.all(Array.from({ length: Math.min(maxConcurrency, values.length) }, async () => {
|
|
381
|
+
while (true) {
|
|
382
|
+
const index = cursor++;
|
|
383
|
+
if (index >= values.length)
|
|
384
|
+
return;
|
|
385
|
+
results[index] = await mapper(values[index]);
|
|
386
|
+
}
|
|
387
|
+
}));
|
|
388
|
+
return results;
|
|
389
|
+
}
|
|
390
|
+
function providerDiagnostics(provider) {
|
|
391
|
+
try {
|
|
392
|
+
return [...(provider.getLastOperationStatus?.().diagnostics ?? [])];
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function dedupeDiagnostics(diagnostics) {
|
|
399
|
+
const seen = new Set();
|
|
400
|
+
return diagnostics.filter((diagnostic) => {
|
|
401
|
+
const key = `${diagnostic.providerId}\0${diagnostic.kind}\0${diagnostic.phase}`;
|
|
402
|
+
if (seen.has(key))
|
|
403
|
+
return false;
|
|
404
|
+
seen.add(key);
|
|
405
|
+
return true;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Parses complete JSONL lines out of the first `maxPrefixBytes` of a file,
|
|
410
|
+
* taking the first top-level `timestamp` string and the first workspace path
|
|
411
|
+
* (top-level `cwd`, or `payload.cwd` on a Codex `session_meta` row). Partial
|
|
412
|
+
* trailing lines and malformed rows are skipped; never throws.
|
|
413
|
+
*/
|
|
414
|
+
function scanPrefix(sessionPath, maxPrefixBytes) {
|
|
415
|
+
const result = { firstTimestamp: null, workspacePath: null };
|
|
416
|
+
let content;
|
|
417
|
+
let sawWholeFile;
|
|
418
|
+
try {
|
|
419
|
+
const descriptor = fs.openSync(sessionPath, 'r');
|
|
420
|
+
try {
|
|
421
|
+
const size = fs.fstatSync(descriptor).size;
|
|
422
|
+
const readBytes = Math.min(size, Math.max(1, maxPrefixBytes));
|
|
423
|
+
const buffer = Buffer.alloc(readBytes);
|
|
424
|
+
fs.readSync(descriptor, buffer, 0, readBytes, 0);
|
|
425
|
+
content = buffer.toString('utf8');
|
|
426
|
+
sawWholeFile = readBytes >= size;
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
fs.closeSync(descriptor);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
const lines = content.split('\n');
|
|
436
|
+
// The final chunk after the last newline is a partial line unless the whole
|
|
437
|
+
// file fit in the prefix.
|
|
438
|
+
const completeLines = sawWholeFile ? lines : lines.slice(0, -1);
|
|
439
|
+
for (const line of completeLines) {
|
|
440
|
+
const trimmed = line.trim();
|
|
441
|
+
if (!trimmed)
|
|
442
|
+
continue;
|
|
443
|
+
let parsed;
|
|
444
|
+
try {
|
|
445
|
+
parsed = JSON.parse(trimmed);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
451
|
+
continue;
|
|
452
|
+
if (result.firstTimestamp === null && typeof parsed.timestamp === 'string') {
|
|
453
|
+
result.firstTimestamp = parsed.timestamp;
|
|
454
|
+
}
|
|
455
|
+
if (result.workspacePath === null) {
|
|
456
|
+
if (typeof parsed.cwd === 'string' && parsed.cwd) {
|
|
457
|
+
result.workspacePath = parsed.cwd;
|
|
458
|
+
}
|
|
459
|
+
else if (parsed.type === 'session_meta') {
|
|
460
|
+
const payload = parsed.payload;
|
|
461
|
+
if (payload && typeof payload.cwd === 'string' && payload.cwd) {
|
|
462
|
+
result.workspacePath = payload.cwd;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (result.firstTimestamp !== null && result.workspacePath !== null)
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
async function scanPrefixAsync(sessionPath, maxPrefixBytes) {
|
|
472
|
+
const result = { firstTimestamp: null, workspacePath: null };
|
|
473
|
+
let handle = null;
|
|
474
|
+
try {
|
|
475
|
+
handle = await fs.promises.open(sessionPath, 'r');
|
|
476
|
+
const stats = await handle.stat();
|
|
477
|
+
const readBytes = Math.min(stats.size, Math.max(1, maxPrefixBytes));
|
|
478
|
+
const buffer = Buffer.alloc(readBytes);
|
|
479
|
+
const { bytesRead } = await handle.read(buffer, 0, readBytes, 0);
|
|
480
|
+
parsePrefixInto(result, buffer.toString('utf8', 0, bytesRead), bytesRead >= stats.size);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
await handle?.close().catch(() => undefined);
|
|
487
|
+
}
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
function parsePrefixInto(result, content, sawWholeFile) {
|
|
491
|
+
const lines = content.split('\n');
|
|
492
|
+
const completeLines = sawWholeFile ? lines : lines.slice(0, -1);
|
|
493
|
+
for (const line of completeLines) {
|
|
494
|
+
const trimmed = line.trim();
|
|
495
|
+
if (!trimmed)
|
|
496
|
+
continue;
|
|
497
|
+
let parsed;
|
|
498
|
+
try {
|
|
499
|
+
parsed = JSON.parse(trimmed);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
505
|
+
continue;
|
|
506
|
+
if (result.firstTimestamp === null && typeof parsed.timestamp === 'string') {
|
|
507
|
+
result.firstTimestamp = parsed.timestamp;
|
|
508
|
+
}
|
|
509
|
+
if (result.workspacePath === null) {
|
|
510
|
+
if (typeof parsed.cwd === 'string' && parsed.cwd)
|
|
511
|
+
result.workspacePath = parsed.cwd;
|
|
512
|
+
else if (parsed.type === 'session_meta') {
|
|
513
|
+
const payload = parsed.payload;
|
|
514
|
+
if (payload && typeof payload.cwd === 'string' && payload.cwd) {
|
|
515
|
+
result.workspacePath = payload.cwd;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (result.firstTimestamp !== null && result.workspacePath !== null)
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|