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,1034 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI session provider for the shared package.
|
|
3
|
+
*
|
|
4
|
+
* Implements SessionProviderBase for monitoring Codex CLI sessions.
|
|
5
|
+
* Codex stores sessions as JSONL rollout files in:
|
|
6
|
+
* ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl
|
|
7
|
+
* with an optional SQLite index at ~/.codex/state.sqlite.
|
|
8
|
+
*
|
|
9
|
+
* Ported from sidekick-vscode/src/services/providers/CodexSessionProvider.ts
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { StringDecoder } from 'string_decoder';
|
|
14
|
+
import { readSessionContextSnapshot } from '../context/sessionContext';
|
|
15
|
+
import { readSessionFileStats } from '../sessionStats';
|
|
16
|
+
import { CodexRolloutParser } from '../parsers/codexParser';
|
|
17
|
+
import { CodexDatabase, spawnParentFromSource } from './codexDatabase';
|
|
18
|
+
import { cachedSubagentStats } from '../parsers/subagentStatsCache';
|
|
19
|
+
import { DEFAULT_ROLLOUT_WALK_MAX_DEPTH, extractRolloutSessionId as extractSessionId, isRolloutFile, walkRolloutFiles, } from './rolloutWalker';
|
|
20
|
+
import { getCodexSessionsDirs } from '../codexPaths';
|
|
21
|
+
import { cwdMatches, getSessionsDirs, listCodexSessionFilesAsync, normalizePath, readFirstLines, readSessionCwd, readSessionCwdAsync, readSessionMeta, } from './codexRollouts';
|
|
22
|
+
import { DirectoryListingCache, splitWatchedPath } from './directoryListingCache';
|
|
23
|
+
import { ProviderDiagnosticTracker, diagnosticFromRuntimeStatus } from './diagnostics';
|
|
24
|
+
import { getModelContextWindowSize } from '../modelContext';
|
|
25
|
+
import { recordObservedContextWindow } from '../observedContextWindows';
|
|
26
|
+
import { extractNormalizedUsage, normalizeProviderUsage } from '../usageNormalization';
|
|
27
|
+
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helper functions
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/** Truncate a string to maxLen with ellipsis. */
|
|
32
|
+
function truncate(text, maxLen) {
|
|
33
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
34
|
+
if (trimmed.length > maxLen) {
|
|
35
|
+
return trimmed.substring(0, maxLen - 3) + '...';
|
|
36
|
+
}
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
/** Every rollout under a directory, newest first (one capped walk). */
|
|
40
|
+
function findRolloutFiles(dir) {
|
|
41
|
+
return walkRolloutFiles([dir]);
|
|
42
|
+
}
|
|
43
|
+
function hasRolloutFile(dir) {
|
|
44
|
+
return walkRolloutFiles([dir], { limit: 1 }).length > 0;
|
|
45
|
+
}
|
|
46
|
+
/** Every rollout across the monitored homes, newest first, deduplicated by path. */
|
|
47
|
+
function findRolloutFilesInConfiguredHomes(options = {}) {
|
|
48
|
+
return walkRolloutFiles(getSessionsDirs(), options);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Locates the rollout file for a Codex session id by walking
|
|
52
|
+
* `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`, then
|
|
53
|
+
* `<home>/archived_sessions/`, where Codex moves the rollouts of archived
|
|
54
|
+
* threads.
|
|
55
|
+
*
|
|
56
|
+
* Filename-only match, case-insensitive on the id — no file contents are read
|
|
57
|
+
* and files are only stat'ed once their name matches, so the walk stays cheap
|
|
58
|
+
* even over a large history. When the same id exists in more than one home,
|
|
59
|
+
* the most recently modified copy wins. Returns `null` for an unknown, empty,
|
|
60
|
+
* or whitespace id. Consumers who want the database's answer instead can query
|
|
61
|
+
* the already-public `CodexDatabase` (requires the external `sqlite3` binary).
|
|
62
|
+
*/
|
|
63
|
+
export function findCodexRolloutFile(sessionId, options = {}) {
|
|
64
|
+
const normalizedId = sessionId.trim().toLowerCase();
|
|
65
|
+
if (!normalizedId)
|
|
66
|
+
return null;
|
|
67
|
+
const sessionsDirs = options.codexHome
|
|
68
|
+
? getCodexSessionsDirs([options.codexHome])
|
|
69
|
+
: getSessionsDirs();
|
|
70
|
+
// Name-only filter, one walk across the homes, newest modification first.
|
|
71
|
+
return walkRolloutFiles(sessionsDirs, { sessionId: normalizedId })[0]?.path ?? null;
|
|
72
|
+
}
|
|
73
|
+
/** Pick the best sessions directory for UI/search entrypoints. */
|
|
74
|
+
function getSessionsDir() {
|
|
75
|
+
const dirs = getCodexSessionsDirs(undefined, { includeArchived: false });
|
|
76
|
+
for (const dir of dirs) {
|
|
77
|
+
if (hasRolloutFile(dir)) {
|
|
78
|
+
return dir;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return dirs[0] ?? path.join(process.cwd(), '.codex', 'sessions');
|
|
82
|
+
}
|
|
83
|
+
/** Pick the home that owns the preferred sessions directory. */
|
|
84
|
+
function getCodexHome() {
|
|
85
|
+
return path.dirname(getSessionsDir());
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if text looks like system-injected context rather than a real user prompt.
|
|
89
|
+
* Codex prepends AGENTS.md, environment_context, permissions, and collaboration_mode
|
|
90
|
+
* as user/developer messages before the actual user prompt.
|
|
91
|
+
*/
|
|
92
|
+
function isSystemInjection(text) {
|
|
93
|
+
const t = text.trimStart();
|
|
94
|
+
return t.startsWith('<') || t.startsWith('#');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read the first user message text from a rollout file.
|
|
98
|
+
* Reads up to 20 lines to find a real user message (lines can be 10KB+ each).
|
|
99
|
+
* Skips system-injected context (AGENTS.md, environment_context, etc.).
|
|
100
|
+
*/
|
|
101
|
+
function extractFirstUserMessage(rolloutPath) {
|
|
102
|
+
try {
|
|
103
|
+
return extractFirstUserMessageFromLines(readFirstLines(rolloutPath, 20));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function extractFirstUserMessageFromLines(lines) {
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
const trimmed = line.trim();
|
|
112
|
+
if (!trimmed)
|
|
113
|
+
continue;
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(trimmed);
|
|
116
|
+
// Check response_item with user message
|
|
117
|
+
if (parsed.type === 'response_item') {
|
|
118
|
+
const payload = parsed.payload;
|
|
119
|
+
if (payload.role === 'user') {
|
|
120
|
+
const content = payload.content;
|
|
121
|
+
if (typeof content === 'string' && content.trim() && !isSystemInjection(content)) {
|
|
122
|
+
return truncate(content, 60);
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(content)) {
|
|
125
|
+
for (const part of content) {
|
|
126
|
+
const p = part;
|
|
127
|
+
if (p.text?.trim() && !isSystemInjection(p.text)) {
|
|
128
|
+
return truncate(p.text, 60);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Check event_msg with user_message
|
|
135
|
+
if (parsed.type === 'event_msg') {
|
|
136
|
+
const payload = parsed.payload;
|
|
137
|
+
if (payload.type === 'user_message' && payload.message?.trim()) {
|
|
138
|
+
return truncate(payload.message, 60);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Skip malformed lines
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
async function extractFirstUserMessageAsync(rolloutPath) {
|
|
149
|
+
let handle = null;
|
|
150
|
+
try {
|
|
151
|
+
handle = await fs.promises.open(rolloutPath, 'r');
|
|
152
|
+
const buffer = Buffer.alloc(256 * 1024);
|
|
153
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
154
|
+
return extractFirstUserMessageFromLines(buffer.toString('utf8', 0, bytesRead).split('\n').slice(0, 20));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await handle?.close().catch(() => undefined);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Extract searchable text from a rollout line. */
|
|
164
|
+
function extractSearchableText(line) {
|
|
165
|
+
const parts = [];
|
|
166
|
+
switch (line.type) {
|
|
167
|
+
case 'session_meta': {
|
|
168
|
+
const payload = line.payload;
|
|
169
|
+
if (payload.cwd)
|
|
170
|
+
parts.push(payload.cwd);
|
|
171
|
+
if (payload.source)
|
|
172
|
+
parts.push(payload.source);
|
|
173
|
+
if (payload.base_instructions?.text)
|
|
174
|
+
parts.push(payload.base_instructions.text);
|
|
175
|
+
if (payload.git?.branch)
|
|
176
|
+
parts.push(payload.git.branch);
|
|
177
|
+
if (payload.git?.commit)
|
|
178
|
+
parts.push(payload.git.commit);
|
|
179
|
+
return parts.join(' ');
|
|
180
|
+
}
|
|
181
|
+
case 'response_item': {
|
|
182
|
+
const item = line.payload;
|
|
183
|
+
if (item.type === 'message') {
|
|
184
|
+
parts.push(extractTextFromUnknownContent(item.content));
|
|
185
|
+
}
|
|
186
|
+
if (item.type === 'reasoning') {
|
|
187
|
+
const summary = item.summary;
|
|
188
|
+
if (Array.isArray(summary)) {
|
|
189
|
+
for (const entry of summary) {
|
|
190
|
+
if (typeof entry.text === 'string')
|
|
191
|
+
parts.push(entry.text);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (item.type === 'function_call') {
|
|
196
|
+
if (typeof item.name === 'string')
|
|
197
|
+
parts.push(item.name);
|
|
198
|
+
if (typeof item.arguments === 'string')
|
|
199
|
+
parts.push(item.arguments);
|
|
200
|
+
}
|
|
201
|
+
if (item.type === 'function_call_output' && typeof item.output === 'string') {
|
|
202
|
+
parts.push(item.output);
|
|
203
|
+
}
|
|
204
|
+
if (item.type === 'local_shell_call') {
|
|
205
|
+
const action = item.action;
|
|
206
|
+
const command = action?.command;
|
|
207
|
+
if (Array.isArray(command))
|
|
208
|
+
parts.push(command.map(String).join(' '));
|
|
209
|
+
if (typeof action?.workdir === 'string')
|
|
210
|
+
parts.push(action.workdir);
|
|
211
|
+
}
|
|
212
|
+
if (item.type === 'custom_tool_call') {
|
|
213
|
+
if (typeof item.name === 'string')
|
|
214
|
+
parts.push(item.name);
|
|
215
|
+
if (typeof item.input === 'string')
|
|
216
|
+
parts.push(item.input);
|
|
217
|
+
}
|
|
218
|
+
if (item.type === 'custom_tool_call_output' && typeof item.output === 'string') {
|
|
219
|
+
parts.push(item.output);
|
|
220
|
+
}
|
|
221
|
+
return parts.filter(Boolean).join(' ');
|
|
222
|
+
}
|
|
223
|
+
case 'event_msg': {
|
|
224
|
+
const event = line.payload;
|
|
225
|
+
for (const key of [
|
|
226
|
+
'type',
|
|
227
|
+
'message',
|
|
228
|
+
'result',
|
|
229
|
+
'stdout',
|
|
230
|
+
'stderr',
|
|
231
|
+
'summary',
|
|
232
|
+
'error',
|
|
233
|
+
'code',
|
|
234
|
+
'tool_name',
|
|
235
|
+
'server_name',
|
|
236
|
+
]) {
|
|
237
|
+
if (typeof event[key] === 'string')
|
|
238
|
+
parts.push(event[key]);
|
|
239
|
+
}
|
|
240
|
+
const command = event.command;
|
|
241
|
+
if (Array.isArray(command))
|
|
242
|
+
parts.push(command.map(String).join(' '));
|
|
243
|
+
if (event.arguments && typeof event.arguments === 'object')
|
|
244
|
+
parts.push(JSON.stringify(event.arguments));
|
|
245
|
+
return parts.filter(Boolean).join(' ');
|
|
246
|
+
}
|
|
247
|
+
case 'compacted': {
|
|
248
|
+
const payload = line.payload;
|
|
249
|
+
return payload.summary || '';
|
|
250
|
+
}
|
|
251
|
+
case 'turn_context': {
|
|
252
|
+
const payload = line.payload;
|
|
253
|
+
for (const key of ['model', 'cwd', 'approval_policy', 'sandbox_policy', 'effort']) {
|
|
254
|
+
if (typeof payload[key] === 'string')
|
|
255
|
+
parts.push(payload[key]);
|
|
256
|
+
}
|
|
257
|
+
return parts.join(' ');
|
|
258
|
+
}
|
|
259
|
+
default:
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function extractTextFromUnknownContent(content) {
|
|
264
|
+
if (typeof content === 'string')
|
|
265
|
+
return content;
|
|
266
|
+
if (!Array.isArray(content))
|
|
267
|
+
return '';
|
|
268
|
+
return content
|
|
269
|
+
.map((part) => {
|
|
270
|
+
const p = part;
|
|
271
|
+
return typeof p.text === 'string' ? p.text : '';
|
|
272
|
+
})
|
|
273
|
+
.filter(Boolean)
|
|
274
|
+
.join(' ');
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// CodexReader — incremental JSONL reader wrapping CodexRolloutParser
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
/**
|
|
280
|
+
* Incremental JSONL reader for Codex rollout files.
|
|
281
|
+
* Tracks byte position and feeds lines through CodexRolloutParser.
|
|
282
|
+
*/
|
|
283
|
+
class CodexReader {
|
|
284
|
+
rolloutPath;
|
|
285
|
+
onContextWindowLimit;
|
|
286
|
+
onRateLimits;
|
|
287
|
+
onTokenUsage;
|
|
288
|
+
parser;
|
|
289
|
+
filePosition = 0;
|
|
290
|
+
/** Cursor after the last complete line, safe to persist across restarts. */
|
|
291
|
+
committedPosition = 0;
|
|
292
|
+
lineBuffer = '';
|
|
293
|
+
_wasTruncated = false;
|
|
294
|
+
decoder = new StringDecoder('utf8');
|
|
295
|
+
constructor(rolloutPath, onContextWindowLimit, onRateLimits, onTokenUsage) {
|
|
296
|
+
this.rolloutPath = rolloutPath;
|
|
297
|
+
this.onContextWindowLimit = onContextWindowLimit;
|
|
298
|
+
this.onRateLimits = onRateLimits;
|
|
299
|
+
this.onTokenUsage = onTokenUsage;
|
|
300
|
+
this.parser = new CodexRolloutParser();
|
|
301
|
+
}
|
|
302
|
+
/** Get the parser's session metadata. */
|
|
303
|
+
getSessionMeta() {
|
|
304
|
+
return this.parser.getSessionMeta();
|
|
305
|
+
}
|
|
306
|
+
readNew(endPosition) {
|
|
307
|
+
this._wasTruncated = false;
|
|
308
|
+
const events = [];
|
|
309
|
+
try {
|
|
310
|
+
if (!fs.existsSync(this.rolloutPath))
|
|
311
|
+
return [];
|
|
312
|
+
const stats = fs.statSync(this.rolloutPath);
|
|
313
|
+
const currentSize = Math.min(stats.size, endPosition ?? stats.size);
|
|
314
|
+
// Handle truncation
|
|
315
|
+
if (currentSize < this.filePosition) {
|
|
316
|
+
this._wasTruncated = true;
|
|
317
|
+
this.filePosition = 0;
|
|
318
|
+
this.committedPosition = 0;
|
|
319
|
+
this.lineBuffer = '';
|
|
320
|
+
this.parser.reset();
|
|
321
|
+
this.decoder = new StringDecoder('utf8');
|
|
322
|
+
}
|
|
323
|
+
// No new content
|
|
324
|
+
if (currentSize <= this.filePosition)
|
|
325
|
+
return [];
|
|
326
|
+
// Read new bytes
|
|
327
|
+
const fd = fs.openSync(this.rolloutPath, 'r');
|
|
328
|
+
const bufferSize = currentSize - this.filePosition;
|
|
329
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
330
|
+
let bytesRead = 0;
|
|
331
|
+
try {
|
|
332
|
+
bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
fs.closeSync(fd);
|
|
336
|
+
}
|
|
337
|
+
const bytes = buffer.subarray(0, bytesRead);
|
|
338
|
+
const newline = bytes.lastIndexOf(0x0a);
|
|
339
|
+
if (newline >= 0)
|
|
340
|
+
this.committedPosition = this.filePosition + newline + 1;
|
|
341
|
+
const chunk = this.decoder.write(bytes);
|
|
342
|
+
this.filePosition += bytesRead;
|
|
343
|
+
// Process lines
|
|
344
|
+
const text = this.lineBuffer + chunk;
|
|
345
|
+
const lines = text.split('\n');
|
|
346
|
+
// Last element may be incomplete
|
|
347
|
+
this.lineBuffer = lines.pop() || '';
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
const trimmed = line.trim();
|
|
350
|
+
if (!trimmed)
|
|
351
|
+
continue;
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(trimmed);
|
|
354
|
+
events.push(...this.parser.convertLine(parsed));
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Skip malformed JSON lines
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Propagate model_context_window from token_count events to the provider.
|
|
361
|
+
// The model comes along so the value can be persisted per model — the
|
|
362
|
+
// window Codex reports is tier-specific, not the published maximum.
|
|
363
|
+
const mcw = this.parser.getModelContextWindow();
|
|
364
|
+
if (mcw && this.onContextWindowLimit) {
|
|
365
|
+
this.onContextWindowLimit(mcw, this.parser.getCurrentModel() ?? undefined);
|
|
366
|
+
}
|
|
367
|
+
// Propagate rate_limits from token_count events to the provider
|
|
368
|
+
const rateLimits = this.parser.getLastRateLimits();
|
|
369
|
+
if (rateLimits && this.onRateLimits) {
|
|
370
|
+
this.onRateLimits(rateLimits);
|
|
371
|
+
}
|
|
372
|
+
// Propagate last token usage snapshot to the provider
|
|
373
|
+
const lastUsage = this.parser.getLastTokenUsage();
|
|
374
|
+
if (lastUsage && this.onTokenUsage) {
|
|
375
|
+
const normalized = normalizeProviderUsage({
|
|
376
|
+
semantics: 'openai',
|
|
377
|
+
provider: 'openai',
|
|
378
|
+
source: 'codex-reader-snapshot',
|
|
379
|
+
model: this.parser.getCurrentModel() || undefined,
|
|
380
|
+
inputTokens: lastUsage.input_tokens,
|
|
381
|
+
outputTokens: lastUsage.output_tokens,
|
|
382
|
+
cacheReadTokens: lastUsage.cached_input_tokens,
|
|
383
|
+
reasoningTokens: lastUsage.reasoning_output_tokens,
|
|
384
|
+
});
|
|
385
|
+
this.onTokenUsage({
|
|
386
|
+
inputTokens: normalized.uncachedInputTokens,
|
|
387
|
+
outputTokens: normalized.outputTokens,
|
|
388
|
+
cacheWriteTokens: normalized.cacheWriteTokens,
|
|
389
|
+
cacheReadTokens: normalized.cacheReadTokens,
|
|
390
|
+
reasoningTokens: normalized.reasoningTokens,
|
|
391
|
+
model: this.parser.getCurrentModel() || 'unknown',
|
|
392
|
+
timestamp: new Date(),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// Skip read errors
|
|
398
|
+
}
|
|
399
|
+
return events;
|
|
400
|
+
}
|
|
401
|
+
readAll() {
|
|
402
|
+
this.reset();
|
|
403
|
+
return this.readNew();
|
|
404
|
+
}
|
|
405
|
+
reset() {
|
|
406
|
+
this.filePosition = 0;
|
|
407
|
+
this.committedPosition = 0;
|
|
408
|
+
this.lineBuffer = '';
|
|
409
|
+
this.parser.reset();
|
|
410
|
+
this.decoder = new StringDecoder('utf8');
|
|
411
|
+
this._wasTruncated = false;
|
|
412
|
+
}
|
|
413
|
+
exists() {
|
|
414
|
+
return fs.existsSync(this.rolloutPath);
|
|
415
|
+
}
|
|
416
|
+
flush() {
|
|
417
|
+
this.lineBuffer += this.decoder.end();
|
|
418
|
+
// Process any remaining data in the line buffer
|
|
419
|
+
if (this.lineBuffer.trim()) {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(this.lineBuffer.trim());
|
|
422
|
+
this.parser.convertLine(parsed);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// Not a complete line yet
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
this.decoder = new StringDecoder('utf8');
|
|
429
|
+
}
|
|
430
|
+
getPosition() {
|
|
431
|
+
return this.committedPosition;
|
|
432
|
+
}
|
|
433
|
+
seekTo(position) {
|
|
434
|
+
// Rebuild model, turn, and cumulative-token context before resuming.
|
|
435
|
+
// The prefix is parsed only; its events are already in the consumer snapshot.
|
|
436
|
+
this.reset();
|
|
437
|
+
if (position > 0)
|
|
438
|
+
this.readNew(position);
|
|
439
|
+
}
|
|
440
|
+
wasTruncated() {
|
|
441
|
+
return this._wasTruncated;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// CodexProvider — full SessionProviderBase implementation
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
/**
|
|
448
|
+
* Session provider for Codex CLI.
|
|
449
|
+
*
|
|
450
|
+
* Uses SQLite database as optional index, with file-system scanning
|
|
451
|
+
* of the YYYY/MM/DD directory structure as primary discovery method.
|
|
452
|
+
*/
|
|
453
|
+
export class CodexProvider {
|
|
454
|
+
id = 'codex';
|
|
455
|
+
displayName = 'Codex CLI';
|
|
456
|
+
db = null;
|
|
457
|
+
dbInitialized = false;
|
|
458
|
+
dbStatus = { available: false, kind: 'db_missing' };
|
|
459
|
+
diagnostics;
|
|
460
|
+
dynamicContextWindowLimit = null;
|
|
461
|
+
lastRateLimitsData = null;
|
|
462
|
+
lastTokenUsageData = null;
|
|
463
|
+
/** Dated rollout-directory listings reused across async enumerations. */
|
|
464
|
+
directoryCache = new DirectoryListingCache();
|
|
465
|
+
constructor(options = {}) {
|
|
466
|
+
this.diagnostics = new ProviderDiagnosticTracker(this.id, options);
|
|
467
|
+
}
|
|
468
|
+
// --- Database ---
|
|
469
|
+
/** Lazy-initialize database connection. */
|
|
470
|
+
ensureDb() {
|
|
471
|
+
if (this.dbInitialized)
|
|
472
|
+
return this.db;
|
|
473
|
+
this.dbInitialized = true;
|
|
474
|
+
const codexHome = getCodexHome();
|
|
475
|
+
const db = new CodexDatabase(codexHome);
|
|
476
|
+
if (db.isAvailable() && db.open()) {
|
|
477
|
+
this.db = db;
|
|
478
|
+
}
|
|
479
|
+
if (typeof db.getRuntimeStatus === 'function') {
|
|
480
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
481
|
+
}
|
|
482
|
+
return this.db;
|
|
483
|
+
}
|
|
484
|
+
async ensureDbAsync() {
|
|
485
|
+
if (this.db)
|
|
486
|
+
return this.db;
|
|
487
|
+
return new CodexDatabase(getCodexHome());
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Cache an async-created database only after an operation proved it usable.
|
|
491
|
+
* Sync paths treat a cached `db` as "open() succeeded", so adopting an
|
|
492
|
+
* unproven instance would silently disable their file fallbacks.
|
|
493
|
+
*/
|
|
494
|
+
adoptDbIfAvailable(db) {
|
|
495
|
+
if (this.db || !db.getRuntimeStatus().available)
|
|
496
|
+
return;
|
|
497
|
+
this.db = db;
|
|
498
|
+
this.dbInitialized = true;
|
|
499
|
+
}
|
|
500
|
+
getRuntimeStatus() {
|
|
501
|
+
const db = this.ensureDb();
|
|
502
|
+
return db && typeof db.getRuntimeStatus === 'function' ? db.getRuntimeStatus() : this.dbStatus;
|
|
503
|
+
}
|
|
504
|
+
getLastOperationStatus() {
|
|
505
|
+
return this.diagnostics.getLastOperationStatus();
|
|
506
|
+
}
|
|
507
|
+
// --- Path resolution ---
|
|
508
|
+
getSessionDirectory(workspacePath) {
|
|
509
|
+
// Codex doesn't use workspace-encoded directories like Claude Code.
|
|
510
|
+
// All sessions live under ~/.codex/sessions/ in date-based subdirs.
|
|
511
|
+
// We return the sessions base dir; session matching is done by CWD in the data.
|
|
512
|
+
const db = this.ensureDb();
|
|
513
|
+
if (db) {
|
|
514
|
+
const thread = db.getMostRecentThread(workspacePath);
|
|
515
|
+
if (thread?.rollout_path) {
|
|
516
|
+
this.recordDatabaseFallback('getSessionDirectory', true);
|
|
517
|
+
return path.dirname(thread.rollout_path);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const directory = getSessionsDir();
|
|
521
|
+
this.recordDatabaseFallback('getSessionDirectory', true);
|
|
522
|
+
return directory;
|
|
523
|
+
}
|
|
524
|
+
discoverSessionDirectory(workspacePath) {
|
|
525
|
+
// DB: find latest rollout directory for this workspace
|
|
526
|
+
const db = this.ensureDb();
|
|
527
|
+
if (db) {
|
|
528
|
+
const thread = db.getMostRecentThread(workspacePath);
|
|
529
|
+
if (thread?.rollout_path && fs.existsSync(thread.rollout_path)) {
|
|
530
|
+
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
531
|
+
return path.dirname(thread.rollout_path);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// File scan: look for any rollout file whose session_meta.cwd matches
|
|
535
|
+
// (newest first; the cwd read is cached per path).
|
|
536
|
+
const files = findRolloutFilesInConfiguredHomes();
|
|
537
|
+
if (files.length === 0) {
|
|
538
|
+
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
for (const file of files) {
|
|
542
|
+
const cwd = readSessionCwd(file.path);
|
|
543
|
+
if (cwd && cwdMatches(cwd, workspacePath)) {
|
|
544
|
+
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
545
|
+
return path.dirname(file.path);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Fallback: return the most relevant base sessions dir if one exists
|
|
549
|
+
const directory = getSessionsDir();
|
|
550
|
+
this.recordDatabaseFallback('discoverSessionDirectory', true);
|
|
551
|
+
return directory;
|
|
552
|
+
}
|
|
553
|
+
// --- Session discovery ---
|
|
554
|
+
findActiveSession(workspacePath) {
|
|
555
|
+
// DB first
|
|
556
|
+
const db = this.ensureDb();
|
|
557
|
+
if (db) {
|
|
558
|
+
const thread = db.getMostRecentThread(workspacePath);
|
|
559
|
+
if (thread?.rollout_path && fs.existsSync(thread.rollout_path)) {
|
|
560
|
+
this.recordDatabaseFallback('findActiveSession', true);
|
|
561
|
+
return thread.rollout_path;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// File scan fallback (newest first; cwd reads are cached per path)
|
|
565
|
+
for (const file of findRolloutFilesInConfiguredHomes()) {
|
|
566
|
+
const cwd = readSessionCwd(file.path);
|
|
567
|
+
if (cwd && cwdMatches(cwd, workspacePath)) {
|
|
568
|
+
this.recordDatabaseFallback('findActiveSession', true);
|
|
569
|
+
return file.path;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
this.recordDatabaseFallback('findActiveSession', true);
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
findAllSessions(workspacePath) {
|
|
576
|
+
// DB first
|
|
577
|
+
const db = this.ensureDb();
|
|
578
|
+
if (db) {
|
|
579
|
+
const threads = db.getThreadsByCwd(workspacePath);
|
|
580
|
+
const dbPaths = threads
|
|
581
|
+
.filter((t) => t.rollout_path && fs.existsSync(t.rollout_path))
|
|
582
|
+
.map((t) => t.rollout_path);
|
|
583
|
+
if (dbPaths.length > 0) {
|
|
584
|
+
this.recordDatabaseFallback('findAllSessions', true);
|
|
585
|
+
return dbPaths;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// File scan (newest first; cwd reads are cached per path)
|
|
589
|
+
const sessions = findRolloutFilesInConfiguredHomes()
|
|
590
|
+
.filter((f) => {
|
|
591
|
+
const cwd = readSessionCwd(f.path);
|
|
592
|
+
return cwd !== null && cwdMatches(cwd, workspacePath);
|
|
593
|
+
})
|
|
594
|
+
.map((f) => f.path);
|
|
595
|
+
this.recordDatabaseFallback('findAllSessions', true);
|
|
596
|
+
return sessions;
|
|
597
|
+
}
|
|
598
|
+
findSessionById(_workspacePath, sessionId) {
|
|
599
|
+
try {
|
|
600
|
+
const found = findCodexRolloutFile(sessionId);
|
|
601
|
+
this.recordDatabaseFallback('findSessionById', true);
|
|
602
|
+
return found;
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
this.recordDatabaseFallback('findSessionById', false);
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/** Backward-compatible alias for findAllSessions. */
|
|
610
|
+
findSessionFiles(workspacePath) {
|
|
611
|
+
return this.findAllSessions(workspacePath);
|
|
612
|
+
}
|
|
613
|
+
findSessionsInDirectory(dir) {
|
|
614
|
+
return findRolloutFiles(dir).map((f) => f.path);
|
|
615
|
+
}
|
|
616
|
+
listAllSessionFiles() {
|
|
617
|
+
const files = findRolloutFilesInConfiguredHomes();
|
|
618
|
+
this.recordDatabaseFallback('listAllSessionFiles', true);
|
|
619
|
+
return files;
|
|
620
|
+
}
|
|
621
|
+
async listSessionFilesAsync(workspacePath, options = {}) {
|
|
622
|
+
const results = await listCodexSessionFilesAsync(workspacePath, options, this.directoryCache);
|
|
623
|
+
// Filesystem enumeration bypasses the database, so record that here as
|
|
624
|
+
// the sync listing paths do; otherwise `getLastOperationStatus()` keeps
|
|
625
|
+
// describing whichever operation ran before this one.
|
|
626
|
+
this.recordDatabaseFallback('listSessionFilesAsync', true);
|
|
627
|
+
return results;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Resolve a recursive-watch event under a sessions root with one stat.
|
|
631
|
+
* Rollouts are matched by name at any depth the walker would visit; a
|
|
632
|
+
* dated directory appearing or vanishing needs a full walk.
|
|
633
|
+
*/
|
|
634
|
+
async statWatchedSessionFile(root, relativePath, workspacePath) {
|
|
635
|
+
const segments = splitWatchedPath(relativePath);
|
|
636
|
+
if (!segments)
|
|
637
|
+
return { status: 'unknown' };
|
|
638
|
+
const fullPath = path.join(root, ...segments);
|
|
639
|
+
const name = segments[segments.length - 1];
|
|
640
|
+
if (segments.length - 1 > DEFAULT_ROLLOUT_WALK_MAX_DEPTH)
|
|
641
|
+
return { status: 'ignored' };
|
|
642
|
+
if (!isRolloutFile(name)) {
|
|
643
|
+
this.directoryCache.invalidate(fullPath);
|
|
644
|
+
try {
|
|
645
|
+
const stat = await fs.promises.stat(fullPath);
|
|
646
|
+
return stat.isDirectory() ? { status: 'unknown' } : { status: 'ignored' };
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return { status: 'unknown' };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const sessionId = extractSessionId(name);
|
|
653
|
+
const stat = await this.directoryCache.refreshFile(fullPath);
|
|
654
|
+
if (!stat || stat.sizeBytes <= 0)
|
|
655
|
+
return { status: 'missing', path: fullPath, sessionId };
|
|
656
|
+
const file = {
|
|
657
|
+
path: fullPath,
|
|
658
|
+
mtime: new Date(stat.mtimeMs),
|
|
659
|
+
sizeBytes: stat.sizeBytes,
|
|
660
|
+
sessionId,
|
|
661
|
+
};
|
|
662
|
+
if (workspacePath) {
|
|
663
|
+
const cwd = await readSessionCwdAsync(fullPath);
|
|
664
|
+
if (!cwd || !cwdMatches(cwd, workspacePath))
|
|
665
|
+
return { status: 'ignored' };
|
|
666
|
+
file.workspacePath = cwd;
|
|
667
|
+
}
|
|
668
|
+
return { status: 'present', file };
|
|
669
|
+
}
|
|
670
|
+
getAllProjectFolders(workspacePath) {
|
|
671
|
+
const folders = [];
|
|
672
|
+
const seenCwds = new Map();
|
|
673
|
+
// DB first
|
|
674
|
+
const db = this.ensureDb();
|
|
675
|
+
if (db) {
|
|
676
|
+
const cwdStats = db.getAllDistinctCwds();
|
|
677
|
+
const sessionsDir = getSessionsDir();
|
|
678
|
+
for (const stat of cwdStats) {
|
|
679
|
+
seenCwds.set(stat.cwd, {
|
|
680
|
+
dir: sessionsDir,
|
|
681
|
+
name: stat.cwd,
|
|
682
|
+
encodedName: stat.cwd,
|
|
683
|
+
sessionCount: stat.count,
|
|
684
|
+
lastModified: new Date(stat.lastUpdated),
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// File scan supplement. When the index answered, only rollouts written
|
|
689
|
+
// after its last update can be missing from it, so older files are not
|
|
690
|
+
// opened; the cwd read itself is cached per path either way.
|
|
691
|
+
const indexedUpTo = db ? db.getDbMtime() : 0;
|
|
692
|
+
for (const file of findRolloutFilesInConfiguredHomes()) {
|
|
693
|
+
if (db && indexedUpTo > 0 && file.mtime.getTime() <= indexedUpTo)
|
|
694
|
+
continue;
|
|
695
|
+
const cwd = readSessionCwd(file.path);
|
|
696
|
+
if (!cwd)
|
|
697
|
+
continue;
|
|
698
|
+
const existing = seenCwds.get(cwd);
|
|
699
|
+
if (existing) {
|
|
700
|
+
// Update mtime if newer
|
|
701
|
+
if (file.mtime > existing.lastModified) {
|
|
702
|
+
existing.lastModified = file.mtime;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
seenCwds.set(cwd, {
|
|
707
|
+
dir: path.dirname(file.path),
|
|
708
|
+
name: cwd,
|
|
709
|
+
encodedName: cwd,
|
|
710
|
+
sessionCount: 1,
|
|
711
|
+
lastModified: file.mtime,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
folders.push(...seenCwds.values());
|
|
716
|
+
// Sort: current workspace first, then by recency
|
|
717
|
+
const normalizedWorkspace = workspacePath ? normalizePath(workspacePath) : null;
|
|
718
|
+
folders.sort((a, b) => {
|
|
719
|
+
if (normalizedWorkspace) {
|
|
720
|
+
const aMatch = cwdMatches(a.name, normalizedWorkspace);
|
|
721
|
+
const bMatch = cwdMatches(b.name, normalizedWorkspace);
|
|
722
|
+
if (aMatch && !bMatch)
|
|
723
|
+
return -1;
|
|
724
|
+
if (!aMatch && bMatch)
|
|
725
|
+
return 1;
|
|
726
|
+
}
|
|
727
|
+
return b.lastModified.getTime() - a.lastModified.getTime();
|
|
728
|
+
});
|
|
729
|
+
this.recordDatabaseFallback('getAllProjectFolders', true);
|
|
730
|
+
return folders;
|
|
731
|
+
}
|
|
732
|
+
// --- File identification ---
|
|
733
|
+
isSessionFile(filename) {
|
|
734
|
+
return isRolloutFile(filename);
|
|
735
|
+
}
|
|
736
|
+
getSessionId(sessionPath) {
|
|
737
|
+
return extractSessionId(path.basename(sessionPath));
|
|
738
|
+
}
|
|
739
|
+
encodeWorkspacePath(workspacePath) {
|
|
740
|
+
// Codex doesn't encode workspace paths in directory names
|
|
741
|
+
return workspacePath;
|
|
742
|
+
}
|
|
743
|
+
/** Thread title or first user message from the SQLite index; null without it. */
|
|
744
|
+
extractSessionLabelFromDb(sessionPath) {
|
|
745
|
+
const db = this.ensureDb();
|
|
746
|
+
if (!db)
|
|
747
|
+
return null;
|
|
748
|
+
const sessionId = this.getSessionId(sessionPath);
|
|
749
|
+
const thread = db.getThread(sessionId);
|
|
750
|
+
if (thread?.title) {
|
|
751
|
+
this.recordDatabaseFallback('extractSessionLabel', true);
|
|
752
|
+
return truncate(thread.title, 60);
|
|
753
|
+
}
|
|
754
|
+
if (thread?.first_user_message) {
|
|
755
|
+
this.recordDatabaseFallback('extractSessionLabel', true);
|
|
756
|
+
return truncate(thread.first_user_message, 60);
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
extractSessionLabel(sessionPath) {
|
|
761
|
+
const fromDb = this.extractSessionLabelFromDb(sessionPath);
|
|
762
|
+
if (fromDb)
|
|
763
|
+
return fromDb;
|
|
764
|
+
// File fallback: parse first user message
|
|
765
|
+
const label = extractFirstUserMessage(sessionPath);
|
|
766
|
+
this.recordDatabaseFallback('extractSessionLabel', true);
|
|
767
|
+
return label;
|
|
768
|
+
}
|
|
769
|
+
async extractSessionLabelsAsync(sessionPaths) {
|
|
770
|
+
const results = new Map();
|
|
771
|
+
const byId = new Map(sessionPaths.map((sessionPath) => [this.getSessionId(sessionPath), sessionPath]));
|
|
772
|
+
const db = await this.ensureDbAsync();
|
|
773
|
+
const threads = await db.getThreadsByIdsAsync([...byId.keys()]);
|
|
774
|
+
for (const thread of threads) {
|
|
775
|
+
const sessionPath = byId.get(thread.id);
|
|
776
|
+
if (!sessionPath)
|
|
777
|
+
continue;
|
|
778
|
+
const label = thread.title ?? thread.first_user_message;
|
|
779
|
+
if (label)
|
|
780
|
+
results.set(sessionPath, truncate(label, 60));
|
|
781
|
+
}
|
|
782
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
783
|
+
this.adoptDbIfAvailable(db);
|
|
784
|
+
for (const sessionPath of sessionPaths) {
|
|
785
|
+
if (!results.has(sessionPath)) {
|
|
786
|
+
results.set(sessionPath, await extractFirstUserMessageAsync(sessionPath));
|
|
787
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
this.recordDatabaseFallback('extractSessionLabelsAsync', true);
|
|
791
|
+
return results;
|
|
792
|
+
}
|
|
793
|
+
// --- Data reading ---
|
|
794
|
+
createReader(sessionPath) {
|
|
795
|
+
return new CodexReader(sessionPath, (limit, modelId) => {
|
|
796
|
+
this.dynamicContextWindowLimit = limit;
|
|
797
|
+
if (modelId) {
|
|
798
|
+
// Fire-and-forget: persist so historical/static views keep this
|
|
799
|
+
// tier-specific window instead of falling back to the catalog maximum.
|
|
800
|
+
void recordObservedContextWindow(modelId, limit).catch(() => {
|
|
801
|
+
/* non-fatal; the in-memory limit above still applies */
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}, (limits) => {
|
|
805
|
+
this.lastRateLimitsData = limits;
|
|
806
|
+
}, (usage) => {
|
|
807
|
+
this.lastTokenUsageData = usage;
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
// --- Subagent support ---
|
|
811
|
+
scanSubagents(_sessionDir, sessionId) {
|
|
812
|
+
// Codex tracks forked sessions via forked_from_id in session_meta.
|
|
813
|
+
// Use DB when available, fall back to filesystem scanning.
|
|
814
|
+
const subagents = [];
|
|
815
|
+
// DB approach: spawned subagents (parent recorded in `threads.source`),
|
|
816
|
+
// plus forked children on older schemas that carry `forked_from_id`.
|
|
817
|
+
const db = this.ensureDb();
|
|
818
|
+
if (db) {
|
|
819
|
+
const seen = new Set();
|
|
820
|
+
const children = [
|
|
821
|
+
...db.getSpawnedChildThreads(sessionId),
|
|
822
|
+
...db.getThreadsByForkedFromId(sessionId),
|
|
823
|
+
].filter((thread) => !seen.has(thread.id) && seen.add(thread.id));
|
|
824
|
+
for (const thread of children) {
|
|
825
|
+
const built = cachedSubagentStats(thread.rollout_path, () => this.buildSubagentStats(thread.id, thread.rollout_path, thread.title || thread.agent_nickname));
|
|
826
|
+
if (built) {
|
|
827
|
+
subagents.push(thread.agent_role && !built.agentType
|
|
828
|
+
? { ...built, agentType: thread.agent_role }
|
|
829
|
+
: built);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (children.length > 0)
|
|
833
|
+
return subagents;
|
|
834
|
+
}
|
|
835
|
+
// Filesystem fallback: scan recent rollout files for a matching
|
|
836
|
+
// forked_from_id. The walk stops after the 50 newest-dated files instead
|
|
837
|
+
// of enumerating the whole history and slicing afterwards.
|
|
838
|
+
for (const file of findRolloutFilesInConfiguredHomes({ limit: 50 })) {
|
|
839
|
+
const meta = readSessionMeta(file.path);
|
|
840
|
+
if (meta &&
|
|
841
|
+
(meta.forked_from_id === sessionId || spawnParentFromSource(meta.source) === sessionId)) {
|
|
842
|
+
const childId = extractSessionId(path.basename(file.path));
|
|
843
|
+
// `source` is a plain string for top-level sessions but an object
|
|
844
|
+
// (`{ subagent: { thread_spawn: … } }`) for spawned children.
|
|
845
|
+
const description = typeof meta.source === 'string' ? meta.source : undefined;
|
|
846
|
+
const stats = cachedSubagentStats(file.path, () => this.buildSubagentStats(childId, file.path, description));
|
|
847
|
+
if (stats)
|
|
848
|
+
subagents.push(stats);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return subagents;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Builds SubagentStats for a forked session by reading its rollout file.
|
|
855
|
+
*/
|
|
856
|
+
buildSubagentStats(agentId, rolloutPath, description) {
|
|
857
|
+
if (!rolloutPath || !fs.existsSync(rolloutPath))
|
|
858
|
+
return null;
|
|
859
|
+
const stats = {
|
|
860
|
+
agentId,
|
|
861
|
+
description: description || undefined,
|
|
862
|
+
toolCalls: [],
|
|
863
|
+
inputTokens: 0,
|
|
864
|
+
outputTokens: 0,
|
|
865
|
+
};
|
|
866
|
+
try {
|
|
867
|
+
const parser = new CodexRolloutParser();
|
|
868
|
+
const content = fs.readFileSync(rolloutPath, 'utf8');
|
|
869
|
+
const lines = content.split('\n');
|
|
870
|
+
for (const line of lines) {
|
|
871
|
+
const trimmed = line.trim();
|
|
872
|
+
if (!trimmed)
|
|
873
|
+
continue;
|
|
874
|
+
try {
|
|
875
|
+
const parsed = JSON.parse(trimmed);
|
|
876
|
+
// Track timestamps
|
|
877
|
+
if (parsed.timestamp) {
|
|
878
|
+
const ts = new Date(parsed.timestamp);
|
|
879
|
+
if (!stats.startTime || ts < stats.startTime)
|
|
880
|
+
stats.startTime = ts;
|
|
881
|
+
if (!stats.endTime || ts > stats.endTime)
|
|
882
|
+
stats.endTime = ts;
|
|
883
|
+
}
|
|
884
|
+
const events = parser.convertLine(parsed);
|
|
885
|
+
for (const event of events) {
|
|
886
|
+
// Collect tool calls
|
|
887
|
+
if (event.type === 'assistant' && Array.isArray(event.message.content)) {
|
|
888
|
+
for (const block of event.message.content) {
|
|
889
|
+
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
890
|
+
stats.toolCalls.push({
|
|
891
|
+
name: block.name,
|
|
892
|
+
input: block.input || {},
|
|
893
|
+
timestamp: new Date(event.timestamp),
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// Accumulate tokens from usage events (duplicate token_count events
|
|
899
|
+
// are already dropped by the parser).
|
|
900
|
+
const usage = extractNormalizedUsage(event);
|
|
901
|
+
if (usage)
|
|
902
|
+
addUsageToSubagent(stats, usage);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
/* skip malformed lines */
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (stats.startTime && stats.endTime) {
|
|
910
|
+
stats.durationMs = stats.endTime.getTime() - stats.startTime.getTime();
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
/* skip unreadable files */
|
|
915
|
+
}
|
|
916
|
+
return stats;
|
|
917
|
+
}
|
|
918
|
+
// --- Cross-session search ---
|
|
919
|
+
searchInSession(sessionPath, query, maxResults) {
|
|
920
|
+
const results = [];
|
|
921
|
+
const queryLower = query.toLowerCase();
|
|
922
|
+
try {
|
|
923
|
+
const content = fs.readFileSync(sessionPath, 'utf8');
|
|
924
|
+
const lines = content.split('\n');
|
|
925
|
+
for (const line of lines) {
|
|
926
|
+
if (results.length >= maxResults)
|
|
927
|
+
break;
|
|
928
|
+
if (!line.trim())
|
|
929
|
+
continue;
|
|
930
|
+
const lineLower = line.toLowerCase();
|
|
931
|
+
if (!lineLower.includes(queryLower))
|
|
932
|
+
continue;
|
|
933
|
+
try {
|
|
934
|
+
const parsed = JSON.parse(line);
|
|
935
|
+
const text = extractSearchableText(parsed);
|
|
936
|
+
if (!text)
|
|
937
|
+
continue;
|
|
938
|
+
const textLower = text.toLowerCase();
|
|
939
|
+
const matchIdx = textLower.indexOf(queryLower);
|
|
940
|
+
if (matchIdx < 0)
|
|
941
|
+
continue;
|
|
942
|
+
const start = Math.max(0, matchIdx - 40);
|
|
943
|
+
const end = Math.min(text.length, matchIdx + query.length + 40);
|
|
944
|
+
const snippet = (start > 0 ? '...' : '') +
|
|
945
|
+
text.substring(start, end) +
|
|
946
|
+
(end < text.length ? '...' : '');
|
|
947
|
+
results.push({
|
|
948
|
+
sessionPath,
|
|
949
|
+
line: snippet.replace(/\n/g, ' '),
|
|
950
|
+
eventType: parsed.type,
|
|
951
|
+
timestamp: parsed.timestamp || '',
|
|
952
|
+
projectPath: readSessionCwd(sessionPath) || sessionPath,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
catch {
|
|
956
|
+
// Skip malformed JSON
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
// Skip unreadable files
|
|
962
|
+
}
|
|
963
|
+
return results;
|
|
964
|
+
}
|
|
965
|
+
getProjectsBaseDir() {
|
|
966
|
+
return getSessionsDir();
|
|
967
|
+
}
|
|
968
|
+
getWatchRoots() {
|
|
969
|
+
return getSessionsDirs();
|
|
970
|
+
}
|
|
971
|
+
// --- Stats ---
|
|
972
|
+
readSessionStats(sessionPath) {
|
|
973
|
+
// One reader pass computes stats and the label: the SQLite thread title is
|
|
974
|
+
// used when the index is available, otherwise the first user prompt comes
|
|
975
|
+
// from the events already read, so the rollout is never opened twice.
|
|
976
|
+
return readSessionFileStats(this, sessionPath, {
|
|
977
|
+
resolveLabel: () => this.extractSessionLabelFromDb(sessionPath),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
readSessionContextSnapshot(sessionPath, options = {}) {
|
|
981
|
+
return readSessionContextSnapshot(this, sessionPath, options);
|
|
982
|
+
}
|
|
983
|
+
// --- Optional methods ---
|
|
984
|
+
getContextWindowLimit(modelId) {
|
|
985
|
+
// Prefer actual model_context_window reported by token_count events
|
|
986
|
+
if (this.dynamicContextWindowLimit)
|
|
987
|
+
return this.dynamicContextWindowLimit;
|
|
988
|
+
return getModelContextWindowSize(modelId);
|
|
989
|
+
}
|
|
990
|
+
getCurrentUsageSnapshot(_sessionPath) {
|
|
991
|
+
// Return the last token usage snapshot received via the CodexReader callback.
|
|
992
|
+
// This is populated in real-time as token_count events arrive during active monitoring.
|
|
993
|
+
return this.lastTokenUsageData;
|
|
994
|
+
}
|
|
995
|
+
computeContextSize(usage) {
|
|
996
|
+
// Codex usage is normalized to the disjoint TokenUsage shape at the reader
|
|
997
|
+
// boundary, so reconstruct the full prompt/context from uncached + cached.
|
|
998
|
+
return usage.inputTokens + usage.cacheReadTokens;
|
|
999
|
+
}
|
|
1000
|
+
// --- Rate limits (for vscode wrapper to build QuotaState) ---
|
|
1001
|
+
/** Get the last rate limits data received from token_count events. */
|
|
1002
|
+
getLastRateLimits() {
|
|
1003
|
+
return this.lastRateLimitsData;
|
|
1004
|
+
}
|
|
1005
|
+
// --- Lifecycle ---
|
|
1006
|
+
dispose() {
|
|
1007
|
+
this.db?.close();
|
|
1008
|
+
this.db = null;
|
|
1009
|
+
this.dbInitialized = false;
|
|
1010
|
+
this.dbStatus = { available: false, kind: 'db_missing' };
|
|
1011
|
+
this.dynamicContextWindowLimit = null;
|
|
1012
|
+
this.lastRateLimitsData = null;
|
|
1013
|
+
this.lastTokenUsageData = null;
|
|
1014
|
+
this.directoryCache.invalidate();
|
|
1015
|
+
}
|
|
1016
|
+
recordDatabaseFallback(operation, usable) {
|
|
1017
|
+
if (!this.dbInitialized) {
|
|
1018
|
+
this.diagnostics.available(operation);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const status = this.getRuntimeStatus();
|
|
1022
|
+
const diagnostic = diagnosticFromRuntimeStatus(this.id, 'query', status);
|
|
1023
|
+
if (!diagnostic) {
|
|
1024
|
+
this.diagnostics.available(operation);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
this.diagnostics.degraded(operation, status, {
|
|
1028
|
+
kind: diagnostic.kind,
|
|
1029
|
+
severity: diagnostic.severity,
|
|
1030
|
+
phase: diagnostic.phase,
|
|
1031
|
+
message: diagnostic.message,
|
|
1032
|
+
}, usable);
|
|
1033
|
+
}
|
|
1034
|
+
}
|