sidekick-shared 0.18.2 → 0.18.3
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/dist/codexQuotaWatcher.d.ts +8 -0
- package/dist/codexQuotaWatcher.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -2
- package/dist/quotaHistory.d.ts +64 -0
- package/dist/quotaHistory.js +385 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import type { FSWatcher } from 'fs';
|
|
3
|
+
import type { QuotaHistorySample } from './quotaHistory';
|
|
3
4
|
import { CodexProvider } from './providers/codex';
|
|
4
5
|
import type { SavedAccountProfile } from './accountRegistry';
|
|
5
6
|
import type { Disposable } from './quotaPoller';
|
|
@@ -8,6 +9,7 @@ import type { ProviderQuotaState } from './providerQuota';
|
|
|
8
9
|
type CodexAccountReader = () => SavedAccountProfile | null;
|
|
9
10
|
type SnapshotReader = (providerId: 'codex', accountId: string) => QuotaState | null;
|
|
10
11
|
type SnapshotWriter = (providerId: 'codex', accountId: string, quota: QuotaState) => void;
|
|
12
|
+
type HistoryAppender = (sample: QuotaHistorySample) => void | Promise<void>;
|
|
11
13
|
type WatchFile = (filename: fs.PathLike, listener: fs.WatchListener<string>) => FSWatcher;
|
|
12
14
|
export interface CodexQuotaWatcherOptions {
|
|
13
15
|
discoveryPollIntervalMs?: number;
|
|
@@ -18,6 +20,10 @@ export interface CodexQuotaWatcherOptions {
|
|
|
18
20
|
readSnapshot?: SnapshotReader;
|
|
19
21
|
writeSnapshot?: SnapshotWriter;
|
|
20
22
|
watchFile?: WatchFile;
|
|
23
|
+
/** Stable workspace identifier. When provided, live quotas are appended to the per-workspace history JSONL. */
|
|
24
|
+
workspaceId?: string;
|
|
25
|
+
/** Override the history append function (used by tests). Default: `appendQuotaHistorySample`. */
|
|
26
|
+
appendHistorySample?: HistoryAppender;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Watches the active Codex rollout for quota snapshots and falls back to the
|
|
@@ -33,6 +39,8 @@ export declare class CodexQuotaWatcher implements Disposable {
|
|
|
33
39
|
private readonly watchFile;
|
|
34
40
|
private readonly maxTailBytes;
|
|
35
41
|
private readonly maxSessionFiles;
|
|
42
|
+
private readonly workspaceId;
|
|
43
|
+
private readonly appendHistorySample;
|
|
36
44
|
private readonly listeners;
|
|
37
45
|
private discoveryTimer;
|
|
38
46
|
private provider;
|
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const codexProfiles_1 = require("./codexProfiles");
|
|
39
39
|
const codexQuota_1 = require("./codexQuota");
|
|
40
40
|
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
41
|
+
const quotaHistory_1 = require("./quotaHistory");
|
|
41
42
|
const codex_1 = require("./providers/codex");
|
|
42
43
|
const DEFAULT_DISCOVERY_POLL_INTERVAL_MS = 30_000;
|
|
43
44
|
function accountEmail(account) {
|
|
@@ -80,6 +81,8 @@ class CodexQuotaWatcher {
|
|
|
80
81
|
watchFile;
|
|
81
82
|
maxTailBytes;
|
|
82
83
|
maxSessionFiles;
|
|
84
|
+
workspaceId;
|
|
85
|
+
appendHistorySample;
|
|
83
86
|
listeners = [];
|
|
84
87
|
discoveryTimer;
|
|
85
88
|
provider = null;
|
|
@@ -98,6 +101,8 @@ class CodexQuotaWatcher {
|
|
|
98
101
|
this.watchFile = options.watchFile ?? fs.watch;
|
|
99
102
|
this.maxTailBytes = options.maxTailBytes;
|
|
100
103
|
this.maxSessionFiles = options.maxSessionFiles;
|
|
104
|
+
this.workspaceId = options.workspaceId;
|
|
105
|
+
this.appendHistorySample = options.appendHistorySample ?? quotaHistory_1.appendQuotaHistorySample;
|
|
101
106
|
}
|
|
102
107
|
start() {
|
|
103
108
|
if (this.running)
|
|
@@ -196,6 +201,31 @@ class CodexQuotaWatcher {
|
|
|
196
201
|
const account = this.getActiveAccount();
|
|
197
202
|
if (account) {
|
|
198
203
|
this.writeSnapshot('codex', account.id, liveQuota);
|
|
204
|
+
if (this.workspaceId) {
|
|
205
|
+
const sample = {
|
|
206
|
+
timestamp: liveQuota.capturedAt ?? new Date().toISOString(),
|
|
207
|
+
runtimeProvider: 'codex',
|
|
208
|
+
providerId: account.id,
|
|
209
|
+
workspaceId: this.workspaceId,
|
|
210
|
+
fiveHour: { utilization: liveQuota.fiveHour.utilization, resetsAt: liveQuota.fiveHour.resetsAt },
|
|
211
|
+
sevenDay: { utilization: liveQuota.sevenDay.utilization, resetsAt: liveQuota.sevenDay.resetsAt },
|
|
212
|
+
available: liveQuota.available,
|
|
213
|
+
error: liveQuota.error,
|
|
214
|
+
source: liveQuota.source,
|
|
215
|
+
stale: liveQuota.stale,
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
const result = this.appendHistorySample(sample);
|
|
219
|
+
if (result && typeof result.catch === 'function') {
|
|
220
|
+
result.catch(() => {
|
|
221
|
+
// History append must never break the live emission path.
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Synchronous errors swallowed for the same reason.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
199
229
|
}
|
|
200
230
|
this.emitState(enrichQuotaState({
|
|
201
231
|
...liveQuota,
|
package/dist/index.d.ts
CHANGED
|
@@ -104,6 +104,8 @@ export type { QuotaFailureDescriptor } from './quotaPresentation';
|
|
|
104
104
|
export { QuotaPoller } from './quotaPoller';
|
|
105
105
|
export type { QuotaPollerOptions } from './quotaPoller';
|
|
106
106
|
export { readQuotaSnapshot, writeQuotaSnapshot } from './quotaSnapshots';
|
|
107
|
+
export { appendQuotaHistorySample, readQuotaHistoryRange, readQuotaHistoryDailyBuckets, pruneQuotaHistory, getWorkspaceIdFromPath, } from './quotaHistory';
|
|
108
|
+
export type { QuotaHistorySample, QuotaHistoryAppendOptions, QuotaHistoryRangeOptions, QuotaHistoryDailyBucket, QuotaHistoryRuntimeProvider, } from './quotaHistory';
|
|
107
109
|
export { fetchCodexQuotaFromApi, quotaFromCodexRateLimits, readLatestCodexQuotaFromRollouts, resolveCodexQuota, resolveCodexQuotaFromLocalSources, } from './codexQuota';
|
|
108
110
|
export type { CodexQuotaApiOptions, CodexQuotaCreditsSnapshot, CodexQuotaResolveOptions, CodexQuotaResolveSource, } from './codexQuota';
|
|
109
111
|
export { CodexQuotaWatcher } from './codexQuotaWatcher';
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.findActiveClaudeSession = exports.discoverSessionDirectory = exports.getClaudeSessionDirectory = exports.encodeClaudeWorkspacePath = exports.detectSessionActivity = exports.extractTaskInfo = exports.scanSubagentDir = exports.normalizeCodexToolInput = exports.normalizeCodexToolName = exports.extractPatchFilePaths = exports.CodexRolloutParser = exports.parseDbPartData = exports.parseDbMessageData = exports.convertOpenCodeMessage = exports.detectPlanModeFromText = exports.normalizeToolInput = exports.normalizeToolName = exports.TRUNCATION_PATTERNS = exports.JsonlParser = exports.CodexProvider = exports.OpenCodeProvider = exports.ClaudeCodeProvider = exports.getAllDetectedProviders = exports.detectProvider = exports.readClaudeCodePlanFiles = exports.getPlanAnalytics = exports.writePlans = exports.getLatestPlan = exports.readPlans = exports.readLatestHandoff = exports.readHistory = exports.readNotes = exports.readDecisions = exports.readTasks = exports.getProjectSlugRaw = exports.getProjectSlug = exports.encodeWorkspacePath = exports.getGlobalDataPath = exports.getProjectDataPath = exports.getConfigDir = exports.MAX_PLANS_PER_PROJECT = exports.PLAN_SCHEMA_VERSION = exports.createEmptyTokenTotals = exports.HISTORICAL_DATA_SCHEMA_VERSION = exports.STALENESS_THRESHOLDS = exports.IMPORTANCE_DECAY_FACTORS = exports.KNOWLEDGE_NOTE_SCHEMA_VERSION = exports.DECISION_LOG_SCHEMA_VERSION = exports.normalizeTaskStatus = exports.TASK_PERSISTENCE_SCHEMA_VERSION = void 0;
|
|
7
7
|
exports.deleteSnapshot = exports.loadSnapshot = exports.saveSnapshot = exports.parseTodoDependencies = exports.EventAggregator = exports.getRandomPhrase = exports.PHRASE_CATEGORIES = exports.ALL_PHRASES = exports.HIGHLIGHT_CSS = exports.clearHighlightCache = exports.highlightEvent = exports.formatSessionJson = exports.formatSessionMarkdown = exports.formatSessionText = exports.classifyNoise = exports.shouldMergeWithPrevious = exports.classifyFollowEvent = exports.classifyMessage = exports.getSoftNoiseReason = exports.isHardNoiseFollowEvent = exports.isHardNoise = exports.formatToolSummary = exports.formatTokenCount = exports.formatDurationMs = exports.createJsonlTail = exports.toFollowEvents = exports.createWatcher = exports.parseChangelog = exports.extractProposedPlanShared = exports.parsePlanMarkdownShared = exports.PlanExtractor = exports.composeContext = exports.FilterEngine = exports.searchSessions = exports.CodexDatabase = exports.OpenCodeDatabase = exports.discoverDebugLogs = exports.collapseDuplicates = exports.filterByLevel = exports.parseDebugLog = exports.scanSubagentTraces = exports.findAllSessionsWithWorktrees = exports.discoverWorktreeSiblings = exports.resolveWorktreeMainRepo = exports.getAllClaudeProjectFolders = exports.decodeEncodedPath = exports.getMostRecentlyActiveSessionDir = exports.findSubdirectorySessionDirs = exports.findSessionsInDirectory = exports.findAllClaudeSessions = void 0;
|
|
8
|
-
exports.
|
|
9
|
-
exports.fetchPeakHoursStatus = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.extractToolCalls = exports.extractToolCall = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = exports.MultiProviderQuotaService = exports.CodexQuotaWatcher = exports.resolveCodexQuotaFromLocalSources = exports.resolveCodexQuota = exports.readLatestCodexQuotaFromRollouts = exports.quotaFromCodexRateLimits = void 0;
|
|
8
|
+
exports.appendQuotaHistorySample = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.QuotaPoller = exports.describeQuotaFailure = exports.fetchQuota = exports.removeCodexAccount = exports.switchToCodexAccount = exports.finalizeCodexAccount = exports.prepareCodexAccount = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.getActiveCodexAccount = exports.listCodexAccounts = exports.getSystemCodexHome = exports.getCodexMonitoringHomes = exports.getCodexProfileHome = exports.getCodexProfilesDir = exports.getActiveAccountStatus = exports.removeSavedAccountProfile = exports.replaceSavedAccountProfiles = exports.setActiveSavedAccount = exports.upsertSavedAccountProfile = exports.getActiveSavedAccount = exports.listSavedAccountProfiles = exports.writeSavedAccountRegistry = exports.readSavedAccountRegistry = exports.getAccountsDir = exports.isMultiAccountEnabled = exports.getActiveAccount = exports.listAccounts = exports.removeAccount = exports.switchToAccount = exports.addCurrentAccount = exports.readActiveClaudeAccount = exports.writeAccountRegistry = exports.readAccountRegistry = exports.ensureDefaultAccounts = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.writeActiveCredentials = exports.readActiveCredentials = exports.openInBrowser = exports.parseTranscript = exports.generateHtmlReport = exports.PatternExtractor = exports.HeatmapTracker = exports.FrequencyTracker = exports.getSnapshotPath = exports.isSnapshotValid = void 0;
|
|
9
|
+
exports.fetchPeakHoursStatus = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.extractToolCalls = exports.extractToolCall = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = exports.MultiProviderQuotaService = exports.CodexQuotaWatcher = exports.resolveCodexQuotaFromLocalSources = exports.resolveCodexQuota = exports.readLatestCodexQuotaFromRollouts = exports.quotaFromCodexRateLimits = exports.fetchCodexQuotaFromApi = exports.getWorkspaceIdFromPath = exports.pruneQuotaHistory = exports.readQuotaHistoryDailyBuckets = exports.readQuotaHistoryRange = void 0;
|
|
10
10
|
var taskPersistence_1 = require("./types/taskPersistence");
|
|
11
11
|
Object.defineProperty(exports, "TASK_PERSISTENCE_SCHEMA_VERSION", { enumerable: true, get: function () { return taskPersistence_1.TASK_PERSISTENCE_SCHEMA_VERSION; } });
|
|
12
12
|
Object.defineProperty(exports, "normalizeTaskStatus", { enumerable: true, get: function () { return taskPersistence_1.normalizeTaskStatus; } });
|
|
@@ -237,6 +237,12 @@ Object.defineProperty(exports, "QuotaPoller", { enumerable: true, get: function
|
|
|
237
237
|
var quotaSnapshots_1 = require("./quotaSnapshots");
|
|
238
238
|
Object.defineProperty(exports, "readQuotaSnapshot", { enumerable: true, get: function () { return quotaSnapshots_1.readQuotaSnapshot; } });
|
|
239
239
|
Object.defineProperty(exports, "writeQuotaSnapshot", { enumerable: true, get: function () { return quotaSnapshots_1.writeQuotaSnapshot; } });
|
|
240
|
+
var quotaHistory_1 = require("./quotaHistory");
|
|
241
|
+
Object.defineProperty(exports, "appendQuotaHistorySample", { enumerable: true, get: function () { return quotaHistory_1.appendQuotaHistorySample; } });
|
|
242
|
+
Object.defineProperty(exports, "readQuotaHistoryRange", { enumerable: true, get: function () { return quotaHistory_1.readQuotaHistoryRange; } });
|
|
243
|
+
Object.defineProperty(exports, "readQuotaHistoryDailyBuckets", { enumerable: true, get: function () { return quotaHistory_1.readQuotaHistoryDailyBuckets; } });
|
|
244
|
+
Object.defineProperty(exports, "pruneQuotaHistory", { enumerable: true, get: function () { return quotaHistory_1.pruneQuotaHistory; } });
|
|
245
|
+
Object.defineProperty(exports, "getWorkspaceIdFromPath", { enumerable: true, get: function () { return quotaHistory_1.getWorkspaceIdFromPath; } });
|
|
240
246
|
var codexQuota_1 = require("./codexQuota");
|
|
241
247
|
Object.defineProperty(exports, "fetchCodexQuotaFromApi", { enumerable: true, get: function () { return codexQuota_1.fetchCodexQuotaFromApi; } });
|
|
242
248
|
Object.defineProperty(exports, "quotaFromCodexRateLimits", { enumerable: true, get: function () { return codexQuota_1.quotaFromCodexRateLimits; } });
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only JSONL history of quota samples, scoped per workspace and per runtime provider.
|
|
3
|
+
*
|
|
4
|
+
* Sibling to `quotaSnapshots.ts`. Where snapshots persist a single most-recent sample per
|
|
5
|
+
* (provider, account), this module accumulates time-series samples so consumers (the VS Code
|
|
6
|
+
* dashboard, the `sidekick quota history` CLI, contextful_desktop) can render heatmaps and
|
|
7
|
+
* trend visualisations over a 13-week window.
|
|
8
|
+
*/
|
|
9
|
+
export type QuotaHistoryRuntimeProvider = 'claude' | 'codex';
|
|
10
|
+
export interface QuotaHistorySample {
|
|
11
|
+
timestamp: string;
|
|
12
|
+
runtimeProvider: QuotaHistoryRuntimeProvider;
|
|
13
|
+
providerId: string;
|
|
14
|
+
workspaceId: string;
|
|
15
|
+
fiveHour: {
|
|
16
|
+
utilization: number;
|
|
17
|
+
resetsAt: string;
|
|
18
|
+
};
|
|
19
|
+
sevenDay: {
|
|
20
|
+
utilization: number;
|
|
21
|
+
resetsAt: string;
|
|
22
|
+
};
|
|
23
|
+
available: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
source?: 'session' | 'cache' | 'api';
|
|
26
|
+
stale?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface QuotaHistoryAppendOptions {
|
|
29
|
+
/** Drop the sample if the most recent in-store sample is younger than this many ms. Default: 60_000. */
|
|
30
|
+
minIntervalMs?: number;
|
|
31
|
+
/** Retention window in days. Default: 91 (13 weeks). */
|
|
32
|
+
retentionDays?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface QuotaHistoryRangeOptions {
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
provider: QuotaHistoryRuntimeProvider;
|
|
37
|
+
/** Inclusive ISO start. Default: 13 weeks ago. */
|
|
38
|
+
from?: string;
|
|
39
|
+
/** Inclusive ISO end. Default: now. */
|
|
40
|
+
to?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface QuotaHistoryDailyBucket {
|
|
43
|
+
date: string;
|
|
44
|
+
samples: number;
|
|
45
|
+
maxUtilizationFiveHour: number;
|
|
46
|
+
maxUtilizationSevenDay: number;
|
|
47
|
+
avgUtilizationFiveHour: number;
|
|
48
|
+
avgUtilizationSevenDay: number;
|
|
49
|
+
anyUnavailable: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Stable, opaque workspace identifier — first 16 hex chars of sha256(realpath).
|
|
53
|
+
* Shared between the VS Code extension (workspace folder fsPath) and the CLI (process.cwd()).
|
|
54
|
+
*/
|
|
55
|
+
export declare function getWorkspaceIdFromPath(workspacePath: string): string;
|
|
56
|
+
export declare function appendQuotaHistorySample(sample: QuotaHistorySample, options?: QuotaHistoryAppendOptions): Promise<void>;
|
|
57
|
+
export declare function readQuotaHistoryRange(options: QuotaHistoryRangeOptions): Promise<QuotaHistorySample[]>;
|
|
58
|
+
export declare function readQuotaHistoryDailyBuckets(options: QuotaHistoryRangeOptions): Promise<QuotaHistoryDailyBucket[]>;
|
|
59
|
+
export declare function pruneQuotaHistory(workspaceId: string, provider: QuotaHistoryRuntimeProvider, retentionDays?: number): Promise<{
|
|
60
|
+
kept: number;
|
|
61
|
+
pruned: number;
|
|
62
|
+
}>;
|
|
63
|
+
/** Test-only: wipe the in-memory mutex/debounce state. Not exported from index.ts. */
|
|
64
|
+
export declare function _resetQuotaHistoryInMemoryStateForTests(): void;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Append-only JSONL history of quota samples, scoped per workspace and per runtime provider.
|
|
4
|
+
*
|
|
5
|
+
* Sibling to `quotaSnapshots.ts`. Where snapshots persist a single most-recent sample per
|
|
6
|
+
* (provider, account), this module accumulates time-series samples so consumers (the VS Code
|
|
7
|
+
* dashboard, the `sidekick quota history` CLI, contextful_desktop) can render heatmaps and
|
|
8
|
+
* trend visualisations over a 13-week window.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.getWorkspaceIdFromPath = getWorkspaceIdFromPath;
|
|
45
|
+
exports.appendQuotaHistorySample = appendQuotaHistorySample;
|
|
46
|
+
exports.readQuotaHistoryRange = readQuotaHistoryRange;
|
|
47
|
+
exports.readQuotaHistoryDailyBuckets = readQuotaHistoryDailyBuckets;
|
|
48
|
+
exports.pruneQuotaHistory = pruneQuotaHistory;
|
|
49
|
+
exports._resetQuotaHistoryInMemoryStateForTests = _resetQuotaHistoryInMemoryStateForTests;
|
|
50
|
+
const crypto = __importStar(require("crypto"));
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const paths_1 = require("./paths");
|
|
54
|
+
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
55
|
+
const DEFAULT_MIN_INTERVAL_MS = 60_000;
|
|
56
|
+
const DEFAULT_RETENTION_DAYS = 91;
|
|
57
|
+
const PRUNE_FILESIZE_THRESHOLD = 16 * 1024;
|
|
58
|
+
const MS_PER_DAY = 86_400_000;
|
|
59
|
+
/** Per-file append serialization across in-process callers. */
|
|
60
|
+
const appendChains = new Map();
|
|
61
|
+
/** Per-file last-write timestamp cache, used for the debounce check. */
|
|
62
|
+
const lastWriteCache = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* Stable, opaque workspace identifier — first 16 hex chars of sha256(realpath).
|
|
65
|
+
* Shared between the VS Code extension (workspace folder fsPath) and the CLI (process.cwd()).
|
|
66
|
+
*/
|
|
67
|
+
function getWorkspaceIdFromPath(workspacePath) {
|
|
68
|
+
let resolved;
|
|
69
|
+
try {
|
|
70
|
+
resolved = fs.realpathSync(workspacePath);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
resolved = path.resolve(workspacePath);
|
|
74
|
+
}
|
|
75
|
+
return crypto.createHash('sha256').update(resolved).digest('hex').slice(0, 16);
|
|
76
|
+
}
|
|
77
|
+
function getHistoryFilePath(workspaceId, provider) {
|
|
78
|
+
return path.join((0, paths_1.getConfigDir)(), 'quota-history', workspaceId, `${provider}.jsonl`);
|
|
79
|
+
}
|
|
80
|
+
function ensureHistoryDir(workspaceId) {
|
|
81
|
+
fs.mkdirSync(path.join((0, paths_1.getConfigDir)(), 'quota-history', workspaceId), { recursive: true, mode: 0o700 });
|
|
82
|
+
}
|
|
83
|
+
function parseSampleLine(line) {
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
if (!trimmed)
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(trimmed);
|
|
89
|
+
if (parsed &&
|
|
90
|
+
typeof parsed === 'object' &&
|
|
91
|
+
typeof parsed.timestamp === 'string' &&
|
|
92
|
+
parsed.fiveHour &&
|
|
93
|
+
parsed.sevenDay) {
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function readLastSampleTimestampMs(filePath) {
|
|
103
|
+
let fd;
|
|
104
|
+
try {
|
|
105
|
+
fd = fs.openSync(filePath, 'r');
|
|
106
|
+
const stat = fs.fstatSync(fd);
|
|
107
|
+
if (stat.size === 0)
|
|
108
|
+
return null;
|
|
109
|
+
const chunkSize = 4096;
|
|
110
|
+
let remaining = stat.size;
|
|
111
|
+
let buffer = Buffer.alloc(0);
|
|
112
|
+
while (remaining > 0) {
|
|
113
|
+
const readSize = Math.min(chunkSize, remaining);
|
|
114
|
+
const chunk = Buffer.alloc(readSize);
|
|
115
|
+
fs.readSync(fd, chunk, 0, readSize, remaining - readSize);
|
|
116
|
+
buffer = Buffer.concat([chunk, buffer]);
|
|
117
|
+
remaining -= readSize;
|
|
118
|
+
const text = buffer.toString('utf8');
|
|
119
|
+
// Look for the last newline that's not at end-of-text — that's the boundary of the final record.
|
|
120
|
+
const trimmedRight = text.replace(/\n+$/, '');
|
|
121
|
+
const lastNewline = trimmedRight.lastIndexOf('\n');
|
|
122
|
+
if (lastNewline >= 0) {
|
|
123
|
+
const lastLine = trimmedRight.slice(lastNewline + 1);
|
|
124
|
+
const sample = parseSampleLine(lastLine);
|
|
125
|
+
return sample ? Date.parse(sample.timestamp) || null : null;
|
|
126
|
+
}
|
|
127
|
+
if (remaining === 0) {
|
|
128
|
+
const sample = parseSampleLine(trimmedRight);
|
|
129
|
+
return sample ? Date.parse(sample.timestamp) || null : null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err?.code === 'ENOENT')
|
|
136
|
+
return null;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (fd !== undefined) {
|
|
141
|
+
try {
|
|
142
|
+
fs.closeSync(fd);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// best effort
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function atomicRewriteFile(filePath, contents) {
|
|
151
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
152
|
+
try {
|
|
153
|
+
fs.writeFileSync(tmp, contents, { encoding: 'utf8', mode: 0o600 });
|
|
154
|
+
fs.renameSync(tmp, filePath);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
try {
|
|
158
|
+
fs.rmSync(tmp, { force: true });
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Best effort cleanup only.
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function pruneFileSync(filePath, retentionDays) {
|
|
167
|
+
let stat;
|
|
168
|
+
try {
|
|
169
|
+
stat = fs.statSync(filePath);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return { kept: 0, pruned: 0 };
|
|
173
|
+
}
|
|
174
|
+
if (stat.size === 0)
|
|
175
|
+
return { kept: 0, pruned: 0 };
|
|
176
|
+
const cutoffMs = Date.now() - retentionDays * MS_PER_DAY;
|
|
177
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
178
|
+
const lines = raw.split('\n');
|
|
179
|
+
const keptLines = [];
|
|
180
|
+
let pruned = 0;
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
if (!line.trim())
|
|
183
|
+
continue;
|
|
184
|
+
const sample = parseSampleLine(line);
|
|
185
|
+
if (!sample) {
|
|
186
|
+
// Drop malformed lines during prune so the file self-heals.
|
|
187
|
+
pruned += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const ts = Date.parse(sample.timestamp);
|
|
191
|
+
if (Number.isFinite(ts) && ts >= cutoffMs) {
|
|
192
|
+
keptLines.push(line);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
pruned += 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (pruned > 0) {
|
|
199
|
+
atomicRewriteFile(filePath, keptLines.length > 0 ? keptLines.join('\n') + '\n' : '');
|
|
200
|
+
}
|
|
201
|
+
return { kept: keptLines.length, pruned };
|
|
202
|
+
}
|
|
203
|
+
function sampleToQuotaState(sample) {
|
|
204
|
+
const providerId = sample.runtimeProvider === 'claude' ? 'claude-code' : 'codex';
|
|
205
|
+
return {
|
|
206
|
+
fiveHour: { utilization: sample.fiveHour.utilization, resetsAt: sample.fiveHour.resetsAt },
|
|
207
|
+
sevenDay: { utilization: sample.sevenDay.utilization, resetsAt: sample.sevenDay.resetsAt },
|
|
208
|
+
available: sample.available,
|
|
209
|
+
error: sample.error,
|
|
210
|
+
providerId,
|
|
211
|
+
source: sample.source ?? 'session',
|
|
212
|
+
capturedAt: sample.timestamp,
|
|
213
|
+
stale: sample.stale,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async function runAppend(sample, filePath, options) {
|
|
217
|
+
let lastTs = lastWriteCache.get(filePath);
|
|
218
|
+
if (lastTs === undefined) {
|
|
219
|
+
const fromDisk = readLastSampleTimestampMs(filePath);
|
|
220
|
+
if (fromDisk !== null) {
|
|
221
|
+
lastTs = fromDisk;
|
|
222
|
+
lastWriteCache.set(filePath, fromDisk);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const sampleTs = Date.parse(sample.timestamp);
|
|
226
|
+
if (lastTs !== undefined && Number.isFinite(sampleTs) && sampleTs - lastTs < options.minIntervalMs) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
ensureHistoryDir(sample.workspaceId);
|
|
230
|
+
const line = JSON.stringify(sample) + '\n';
|
|
231
|
+
await fs.promises.appendFile(filePath, line, { encoding: 'utf8', mode: 0o600 });
|
|
232
|
+
lastWriteCache.set(filePath, Number.isFinite(sampleTs) ? sampleTs : Date.now());
|
|
233
|
+
// Opportunistic prune. Skip when the file is small to avoid pointless rewrite churn.
|
|
234
|
+
try {
|
|
235
|
+
const stat = await fs.promises.stat(filePath);
|
|
236
|
+
if (stat.size >= PRUNE_FILESIZE_THRESHOLD) {
|
|
237
|
+
pruneFileSync(filePath, options.retentionDays);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// ignore
|
|
242
|
+
}
|
|
243
|
+
// Backwards-compat: keep the latest-snapshot store hot so existing callers (DashboardViewProvider,
|
|
244
|
+
// codex session provider, contextful_desktop) don't have to query history for "latest".
|
|
245
|
+
try {
|
|
246
|
+
const providerId = sample.runtimeProvider === 'claude' ? 'claude-code' : 'codex';
|
|
247
|
+
(0, quotaSnapshots_1.writeQuotaSnapshot)(providerId, sample.providerId, sampleToQuotaState(sample));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Snapshot write failures must not poison the history append path.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function appendQuotaHistorySample(sample, options = {}) {
|
|
254
|
+
const resolved = {
|
|
255
|
+
minIntervalMs: options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS,
|
|
256
|
+
retentionDays: options.retentionDays ?? DEFAULT_RETENTION_DAYS,
|
|
257
|
+
};
|
|
258
|
+
const filePath = getHistoryFilePath(sample.workspaceId, sample.runtimeProvider);
|
|
259
|
+
const previous = appendChains.get(filePath) ?? Promise.resolve();
|
|
260
|
+
const next = previous.then(() => runAppend(sample, filePath, resolved)).catch(() => {
|
|
261
|
+
// Swallow chain-level errors so a single failure doesn't break subsequent appends.
|
|
262
|
+
});
|
|
263
|
+
appendChains.set(filePath, next);
|
|
264
|
+
try {
|
|
265
|
+
await next;
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
if (appendChains.get(filePath) === next) {
|
|
269
|
+
appendChains.delete(filePath);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function defaultRangeMs() {
|
|
274
|
+
const toMs = Date.now();
|
|
275
|
+
const fromMs = toMs - DEFAULT_RETENTION_DAYS * MS_PER_DAY;
|
|
276
|
+
return { fromMs, toMs };
|
|
277
|
+
}
|
|
278
|
+
async function readQuotaHistoryRange(options) {
|
|
279
|
+
const filePath = getHistoryFilePath(options.workspaceId, options.provider);
|
|
280
|
+
let raw;
|
|
281
|
+
try {
|
|
282
|
+
raw = await fs.promises.readFile(filePath, 'utf8');
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
if (err?.code === 'ENOENT')
|
|
286
|
+
return [];
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
const { fromMs: defaultFromMs, toMs: defaultToMs } = defaultRangeMs();
|
|
290
|
+
const fromMs = options.from ? Date.parse(options.from) : defaultFromMs;
|
|
291
|
+
const toMs = options.to ? Date.parse(options.to) : defaultToMs;
|
|
292
|
+
const samples = [];
|
|
293
|
+
for (const line of raw.split('\n')) {
|
|
294
|
+
const sample = parseSampleLine(line);
|
|
295
|
+
if (!sample)
|
|
296
|
+
continue;
|
|
297
|
+
const ts = Date.parse(sample.timestamp);
|
|
298
|
+
if (!Number.isFinite(ts))
|
|
299
|
+
continue;
|
|
300
|
+
if (ts < fromMs || ts > toMs)
|
|
301
|
+
continue;
|
|
302
|
+
samples.push(sample);
|
|
303
|
+
}
|
|
304
|
+
samples.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
305
|
+
return samples;
|
|
306
|
+
}
|
|
307
|
+
function utcDateString(ms) {
|
|
308
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
309
|
+
}
|
|
310
|
+
function addDaysUtc(dateString, days) {
|
|
311
|
+
const ms = Date.parse(`${dateString}T00:00:00Z`) + days * MS_PER_DAY;
|
|
312
|
+
return utcDateString(ms);
|
|
313
|
+
}
|
|
314
|
+
async function readQuotaHistoryDailyBuckets(options) {
|
|
315
|
+
const samples = await readQuotaHistoryRange(options);
|
|
316
|
+
const { fromMs: defaultFromMs, toMs: defaultToMs } = defaultRangeMs();
|
|
317
|
+
const fromMs = options.from ? Date.parse(options.from) : defaultFromMs;
|
|
318
|
+
const toMs = options.to ? Date.parse(options.to) : defaultToMs;
|
|
319
|
+
const startDate = utcDateString(fromMs);
|
|
320
|
+
const endDate = utcDateString(toMs);
|
|
321
|
+
const grouped = new Map();
|
|
322
|
+
for (const sample of samples) {
|
|
323
|
+
const day = sample.timestamp.slice(0, 10);
|
|
324
|
+
const bucket = grouped.get(day);
|
|
325
|
+
if (bucket) {
|
|
326
|
+
bucket.push(sample);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
grouped.set(day, [sample]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const buckets = [];
|
|
333
|
+
let cursor = startDate;
|
|
334
|
+
// Safety bound — 26 weeks worst case = 182 days; we cap iteration well above that.
|
|
335
|
+
for (let i = 0; i <= 366 && cursor <= endDate; i += 1) {
|
|
336
|
+
const daySamples = grouped.get(cursor);
|
|
337
|
+
if (!daySamples || daySamples.length === 0) {
|
|
338
|
+
buckets.push({
|
|
339
|
+
date: cursor,
|
|
340
|
+
samples: 0,
|
|
341
|
+
maxUtilizationFiveHour: 0,
|
|
342
|
+
maxUtilizationSevenDay: 0,
|
|
343
|
+
avgUtilizationFiveHour: 0,
|
|
344
|
+
avgUtilizationSevenDay: 0,
|
|
345
|
+
anyUnavailable: false,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
let maxFive = 0;
|
|
350
|
+
let maxSeven = 0;
|
|
351
|
+
let sumFive = 0;
|
|
352
|
+
let sumSeven = 0;
|
|
353
|
+
let anyUnavailable = false;
|
|
354
|
+
for (const s of daySamples) {
|
|
355
|
+
maxFive = Math.max(maxFive, s.fiveHour.utilization);
|
|
356
|
+
maxSeven = Math.max(maxSeven, s.sevenDay.utilization);
|
|
357
|
+
sumFive += s.fiveHour.utilization;
|
|
358
|
+
sumSeven += s.sevenDay.utilization;
|
|
359
|
+
if (!s.available)
|
|
360
|
+
anyUnavailable = true;
|
|
361
|
+
}
|
|
362
|
+
const n = daySamples.length;
|
|
363
|
+
buckets.push({
|
|
364
|
+
date: cursor,
|
|
365
|
+
samples: n,
|
|
366
|
+
maxUtilizationFiveHour: maxFive,
|
|
367
|
+
maxUtilizationSevenDay: maxSeven,
|
|
368
|
+
avgUtilizationFiveHour: Math.round((sumFive / n) * 100) / 100,
|
|
369
|
+
avgUtilizationSevenDay: Math.round((sumSeven / n) * 100) / 100,
|
|
370
|
+
anyUnavailable,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
cursor = addDaysUtc(cursor, 1);
|
|
374
|
+
}
|
|
375
|
+
return buckets;
|
|
376
|
+
}
|
|
377
|
+
async function pruneQuotaHistory(workspaceId, provider, retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
378
|
+
const filePath = getHistoryFilePath(workspaceId, provider);
|
|
379
|
+
return pruneFileSync(filePath, retentionDays);
|
|
380
|
+
}
|
|
381
|
+
/** Test-only: wipe the in-memory mutex/debounce state. Not exported from index.ts. */
|
|
382
|
+
function _resetQuotaHistoryInMemoryStateForTests() {
|
|
383
|
+
appendChains.clear();
|
|
384
|
+
lastWriteCache.clear();
|
|
385
|
+
}
|
package/package.json
CHANGED