sidekick-shared 0.25.0 → 0.26.1
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 +65 -0
- package/README.md +50 -3
- package/dist/accountChanges.js +22 -3
- package/dist/accountRegistry.d.ts +22 -1
- package/dist/accountRegistry.js +37 -14
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +11 -1
- package/dist/browser.js +29 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +46 -12
- package/dist/index.js +112 -24
- package/dist/modelContext.js +8 -3
- package/dist/modelInfo.js +45 -27
- package/dist/node.d.ts +19 -0
- package/dist/node.js +47 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +3 -8
- package/dist/observedSessionCollector.js +10 -27
- package/dist/parsers/sessionActivityDetector.d.ts +24 -2
- package/dist/parsers/sessionActivityDetector.js +95 -17
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providers/claudeCode.js +11 -96
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +94 -259
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/factory.d.ts +6 -0
- package/dist/providers/openCode.d.ts +2 -0
- package/dist/providers/openCode.js +105 -121
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +30 -0
- package/dist/quota.d.ts +30 -3
- package/dist/quota.js +35 -3
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaPresentation.js +17 -5
- package/dist/quotaResolver.d.ts +83 -0
- package/dist/quotaResolver.js +234 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/quota.d.ts +139 -0
- package/dist/schemas/quota.js +17 -2
- package/dist/schemas/quotaHistory.d.ts +1 -0
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionPreviews.d.ts +7 -1
- package/dist/sessionPreviews.js +15 -3
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +2 -0
- package/dist/types/observedSessionV1.js +75 -18
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -201,7 +201,14 @@ function readAccountRegistry() {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
function writeAccountRegistry(registry) {
|
|
204
|
-
|
|
204
|
+
(0, accountRegistry_1.replaceSavedAccountProfiles)('claude-code', mapClaudeProfiles(registry), registry.activeAccountUuid);
|
|
205
|
+
}
|
|
206
|
+
/** For callers already holding the registry lock (see withSavedAccountRegistryLock). */
|
|
207
|
+
function writeAccountRegistryUnlocked(registry) {
|
|
208
|
+
(0, accountRegistry_1.replaceSavedAccountProfilesUnlocked)('claude-code', mapClaudeProfiles(registry), registry.activeAccountUuid);
|
|
209
|
+
}
|
|
210
|
+
function mapClaudeProfiles(registry) {
|
|
211
|
+
return registry.accounts.map((account) => ({
|
|
205
212
|
id: account.uuid,
|
|
206
213
|
providerId: 'claude-code',
|
|
207
214
|
providerAccountId: account.uuid,
|
|
@@ -212,7 +219,6 @@ function writeAccountRegistry(registry) {
|
|
|
212
219
|
email: account.email,
|
|
213
220
|
},
|
|
214
221
|
}));
|
|
215
|
-
(0, accountRegistry_1.replaceSavedAccountProfiles)('claude-code', mappedClaudeProfiles, registry.activeAccountUuid);
|
|
216
222
|
}
|
|
217
223
|
// ── Read active Claude account ───────────────────────────────────────────
|
|
218
224
|
function readActiveClaudeAccount() {
|
|
@@ -254,17 +260,34 @@ function addCurrentAccount(label) {
|
|
|
254
260
|
return { success: false, error: 'Could not read Claude config file.' };
|
|
255
261
|
}
|
|
256
262
|
ensureDirs();
|
|
257
|
-
// 4
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
// 4–7 are one read-modify-write of the registry: hold the registry lock
|
|
264
|
+
// across them so a concurrent add from another process is not dropped
|
|
265
|
+
// when this write replaces the whole Claude profile set.
|
|
266
|
+
return (0, accountRegistry_1.withSavedAccountRegistryLock)(() => {
|
|
267
|
+
// 4. Load or create registry
|
|
268
|
+
const registry = readAccountRegistry() ?? {
|
|
269
|
+
version: 1,
|
|
270
|
+
activeAccountUuid: null,
|
|
271
|
+
accounts: [],
|
|
272
|
+
};
|
|
273
|
+
// 5. Check if account already exists — update credentials + label
|
|
274
|
+
const existing = registry.accounts.find((a) => a.uuid === active.uuid);
|
|
275
|
+
if (existing) {
|
|
276
|
+
if (label !== undefined)
|
|
277
|
+
existing.label = label;
|
|
278
|
+
(0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
279
|
+
(0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
280
|
+
try {
|
|
281
|
+
writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
/* flat backups remain available */
|
|
285
|
+
}
|
|
286
|
+
registry.activeAccountUuid = active.uuid;
|
|
287
|
+
writeAccountRegistryUnlocked(registry);
|
|
288
|
+
return { success: true };
|
|
289
|
+
}
|
|
290
|
+
// 6. New account — back up credentials
|
|
268
291
|
(0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
269
292
|
(0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
270
293
|
try {
|
|
@@ -273,29 +296,17 @@ function addCurrentAccount(label) {
|
|
|
273
296
|
catch {
|
|
274
297
|
/* flat backups remain available */
|
|
275
298
|
}
|
|
299
|
+
// 7. Add entry
|
|
300
|
+
registry.accounts.push({
|
|
301
|
+
uuid: active.uuid,
|
|
302
|
+
email: active.email,
|
|
303
|
+
label,
|
|
304
|
+
addedAt: new Date().toISOString(),
|
|
305
|
+
});
|
|
276
306
|
registry.activeAccountUuid = active.uuid;
|
|
277
|
-
|
|
307
|
+
writeAccountRegistryUnlocked(registry);
|
|
278
308
|
return { success: true };
|
|
279
|
-
}
|
|
280
|
-
// 6. New account — back up credentials
|
|
281
|
-
(0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
282
|
-
(0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
283
|
-
try {
|
|
284
|
-
writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
/* flat backups remain available */
|
|
288
|
-
}
|
|
289
|
-
// 7. Add entry
|
|
290
|
-
registry.accounts.push({
|
|
291
|
-
uuid: active.uuid,
|
|
292
|
-
email: active.email,
|
|
293
|
-
label,
|
|
294
|
-
addedAt: new Date().toISOString(),
|
|
295
309
|
});
|
|
296
|
-
registry.activeAccountUuid = active.uuid;
|
|
297
|
-
writeAccountRegistry(registry);
|
|
298
|
-
return { success: true };
|
|
299
310
|
}
|
|
300
311
|
// ── Switch to account ────────────────────────────────────────────────────
|
|
301
312
|
/**
|
|
@@ -486,34 +497,37 @@ function reconcileClaudeAuthState() {
|
|
|
486
497
|
}
|
|
487
498
|
// ── Remove account ───────────────────────────────────────────────────────
|
|
488
499
|
function removeAccount(uuid) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
registry.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
500
|
+
// One read-modify-write under the registry lock (see addCurrentAccount).
|
|
501
|
+
return (0, accountRegistry_1.withSavedAccountRegistryLock)(() => {
|
|
502
|
+
const registry = readAccountRegistry();
|
|
503
|
+
if (!registry) {
|
|
504
|
+
return { success: false, error: 'No account registry found.' };
|
|
505
|
+
}
|
|
506
|
+
const idx = registry.accounts.findIndex((a) => a.uuid === uuid);
|
|
507
|
+
if (idx === -1) {
|
|
508
|
+
return { success: false, error: `Account ${uuid} not found.` };
|
|
509
|
+
}
|
|
510
|
+
// Remove backed-up files
|
|
511
|
+
try {
|
|
512
|
+
fs.unlinkSync(path.join(getCredentialsDir(), `${uuid}.credentials.json`));
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
/* ok */
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
fs.unlinkSync(path.join(getConfigsDir(), `${uuid}.config.json`));
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
/* ok */
|
|
522
|
+
}
|
|
523
|
+
// Remove from registry
|
|
524
|
+
registry.accounts.splice(idx, 1);
|
|
525
|
+
if (registry.activeAccountUuid === uuid) {
|
|
526
|
+
registry.activeAccountUuid = registry.accounts[0]?.uuid ?? null;
|
|
527
|
+
}
|
|
528
|
+
writeAccountRegistryUnlocked(registry);
|
|
529
|
+
return { success: true };
|
|
530
|
+
});
|
|
517
531
|
}
|
|
518
532
|
// ── Query helpers ────────────────────────────────────────────────────────
|
|
519
533
|
function listAccounts() {
|
|
@@ -546,7 +560,8 @@ function getActiveAccount() {
|
|
|
546
560
|
* re-pointed so registry-keyed data tracks reality too. Never creates or deletes
|
|
547
561
|
* profiles; an unknown live account is shown as-is with no label and no write.
|
|
548
562
|
*/
|
|
549
|
-
function resolveActiveClaudeAccount() {
|
|
563
|
+
function resolveActiveClaudeAccount(options = {}) {
|
|
564
|
+
const selfHeal = options.selfHeal ?? true;
|
|
550
565
|
const live = readActiveClaudeAccount();
|
|
551
566
|
if (live) {
|
|
552
567
|
const profiles = (0, accountRegistry_1.listSavedAccountProfiles)('claude-code');
|
|
@@ -554,12 +569,17 @@ function resolveActiveClaudeAccount() {
|
|
|
554
569
|
profiles.find((p) => (p.email ?? p.metadata?.email) === live.email);
|
|
555
570
|
if (match) {
|
|
556
571
|
const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
|
|
557
|
-
|
|
572
|
+
// Hot paths (the status line runs on every prompt) pass selfHeal: false
|
|
573
|
+
// so a persistently failing registry write never becomes a write per
|
|
574
|
+
// prompt; the next ordinary command repairs the pointer instead.
|
|
575
|
+
if (selfHeal && (!active || active.id !== match.id)) {
|
|
558
576
|
// Self-heal is best-effort: a registry write failure (read-only/full
|
|
559
577
|
// disk) must never break display or extension activation. We still
|
|
560
578
|
// return the correct live identity below.
|
|
579
|
+
// Silent: this is a read path repairing a pointer to the profile that
|
|
580
|
+
// already matches the live login, so subscribers have nothing new to see.
|
|
561
581
|
try {
|
|
562
|
-
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', match.id);
|
|
582
|
+
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', match.id, { silent: true });
|
|
563
583
|
}
|
|
564
584
|
catch {
|
|
565
585
|
/* keep going with the live identity */
|
|
@@ -14,7 +14,7 @@ import type { SerializedFrequencyState } from './FrequencyTracker';
|
|
|
14
14
|
import type { SerializedHeatmapState } from './HeatmapTracker';
|
|
15
15
|
import type { SerializedPatternState } from './PatternExtractor';
|
|
16
16
|
/** Schema version for serialized snapshots. */
|
|
17
|
-
export declare const SNAPSHOT_SCHEMA_VERSION =
|
|
17
|
+
export declare const SNAPSHOT_SCHEMA_VERSION = 5;
|
|
18
18
|
/**
|
|
19
19
|
* JSON-serializable snapshot of EventAggregator state.
|
|
20
20
|
* Used by session snapshot sidecar files for fast resume.
|
|
@@ -28,7 +28,14 @@ export interface SerializedAggregatorState {
|
|
|
28
28
|
cacheRead: number;
|
|
29
29
|
reasoning: number;
|
|
30
30
|
total: number;
|
|
31
|
+
/** @deprecated Mirrors `costUsd`; kept so older readers of the field keep working. */
|
|
31
32
|
reportedCost: number;
|
|
33
|
+
costUsd: number;
|
|
34
|
+
reportedCostUsd: number;
|
|
35
|
+
estimatedCostUsd: number;
|
|
36
|
+
reportedCalls: number;
|
|
37
|
+
estimatedCalls: number;
|
|
38
|
+
unpricedCalls: number;
|
|
32
39
|
};
|
|
33
40
|
modelUsage: Array<[
|
|
34
41
|
string,
|
|
@@ -100,7 +107,13 @@ export declare class EventAggregator {
|
|
|
100
107
|
private cacheReadTokens;
|
|
101
108
|
private reasoningTokens;
|
|
102
109
|
private totalTokens;
|
|
103
|
-
|
|
110
|
+
/** Running cost in USD (reported + estimated). */
|
|
111
|
+
private costUsd;
|
|
112
|
+
private reportedCostUsd;
|
|
113
|
+
private estimatedCostUsd;
|
|
114
|
+
private reportedCalls;
|
|
115
|
+
private estimatedCalls;
|
|
116
|
+
private unpricedCalls;
|
|
104
117
|
private modelUsage;
|
|
105
118
|
private currentContextSize;
|
|
106
119
|
private previousContextSize;
|
|
@@ -16,6 +16,7 @@ const planExtractor_1 = require("../parsers/planExtractor");
|
|
|
16
16
|
const eventBridge_1 = require("../watchers/eventBridge");
|
|
17
17
|
const toolSummary_1 = require("../formatters/toolSummary");
|
|
18
18
|
const noiseClassifier_1 = require("../formatters/noiseClassifier");
|
|
19
|
+
const costProvenance_1 = require("./costProvenance");
|
|
19
20
|
const FrequencyTracker_1 = require("./FrequencyTracker");
|
|
20
21
|
const HeatmapTracker_1 = require("./HeatmapTracker");
|
|
21
22
|
const PatternExtractor_1 = require("./PatternExtractor");
|
|
@@ -30,7 +31,7 @@ const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
|
|
|
30
31
|
const DEFAULT_BURN_SAMPLE_MS = 10_000;
|
|
31
32
|
const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
|
|
32
33
|
/** Schema version for serialized snapshots. */
|
|
33
|
-
exports.SNAPSHOT_SCHEMA_VERSION =
|
|
34
|
+
exports.SNAPSHOT_SCHEMA_VERSION = 5;
|
|
34
35
|
// ── Goal gate detection regex ──
|
|
35
36
|
const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
|
|
36
37
|
// ── Todo dependency parsing ──
|
|
@@ -78,7 +79,13 @@ class EventAggregator {
|
|
|
78
79
|
cacheReadTokens = 0;
|
|
79
80
|
reasoningTokens = 0;
|
|
80
81
|
totalTokens = 0;
|
|
81
|
-
|
|
82
|
+
/** Running cost in USD (reported + estimated). */
|
|
83
|
+
costUsd = 0;
|
|
84
|
+
reportedCostUsd = 0;
|
|
85
|
+
estimatedCostUsd = 0;
|
|
86
|
+
reportedCalls = 0;
|
|
87
|
+
estimatedCalls = 0;
|
|
88
|
+
unpricedCalls = 0;
|
|
82
89
|
// Per-model usage
|
|
83
90
|
modelUsage = new Map();
|
|
84
91
|
// Context size tracking
|
|
@@ -262,8 +269,10 @@ class EventAggregator {
|
|
|
262
269
|
this.attributePlanUsage(normalized.totalTokens, cost);
|
|
263
270
|
}
|
|
264
271
|
else if (typeof event.cost === 'number' && Number.isFinite(event.cost) && event.cost >= 0) {
|
|
265
|
-
// Compatibility for cost-only follow events.
|
|
266
|
-
this.
|
|
272
|
+
// Compatibility for cost-only follow events (provider-reported by definition).
|
|
273
|
+
this.costUsd += event.cost;
|
|
274
|
+
this.reportedCostUsd += event.cost;
|
|
275
|
+
this.reportedCalls += 1;
|
|
267
276
|
}
|
|
268
277
|
// 4. Tool tracking from FollowEvent
|
|
269
278
|
if (event.type === 'tool_use' && event.toolName) {
|
|
@@ -309,7 +318,16 @@ class EventAggregator {
|
|
|
309
318
|
cacheReadTokens: this.cacheReadTokens,
|
|
310
319
|
reasoningTokens: this.reasoningTokens,
|
|
311
320
|
totalTokens: this.totalTokens,
|
|
312
|
-
|
|
321
|
+
costUsd: this.costUsd,
|
|
322
|
+
reportedCostUsd: this.reportedCostUsd,
|
|
323
|
+
estimatedCostUsd: this.estimatedCostUsd,
|
|
324
|
+
unpricedCalls: this.unpricedCalls,
|
|
325
|
+
costProvenance: (0, costProvenance_1.classifyCostProvenance)({
|
|
326
|
+
reportedCalls: this.reportedCalls,
|
|
327
|
+
estimatedCalls: this.estimatedCalls,
|
|
328
|
+
unpricedCalls: this.unpricedCalls,
|
|
329
|
+
}),
|
|
330
|
+
reportedCost: this.costUsd,
|
|
313
331
|
};
|
|
314
332
|
}
|
|
315
333
|
getModelStats() {
|
|
@@ -427,7 +445,12 @@ class EventAggregator {
|
|
|
427
445
|
this.cacheReadTokens = 0;
|
|
428
446
|
this.reasoningTokens = 0;
|
|
429
447
|
this.totalTokens = 0;
|
|
430
|
-
this.
|
|
448
|
+
this.costUsd = 0;
|
|
449
|
+
this.reportedCostUsd = 0;
|
|
450
|
+
this.estimatedCostUsd = 0;
|
|
451
|
+
this.reportedCalls = 0;
|
|
452
|
+
this.estimatedCalls = 0;
|
|
453
|
+
this.unpricedCalls = 0;
|
|
431
454
|
this.modelUsage.clear();
|
|
432
455
|
this.currentContextSize = 0;
|
|
433
456
|
this.previousContextSize = 0;
|
|
@@ -503,7 +526,13 @@ class EventAggregator {
|
|
|
503
526
|
cacheRead: this.cacheReadTokens,
|
|
504
527
|
reasoning: this.reasoningTokens,
|
|
505
528
|
total: this.totalTokens,
|
|
506
|
-
reportedCost: this.
|
|
529
|
+
reportedCost: this.costUsd,
|
|
530
|
+
costUsd: this.costUsd,
|
|
531
|
+
reportedCostUsd: this.reportedCostUsd,
|
|
532
|
+
estimatedCostUsd: this.estimatedCostUsd,
|
|
533
|
+
reportedCalls: this.reportedCalls,
|
|
534
|
+
estimatedCalls: this.estimatedCalls,
|
|
535
|
+
unpricedCalls: this.unpricedCalls,
|
|
507
536
|
},
|
|
508
537
|
modelUsage: Array.from(this.modelUsage.entries()),
|
|
509
538
|
contextSize: this.currentContextSize,
|
|
@@ -550,7 +579,12 @@ class EventAggregator {
|
|
|
550
579
|
this.cacheReadTokens = state.tokens.cacheRead;
|
|
551
580
|
this.reasoningTokens = state.tokens.reasoning;
|
|
552
581
|
this.totalTokens = state.tokens.total;
|
|
553
|
-
this.
|
|
582
|
+
this.costUsd = state.tokens.costUsd;
|
|
583
|
+
this.reportedCostUsd = state.tokens.reportedCostUsd;
|
|
584
|
+
this.estimatedCostUsd = state.tokens.estimatedCostUsd;
|
|
585
|
+
this.reportedCalls = state.tokens.reportedCalls;
|
|
586
|
+
this.estimatedCalls = state.tokens.estimatedCalls;
|
|
587
|
+
this.unpricedCalls = state.tokens.unpricedCalls;
|
|
554
588
|
this.modelUsage = new Map(state.modelUsage);
|
|
555
589
|
this.currentContextSize = state.contextSize;
|
|
556
590
|
this.previousContextSize = state.previousContextSize;
|
|
@@ -619,7 +653,18 @@ class EventAggregator {
|
|
|
619
653
|
this.cacheReadTokens += cacheRead;
|
|
620
654
|
this.reasoningTokens += reasoningTok;
|
|
621
655
|
this.totalTokens += usage.totalTokens;
|
|
622
|
-
this.
|
|
656
|
+
this.costUsd += cost;
|
|
657
|
+
if (pricedUsage.source === 'provider-reported') {
|
|
658
|
+
this.reportedCostUsd += cost;
|
|
659
|
+
this.reportedCalls += 1;
|
|
660
|
+
}
|
|
661
|
+
else if (pricedUsage.source === 'unpriced') {
|
|
662
|
+
this.unpricedCalls += 1;
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
this.estimatedCostUsd += cost;
|
|
666
|
+
this.estimatedCalls += 1;
|
|
667
|
+
}
|
|
623
668
|
// Burn rate sample accumulation
|
|
624
669
|
this.tokensSinceLastSample += usage.totalTokens;
|
|
625
670
|
// Context size computation
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AggregatedCostProvenance, AggregatedTokens } from './types';
|
|
2
|
+
/** The fields `describeCostProvenance` reads; any `AggregatedTokens` qualifies. */
|
|
3
|
+
export type AggregatedCostProvenanceInput = Pick<AggregatedTokens, 'costProvenance' | 'unpricedCalls'>;
|
|
4
|
+
/**
|
|
5
|
+
* Human-readable qualifier for an aggregate cost, for report footers and
|
|
6
|
+
* table cells. Empty when there is nothing to qualify.
|
|
7
|
+
*/
|
|
8
|
+
export declare function describeCostProvenance(tokens: AggregatedCostProvenanceInput): string;
|
|
9
|
+
/** Classify a cost breakdown by how many events fell into each bucket. */
|
|
10
|
+
export declare function classifyCostProvenance(counts: {
|
|
11
|
+
reportedCalls: number;
|
|
12
|
+
estimatedCalls: number;
|
|
13
|
+
unpricedCalls: number;
|
|
14
|
+
}): AggregatedCostProvenance;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeCostProvenance = describeCostProvenance;
|
|
4
|
+
exports.classifyCostProvenance = classifyCostProvenance;
|
|
5
|
+
const PROVENANCE_LABELS = {
|
|
6
|
+
reported: 'provider-reported',
|
|
7
|
+
estimated: 'estimated from catalog pricing',
|
|
8
|
+
mixed: 'partly provider-reported, partly estimated',
|
|
9
|
+
unpriced: 'unpriced',
|
|
10
|
+
none: '',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Human-readable qualifier for an aggregate cost, for report footers and
|
|
14
|
+
* table cells. Empty when there is nothing to qualify.
|
|
15
|
+
*/
|
|
16
|
+
function describeCostProvenance(tokens) {
|
|
17
|
+
const base = PROVENANCE_LABELS[tokens.costProvenance] ?? '';
|
|
18
|
+
const unpriced = tokens.unpricedCalls > 0
|
|
19
|
+
? `${tokens.unpricedCalls} unpriced call${tokens.unpricedCalls === 1 ? '' : 's'}`
|
|
20
|
+
: '';
|
|
21
|
+
if (base && unpriced)
|
|
22
|
+
return `${base} · ${unpriced}`;
|
|
23
|
+
return base || unpriced;
|
|
24
|
+
}
|
|
25
|
+
/** Classify a cost breakdown by how many events fell into each bucket. */
|
|
26
|
+
function classifyCostProvenance(counts) {
|
|
27
|
+
const { reportedCalls, estimatedCalls, unpricedCalls } = counts;
|
|
28
|
+
if (reportedCalls > 0 && estimatedCalls > 0)
|
|
29
|
+
return 'mixed';
|
|
30
|
+
if (reportedCalls > 0)
|
|
31
|
+
return 'reported';
|
|
32
|
+
if (estimatedCalls > 0)
|
|
33
|
+
return 'estimated';
|
|
34
|
+
return unpricedCalls > 0 ? 'unpriced' : 'none';
|
|
35
|
+
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @module aggregation/snapshot
|
|
9
9
|
*/
|
|
10
10
|
import { type SerializedAggregatorState } from './EventAggregator';
|
|
11
|
+
/** Newest snapshots kept on disk; older ones are pruned opportunistically. */
|
|
12
|
+
export declare const MAX_SNAPSHOT_FILES = 200;
|
|
11
13
|
/** Snapshot sidecar file content. */
|
|
12
14
|
export interface SessionSnapshot {
|
|
13
15
|
/** Schema version for forward-compatibility checks. */
|
|
@@ -32,10 +34,19 @@ export declare function getSnapshotPath(sessionId: string): string;
|
|
|
32
34
|
/**
|
|
33
35
|
* Saves a session snapshot to disk.
|
|
34
36
|
*
|
|
35
|
-
* Creates the snapshots directory if it doesn't exist.
|
|
36
|
-
*
|
|
37
|
+
* Creates the snapshots directory if it doesn't exist. Uses the shared
|
|
38
|
+
* fsynced atomic writer with a per-process temp name, so the CLI dashboard and
|
|
39
|
+
* the extension host snapshotting the same session cannot collide on a shared
|
|
40
|
+
* `.tmp` file. Every {@link PRUNE_EVERY_N_SAVES} saves the directory is
|
|
41
|
+
* trimmed to the newest {@link MAX_SNAPSHOT_FILES}; snapshots are caches, so
|
|
42
|
+
* dropping an old one only costs a replay.
|
|
37
43
|
*/
|
|
38
44
|
export declare function saveSnapshot(snapshot: SessionSnapshot): void;
|
|
45
|
+
/**
|
|
46
|
+
* Deletes all but the newest `keep` snapshot files (by modification time).
|
|
47
|
+
* Returns how many files were removed. Best-effort; never throws.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pruneSnapshots(keep?: number): number;
|
|
39
50
|
/**
|
|
40
51
|
* Loads a session snapshot from disk if it exists and is valid.
|
|
41
52
|
*
|
|
@@ -42,17 +42,25 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
42
|
};
|
|
43
43
|
})();
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.MAX_SNAPSHOT_FILES = void 0;
|
|
45
46
|
exports.getSnapshotPath = getSnapshotPath;
|
|
46
47
|
exports.saveSnapshot = saveSnapshot;
|
|
48
|
+
exports.pruneSnapshots = pruneSnapshots;
|
|
47
49
|
exports.loadSnapshot = loadSnapshot;
|
|
48
50
|
exports.deleteSnapshot = deleteSnapshot;
|
|
49
51
|
exports.isSnapshotValid = isSnapshotValid;
|
|
50
52
|
const fs = __importStar(require("fs"));
|
|
51
53
|
const path = __importStar(require("path"));
|
|
52
54
|
const paths_1 = require("../paths");
|
|
55
|
+
const atomic_1 = require("../writers/atomic");
|
|
53
56
|
const EventAggregator_1 = require("./EventAggregator");
|
|
54
57
|
/** Current schema version — bump when format changes. */
|
|
55
58
|
const SNAPSHOT_VERSION = EventAggregator_1.SNAPSHOT_SCHEMA_VERSION;
|
|
59
|
+
/** Newest snapshots kept on disk; older ones are pruned opportunistically. */
|
|
60
|
+
exports.MAX_SNAPSHOT_FILES = 200;
|
|
61
|
+
/** Prune runs once per this many saves, so steady-state writes stay one file each. */
|
|
62
|
+
const PRUNE_EVERY_N_SAVES = 25;
|
|
63
|
+
let savesSincePrune = 0;
|
|
56
64
|
/** Returns the directory for snapshot files. */
|
|
57
65
|
function getSnapshotsDir() {
|
|
58
66
|
return path.join((0, paths_1.getConfigDir)(), 'snapshots');
|
|
@@ -66,8 +74,12 @@ function getSnapshotPath(sessionId) {
|
|
|
66
74
|
/**
|
|
67
75
|
* Saves a session snapshot to disk.
|
|
68
76
|
*
|
|
69
|
-
* Creates the snapshots directory if it doesn't exist.
|
|
70
|
-
*
|
|
77
|
+
* Creates the snapshots directory if it doesn't exist. Uses the shared
|
|
78
|
+
* fsynced atomic writer with a per-process temp name, so the CLI dashboard and
|
|
79
|
+
* the extension host snapshotting the same session cannot collide on a shared
|
|
80
|
+
* `.tmp` file. Every {@link PRUNE_EVERY_N_SAVES} saves the directory is
|
|
81
|
+
* trimmed to the newest {@link MAX_SNAPSHOT_FILES}; snapshots are caches, so
|
|
82
|
+
* dropping an old one only costs a replay.
|
|
71
83
|
*/
|
|
72
84
|
function saveSnapshot(snapshot) {
|
|
73
85
|
try {
|
|
@@ -75,15 +87,54 @@ function saveSnapshot(snapshot) {
|
|
|
75
87
|
if (!fs.existsSync(dir)) {
|
|
76
88
|
fs.mkdirSync(dir, { recursive: true });
|
|
77
89
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
(0, atomic_1.atomicWriteJsonSync)(getSnapshotPath(snapshot.sessionId), snapshot);
|
|
91
|
+
savesSincePrune += 1;
|
|
92
|
+
if (savesSincePrune >= PRUNE_EVERY_N_SAVES) {
|
|
93
|
+
savesSincePrune = 0;
|
|
94
|
+
pruneSnapshots(exports.MAX_SNAPSHOT_FILES);
|
|
95
|
+
}
|
|
82
96
|
}
|
|
83
97
|
catch {
|
|
84
98
|
// Non-critical — snapshot failure should never break session monitoring
|
|
85
99
|
}
|
|
86
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Deletes all but the newest `keep` snapshot files (by modification time).
|
|
103
|
+
* Returns how many files were removed. Best-effort; never throws.
|
|
104
|
+
*/
|
|
105
|
+
function pruneSnapshots(keep = exports.MAX_SNAPSHOT_FILES) {
|
|
106
|
+
try {
|
|
107
|
+
const dir = getSnapshotsDir();
|
|
108
|
+
const entries = fs
|
|
109
|
+
.readdirSync(dir)
|
|
110
|
+
.filter((name) => name.endsWith('.json'))
|
|
111
|
+
.map((name) => {
|
|
112
|
+
const filePath = path.join(dir, name);
|
|
113
|
+
try {
|
|
114
|
+
return { filePath, mtimeMs: fs.statSync(filePath).mtimeMs };
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.filter((entry) => entry !== null)
|
|
121
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
122
|
+
let removed = 0;
|
|
123
|
+
for (const entry of entries.slice(Math.max(0, keep))) {
|
|
124
|
+
try {
|
|
125
|
+
fs.unlinkSync(entry.filePath);
|
|
126
|
+
removed += 1;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Already gone or locked; skip.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return removed;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
87
138
|
/**
|
|
88
139
|
* Loads a session snapshot from disk if it exists and is valid.
|
|
89
140
|
*
|
|
@@ -50,7 +50,17 @@ export interface EventAggregatorOptions {
|
|
|
50
50
|
/** Read a plan file from disk (fallback when Edit tool is used instead of Write). */
|
|
51
51
|
readPlanFile?: (path: string) => string | null;
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* How an aggregate cost figure was produced.
|
|
55
|
+
*
|
|
56
|
+
* - `reported` — every priced event carried a provider-reported cost.
|
|
57
|
+
* - `estimated` — every priced event was priced from the model catalog.
|
|
58
|
+
* - `mixed` — some events reported, some estimated.
|
|
59
|
+
* - `unpriced` — no event could be priced at all.
|
|
60
|
+
* - `none` — no usage events yet.
|
|
61
|
+
*/
|
|
62
|
+
export type AggregatedCostProvenance = 'reported' | 'estimated' | 'mixed' | 'unpriced' | 'none';
|
|
63
|
+
/** Token accumulation totals (aggregation-specific, includes cost). */
|
|
54
64
|
export interface AggregatedTokens {
|
|
55
65
|
inputTokens: number;
|
|
56
66
|
outputTokens: number;
|
|
@@ -59,6 +69,25 @@ export interface AggregatedTokens {
|
|
|
59
69
|
reasoningTokens: number;
|
|
60
70
|
/** Overall total after applying provider reasoning-in-output semantics. */
|
|
61
71
|
totalTokens: number;
|
|
72
|
+
/**
|
|
73
|
+
* Cost in USD for the priced portion of the session. Read `costProvenance`
|
|
74
|
+
* before labelling it: only `'reported'` came from the provider; anything
|
|
75
|
+
* else includes Sidekick's own catalog estimate.
|
|
76
|
+
*/
|
|
77
|
+
costUsd: number;
|
|
78
|
+
/** Portion of `costUsd` the provider reported itself. */
|
|
79
|
+
reportedCostUsd: number;
|
|
80
|
+
/** Portion of `costUsd` Sidekick estimated from catalog pricing. */
|
|
81
|
+
estimatedCostUsd: number;
|
|
82
|
+
/** Usage events that had neither a reported cost nor a catalog price. */
|
|
83
|
+
unpricedCalls: number;
|
|
84
|
+
/** Classification of `costUsd`. */
|
|
85
|
+
costProvenance: AggregatedCostProvenance;
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated Same value as `costUsd`. Despite the name it has always mixed
|
|
88
|
+
* catalog estimates with provider-reported figures; use `costUsd` together
|
|
89
|
+
* with `costProvenance`.
|
|
90
|
+
*/
|
|
62
91
|
reportedCost: number;
|
|
63
92
|
}
|
|
64
93
|
/** Per-model usage breakdown. */
|
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.calculateCompactionLedger = calculateCompactionLedger;
|
|
4
4
|
exports.formatCompactionLedger = formatCompactionLedger;
|
|
5
|
-
const
|
|
5
|
+
const usageNormalization_1 = require("../usageNormalization");
|
|
6
6
|
function calculateCompactionLedger(events, model) {
|
|
7
7
|
const tokensEvicted = events.reduce((sum, event) => sum + Math.max(0, event.tokensReclaimed), 0);
|
|
8
8
|
const reportedCount = events.filter((event) => event.source === 'reported').length;
|
|
9
9
|
const heuristicCount = events.length - reportedCount;
|
|
10
|
-
|
|
10
|
+
// Re-establishing evicted context means sending it again as uncached input.
|
|
11
|
+
// Price it through the same normalized path every other cost figure uses so
|
|
12
|
+
// the ledger can never disagree with the session total on the same tokens.
|
|
13
|
+
const reestablishment = model
|
|
14
|
+
? (0, usageNormalization_1.calculateNormalizedUsageCost)({
|
|
15
|
+
usage: (0, usageNormalization_1.normalizeProviderUsage)({
|
|
16
|
+
semantics: 'sidekick',
|
|
17
|
+
inputTokens: tokensEvicted,
|
|
18
|
+
reasoningIncludedInOutput: true,
|
|
19
|
+
}),
|
|
20
|
+
modelId: model,
|
|
21
|
+
})
|
|
22
|
+
: null;
|
|
11
23
|
return {
|
|
12
24
|
count: events.length,
|
|
13
25
|
tokensEvicted,
|
|
14
|
-
reestablishmentCostUsd:
|
|
15
|
-
? (0, modelInfo_1.calculateCostWithPricing)({ inputTokens: tokensEvicted, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 }, pricing)
|
|
16
|
-
: null,
|
|
26
|
+
reestablishmentCostUsd: reestablishment?.costUsd ?? null,
|
|
17
27
|
source: reportedCount > 0 && heuristicCount > 0
|
|
18
28
|
? 'mixed'
|
|
19
29
|
: reportedCount > 0
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.scoreSessionQuality = scoreSessionQuality;
|
|
4
4
|
exports.calculateQualityTrend = calculateQualityTrend;
|
|
5
|
+
const tokenSummary_1 = require("../tokenSummary");
|
|
5
6
|
function awarded(maximum, ratio) {
|
|
6
7
|
return Math.round(maximum * Math.max(0, Math.min(1, ratio)) * 10) / 10;
|
|
7
8
|
}
|
|
@@ -10,7 +11,7 @@ function scoreSessionQuality(metrics) {
|
|
|
10
11
|
const latencyRatio = latencyMs > 0 ? 1 - Math.min(1, latencyMs / 15_000) : 1;
|
|
11
12
|
const completedCalls = metrics.toolStats.reduce((sum, tool) => sum + tool.completedCount, 0);
|
|
12
13
|
const errorRate = completedCalls > 0 ? metrics.errorRollup.totalFailures / completedCalls : 0;
|
|
13
|
-
const contextTokens = Math.max(1,
|
|
14
|
+
const contextTokens = Math.max(1, (0, tokenSummary_1.summarizeTokens)(metrics.tokens).context);
|
|
14
15
|
const evicted = metrics.compactionEvents.reduce((sum, event) => sum + event.tokensReclaimed, 0);
|
|
15
16
|
const contextRatio = 1 - Math.min(1, evicted / contextTokens);
|
|
16
17
|
const start = metrics.sessionStartTime ? Date.parse(metrics.sessionStartTime) : NaN;
|