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,450 @@
|
|
|
1
|
+
import { resolveActiveClaudeAccount } from './accounts';
|
|
2
|
+
import { CodexQuotaWatcher } from './codexQuotaWatcher';
|
|
3
|
+
import { readClaudeMaxCredentials } from './credentials';
|
|
4
|
+
import { fetchPeakHoursStatus } from './peakHours';
|
|
5
|
+
import { fetchQuota } from './quota';
|
|
6
|
+
import { describeQuotaFailure } from './quotaPresentation';
|
|
7
|
+
import { onAccountsChanged } from './accountChanges';
|
|
8
|
+
const ACTIVE_POLL_INTERVAL_MS = 60_000;
|
|
9
|
+
const IDLE_POLL_INTERVAL_MS = 300_000;
|
|
10
|
+
const TRANSIENT_FAILURE_BACKOFF_MS = [30_000, 60_000, 120_000, 240_000, 300_000];
|
|
11
|
+
const PEAK_HOURS_CACHE_MAX_AGE_MS = 10 * 60 * 1000;
|
|
12
|
+
const SIGNED_OUT_ERROR = 'Sign in to a Claude account to view quota';
|
|
13
|
+
function makeUnavailableClaudeState(error, failureKind) {
|
|
14
|
+
return {
|
|
15
|
+
runtimeProvider: 'claude',
|
|
16
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
17
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
18
|
+
available: false,
|
|
19
|
+
error,
|
|
20
|
+
failureKind,
|
|
21
|
+
providerId: 'claude-code',
|
|
22
|
+
source: 'api',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function formatDelay(ms) {
|
|
26
|
+
if (ms % 60_000 === 0)
|
|
27
|
+
return `${ms / 60_000}m`;
|
|
28
|
+
if (ms % 1_000 === 0)
|
|
29
|
+
return `${ms / 1_000}s`;
|
|
30
|
+
return `${ms}ms`;
|
|
31
|
+
}
|
|
32
|
+
function unavailablePeakHoursState() {
|
|
33
|
+
return {
|
|
34
|
+
status: 'unknown',
|
|
35
|
+
isPeak: false,
|
|
36
|
+
sessionLimitSpeed: 'unknown',
|
|
37
|
+
label: 'Peak hours unavailable',
|
|
38
|
+
peakHoursDescription: '',
|
|
39
|
+
nextChange: null,
|
|
40
|
+
minutesUntilChange: null,
|
|
41
|
+
note: '',
|
|
42
|
+
updatedAt: new Date().toISOString(),
|
|
43
|
+
unavailable: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export class MultiProviderQuotaService {
|
|
47
|
+
activeIntervalMs;
|
|
48
|
+
idleIntervalMs;
|
|
49
|
+
transientFailureBackoffMs;
|
|
50
|
+
peakHoursCacheMaxAgeMs;
|
|
51
|
+
includePeakHours;
|
|
52
|
+
readClaudeCredentials;
|
|
53
|
+
readClaudeAccount;
|
|
54
|
+
fetchClaudeQuota;
|
|
55
|
+
fetchPeakHours;
|
|
56
|
+
log;
|
|
57
|
+
subscribeAccountsChanged;
|
|
58
|
+
listeners = [];
|
|
59
|
+
codexWatcher;
|
|
60
|
+
ownsCodexWatcher;
|
|
61
|
+
codexSubscription = null;
|
|
62
|
+
zaiWatcher;
|
|
63
|
+
ownsZaiWatcher;
|
|
64
|
+
zaiSubscription = null;
|
|
65
|
+
timer;
|
|
66
|
+
polling = false;
|
|
67
|
+
quotaByProvider = {};
|
|
68
|
+
lastEmittedClaudeState = null;
|
|
69
|
+
lastSuccessfulClaudeState = null;
|
|
70
|
+
pollingMode = 'idle';
|
|
71
|
+
transientFailureStreak = 0;
|
|
72
|
+
cachedFallbackActive = false;
|
|
73
|
+
authUnavailableActive = false;
|
|
74
|
+
currentPeakHours;
|
|
75
|
+
lastFreshPeakHoursAt = 0;
|
|
76
|
+
peakHoursErrorLogged = false;
|
|
77
|
+
claudeDormant = false;
|
|
78
|
+
accountSubscription = null;
|
|
79
|
+
pollInFlight = false;
|
|
80
|
+
pendingWake = false;
|
|
81
|
+
constructor(options = {}) {
|
|
82
|
+
this.activeIntervalMs = options.activeIntervalMs ?? ACTIVE_POLL_INTERVAL_MS;
|
|
83
|
+
this.idleIntervalMs = options.idleIntervalMs ?? IDLE_POLL_INTERVAL_MS;
|
|
84
|
+
this.transientFailureBackoffMs =
|
|
85
|
+
options.transientFailureBackoffMs ?? TRANSIENT_FAILURE_BACKOFF_MS;
|
|
86
|
+
this.peakHoursCacheMaxAgeMs = options.peakHoursCacheMaxAgeMs ?? PEAK_HOURS_CACHE_MAX_AGE_MS;
|
|
87
|
+
this.includePeakHours = options.includePeakHours ?? true;
|
|
88
|
+
this.readClaudeCredentials = options.readClaudeCredentials ?? readClaudeMaxCredentials;
|
|
89
|
+
this.readClaudeAccount = options.readClaudeAccount ?? resolveActiveClaudeAccount;
|
|
90
|
+
this.fetchClaudeQuota = options.fetchClaudeQuota ?? fetchQuota;
|
|
91
|
+
this.fetchPeakHours = options.fetchPeakHours ?? fetchPeakHoursStatus;
|
|
92
|
+
this.subscribeAccountsChanged = options.subscribeAccountsChanged ?? onAccountsChanged;
|
|
93
|
+
this.log = options.log;
|
|
94
|
+
if (options.codexWatcher !== undefined) {
|
|
95
|
+
this.codexWatcher = options.codexWatcher;
|
|
96
|
+
this.ownsCodexWatcher = false;
|
|
97
|
+
}
|
|
98
|
+
else if (options.codexWorkspacePath) {
|
|
99
|
+
this.codexWatcher = new CodexQuotaWatcher(options.codexWorkspacePath);
|
|
100
|
+
this.ownsCodexWatcher = true;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.codexWatcher = null;
|
|
104
|
+
this.ownsCodexWatcher = false;
|
|
105
|
+
}
|
|
106
|
+
this.codexSubscription =
|
|
107
|
+
this.codexWatcher?.onUpdate((state) => {
|
|
108
|
+
this.updateProviderQuota('codex', state);
|
|
109
|
+
}) ?? null;
|
|
110
|
+
if (options.zaiWatcher !== undefined) {
|
|
111
|
+
this.zaiWatcher = options.zaiWatcher;
|
|
112
|
+
this.ownsZaiWatcher = false;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.zaiWatcher = null;
|
|
116
|
+
this.ownsZaiWatcher = false;
|
|
117
|
+
}
|
|
118
|
+
this.zaiSubscription =
|
|
119
|
+
this.zaiWatcher?.onUpdate((state) => {
|
|
120
|
+
this.updateProviderQuota('zai', state);
|
|
121
|
+
}) ?? null;
|
|
122
|
+
}
|
|
123
|
+
startPolling() {
|
|
124
|
+
if (this.polling)
|
|
125
|
+
return;
|
|
126
|
+
this.polling = true;
|
|
127
|
+
this.accountSubscription ??= this.subscribeAccountsChanged(() => this.wakeForAccountChange());
|
|
128
|
+
this.codexWatcher?.start();
|
|
129
|
+
this.zaiWatcher?.start();
|
|
130
|
+
this.wakeForAccountChange();
|
|
131
|
+
this.log?.('[Quota] Polling started');
|
|
132
|
+
}
|
|
133
|
+
stopPolling() {
|
|
134
|
+
if (!this.polling)
|
|
135
|
+
return;
|
|
136
|
+
this.polling = false;
|
|
137
|
+
this.claudeDormant = false;
|
|
138
|
+
if (this.timer) {
|
|
139
|
+
clearTimeout(this.timer);
|
|
140
|
+
this.timer = undefined;
|
|
141
|
+
}
|
|
142
|
+
this.codexWatcher?.stop();
|
|
143
|
+
this.zaiWatcher?.stop();
|
|
144
|
+
this.accountSubscription?.dispose();
|
|
145
|
+
this.accountSubscription = null;
|
|
146
|
+
this.log?.('[Quota] Polling stopped');
|
|
147
|
+
}
|
|
148
|
+
setPollingMode(mode) {
|
|
149
|
+
if (this.pollingMode === mode)
|
|
150
|
+
return;
|
|
151
|
+
this.pollingMode = mode;
|
|
152
|
+
if (!this.polling)
|
|
153
|
+
return;
|
|
154
|
+
if (this.transientFailureStreak > 0 && this.timer)
|
|
155
|
+
return;
|
|
156
|
+
if (this.timer) {
|
|
157
|
+
clearTimeout(this.timer);
|
|
158
|
+
this.timer = undefined;
|
|
159
|
+
}
|
|
160
|
+
this.scheduleNextPoll();
|
|
161
|
+
}
|
|
162
|
+
onUpdate(cb) {
|
|
163
|
+
this.listeners.push(cb);
|
|
164
|
+
return {
|
|
165
|
+
dispose: () => {
|
|
166
|
+
const index = this.listeners.indexOf(cb);
|
|
167
|
+
if (index >= 0)
|
|
168
|
+
this.listeners.splice(index, 1);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
onQuotaUpdate(cb) {
|
|
173
|
+
return this.onUpdate(cb);
|
|
174
|
+
}
|
|
175
|
+
getLatest() {
|
|
176
|
+
return { ...this.quotaByProvider };
|
|
177
|
+
}
|
|
178
|
+
updateProviderQuota(provider, state) {
|
|
179
|
+
if (provider === 'claude') {
|
|
180
|
+
const nextState = this.withClaudeAccountDetails({
|
|
181
|
+
...state,
|
|
182
|
+
runtimeProvider: 'claude',
|
|
183
|
+
providerId: 'claude-code',
|
|
184
|
+
});
|
|
185
|
+
this.lastEmittedClaudeState = nextState;
|
|
186
|
+
if (nextState.available && nextState.stale !== true) {
|
|
187
|
+
this.lastSuccessfulClaudeState = nextState;
|
|
188
|
+
}
|
|
189
|
+
this.quotaByProvider = {
|
|
190
|
+
...this.quotaByProvider,
|
|
191
|
+
claude: nextState,
|
|
192
|
+
};
|
|
193
|
+
this.emit();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (provider === 'zai') {
|
|
197
|
+
const nextState = {
|
|
198
|
+
...state,
|
|
199
|
+
runtimeProvider: 'zai',
|
|
200
|
+
providerId: 'zai',
|
|
201
|
+
};
|
|
202
|
+
this.quotaByProvider = {
|
|
203
|
+
...this.quotaByProvider,
|
|
204
|
+
zai: nextState,
|
|
205
|
+
};
|
|
206
|
+
this.emit();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const nextState = {
|
|
210
|
+
...state,
|
|
211
|
+
runtimeProvider: 'codex',
|
|
212
|
+
providerId: 'codex',
|
|
213
|
+
};
|
|
214
|
+
this.quotaByProvider = {
|
|
215
|
+
...this.quotaByProvider,
|
|
216
|
+
codex: nextState,
|
|
217
|
+
};
|
|
218
|
+
this.emit();
|
|
219
|
+
}
|
|
220
|
+
dispose() {
|
|
221
|
+
this.stopPolling();
|
|
222
|
+
this.codexSubscription?.dispose();
|
|
223
|
+
this.codexSubscription = null;
|
|
224
|
+
if (this.ownsCodexWatcher) {
|
|
225
|
+
this.codexWatcher?.dispose();
|
|
226
|
+
}
|
|
227
|
+
this.zaiSubscription?.dispose();
|
|
228
|
+
this.zaiSubscription = null;
|
|
229
|
+
if (this.ownsZaiWatcher) {
|
|
230
|
+
this.zaiWatcher?.dispose();
|
|
231
|
+
}
|
|
232
|
+
this.listeners.splice(0, this.listeners.length);
|
|
233
|
+
}
|
|
234
|
+
async refreshPeakHours() {
|
|
235
|
+
if (!this.includePeakHours)
|
|
236
|
+
return;
|
|
237
|
+
let result;
|
|
238
|
+
try {
|
|
239
|
+
result = await this.fetchPeakHours();
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (!this.peakHoursErrorLogged) {
|
|
243
|
+
this.log?.('[Quota] Peak-hours fetch threw unexpectedly', error);
|
|
244
|
+
this.peakHoursErrorLogged = true;
|
|
245
|
+
}
|
|
246
|
+
result = unavailablePeakHoursState();
|
|
247
|
+
}
|
|
248
|
+
if (!result.unavailable) {
|
|
249
|
+
this.currentPeakHours = result;
|
|
250
|
+
this.lastFreshPeakHoursAt = Date.now();
|
|
251
|
+
this.peakHoursErrorLogged = false;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const cachedIsFresh = this.currentPeakHours !== undefined &&
|
|
255
|
+
!this.currentPeakHours.unavailable &&
|
|
256
|
+
Date.now() - this.lastFreshPeakHoursAt < this.peakHoursCacheMaxAgeMs;
|
|
257
|
+
if (cachedIsFresh)
|
|
258
|
+
return;
|
|
259
|
+
this.currentPeakHours = result;
|
|
260
|
+
}
|
|
261
|
+
async poll() {
|
|
262
|
+
if (!this.polling || this.pollInFlight) {
|
|
263
|
+
this.pendingWake = this.polling;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.pollInFlight = true;
|
|
267
|
+
this.pendingWake = false;
|
|
268
|
+
try {
|
|
269
|
+
let accountPresent = false;
|
|
270
|
+
try {
|
|
271
|
+
accountPresent = this.readClaudeAccount().source !== 'none';
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
accountPresent = false;
|
|
275
|
+
}
|
|
276
|
+
if (!accountPresent) {
|
|
277
|
+
this.claudeDormant = true;
|
|
278
|
+
this.resetTransientFailureState();
|
|
279
|
+
this.authUnavailableActive = false;
|
|
280
|
+
this.lastSuccessfulClaudeState = null;
|
|
281
|
+
// Stay visible while dormant: consumers render their sign-in prompt
|
|
282
|
+
// from an unavailable auth entry, not from a missing map key.
|
|
283
|
+
if (this.quotaByProvider.claude?.error !== SIGNED_OUT_ERROR) {
|
|
284
|
+
this.lastEmittedClaudeState = null;
|
|
285
|
+
this.emitClaudeState(makeUnavailableClaudeState(SIGNED_OUT_ERROR, 'auth'));
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.claudeDormant = false;
|
|
290
|
+
await this.refreshPeakHours();
|
|
291
|
+
const creds = await this.readClaudeCredentials();
|
|
292
|
+
if (!creds) {
|
|
293
|
+
this.handleUnavailableState(makeUnavailableClaudeState('Renew your Claude Max OAuth to view quota', 'auth'));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const state = await this.fetchClaudeQuota(creds.accessToken);
|
|
297
|
+
if (!state.available) {
|
|
298
|
+
this.handleUnavailableState({
|
|
299
|
+
...state,
|
|
300
|
+
runtimeProvider: 'claude',
|
|
301
|
+
providerId: 'claude-code',
|
|
302
|
+
source: state.source ?? 'api',
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const recoveredAttempts = this.transientFailureStreak;
|
|
307
|
+
const recoveredFromFallback = recoveredAttempts > 0;
|
|
308
|
+
this.resetTransientFailureState();
|
|
309
|
+
this.authUnavailableActive = false;
|
|
310
|
+
if (recoveredFromFallback) {
|
|
311
|
+
this.log?.(`[Quota] Fetch recovered after ${recoveredAttempts} failed attempt(s)`);
|
|
312
|
+
}
|
|
313
|
+
this.emitClaudeState({
|
|
314
|
+
...state,
|
|
315
|
+
runtimeProvider: 'claude',
|
|
316
|
+
providerId: 'claude-code',
|
|
317
|
+
source: state.source ?? 'api',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (this.transientFailureStreak === 0) {
|
|
322
|
+
this.log?.('[Quota] Poll error', error);
|
|
323
|
+
}
|
|
324
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
325
|
+
this.handleTransientFailure(makeUnavailableClaudeState(message));
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
this.pollInFlight = false;
|
|
329
|
+
if (this.pendingWake) {
|
|
330
|
+
this.pendingWake = false;
|
|
331
|
+
void this.poll();
|
|
332
|
+
}
|
|
333
|
+
else if (!this.claudeDormant) {
|
|
334
|
+
this.scheduleNextPoll();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
wakeForAccountChange() {
|
|
339
|
+
if (!this.polling)
|
|
340
|
+
return;
|
|
341
|
+
if (this.timer) {
|
|
342
|
+
clearTimeout(this.timer);
|
|
343
|
+
this.timer = undefined;
|
|
344
|
+
}
|
|
345
|
+
this.codexWatcher?.start();
|
|
346
|
+
this.zaiWatcher?.start();
|
|
347
|
+
if (this.pollInFlight) {
|
|
348
|
+
this.pendingWake = true;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
void this.poll();
|
|
352
|
+
}
|
|
353
|
+
emitClaudeState(state) {
|
|
354
|
+
const withPeakHours = this.currentPeakHours !== undefined ? { ...state, peakHours: this.currentPeakHours } : state;
|
|
355
|
+
const withFailure = {
|
|
356
|
+
...withPeakHours,
|
|
357
|
+
failure: describeQuotaFailure(withPeakHours),
|
|
358
|
+
};
|
|
359
|
+
this.updateProviderQuota('claude', withFailure);
|
|
360
|
+
}
|
|
361
|
+
handleUnavailableState(state) {
|
|
362
|
+
if (state.failureKind === 'auth') {
|
|
363
|
+
this.resetTransientFailureState();
|
|
364
|
+
this.authUnavailableActive = true;
|
|
365
|
+
this.emitClaudeState(state);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
this.handleTransientFailure(state);
|
|
369
|
+
}
|
|
370
|
+
handleTransientFailure(state) {
|
|
371
|
+
this.authUnavailableActive = false;
|
|
372
|
+
this.transientFailureStreak += 1;
|
|
373
|
+
const retryDelay = this.getTransientFailureDelay();
|
|
374
|
+
const hasVisibleCachedSuccess = this.lastSuccessfulClaudeState !== null && this.lastEmittedClaudeState?.available === true;
|
|
375
|
+
if (this.transientFailureStreak === 1) {
|
|
376
|
+
if (hasVisibleCachedSuccess) {
|
|
377
|
+
this.log?.(`[Quota] Fetch failed, using cached data; retrying in ${formatDelay(retryDelay)}`);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
this.log?.(`[Quota] Fetch unavailable; retrying in ${formatDelay(retryDelay)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
this.cachedFallbackActive = hasVisibleCachedSuccess;
|
|
384
|
+
if (hasVisibleCachedSuccess) {
|
|
385
|
+
this.emitClaudeState({
|
|
386
|
+
...this.lastSuccessfulClaudeState,
|
|
387
|
+
error: state.error,
|
|
388
|
+
failureKind: state.failureKind,
|
|
389
|
+
httpStatus: state.httpStatus,
|
|
390
|
+
retryAfterMs: state.retryAfterMs,
|
|
391
|
+
source: 'cache',
|
|
392
|
+
stale: true,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
this.emitClaudeState(state);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
getTransientFailureDelay() {
|
|
400
|
+
return this.transientFailureBackoffMs[Math.min(this.transientFailureStreak - 1, this.transientFailureBackoffMs.length - 1)];
|
|
401
|
+
}
|
|
402
|
+
resetTransientFailureState() {
|
|
403
|
+
this.transientFailureStreak = 0;
|
|
404
|
+
this.cachedFallbackActive = false;
|
|
405
|
+
}
|
|
406
|
+
scheduleNextPoll() {
|
|
407
|
+
if (!this.polling)
|
|
408
|
+
return;
|
|
409
|
+
if (this.timer) {
|
|
410
|
+
clearTimeout(this.timer);
|
|
411
|
+
}
|
|
412
|
+
let delay = this.pollingMode === 'active' ? this.activeIntervalMs : this.idleIntervalMs;
|
|
413
|
+
if (this.transientFailureStreak > 0) {
|
|
414
|
+
delay = this.getTransientFailureDelay();
|
|
415
|
+
}
|
|
416
|
+
else if (this.authUnavailableActive || this.lastEmittedClaudeState?.available !== true) {
|
|
417
|
+
delay = this.idleIntervalMs;
|
|
418
|
+
}
|
|
419
|
+
else if (this.cachedFallbackActive) {
|
|
420
|
+
delay = this.idleIntervalMs;
|
|
421
|
+
}
|
|
422
|
+
this.timer = setTimeout(() => void this.poll(), delay);
|
|
423
|
+
}
|
|
424
|
+
withClaudeAccountDetails(state) {
|
|
425
|
+
const resolved = this.readClaudeAccount();
|
|
426
|
+
const accountEmail = resolved.email;
|
|
427
|
+
if (!accountEmail) {
|
|
428
|
+
const nextState = { ...state };
|
|
429
|
+
delete nextState.accountLabel;
|
|
430
|
+
delete nextState.accountDetail;
|
|
431
|
+
return nextState;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
...state,
|
|
435
|
+
accountLabel: resolved.label ?? accountEmail,
|
|
436
|
+
accountDetail: accountEmail,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
emit() {
|
|
440
|
+
const snapshot = { ...this.quotaByProvider };
|
|
441
|
+
for (const listener of this.listeners) {
|
|
442
|
+
try {
|
|
443
|
+
listener(snapshot);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// Listener failures should not break polling.
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
package/dist/esm/node.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only entry.
|
|
3
|
+
*
|
|
4
|
+
* Pricing catalog hydration from the LiteLLM source. Requires `node:fs`
|
|
5
|
+
* and `node:path`; safe for extension-host and CLI consumers. Not safe
|
|
6
|
+
* for browser bundles — use `sidekick-shared/browser` there.
|
|
7
|
+
*/
|
|
8
|
+
export { hydratePricingCatalog, normalizeLiteLlmCatalog, normalizeLiteLlmContextWindows, LITELLM_CATALOG_URL, } from './pricingCatalog';
|
|
9
|
+
export { loadObservedContextWindows, recordObservedContextWindow, getObservedContextWindowPath, } from './observedContextWindows';
|
|
10
|
+
export { listRecentSessions, readSessionTranscript } from './sessionTranscripts';
|
|
11
|
+
export { collectPromptHistory, isPromptHistorySessionSettled, parsePromptHistoryCursor, } from './promptHistory';
|
|
12
|
+
export { collectSessionPromptHistory } from './sessionPromptHistory';
|
|
13
|
+
export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
|
|
14
|
+
export { computeSessionFileStats, firstUserPrompt, providerContextSizeFn, readSessionFileStats, } from './sessionStats';
|
|
15
|
+
export { readSessionReportInputs } from './report/sessionReportInputs';
|
|
16
|
+
export { fingerprintString, sessionFingerprintParts } from './sessionFingerprint';
|
|
17
|
+
export { STATE_FILE_NAME, STATE_FILE_SCHEMA_VERSION, billingBlockToStateFile, getStateFilePath, quotaToStateFile, readStateFile, writeStateFile, } from './stateFile';
|
|
18
|
+
export { BILLING_BLOCK_DURATION_MS, computeBillingBlocks, findActiveBillingBlock, } from './usage/billingBlocks';
|
|
19
|
+
export { MAX_USAGE_CACHE_FILES, USAGE_CACHE_VERSION, collectUsageEvents, getUsageCacheDir, pruneUsageCache, } from './usage/usageEvents';
|
|
20
|
+
export { collectSessionTokenTotals, combineSessionTokenTotals, readMainThreadTokenTotals, scanSessionSubagents, subagentTokenTotals, } from './sessionTokenTotals';
|
|
21
|
+
export { applySessionSummary, isFileImported, markFileImported, removeSessionSummary, sessionSummaryFromStats, } from './historicalStore';
|
|
22
|
+
export { ACTIVE_SESSION_MTIME_THRESHOLD_MS, importSessionHistory, } from './usage/importSessionHistory';
|
|
23
|
+
export { bucketUsage, summarizeUsageRows, usageBucketKey, weekKey } from './usage/usageReports';
|
|
24
|
+
export { findCodexRolloutFile } from './providers/codex';
|
|
25
|
+
export { walkRolloutFiles, walkRolloutFilesAsync } from './providers/rolloutWalker';
|
|
26
|
+
export { readCodexHistory } from './parsers/codexHistory';
|
|
27
|
+
export { listSessionPreviews, listSessionPreviewsAsync, readSessionPreview, readSessionPreviewAsync, } from './sessionPreviews';
|
|
28
|
+
export { ObservedSessionCollector, observedSessionSourceFromProvider, fileFingerprint, fileFingerprintParts, } from './observedSessionCollector';
|
|
29
|
+
export { DirectoryListingCache, splitWatchedPath } from './providers/directoryListingCache';
|
|
30
|
+
export { createSessionProviders } from './providers/factory';
|
|
31
|
+
export { SYNTHETIC_MODEL } from './usage/claudeUsageDedupe';
|
|
32
|
+
// Sidekick's config root
|
|
33
|
+
export { CONFIG_DIR_ENV_VAR, getConfigDir, getConfigDirOverride, getDefaultConfigDir, } from './paths';
|
|
34
|
+
// Claude Code session directories (Claude's own path encoding)
|
|
35
|
+
export { encodeWorkspacePath as encodeClaudeWorkspacePath, getSessionDirectory as getClaudeSessionDirectory, } from './parsers/sessionPathResolver';
|
|
36
|
+
// Provider homes the CLIs read for this process
|
|
37
|
+
export { getLiveClaudeHome, getDefaultClaudeHome, isDefaultClaudeHome, getClaudeProjectsDir, } from './claudePaths';
|
|
38
|
+
export { getSystemCodexHome, getCodexMonitoringHomes, resolveSidekickCodexHome, getCodexExecutionEnv, getCodexSessionsDirs, CODEX_SESSIONS_SUBDIR, CODEX_ARCHIVED_SESSIONS_SUBDIR, } from './codexPaths';
|
|
39
|
+
export { fetchProviderServiceStatus } from './providerServiceStatus';
|
|
40
|
+
// Tool categories across providers
|
|
41
|
+
export { classifyTool, TOOL_CATEGORIES } from './toolCategory';
|
|
42
|
+
// Workspace roots and git worktrees
|
|
43
|
+
export { resolveWorkspaceRoots } from './workspaceRoots';
|
|
44
|
+
export { discoverWorktreeSiblings, resolveWorktreeMainRepo } from './parsers/sessionPathResolver';
|
|
45
|
+
// Bounded JSONL tail reader
|
|
46
|
+
export { createJsonlTail, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES, DEFAULT_JSONL_TAIL_READ_BYTES, } from './watchers/jsonlTail';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted record of context windows providers actually reported (Node-only).
|
|
3
|
+
*
|
|
4
|
+
* Some agents report the real context window for the running session — Codex
|
|
5
|
+
* emits `model_context_window` on every `token_count` event. That number is the
|
|
6
|
+
* *effective* window for the account and tier, which can sit well below the
|
|
7
|
+
* model's published maximum (e.g. 258,400 observed for `gpt-5.6-sol` against a
|
|
8
|
+
* catalog maximum of 1,050,000).
|
|
9
|
+
*
|
|
10
|
+
* A live session uses that value directly. This module keeps it across runs, so
|
|
11
|
+
* historical and static views don't fall back to a published maximum that would
|
|
12
|
+
* make the context gauge read far too empty.
|
|
13
|
+
*
|
|
14
|
+
* Precedence, most-trusted first: runtime → observed (here) → LiteLLM catalog →
|
|
15
|
+
* static table. See `modelContext.ts`.
|
|
16
|
+
*
|
|
17
|
+
* ## Where the store lives — two seams, one story
|
|
18
|
+
*
|
|
19
|
+
* Reader ({@link loadObservedContextWindows}) and writer
|
|
20
|
+
* ({@link recordObservedContextWindow}) resolve the store path identically:
|
|
21
|
+
* explicit `cacheDir` option → `setConfigDir()` / `SIDEKICK_CONFIG_DIR` (via
|
|
22
|
+
* `getConfigDir()`) → platform default. The global config-dir override is the
|
|
23
|
+
* deployment-level seam external consumers use to relocate every Sidekick
|
|
24
|
+
* store at once; the per-call `cacheDir` is the isolation seam for tests and
|
|
25
|
+
* multi-tenant hosts. Providers deliberately do NOT thread `cacheDir` —
|
|
26
|
+
* `CodexProvider` records observations with default options, which follow the
|
|
27
|
+
* global override, so a default-options load always sees what a
|
|
28
|
+
* default-options record wrote.
|
|
29
|
+
*
|
|
30
|
+
* @module observedContextWindows
|
|
31
|
+
*/
|
|
32
|
+
import { promises as fs } from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { _setObservedContextWindows, _getObservedContextWindows } from './modelContext';
|
|
35
|
+
import { getConfigDir } from './paths';
|
|
36
|
+
import { updateJsonStoreAtomic } from './writers/atomic';
|
|
37
|
+
// ── Constants ──
|
|
38
|
+
const STORE_FILE_NAME = 'observed-context-windows.json';
|
|
39
|
+
const STORE_VERSION = 1;
|
|
40
|
+
/**
|
|
41
|
+
* Observations older than this are ignored on load. A provider reports its
|
|
42
|
+
* context window on every session, so a live model refreshes its entry
|
|
43
|
+
* constantly; an entry that has gone quiet this long belongs to a tier or
|
|
44
|
+
* model the user no longer runs and must not outrank the catalog forever.
|
|
45
|
+
*/
|
|
46
|
+
export const OBSERVED_CONTEXT_WINDOW_TTL_MS = 30 * 86_400_000;
|
|
47
|
+
/** Resolve the on-disk store path. */
|
|
48
|
+
export function getObservedContextWindowPath(cacheDir) {
|
|
49
|
+
return path.join(cacheDir ?? getConfigDir(), STORE_FILE_NAME);
|
|
50
|
+
}
|
|
51
|
+
function emptyStore() {
|
|
52
|
+
return { version: STORE_VERSION, models: {} };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Last value successfully persisted, per resolved store path.
|
|
56
|
+
*
|
|
57
|
+
* The in-memory override table in `modelContext` is process-global by design —
|
|
58
|
+
* a lookup should answer the same way everywhere. That makes it unusable as the
|
|
59
|
+
* write dedupe: with two `cacheDir`s in one process (two temp dirs in a test
|
|
60
|
+
* suite, a multi-tenant host, a CLI and extension host sharing a process), the
|
|
61
|
+
* global would already hold the value from the first store and the second store
|
|
62
|
+
* would never be written at all.
|
|
63
|
+
*/
|
|
64
|
+
const lastPersistedByStore = new Map();
|
|
65
|
+
/** Reset the per-store write dedupe. Test-only; not part of the public API. */
|
|
66
|
+
export function _clearObservedContextWindowWriteCache() {
|
|
67
|
+
lastPersistedByStore.clear();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Keep only well-formed, recent entries; a hand-edited or partly-corrupt file
|
|
71
|
+
* shouldn't poison lookups, and a stale observation shouldn't outrank the
|
|
72
|
+
* catalog (see {@link OBSERVED_CONTEXT_WINDOW_TTL_MS}).
|
|
73
|
+
*/
|
|
74
|
+
function toOverrideMap(store, now = Date.now()) {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [modelId, entry] of Object.entries(store.models ?? {})) {
|
|
77
|
+
const window = entry?.contextWindow;
|
|
78
|
+
if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
|
|
79
|
+
continue;
|
|
80
|
+
const observedAt = typeof entry.observedAt === 'string' ? Date.parse(entry.observedAt) : NaN;
|
|
81
|
+
if (Number.isFinite(observedAt) && now - observedAt > OBSERVED_CONTEXT_WINDOW_TTL_MS)
|
|
82
|
+
continue;
|
|
83
|
+
out[modelId] = window;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
// ── Public API ──
|
|
88
|
+
/**
|
|
89
|
+
* Load persisted observations and apply them as context-window overrides.
|
|
90
|
+
*
|
|
91
|
+
* Always resolves — never throws. A missing, unreadable, or malformed store
|
|
92
|
+
* leaves the overrides empty and lookups fall through to catalog/static.
|
|
93
|
+
*/
|
|
94
|
+
export async function loadObservedContextWindows(options = {}) {
|
|
95
|
+
const storePath = getObservedContextWindowPath(options.cacheDir);
|
|
96
|
+
try {
|
|
97
|
+
const raw = await fs.readFile(storePath, 'utf8');
|
|
98
|
+
const parsed = JSON.parse(raw);
|
|
99
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.models)
|
|
100
|
+
return {};
|
|
101
|
+
const overrides = toOverrideMap(parsed);
|
|
102
|
+
_setObservedContextWindows(overrides);
|
|
103
|
+
return overrides;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const code = err.code;
|
|
107
|
+
if (code !== 'ENOENT') {
|
|
108
|
+
options.logger?.(`observedContextWindows: failed to load ${storePath}: ${String(err)}`);
|
|
109
|
+
}
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Record a provider-reported context window for a model.
|
|
115
|
+
*
|
|
116
|
+
* Applies in memory immediately, then persists — but only when the value
|
|
117
|
+
* actually changed *for that store*. Codex reports a window on every
|
|
118
|
+
* `token_count` event, so an unconditional write would mean disk I/O many times
|
|
119
|
+
* per session for a value that almost never moves.
|
|
120
|
+
*
|
|
121
|
+
* Always resolves — never throws. Persistence failures are logged and dropped;
|
|
122
|
+
* the in-memory override still stands for this process, and the next call
|
|
123
|
+
* retries the write.
|
|
124
|
+
*/
|
|
125
|
+
export async function recordObservedContextWindow(modelId, contextWindow, options = {}) {
|
|
126
|
+
const normalizedId = modelId?.trim().toLowerCase();
|
|
127
|
+
if (!normalizedId)
|
|
128
|
+
return;
|
|
129
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0)
|
|
130
|
+
return;
|
|
131
|
+
const current = _getObservedContextWindows();
|
|
132
|
+
if (current[normalizedId] !== contextWindow) {
|
|
133
|
+
_setObservedContextWindows({ ...current, [normalizedId]: contextWindow });
|
|
134
|
+
}
|
|
135
|
+
const storePath = getObservedContextWindowPath(options.cacheDir);
|
|
136
|
+
const persisted = lastPersistedByStore.get(storePath);
|
|
137
|
+
if (persisted?.get(normalizedId) === contextWindow)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
// Lock-guarded read-modify-write: the CLI and the extension host can both
|
|
141
|
+
// be observing sessions at once, and each should merge rather than clobber.
|
|
142
|
+
await updateJsonStoreAtomic(storePath, emptyStore, (latest) => {
|
|
143
|
+
const models = latest?.models ?? {};
|
|
144
|
+
if (models[normalizedId]?.contextWindow === contextWindow)
|
|
145
|
+
return latest;
|
|
146
|
+
return {
|
|
147
|
+
version: STORE_VERSION,
|
|
148
|
+
models: {
|
|
149
|
+
...models,
|
|
150
|
+
[normalizedId]: { contextWindow, observedAt: new Date().toISOString() },
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
// Recorded only on success, so a transient EACCES retries next time.
|
|
155
|
+
let store = lastPersistedByStore.get(storePath);
|
|
156
|
+
if (!store) {
|
|
157
|
+
store = new Map();
|
|
158
|
+
lastPersistedByStore.set(storePath, store);
|
|
159
|
+
}
|
|
160
|
+
store.set(normalizedId, contextWindow);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
options.logger?.(`observedContextWindows: failed to persist ${storePath}: ${String(err)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|