pi-cache-optimizer 2.8.3 → 2.8.4
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/README.md +19 -14
- package/README.zh-CN.md +19 -13
- package/index.ts +679 -165
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmod, copyFile, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { constants as fsConstants, readFileSync } from "node:fs";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, copyFile, lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants, readFileSync, watch } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import {
|
|
@@ -102,7 +102,19 @@ const STATUS_KEY = "pi-cache-stats";
|
|
|
102
102
|
const STATE_DIR = getAgentDir();
|
|
103
103
|
const STATE_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-stats.json");
|
|
104
104
|
const LEGACY_STATE_FILE_PATH = join(STATE_DIR, "deepseek-cache-optimizer-stats.json");
|
|
105
|
+
const SHARD_STATE_DIR = join(STATE_DIR, "pi-cache-optimizer-stats.d");
|
|
106
|
+
const SHARD_FILES_DIR = join(SHARD_STATE_DIR, "shards");
|
|
107
|
+
const SHARD_EPOCH_DIR = join(SHARD_STATE_DIR, "epochs");
|
|
108
|
+
const SHARD_MODEL_EPOCH_DIR = join(SHARD_EPOCH_DIR, "models");
|
|
109
|
+
const SHARD_MAINTENANCE_DIR = join(SHARD_STATE_DIR, "maintenance");
|
|
110
|
+
const SHARD_GLOBAL_EPOCH_PATH = join(SHARD_EPOCH_DIR, "global.json");
|
|
111
|
+
const SHARD_CLEANUP_LOCK_PATH = join(SHARD_MAINTENANCE_DIR, "cleanup.lock");
|
|
112
|
+
const SHARD_CLEANUP_MARKER_PATH = join(SHARD_MAINTENANCE_DIR, "last-cleanup");
|
|
105
113
|
const CONFIG_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-config.json");
|
|
114
|
+
const SHARD_RETENTION_MS = 48 * 60 * 60 * 1000;
|
|
115
|
+
const SHARD_TEMP_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
116
|
+
const SHARD_CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
117
|
+
const SHARD_CLEANUP_LOCK_STALE_MS = 60 * 60 * 1000;
|
|
106
118
|
const CACHE_PROVIDER_IDS: CacheProviderId[] = ["deepseek", "openai", "claude", "gemini"];
|
|
107
119
|
const OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_OPENAI_CACHE_KEY";
|
|
108
120
|
const NO_OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_NO_OPENAI_CACHE_KEY";
|
|
@@ -379,6 +391,46 @@ type OptimizedSystemPrompt = {
|
|
|
379
391
|
* Contains only numeric counters and booleans — never message content, prompts,
|
|
380
392
|
* payloads, headers, API keys, or model outputs.
|
|
381
393
|
*/
|
|
394
|
+
type PersistedStatsShardV7 = {
|
|
395
|
+
version: 7;
|
|
396
|
+
kind: "pi-cache-optimizer-shard";
|
|
397
|
+
instanceId: string;
|
|
398
|
+
sessionHash: string;
|
|
399
|
+
process: {
|
|
400
|
+
pid: number;
|
|
401
|
+
ppid: number;
|
|
402
|
+
instanceStartedAt: number;
|
|
403
|
+
};
|
|
404
|
+
lifecycle: {
|
|
405
|
+
state: "active" | "closed";
|
|
406
|
+
createdAt: number;
|
|
407
|
+
updatedAt: number;
|
|
408
|
+
closedAt?: number;
|
|
409
|
+
};
|
|
410
|
+
day: string;
|
|
411
|
+
globalEpoch: string;
|
|
412
|
+
models: Record<string, {
|
|
413
|
+
modelEpoch: string;
|
|
414
|
+
provider: string;
|
|
415
|
+
modelId: string;
|
|
416
|
+
modelName?: string;
|
|
417
|
+
api?: string;
|
|
418
|
+
stats: CacheStats;
|
|
419
|
+
}>;
|
|
420
|
+
lastRoutedModel?: PersistedRoutedModelRef;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
type ShardAggregate = {
|
|
424
|
+
bySession: Record<string, Record<string, CacheStats>>;
|
|
425
|
+
totalsByModel: Record<string, CacheStats>;
|
|
426
|
+
instancesBySession: Record<string, number>;
|
|
427
|
+
instancesBySessionModel: Record<string, Record<string, number>>;
|
|
428
|
+
sessionsByModel: Record<string, number>;
|
|
429
|
+
instancesByModel: Record<string, number>;
|
|
430
|
+
modelRefsByKey: Record<string, PersistedRoutedModelRef>;
|
|
431
|
+
lastRoutedModelBySession: Record<string, PersistedRoutedModelRef>;
|
|
432
|
+
};
|
|
433
|
+
|
|
382
434
|
type CacheUsageSample = {
|
|
383
435
|
timestamp: number;
|
|
384
436
|
hit: boolean;
|
|
@@ -1171,6 +1223,7 @@ const CACHE_OPTIMIZER_COMMANDS = [
|
|
|
1171
1223
|
] as const;
|
|
1172
1224
|
const CACHE_OPTIMIZER_CONFIG_ARGUMENTS = ["footer-mode"] as const;
|
|
1173
1225
|
const CACHE_OPTIMIZER_FOOTER_MODES = ["total", "session", "process"] as const;
|
|
1226
|
+
const CACHE_OPTIMIZER_STATS_ARGUMENTS = ["all", "contributors"] as const;
|
|
1174
1227
|
|
|
1175
1228
|
function filterCommandCompletionItems(
|
|
1176
1229
|
values: readonly string[],
|
|
@@ -1209,9 +1262,18 @@ function getCacheOptimizerArgumentCompletions(argumentPrefix: string): CommandCo
|
|
|
1209
1262
|
if (subcommandPrefix === "config") {
|
|
1210
1263
|
return filterCommandCompletionItems(CACHE_OPTIMIZER_CONFIG_ARGUMENTS, "", "config");
|
|
1211
1264
|
}
|
|
1265
|
+
if (subcommandPrefix === "stats") {
|
|
1266
|
+
return filterCommandCompletionItems(CACHE_OPTIMIZER_STATS_ARGUMENTS, "", "stats");
|
|
1267
|
+
}
|
|
1212
1268
|
return filterCommandCompletionItems(CACHE_OPTIMIZER_COMMANDS, parts[0]);
|
|
1213
1269
|
}
|
|
1214
1270
|
|
|
1271
|
+
if (parts[0].toLowerCase() === "stats") {
|
|
1272
|
+
return parts.length === 2
|
|
1273
|
+
? filterCommandCompletionItems(CACHE_OPTIMIZER_STATS_ARGUMENTS, parts[1], "stats")
|
|
1274
|
+
: null;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1215
1277
|
if (parts[0].toLowerCase() !== "config") return null;
|
|
1216
1278
|
|
|
1217
1279
|
if (parts.length === 2) {
|
|
@@ -1267,7 +1329,7 @@ function resolveFooterStatsMode(
|
|
|
1267
1329
|
): { mode: FooterStatsMode; source: FooterStatsModeSource } {
|
|
1268
1330
|
if (configuredMode) return { mode: configuredMode, source: "config" };
|
|
1269
1331
|
const envMode = parseFooterStatsMode(env[FOOTER_MODE_ENV]);
|
|
1270
|
-
return envMode ? { mode: envMode, source: "env" } : { mode: "
|
|
1332
|
+
return envMode ? { mode: envMode, source: "env" } : { mode: "session", source: "default" };
|
|
1271
1333
|
}
|
|
1272
1334
|
|
|
1273
1335
|
function footerStatsMode(
|
|
@@ -3840,6 +3902,87 @@ function formatRecentTrendSummary(samples: CacheUsageSample[], maxCount: number)
|
|
|
3840
3902
|
/**
|
|
3841
3903
|
* Build the output for `/cache-optimizer stats`.
|
|
3842
3904
|
*/
|
|
3905
|
+
function formatCompactStats(stats: CacheStats): string {
|
|
3906
|
+
const percent = stats.totalInputTokens > 0
|
|
3907
|
+
? (stats.cachedInputTokens / stats.totalInputTokens) * 100
|
|
3908
|
+
: 0;
|
|
3909
|
+
return `${stats.hitRequests}/${stats.totalRequests}·${formatTokenCount(stats.cachedInputTokens)}/${formatTokenCount(stats.totalInputTokens)} ${percent.toFixed(1)}%`;
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
function modelFromStatsKey(key: string, ref?: PersistedRoutedModelRef): PiModel | undefined {
|
|
3913
|
+
const slash = key.indexOf("/");
|
|
3914
|
+
if (slash <= 0 || slash >= key.length - 1) return undefined;
|
|
3915
|
+
return routedModelRefToPiModel(ref ?? { provider: key.slice(0, slash), id: key.slice(slash + 1) });
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
function sortStatsEntries(entries: Array<[string, CacheStats]>, activeModelKey?: string): Array<[string, CacheStats]> {
|
|
3919
|
+
return entries.sort(([left], [right]) => {
|
|
3920
|
+
if (left === activeModelKey) return -1;
|
|
3921
|
+
if (right === activeModelKey) return 1;
|
|
3922
|
+
return left.localeCompare(right);
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
function buildSessionStatsOutput(
|
|
3927
|
+
sessionModels: Record<string, CacheStats>,
|
|
3928
|
+
activeModel?: PiModel,
|
|
3929
|
+
modelRefsByKey: Record<string, PersistedRoutedModelRef> = {},
|
|
3930
|
+
): string {
|
|
3931
|
+
const activeKey = activeModel ? modelKey(activeModel) : undefined;
|
|
3932
|
+
const models = { ...sessionModels };
|
|
3933
|
+
if (activeModel && selectAdapterForModel(activeModel) && !models[activeKey!]) {
|
|
3934
|
+
models[activeKey!] = emptyCacheStats();
|
|
3935
|
+
}
|
|
3936
|
+
const entries = sortStatsEntries(Object.entries(models), activeKey);
|
|
3937
|
+
if (entries.length === 0) return "ℹ️ No cache statistics recorded for the current session today.";
|
|
3938
|
+
const lines = ["Scope: current session", `Day: ${currentLocalDay()}`, `Models: ${entries.length}`];
|
|
3939
|
+
for (const [key, stats] of entries) {
|
|
3940
|
+
const model = modelFromStatsKey(key, modelRefsByKey[key]);
|
|
3941
|
+
const adapter = model ? selectAdapterForModel(model) : undefined;
|
|
3942
|
+
lines.push("", `── ${key} ──`, `Adapter: ${adapter?.label ?? "Unknown cache adapter"}`);
|
|
3943
|
+
lines.push(`Requests: ${stats.hitRequests} hit / ${stats.totalRequests} total`);
|
|
3944
|
+
lines.push(`Cached tokens: ${formatTokenCount(stats.cachedInputTokens)} / ${formatTokenCount(stats.totalInputTokens)} input · ${stats.totalInputTokens > 0 ? `${((stats.cachedInputTokens / stats.totalInputTokens) * 100).toFixed(1)}%` : "0.0%"}`);
|
|
3945
|
+
lines.push(`Summary: ${formatCompactStats(stats)}`);
|
|
3946
|
+
if (stats.cacheWriteInputTokens > 0) lines.push(`Cache write: ${formatTokenCount(stats.cacheWriteInputTokens)}`);
|
|
3947
|
+
}
|
|
3948
|
+
return lines.join("\n");
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
function buildAllStatsOutput(aggregate: ShardAggregate): string {
|
|
3952
|
+
const entries = sortStatsEntries(Object.entries(aggregate.totalsByModel));
|
|
3953
|
+
if (entries.length === 0) return "ℹ️ No local cache statistics recorded today.";
|
|
3954
|
+
const sessionCount = Object.keys(aggregate.instancesBySession).length;
|
|
3955
|
+
const instanceCount = Object.values(aggregate.instancesBySession).reduce((sum, value) => sum + value, 0);
|
|
3956
|
+
const lines = ["Scope: all local sessions", `Day: ${currentLocalDay()}`, `Sessions: ${sessionCount}`, `Instances: ${instanceCount}`];
|
|
3957
|
+
for (const [key, stats] of entries) {
|
|
3958
|
+
const model = modelFromStatsKey(key, aggregate.modelRefsByKey[key]);
|
|
3959
|
+
const adapter = model ? selectAdapterForModel(model) : undefined;
|
|
3960
|
+
lines.push("", `── ${key} ──`, `Adapter: ${adapter?.label ?? "Unknown cache adapter"}`);
|
|
3961
|
+
lines.push(`Sessions: ${aggregate.sessionsByModel[key] ?? 0} · Instances: ${aggregate.instancesByModel[key] ?? 0}`);
|
|
3962
|
+
lines.push(`Requests: ${stats.hitRequests} hit / ${stats.totalRequests} total`);
|
|
3963
|
+
lines.push(`Cached tokens: ${formatTokenCount(stats.cachedInputTokens)} / ${formatTokenCount(stats.totalInputTokens)} input · ${stats.totalInputTokens > 0 ? `${((stats.cachedInputTokens / stats.totalInputTokens) * 100).toFixed(1)}%` : "0.0%"}`);
|
|
3964
|
+
lines.push(`Summary: ${formatCompactStats(stats)}`);
|
|
3965
|
+
if (stats.cacheWriteInputTokens > 0) lines.push(`Cache write: ${formatTokenCount(stats.cacheWriteInputTokens)}`);
|
|
3966
|
+
}
|
|
3967
|
+
return lines.join("\n");
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
function buildContributorsStatsOutput(aggregate: ShardAggregate, activeModel: PiModel | undefined, currentSessionHash?: string): string {
|
|
3971
|
+
if (!activeModel) return "ℹ️ No active model selected.";
|
|
3972
|
+
const key = modelKey(activeModel);
|
|
3973
|
+
const rows = Object.entries(aggregate.bySession)
|
|
3974
|
+
.filter(([, models]) => models[key])
|
|
3975
|
+
.sort(([left], [right]) => left === currentSessionHash ? -1 : right === currentSessionHash ? 1 : left.localeCompare(right));
|
|
3976
|
+
if (rows.length === 0) return `ℹ️ No contributors recorded for ${key} today.`;
|
|
3977
|
+
const lines = [`Model: ${key}`, "Scope: contributing local sessions today"];
|
|
3978
|
+
let otherIndex = 0;
|
|
3979
|
+
for (const [sessionHash, models] of rows) {
|
|
3980
|
+
const label = sessionHash === currentSessionHash ? "Current session" : `Other session ${++otherIndex}`;
|
|
3981
|
+
lines.push("", label, ` Instances: ${aggregate.instancesBySessionModel[sessionHash]?.[key] ?? 0}`, ` ${formatCompactStats(models[key])}`);
|
|
3982
|
+
}
|
|
3983
|
+
return lines.join("\n");
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3843
3986
|
function buildStatsOutput(model: PiModel | undefined, adapter: CacheProviderAdapter | undefined, stats: CacheStats | undefined, recentSamples: CacheUsageSample[]): string {
|
|
3844
3987
|
const lines: string[] = [];
|
|
3845
3988
|
|
|
@@ -4248,8 +4391,8 @@ function filterRestorableStatsForSession(
|
|
|
4248
4391
|
* `restoreCacheStats` can migrate them on the next load before the session
|
|
4249
4392
|
* id is known.
|
|
4250
4393
|
*
|
|
4251
|
-
*
|
|
4252
|
-
*
|
|
4394
|
+
* Historical v1-v6 pure helper (no runtime I/O). Permanent migration tests
|
|
4395
|
+
* exercise it directly; v7 startup never reads or writes the obsolete file.
|
|
4253
4396
|
*/
|
|
4254
4397
|
function mergeCacheSessions(
|
|
4255
4398
|
existingSessions: Record<string, Record<string, CacheStats>>,
|
|
@@ -4409,7 +4552,304 @@ async function writePersistedCacheStats(
|
|
|
4409
4552
|
await rename(tempPath, STATE_FILE_PATH);
|
|
4410
4553
|
}
|
|
4411
4554
|
|
|
4555
|
+
function modelEpochPath(modelKeyValue: string): string {
|
|
4556
|
+
return join(SHARD_MODEL_EPOCH_DIR, `${createHash("sha256").update(modelKeyValue).digest("hex")}.json`);
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
function initialEpoch(scope: string): string {
|
|
4560
|
+
return `initial:${scope}`;
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
function parseEpochRecord(value: unknown): string | undefined {
|
|
4564
|
+
const record = asRecord(value);
|
|
4565
|
+
return record?.version === 1 && isNonEmptyString(record.epoch) ? record.epoch.trim() : undefined;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
async function readEpochFile(path: string, fallback: string): Promise<string> {
|
|
4569
|
+
try {
|
|
4570
|
+
return parseEpochRecord(JSON.parse(await readFile(path, "utf8"))) ?? fallback;
|
|
4571
|
+
} catch {
|
|
4572
|
+
return fallback;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
async function readGlobalStatsEpoch(): Promise<string> {
|
|
4577
|
+
return readEpochFile(SHARD_GLOBAL_EPOCH_PATH, initialEpoch("global"));
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
async function readModelStatsEpoch(modelKeyValue: string): Promise<string> {
|
|
4581
|
+
return readEpochFile(modelEpochPath(modelKeyValue), initialEpoch(`model:${modelKeyValue}`));
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
async function writeStatsEpoch(path: string, epoch: string): Promise<void> {
|
|
4585
|
+
await mkdir(dirname(path), { recursive: true });
|
|
4586
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
4587
|
+
await writeFile(tempPath, JSON.stringify({ version: 1, epoch, createdAt: Date.now() }, null, 2) + "\n", "utf8");
|
|
4588
|
+
await rename(tempPath, path);
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
async function advanceGlobalStatsEpoch(): Promise<string> {
|
|
4592
|
+
const epoch = `${Date.now()}-${randomUUID()}`;
|
|
4593
|
+
await writeStatsEpoch(SHARD_GLOBAL_EPOCH_PATH, epoch);
|
|
4594
|
+
return epoch;
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
async function advanceModelStatsEpoch(modelKeyValue: string): Promise<string> {
|
|
4598
|
+
const epoch = `${Date.now()}-${randomUUID()}`;
|
|
4599
|
+
await writeStatsEpoch(modelEpochPath(modelKeyValue), epoch);
|
|
4600
|
+
return epoch;
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
function parsePersistedStatsShardV7(value: unknown): PersistedStatsShardV7 | undefined {
|
|
4604
|
+
const record = asRecord(value);
|
|
4605
|
+
const processRecord = asRecord(record?.process);
|
|
4606
|
+
const lifecycle = asRecord(record?.lifecycle);
|
|
4607
|
+
const rawModels = asRecord(record?.models);
|
|
4608
|
+
if (
|
|
4609
|
+
record?.version !== 7 ||
|
|
4610
|
+
record.kind !== "pi-cache-optimizer-shard" ||
|
|
4611
|
+
!isNonEmptyString(record.instanceId) ||
|
|
4612
|
+
!isNonEmptyString(record.sessionHash) ||
|
|
4613
|
+
!processRecord ||
|
|
4614
|
+
!Number.isInteger(processRecord.pid) ||
|
|
4615
|
+
!Number.isInteger(processRecord.ppid) ||
|
|
4616
|
+
typeof processRecord.instanceStartedAt !== "number" ||
|
|
4617
|
+
!lifecycle ||
|
|
4618
|
+
(lifecycle.state !== "active" && lifecycle.state !== "closed") ||
|
|
4619
|
+
typeof lifecycle.createdAt !== "number" ||
|
|
4620
|
+
typeof lifecycle.updatedAt !== "number" ||
|
|
4621
|
+
!isNonEmptyString(record.day) ||
|
|
4622
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(record.day) ||
|
|
4623
|
+
!isNonEmptyString(record.globalEpoch) ||
|
|
4624
|
+
!rawModels
|
|
4625
|
+
) return undefined;
|
|
4626
|
+
|
|
4627
|
+
const models: PersistedStatsShardV7["models"] = {};
|
|
4628
|
+
for (const [key, rawEntry] of Object.entries(rawModels)) {
|
|
4629
|
+
const entry = asRecord(rawEntry);
|
|
4630
|
+
const stats = parseCacheStats(entry?.stats);
|
|
4631
|
+
if (
|
|
4632
|
+
!entry || !stats || !isNonEmptyString(entry.modelEpoch) ||
|
|
4633
|
+
!isNonEmptyString(entry.provider) || !isNonEmptyString(entry.modelId) ||
|
|
4634
|
+
key !== `${entry.provider.trim()}/${entry.modelId.trim()}`
|
|
4635
|
+
) continue;
|
|
4636
|
+
models[key] = {
|
|
4637
|
+
modelEpoch: entry.modelEpoch.trim(),
|
|
4638
|
+
provider: entry.provider.trim(),
|
|
4639
|
+
modelId: entry.modelId.trim(),
|
|
4640
|
+
...(isNonEmptyString(entry.modelName) ? { modelName: entry.modelName.trim() } : {}),
|
|
4641
|
+
...(isNonEmptyString(entry.api) ? { api: entry.api.trim() } : {}),
|
|
4642
|
+
stats,
|
|
4643
|
+
};
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
const lastRoutedModel = parsePersistedRoutedModelRef(record.lastRoutedModel);
|
|
4647
|
+
return {
|
|
4648
|
+
version: 7,
|
|
4649
|
+
kind: "pi-cache-optimizer-shard",
|
|
4650
|
+
instanceId: record.instanceId.trim(),
|
|
4651
|
+
sessionHash: record.sessionHash.trim(),
|
|
4652
|
+
process: {
|
|
4653
|
+
pid: Number(processRecord.pid),
|
|
4654
|
+
ppid: Number(processRecord.ppid),
|
|
4655
|
+
instanceStartedAt: processRecord.instanceStartedAt,
|
|
4656
|
+
},
|
|
4657
|
+
lifecycle: {
|
|
4658
|
+
state: lifecycle.state,
|
|
4659
|
+
createdAt: lifecycle.createdAt,
|
|
4660
|
+
updatedAt: lifecycle.updatedAt,
|
|
4661
|
+
...(typeof lifecycle.closedAt === "number" ? { closedAt: lifecycle.closedAt } : {}),
|
|
4662
|
+
},
|
|
4663
|
+
day: record.day,
|
|
4664
|
+
globalEpoch: record.globalEpoch.trim(),
|
|
4665
|
+
models,
|
|
4666
|
+
...(lastRoutedModel ? { lastRoutedModel } : {}),
|
|
4667
|
+
};
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4670
|
+
async function writeStatsShardV7(path: string, shard: PersistedStatsShardV7): Promise<void> {
|
|
4671
|
+
await mkdir(dirname(path), { recursive: true });
|
|
4672
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
4673
|
+
await writeFile(tempPath, JSON.stringify(shard, null, 2) + "\n", "utf8");
|
|
4674
|
+
await rename(tempPath, path);
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
async function readValidStatsShardsV7(directory: string = SHARD_FILES_DIR): Promise<PersistedStatsShardV7[]> {
|
|
4678
|
+
let names: string[];
|
|
4679
|
+
try {
|
|
4680
|
+
names = await readdir(directory);
|
|
4681
|
+
} catch (error) {
|
|
4682
|
+
if (getErrorCode(error) === "ENOENT") return [];
|
|
4683
|
+
throw error;
|
|
4684
|
+
}
|
|
4685
|
+
const shards: PersistedStatsShardV7[] = [];
|
|
4686
|
+
for (const name of names) {
|
|
4687
|
+
if (!/^[0-9a-f-]+\.json$/i.test(name)) continue;
|
|
4688
|
+
const path = join(directory, name);
|
|
4689
|
+
try {
|
|
4690
|
+
const info = await lstat(path);
|
|
4691
|
+
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
4692
|
+
const parsed = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
|
|
4693
|
+
const filenameInstanceId = name.slice(0, -".json".length);
|
|
4694
|
+
if (parsed && parsed.instanceId === filenameInstanceId) shards.push(parsed);
|
|
4695
|
+
} catch {
|
|
4696
|
+
// Ignore malformed or transiently unavailable shards. Atomic writers will
|
|
4697
|
+
// publish a complete replacement on the next successful update.
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
return shards;
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
async function aggregateStatsShardsV7(
|
|
4704
|
+
shards: PersistedStatsShardV7[],
|
|
4705
|
+
day: string = currentLocalDay(),
|
|
4706
|
+
): Promise<ShardAggregate> {
|
|
4707
|
+
const globalEpoch = await readGlobalStatsEpoch();
|
|
4708
|
+
const modelEpochs = new Map<string, string>();
|
|
4709
|
+
const result: ShardAggregate = {
|
|
4710
|
+
bySession: {},
|
|
4711
|
+
totalsByModel: {},
|
|
4712
|
+
instancesBySession: {},
|
|
4713
|
+
instancesBySessionModel: {},
|
|
4714
|
+
sessionsByModel: {},
|
|
4715
|
+
instancesByModel: {},
|
|
4716
|
+
modelRefsByKey: {},
|
|
4717
|
+
lastRoutedModelBySession: {},
|
|
4718
|
+
};
|
|
4719
|
+
const routedUpdatedAt = new Map<string, number>();
|
|
4720
|
+
const modelRefUpdatedAt = new Map<string, number>();
|
|
4721
|
+
const modelSessions = new Map<string, Set<string>>();
|
|
4722
|
+
|
|
4723
|
+
for (const shard of shards) {
|
|
4724
|
+
if (shard.day !== day || shard.globalEpoch !== globalEpoch) continue;
|
|
4725
|
+
let contributed = false;
|
|
4726
|
+
if (shard.lastRoutedModel && (routedUpdatedAt.get(shard.sessionHash) ?? -1) < shard.lifecycle.updatedAt) {
|
|
4727
|
+
routedUpdatedAt.set(shard.sessionHash, shard.lifecycle.updatedAt);
|
|
4728
|
+
result.lastRoutedModelBySession[shard.sessionHash] = shard.lastRoutedModel;
|
|
4729
|
+
}
|
|
4730
|
+
for (const [key, entry] of Object.entries(shard.models)) {
|
|
4731
|
+
let epoch = modelEpochs.get(key);
|
|
4732
|
+
if (!epoch) {
|
|
4733
|
+
epoch = await readModelStatsEpoch(key);
|
|
4734
|
+
modelEpochs.set(key, epoch);
|
|
4735
|
+
}
|
|
4736
|
+
if (entry.modelEpoch !== epoch || entry.stats.day !== day) continue;
|
|
4737
|
+
contributed = true;
|
|
4738
|
+
const sessionModels = result.bySession[shard.sessionHash] ??= {};
|
|
4739
|
+
sessionModels[key] = mergeCacheStatsForTotal(sessionModels[key], entry.stats);
|
|
4740
|
+
result.totalsByModel[key] = mergeCacheStatsForTotal(result.totalsByModel[key], entry.stats);
|
|
4741
|
+
result.instancesByModel[key] = (result.instancesByModel[key] ?? 0) + 1;
|
|
4742
|
+
if ((modelRefUpdatedAt.get(key) ?? -1) < shard.lifecycle.updatedAt) {
|
|
4743
|
+
modelRefUpdatedAt.set(key, shard.lifecycle.updatedAt);
|
|
4744
|
+
result.modelRefsByKey[key] = {
|
|
4745
|
+
provider: entry.provider,
|
|
4746
|
+
id: entry.modelId,
|
|
4747
|
+
name: entry.modelName ?? entry.modelId,
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
const sessionInstances = result.instancesBySessionModel[shard.sessionHash] ??= {};
|
|
4751
|
+
sessionInstances[key] = (sessionInstances[key] ?? 0) + 1;
|
|
4752
|
+
const sessions = modelSessions.get(key) ?? new Set<string>();
|
|
4753
|
+
sessions.add(shard.sessionHash);
|
|
4754
|
+
modelSessions.set(key, sessions);
|
|
4755
|
+
}
|
|
4756
|
+
if (contributed) {
|
|
4757
|
+
result.instancesBySession[shard.sessionHash] = (result.instancesBySession[shard.sessionHash] ?? 0) + 1;
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
for (const [key, sessions] of modelSessions) result.sessionsByModel[key] = sessions.size;
|
|
4761
|
+
return result;
|
|
4762
|
+
}
|
|
4763
|
+
|
|
4764
|
+
async function loadStatsShardAggregateV7(directory: string = SHARD_FILES_DIR): Promise<ShardAggregate> {
|
|
4765
|
+
return aggregateStatsShardsV7(await readValidStatsShardsV7(directory));
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
function isProcessAlive(pid: number): boolean {
|
|
4769
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
4770
|
+
try {
|
|
4771
|
+
process.kill(pid, 0);
|
|
4772
|
+
return true;
|
|
4773
|
+
} catch (error) {
|
|
4774
|
+
return getErrorCode(error) === "EPERM";
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
async function removeLegacyStatsFiles(): Promise<void> {
|
|
4779
|
+
for (const path of [STATE_FILE_PATH, LEGACY_STATE_FILE_PATH]) {
|
|
4780
|
+
try {
|
|
4781
|
+
await unlink(path);
|
|
4782
|
+
} catch (error) {
|
|
4783
|
+
if (getErrorCode(error) !== "ENOENT") console.warn(`${LOG_PREFIX}: failed to remove obsolete stats file ${path}`, error);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
async function cleanupStatsShardsV7(now = Date.now(), directory: string = SHARD_FILES_DIR): Promise<number> {
|
|
4789
|
+
let names: string[];
|
|
4790
|
+
try {
|
|
4791
|
+
names = await readdir(directory);
|
|
4792
|
+
} catch (error) {
|
|
4793
|
+
return getErrorCode(error) === "ENOENT" ? 0 : Promise.reject(error);
|
|
4794
|
+
}
|
|
4795
|
+
const today = currentLocalDay();
|
|
4796
|
+
let removed = 0;
|
|
4797
|
+
for (const name of names) {
|
|
4798
|
+
const isShard = /^[0-9a-f-]+\.json$/i.test(name);
|
|
4799
|
+
const isTemp = name.endsWith(".tmp");
|
|
4800
|
+
if (!isShard && !isTemp) continue;
|
|
4801
|
+
const path = join(directory, name);
|
|
4802
|
+
try {
|
|
4803
|
+
const info = await lstat(path);
|
|
4804
|
+
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
4805
|
+
if (isTemp) {
|
|
4806
|
+
if (now - info.mtimeMs < SHARD_TEMP_RETENTION_MS) continue;
|
|
4807
|
+
} else {
|
|
4808
|
+
const parsed = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
|
|
4809
|
+
if (parsed?.day === today) continue;
|
|
4810
|
+
const updatedAt = parsed?.lifecycle.updatedAt ?? info.mtimeMs;
|
|
4811
|
+
if (now - updatedAt < SHARD_RETENTION_MS) continue;
|
|
4812
|
+
if (parsed?.lifecycle.state === "active" && isProcessAlive(parsed.process.pid)) continue;
|
|
4813
|
+
}
|
|
4814
|
+
await unlink(path);
|
|
4815
|
+
removed += 1;
|
|
4816
|
+
} catch {
|
|
4817
|
+
// Best-effort maintenance must never block the extension.
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
return removed;
|
|
4821
|
+
}
|
|
4822
|
+
|
|
4823
|
+
async function maybeCleanupStatsShardsV7(now = Date.now()): Promise<void> {
|
|
4824
|
+
await mkdir(SHARD_MAINTENANCE_DIR, { recursive: true });
|
|
4825
|
+
try {
|
|
4826
|
+
const marker = await stat(SHARD_CLEANUP_MARKER_PATH);
|
|
4827
|
+
if (now - marker.mtimeMs < SHARD_CLEANUP_INTERVAL_MS) return;
|
|
4828
|
+
} catch {}
|
|
4829
|
+
|
|
4830
|
+
try {
|
|
4831
|
+
await mkdir(SHARD_CLEANUP_LOCK_PATH);
|
|
4832
|
+
} catch (error) {
|
|
4833
|
+
if (getErrorCode(error) !== "EEXIST") return;
|
|
4834
|
+
try {
|
|
4835
|
+
const lock = await stat(SHARD_CLEANUP_LOCK_PATH);
|
|
4836
|
+
if (now - lock.mtimeMs <= SHARD_CLEANUP_LOCK_STALE_MS) return;
|
|
4837
|
+
await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
|
|
4838
|
+
await mkdir(SHARD_CLEANUP_LOCK_PATH);
|
|
4839
|
+
} catch {
|
|
4840
|
+
return;
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4412
4843
|
|
|
4844
|
+
try {
|
|
4845
|
+
await cleanupStatsShardsV7(now);
|
|
4846
|
+
const tempPath = `${SHARD_CLEANUP_MARKER_PATH}.${process.pid}.${randomUUID()}.tmp`;
|
|
4847
|
+
await writeFile(tempPath, `${now}\n`, "utf8");
|
|
4848
|
+
await rename(tempPath, SHARD_CLEANUP_MARKER_PATH);
|
|
4849
|
+
} finally {
|
|
4850
|
+
await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4413
4853
|
|
|
4414
4854
|
function isCompatCheckApplicable(model: PiModel): boolean {
|
|
4415
4855
|
return isOpenAICompatibleProxyApi(model.api) && !isOfficialOpenAIBaseUrl(model) && !isPiBuiltInLlamaCppModel(model);
|
|
@@ -6876,6 +7316,10 @@ export const __internals_for_tests = {
|
|
|
6876
7316
|
// Recent sample / stats output / diagnosis helpers
|
|
6877
7317
|
MAX_RECENT_SAMPLES,
|
|
6878
7318
|
buildStatsOutput,
|
|
7319
|
+
buildSessionStatsOutput,
|
|
7320
|
+
buildAllStatsOutput,
|
|
7321
|
+
buildContributorsStatsOutput,
|
|
7322
|
+
formatCompactStats,
|
|
6879
7323
|
buildLowHitDiagnosis,
|
|
6880
7324
|
formatRecentTrendSummary,
|
|
6881
7325
|
formatHitRatio,
|
|
@@ -6923,8 +7367,23 @@ export const __internals_for_tests = {
|
|
|
6923
7367
|
createSerializedAsyncRunner,
|
|
6924
7368
|
writePersistedCacheStats,
|
|
6925
7369
|
readPersistedCacheStats,
|
|
7370
|
+
parsePersistedStatsShardV7,
|
|
7371
|
+
writeStatsShardV7,
|
|
7372
|
+
readValidStatsShardsV7,
|
|
7373
|
+
aggregateStatsShardsV7,
|
|
7374
|
+
loadStatsShardAggregateV7,
|
|
7375
|
+
cleanupStatsShardsV7,
|
|
7376
|
+
removeLegacyStatsFiles,
|
|
7377
|
+
readGlobalStatsEpoch,
|
|
7378
|
+
readModelStatsEpoch,
|
|
7379
|
+
advanceGlobalStatsEpoch,
|
|
7380
|
+
advanceModelStatsEpoch,
|
|
7381
|
+
modelEpochPath,
|
|
6926
7382
|
STATE_FILE_PATH,
|
|
6927
7383
|
LEGACY_STATE_FILE_PATH,
|
|
7384
|
+
SHARD_STATE_DIR,
|
|
7385
|
+
SHARD_FILES_DIR,
|
|
7386
|
+
SHARD_GLOBAL_EPOCH_PATH,
|
|
6928
7387
|
STATE_DIR,
|
|
6929
7388
|
// JSONC surgical edit helpers
|
|
6930
7389
|
MODELS_JSON_PATH,
|
|
@@ -6981,17 +7440,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
6981
7440
|
let lastStatusText: string | undefined;
|
|
6982
7441
|
let persistenceWarningShown = false;
|
|
6983
7442
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
7443
|
+
let shardRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
7444
|
+
let shardWatcher: ReturnType<typeof watch> | undefined;
|
|
6984
7445
|
const enqueuePersist = createSerializedAsyncRunner();
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
const
|
|
7446
|
+
const instanceId = randomUUID();
|
|
7447
|
+
const instanceStartedAt = Date.now();
|
|
7448
|
+
const instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
|
|
7449
|
+
const modelApiByKey = new Map<string, string>();
|
|
7450
|
+
const modelNameByKey = new Map<string, string>();
|
|
7451
|
+
const modelEpochByKey = new Map<string, string>();
|
|
7452
|
+
let currentGlobalEpoch = initialEpoch("global");
|
|
6988
7453
|
let integrityNotificationShown = false;
|
|
6989
7454
|
let currentSessionId = "";
|
|
6990
7455
|
let currentSessionHash = "";
|
|
6991
7456
|
let currentSessionHashSet = false;
|
|
6992
7457
|
let lastActualRoutedModel: PersistedRoutedModelRef | undefined;
|
|
6993
7458
|
let latestCacheHint: PiCacheHintSnapshot | undefined;
|
|
7459
|
+
let shardCreatedAt = Date.now();
|
|
6994
7460
|
const PERSIST_DEBOUNCE_MS = 2000;
|
|
7461
|
+
const SHARD_REFRESH_DEBOUNCE_MS = 250;
|
|
6995
7462
|
|
|
6996
7463
|
function buildObservedRuntimeFixSuggestion(model: PiModel): FixSuggestion | undefined {
|
|
6997
7464
|
const key = modelKey(model);
|
|
@@ -7106,17 +7573,102 @@ export default function (pi: ExtensionAPI) {
|
|
|
7106
7573
|
recentSamplesByModelKey.clear();
|
|
7107
7574
|
}
|
|
7108
7575
|
|
|
7109
|
-
function
|
|
7576
|
+
async function refreshShardAggregate(): Promise<ShardAggregate> {
|
|
7577
|
+
const persistedShards = await readValidStatsShardsV7();
|
|
7578
|
+
const shards = persistedShards.filter((shard) => shard.instanceId !== instanceId);
|
|
7579
|
+
if (currentSessionHashSet) shards.push(buildCurrentStatsShard());
|
|
7580
|
+
const aggregate = await aggregateStatsShardsV7(shards);
|
|
7581
|
+
|
|
7582
|
+
// A reset from another process can advance epochs while this instance is
|
|
7583
|
+
// idle. Adopt those epochs during every aggregate refresh, not only when a
|
|
7584
|
+
// later message_end arrives, so this instance cannot keep an old in-memory
|
|
7585
|
+
// process bucket visible in `process` mode or rewrite stale counters during
|
|
7586
|
+
// shutdown. Model-scoped resets clear only the affected local bucket.
|
|
7587
|
+
const latestGlobalEpoch = await readGlobalStatsEpoch();
|
|
7588
|
+
if (currentGlobalEpoch !== latestGlobalEpoch) {
|
|
7589
|
+
currentGlobalEpoch = latestGlobalEpoch;
|
|
7590
|
+
cacheStatsProcessByModel = {};
|
|
7591
|
+
modelEpochByKey.clear();
|
|
7592
|
+
modelApiByKey.clear();
|
|
7593
|
+
modelNameByKey.clear();
|
|
7594
|
+
lastActualRoutedModel = undefined;
|
|
7595
|
+
} else {
|
|
7596
|
+
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7597
|
+
const latestModelEpoch = await readModelStatsEpoch(key);
|
|
7598
|
+
if ((modelEpochByKey.get(key) ?? initialEpoch(`model:${key}`)) === latestModelEpoch) continue;
|
|
7599
|
+
delete cacheStatsProcessByModel[key];
|
|
7600
|
+
modelEpochByKey.set(key, latestModelEpoch);
|
|
7601
|
+
modelApiByKey.delete(key);
|
|
7602
|
+
modelNameByKey.delete(key);
|
|
7603
|
+
}
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
cacheStatsByModel = {};
|
|
7607
|
+
for (const [sessionHash, models] of Object.entries(aggregate.bySession)) {
|
|
7608
|
+
for (const [key, stats] of Object.entries(models)) {
|
|
7609
|
+
cacheStatsByModel[`${sessionHash}:${key}`] = stats;
|
|
7610
|
+
}
|
|
7611
|
+
}
|
|
7612
|
+
cacheStatsTotalsByModel = aggregate.totalsByModel;
|
|
7613
|
+
if (currentSessionHashSet) {
|
|
7614
|
+
lastActualRoutedModel = aggregate.lastRoutedModelBySession[currentSessionHash];
|
|
7615
|
+
}
|
|
7616
|
+
lastStatusText = undefined;
|
|
7617
|
+
return aggregate;
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
async function ensureCurrentEpochs(): Promise<void> {
|
|
7621
|
+
currentGlobalEpoch = await readGlobalStatsEpoch();
|
|
7622
|
+
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7623
|
+
modelEpochByKey.set(key, await readModelStatsEpoch(key));
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7626
|
+
|
|
7627
|
+
function buildCurrentStatsShard(state: "active" | "closed" = "active"): PersistedStatsShardV7 {
|
|
7628
|
+
const now = Date.now();
|
|
7629
|
+
const models: PersistedStatsShardV7["models"] = {};
|
|
7630
|
+
for (const [key, stats] of Object.entries(cacheStatsProcessByModel)) {
|
|
7631
|
+
const slash = key.indexOf("/");
|
|
7632
|
+
if (slash <= 0 || slash >= key.length - 1) continue;
|
|
7633
|
+
models[key] = {
|
|
7634
|
+
modelEpoch: modelEpochByKey.get(key) ?? initialEpoch(`model:${key}`),
|
|
7635
|
+
provider: key.slice(0, slash),
|
|
7636
|
+
modelId: key.slice(slash + 1),
|
|
7637
|
+
...(modelNameByKey.get(key) ? { modelName: modelNameByKey.get(key) } : {}),
|
|
7638
|
+
...(modelApiByKey.get(key) ? { api: modelApiByKey.get(key) } : {}),
|
|
7639
|
+
stats: cloneCacheStats(stats),
|
|
7640
|
+
};
|
|
7641
|
+
}
|
|
7110
7642
|
return {
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7643
|
+
version: 7,
|
|
7644
|
+
kind: "pi-cache-optimizer-shard",
|
|
7645
|
+
instanceId,
|
|
7646
|
+
sessionHash: currentSessionHash || "_nosession",
|
|
7647
|
+
process: { pid: process.pid, ppid: process.ppid, instanceStartedAt },
|
|
7648
|
+
lifecycle: {
|
|
7649
|
+
state,
|
|
7650
|
+
createdAt: shardCreatedAt,
|
|
7651
|
+
updatedAt: now,
|
|
7652
|
+
...(state === "closed" ? { closedAt: now } : {}),
|
|
7653
|
+
},
|
|
7654
|
+
day: currentLocalDay(),
|
|
7655
|
+
globalEpoch: currentGlobalEpoch,
|
|
7656
|
+
models,
|
|
7657
|
+
...(lastActualRoutedModel ? { lastRoutedModel: { ...lastActualRoutedModel } } : {}),
|
|
7117
7658
|
};
|
|
7118
7659
|
}
|
|
7119
7660
|
|
|
7661
|
+
function scheduleShardRefresh(ctx?: ExtensionContext, model?: PiModel): void {
|
|
7662
|
+
if (shardRefreshTimer !== null) clearTimeout(shardRefreshTimer);
|
|
7663
|
+
shardRefreshTimer = setTimeout(() => {
|
|
7664
|
+
shardRefreshTimer = null;
|
|
7665
|
+
void refreshShardAggregate()
|
|
7666
|
+
.then(() => publishStatus(ctx as ExtensionContext, model ?? ctx?.model))
|
|
7667
|
+
.catch(() => undefined);
|
|
7668
|
+
}, SHARD_REFRESH_DEBOUNCE_MS);
|
|
7669
|
+
shardRefreshTimer.unref?.();
|
|
7670
|
+
}
|
|
7671
|
+
|
|
7120
7672
|
/** Look up visible cumulative stats for a model, falling back to legacy family. */
|
|
7121
7673
|
function getStatsForModel(model: PiModel | undefined, adapter: CacheProviderAdapter): CacheStats {
|
|
7122
7674
|
if (model) {
|
|
@@ -7166,79 +7718,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
7166
7718
|
return created;
|
|
7167
7719
|
}
|
|
7168
7720
|
|
|
7169
|
-
function resetStatsForModel(model: PiModel): void {
|
|
7721
|
+
async function resetStatsForModel(model: PiModel): Promise<void> {
|
|
7170
7722
|
const displayKey = modelKey(model);
|
|
7723
|
+
const nextEpoch = await advanceModelStatsEpoch(displayKey);
|
|
7724
|
+
modelEpochByKey.set(displayKey, nextEpoch);
|
|
7725
|
+
delete cacheStatsProcessByModel[displayKey];
|
|
7726
|
+
delete cacheStatsTotalsByModel[displayKey];
|
|
7171
7727
|
for (const key of Object.keys(cacheStatsByModel)) {
|
|
7172
7728
|
if (modelKeyFromSessionScoped(key) === displayKey) delete cacheStatsByModel[key];
|
|
7173
7729
|
}
|
|
7174
|
-
delete cacheStatsTotalsByModel[displayKey];
|
|
7175
|
-
delete cacheStatsProcessByModel[displayKey];
|
|
7176
|
-
pendingDeletedTotalModelKeys.add(displayKey);
|
|
7177
7730
|
for (const key of Array.from(recentSamplesByModelKey.keys())) {
|
|
7178
7731
|
if (modelKeyFromSessionScoped(key) === displayKey) recentSamplesByModelKey.delete(key);
|
|
7179
7732
|
}
|
|
7180
7733
|
lastStatusText = undefined;
|
|
7181
7734
|
}
|
|
7182
7735
|
|
|
7183
|
-
function resetCurrentSessionStats(): void {
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
if (key.startsWith(prefix)) delete cacheStatsByModel[key];
|
|
7187
|
-
}
|
|
7188
|
-
cacheStatsProcessByModel = {};
|
|
7736
|
+
async function resetCurrentSessionStats(): Promise<void> {
|
|
7737
|
+
currentGlobalEpoch = await advanceGlobalStatsEpoch();
|
|
7738
|
+
cacheStatsByModel = {};
|
|
7189
7739
|
cacheStatsTotalsByModel = {};
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7740
|
+
cacheStatsProcessByModel = {};
|
|
7741
|
+
modelEpochByKey.clear();
|
|
7742
|
+
modelApiByKey.clear();
|
|
7743
|
+
modelNameByKey.clear();
|
|
7744
|
+
clearRecentSamples();
|
|
7194
7745
|
lastActualRoutedModel = undefined;
|
|
7195
7746
|
lastStatusText = undefined;
|
|
7196
7747
|
}
|
|
7197
7748
|
|
|
7198
|
-
function
|
|
7199
|
-
const
|
|
7200
|
-
const statsByModel = Object.fromEntries(
|
|
7201
|
-
Object.entries(state.statsByModel).map(([key, stats]) => [key, { ...stats }]),
|
|
7202
|
-
);
|
|
7203
|
-
const totalsByModel = Object.fromEntries(
|
|
7204
|
-
Object.entries(state.totalsByModel).map(([key, stats]) => [key, { ...stats }]),
|
|
7205
|
-
);
|
|
7206
|
-
const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
|
|
7207
|
-
for (const id of CACHE_PROVIDER_IDS) {
|
|
7208
|
-
const stats = state.legacyFamily[id];
|
|
7209
|
-
if (stats) legacyFamily[id] = { ...stats };
|
|
7210
|
-
}
|
|
7211
|
-
const lastRoutedModelBySession = state.lastRoutedModelBySession
|
|
7212
|
-
? Object.fromEntries(
|
|
7213
|
-
Object.entries(state.lastRoutedModelBySession).map(([key, model]) => [key, { ...model }]),
|
|
7214
|
-
)
|
|
7215
|
-
: undefined;
|
|
7216
|
-
return { statsByModel, totalsByModel, legacyFamily, lastRoutedModelBySession };
|
|
7217
|
-
}
|
|
7218
|
-
|
|
7219
|
-
function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
7220
|
-
const state = snapshotCacheStatsState();
|
|
7221
|
-
const sessionHash = currentSessionHashSet ? currentSessionHash : undefined;
|
|
7222
|
-
const deleteModelKeys = Array.from(pendingDeletedTotalModelKeys);
|
|
7223
|
-
const replaceTotals = replacePersistedTotalsOnNextWrite;
|
|
7224
|
-
// This queued write owns the current intents. Later mutations can enqueue
|
|
7225
|
-
// independent intents without an earlier completion clearing them.
|
|
7226
|
-
pendingDeletedTotalModelKeys.clear();
|
|
7227
|
-
replacePersistedTotalsOnNextWrite = false;
|
|
7228
|
-
|
|
7749
|
+
function persistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
|
|
7750
|
+
const shard = buildCurrentStatsShard(lifecycleState);
|
|
7229
7751
|
return enqueuePersist(async () => {
|
|
7230
|
-
const recoverFromEarlierFailure = forceReplaceTotalsAfterFailure;
|
|
7231
7752
|
try {
|
|
7232
|
-
await
|
|
7233
|
-
deleteModelKeys,
|
|
7234
|
-
replaceTotals: replaceTotals || recoverFromEarlierFailure,
|
|
7235
|
-
});
|
|
7236
|
-
if (recoverFromEarlierFailure) forceReplaceTotalsAfterFailure = false;
|
|
7753
|
+
await writeStatsShardV7(instanceShardPath, shard);
|
|
7237
7754
|
} catch (error) {
|
|
7238
|
-
|
|
7239
|
-
// reset/delete cannot merge stale persisted counters back into state.
|
|
7240
|
-
forceReplaceTotalsAfterFailure = true;
|
|
7241
|
-
console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
|
|
7755
|
+
console.warn(`${LOG_PREFIX}: failed to persist cache stats shard`, error);
|
|
7242
7756
|
if (!persistenceWarningShown) {
|
|
7243
7757
|
persistenceWarningShown = true;
|
|
7244
7758
|
ctx?.ui.notify(
|
|
@@ -7250,61 +7764,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
7250
7764
|
});
|
|
7251
7765
|
}
|
|
7252
7766
|
|
|
7253
|
-
/** Schedule a debounced persist. Coalesces rapid message_end writes
|
|
7254
|
-
* into a single disk write after PERSIST_DEBOUNCE_MS of silence.
|
|
7255
|
-
* In-memory stats remain instantly up-to-date for the footer; only
|
|
7256
|
-
* the on-disk persistence is delayed. */
|
|
7257
7767
|
function schedulePersistCacheStats(ctx?: ExtensionContext): void {
|
|
7258
7768
|
if (persistTimer !== null) clearTimeout(persistTimer);
|
|
7259
7769
|
persistTimer = setTimeout(() => {
|
|
7260
7770
|
persistTimer = null;
|
|
7261
|
-
persistCacheStats(ctx).catch((
|
|
7262
|
-
console.warn(`${LOG_PREFIX}: debounced persist failed`, err);
|
|
7263
|
-
});
|
|
7771
|
+
void persistCacheStats(ctx).then(() => refreshShardAggregate()).catch(() => undefined);
|
|
7264
7772
|
}, PERSIST_DEBOUNCE_MS);
|
|
7773
|
+
persistTimer.unref?.();
|
|
7265
7774
|
}
|
|
7266
7775
|
|
|
7267
|
-
|
|
7268
|
-
* Used on reload and day-rollover where immediate durability matters. */
|
|
7269
|
-
async function flushPersistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
7776
|
+
async function flushPersistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
|
|
7270
7777
|
if (persistTimer !== null) {
|
|
7271
7778
|
clearTimeout(persistTimer);
|
|
7272
7779
|
persistTimer = null;
|
|
7273
7780
|
}
|
|
7274
|
-
|
|
7781
|
+
// Adopt resets performed by another Pi process before capturing the shard
|
|
7782
|
+
// snapshot. Otherwise shutdown/command flush could rewrite stale local
|
|
7783
|
+
// counters under an old epoch even though aggregation already hid them.
|
|
7784
|
+
await refreshShardAggregate();
|
|
7785
|
+
await persistCacheStats(ctx, lifecycleState);
|
|
7786
|
+
await refreshShardAggregate();
|
|
7275
7787
|
}
|
|
7276
7788
|
|
|
7277
7789
|
async function rollOverStatsIfNeeded(ctx?: ExtensionContext): Promise<void> {
|
|
7278
7790
|
const day = currentLocalDay();
|
|
7279
7791
|
let changed = false;
|
|
7280
|
-
|
|
7281
|
-
// Roll over per-model entries.
|
|
7282
|
-
for (const key of Object.keys(cacheStatsByModel)) {
|
|
7283
|
-
const stats = cacheStatsByModel[key];
|
|
7284
|
-
if (stats && stats.day !== day) {
|
|
7285
|
-
cacheStatsByModel[key] = emptyCacheStats(day);
|
|
7286
|
-
changed = true;
|
|
7287
|
-
}
|
|
7288
|
-
}
|
|
7289
|
-
|
|
7290
|
-
// Roll over cumulative provider/model totals.
|
|
7291
7792
|
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7292
|
-
|
|
7293
|
-
if (stats && stats.day !== day) {
|
|
7793
|
+
if (cacheStatsProcessByModel[key]?.day !== day) {
|
|
7294
7794
|
cacheStatsProcessByModel[key] = emptyCacheStats(day);
|
|
7295
7795
|
changed = true;
|
|
7296
7796
|
}
|
|
7297
7797
|
}
|
|
7298
|
-
|
|
7299
|
-
for (const key of Object.keys(cacheStatsTotalsByModel)) {
|
|
7300
|
-
const stats = cacheStatsTotalsByModel[key];
|
|
7301
|
-
if (stats && stats.day !== day) {
|
|
7302
|
-
cacheStatsTotalsByModel[key] = emptyCacheStats(day);
|
|
7303
|
-
changed = true;
|
|
7304
|
-
}
|
|
7305
|
-
}
|
|
7306
|
-
|
|
7307
|
-
// Roll over legacy family entries.
|
|
7308
7798
|
for (const id of CACHE_PROVIDER_IDS) {
|
|
7309
7799
|
const stats = cacheStatsLegacyFamily[id];
|
|
7310
7800
|
if (stats && stats.day !== day) {
|
|
@@ -7312,57 +7802,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
7312
7802
|
changed = true;
|
|
7313
7803
|
}
|
|
7314
7804
|
}
|
|
7315
|
-
|
|
7316
7805
|
if (changed) {
|
|
7806
|
+
shardCreatedAt = Date.now();
|
|
7317
7807
|
lastStatusText = undefined;
|
|
7318
|
-
await
|
|
7808
|
+
await ensureCurrentEpochs();
|
|
7809
|
+
await flushPersistCacheStats(ctx);
|
|
7319
7810
|
}
|
|
7320
7811
|
}
|
|
7321
7812
|
|
|
7322
7813
|
async function restoreCacheStats(reason: string, ctx: ExtensionContext): Promise<void> {
|
|
7323
7814
|
syncSessionHash(ctx);
|
|
7324
|
-
|
|
7815
|
+
lastStatusText = undefined;
|
|
7816
|
+
cacheStatsProcessByModel = {};
|
|
7817
|
+
cacheStatsLegacyFamily = emptyAllCacheStats();
|
|
7818
|
+
modelEpochByKey.clear();
|
|
7819
|
+
modelApiByKey.clear();
|
|
7820
|
+
modelNameByKey.clear();
|
|
7821
|
+
currentGlobalEpoch = await readGlobalStatsEpoch();
|
|
7325
7822
|
if (reason === "reload") {
|
|
7326
|
-
// /reload: preserve session-scoped stats (same session hash).
|
|
7327
|
-
// Pi extension reload creates a fresh closure, so cacheStatsByModel
|
|
7328
|
-
// starts empty. Read persisted data and filter for current session.
|
|
7329
|
-
lastStatusText = undefined;
|
|
7330
7823
|
lastPromptIntegrityWarningAt = 0;
|
|
7331
7824
|
integrityNotificationShown = false;
|
|
7332
7825
|
clearRecentSamples();
|
|
7333
|
-
|
|
7334
|
-
const persisted = await readPersistedCacheStats();
|
|
7335
|
-
cacheStatsByModel = filterRestorableStatsForSession(
|
|
7336
|
-
persisted,
|
|
7337
|
-
currentSessionHashSet ? currentSessionHash : undefined,
|
|
7338
|
-
);
|
|
7339
|
-
cacheStatsProcessByModel = {};
|
|
7340
|
-
cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
|
|
7341
|
-
cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
|
|
7342
|
-
lastActualRoutedModel = currentSessionHashSet
|
|
7343
|
-
? persisted?.lastRoutedModelBySession?.[currentSessionHash]
|
|
7344
|
-
: undefined;
|
|
7345
|
-
|
|
7346
|
-
await rollOverStatsIfNeeded(ctx);
|
|
7347
|
-
return;
|
|
7348
7826
|
}
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
);
|
|
7358
|
-
cacheStatsProcessByModel = {};
|
|
7359
|
-
cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
|
|
7360
|
-
cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
|
|
7361
|
-
lastActualRoutedModel = currentSessionHashSet
|
|
7362
|
-
? persisted?.lastRoutedModelBySession?.[currentSessionHash]
|
|
7363
|
-
: undefined;
|
|
7364
|
-
lastStatusText = undefined;
|
|
7365
|
-
await rollOverStatsIfNeeded(ctx);
|
|
7827
|
+
await removeLegacyStatsFiles();
|
|
7828
|
+
await mkdir(SHARD_FILES_DIR, { recursive: true });
|
|
7829
|
+
await refreshShardAggregate();
|
|
7830
|
+
await flushPersistCacheStats(ctx);
|
|
7831
|
+
await maybeCleanupStatsShardsV7();
|
|
7832
|
+
// Maintenance may remove expired shard files after the initial refresh.
|
|
7833
|
+
// Re-scan before publishing so startup/footer command state cannot retain
|
|
7834
|
+
// counters from files that no longer exist.
|
|
7835
|
+
await refreshShardAggregate();
|
|
7366
7836
|
}
|
|
7367
7837
|
|
|
7368
7838
|
async function publishStatus(ctx: ExtensionContext, model: PiModel | undefined = ctx.model): Promise<void> {
|
|
@@ -7407,8 +7877,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7407
7877
|
}
|
|
7408
7878
|
|
|
7409
7879
|
if (adapter) {
|
|
7410
|
-
// Footer mode defaults to
|
|
7411
|
-
// current-
|
|
7880
|
+
// Footer mode defaults to current-session provider/model totals. Users may
|
|
7881
|
+
// select all-current-day or current-process counters through persistent
|
|
7882
|
+
// command config or the environment variable.
|
|
7412
7883
|
const stats = displayModel
|
|
7413
7884
|
? selectFooterStatsForModel(mode, sessionHash, cacheStatsByModel, cacheStatsTotalsByModel, displayModel, cacheStatsProcessByModel)
|
|
7414
7885
|
: undefined;
|
|
@@ -7492,13 +7963,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
7492
7963
|
pi.on("session_start", async (event, ctx) => {
|
|
7493
7964
|
if (runtimeOptimizerEnabled) requestLongCacheRetention();
|
|
7494
7965
|
await restoreCacheStats(event.reason, ctx);
|
|
7966
|
+
if (ctx.mode === "tui" && !shardWatcher) {
|
|
7967
|
+
try {
|
|
7968
|
+
shardWatcher = watch(SHARD_FILES_DIR, () => scheduleShardRefresh(ctx));
|
|
7969
|
+
shardWatcher.unref?.();
|
|
7970
|
+
} catch {
|
|
7971
|
+
// Lifecycle and explicit command refreshes remain authoritative.
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7974
|
+
await publishStatus(ctx);
|
|
7975
|
+
});
|
|
7976
|
+
|
|
7977
|
+
pi.on("tool_execution_end", async (_event, ctx) => {
|
|
7978
|
+
await refreshShardAggregate();
|
|
7979
|
+
await publishStatus(ctx);
|
|
7980
|
+
});
|
|
7981
|
+
|
|
7982
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
7983
|
+
await refreshShardAggregate();
|
|
7495
7984
|
await publishStatus(ctx);
|
|
7496
7985
|
});
|
|
7497
7986
|
|
|
7498
7987
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
7499
7988
|
try {
|
|
7500
|
-
await flushPersistCacheStats(ctx);
|
|
7989
|
+
await flushPersistCacheStats(ctx, "closed");
|
|
7501
7990
|
} finally {
|
|
7991
|
+
if (shardRefreshTimer !== null) clearTimeout(shardRefreshTimer);
|
|
7992
|
+
shardRefreshTimer = null;
|
|
7993
|
+
shardWatcher?.close();
|
|
7994
|
+
shardWatcher = undefined;
|
|
7502
7995
|
latestCacheHint = undefined;
|
|
7503
7996
|
delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
|
|
7504
7997
|
uninstallCacheHintsService();
|
|
@@ -7508,6 +8001,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7508
8001
|
|
|
7509
8002
|
pi.on("model_select", async (event, ctx) => {
|
|
7510
8003
|
if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(event.model, ctx) ?? event.model, ctx, warnedModels);
|
|
8004
|
+
await refreshShardAggregate();
|
|
7511
8005
|
await publishStatus(ctx, event.model);
|
|
7512
8006
|
});
|
|
7513
8007
|
|
|
@@ -7862,6 +8356,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
7862
8356
|
// Update session, process-local, and cumulative buckets for the actual
|
|
7863
8357
|
// routed model. The process bucket is intentionally never persisted.
|
|
7864
8358
|
if (statsModel) {
|
|
8359
|
+
const key = modelKey(statsModel);
|
|
8360
|
+
const latestGlobalEpoch = await readGlobalStatsEpoch();
|
|
8361
|
+
const latestModelEpoch = await readModelStatsEpoch(key);
|
|
8362
|
+
if (currentGlobalEpoch !== latestGlobalEpoch) {
|
|
8363
|
+
currentGlobalEpoch = latestGlobalEpoch;
|
|
8364
|
+
cacheStatsProcessByModel = {};
|
|
8365
|
+
modelEpochByKey.clear();
|
|
8366
|
+
}
|
|
8367
|
+
if (modelEpochByKey.get(key) !== latestModelEpoch) {
|
|
8368
|
+
delete cacheStatsProcessByModel[key];
|
|
8369
|
+
modelEpochByKey.set(key, latestModelEpoch);
|
|
8370
|
+
}
|
|
8371
|
+
modelApiByKey.set(key, statsModel.api ?? "");
|
|
8372
|
+
modelNameByKey.set(key, statsModel.name || statsModel.id);
|
|
7865
8373
|
const sk = sessionModelKey(statsModel);
|
|
7866
8374
|
addUsageToCacheStats(getOrCreateStatsByModelKey(sk), usage);
|
|
7867
8375
|
addUsageToCacheStats(getOrCreateProcessStatsForModel(statsModel), usage);
|
|
@@ -7871,6 +8379,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7871
8379
|
}
|
|
7872
8380
|
|
|
7873
8381
|
schedulePersistCacheStats(ctx);
|
|
8382
|
+
await refreshShardAggregate();
|
|
7874
8383
|
await publishStatus(ctx, statsModel);
|
|
7875
8384
|
});
|
|
7876
8385
|
|
|
@@ -7881,7 +8390,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7881
8390
|
// disable — disable runtime prompt/cache optimizations for this process
|
|
7882
8391
|
// doctor — show current model/provider/api/baseUrl/compat status
|
|
7883
8392
|
// with low-hit diagnosis
|
|
7884
|
-
// stats — show
|
|
8393
|
+
// stats — show detailed current-session models; `stats all` shows all local shards
|
|
7885
8394
|
// compat — show compat suggestion with file path
|
|
7886
8395
|
// config footer-mode total|session|process — persist footer mode override
|
|
7887
8396
|
// fix — auto-fix compat issues (writes models.json, requires UI)
|
|
@@ -7900,17 +8409,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7900
8409
|
|
|
7901
8410
|
if (subcommand === "enable") {
|
|
7902
8411
|
setRuntimeOptimizerEnabled(true);
|
|
7903
|
-
resetCurrentSessionStats();
|
|
8412
|
+
await resetCurrentSessionStats();
|
|
7904
8413
|
await flushPersistCacheStats(cmdCtx);
|
|
7905
8414
|
await publishStatus(cmdCtx, model);
|
|
7906
8415
|
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
|
|
7907
8416
|
} else if (subcommand === "disable") {
|
|
7908
8417
|
setRuntimeOptimizerEnabled(false);
|
|
7909
|
-
resetCurrentSessionStats();
|
|
8418
|
+
await resetCurrentSessionStats();
|
|
7910
8419
|
await flushPersistCacheStats(cmdCtx);
|
|
7911
8420
|
await publishStatus(cmdCtx, model);
|
|
7912
8421
|
cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process. Local footer stats were reset and will keep collecting while disabled for comparison.\n${formatOptimizerRuntimeMode()}`, "warning");
|
|
7913
8422
|
} else if (subcommand === "doctor") {
|
|
8423
|
+
await refreshShardAggregate();
|
|
7914
8424
|
if (!model) {
|
|
7915
8425
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
7916
8426
|
return;
|
|
@@ -7926,16 +8436,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7926
8436
|
: diagnosis;
|
|
7927
8437
|
cmdCtx.ui.notify(fullDiagnosis, "info");
|
|
7928
8438
|
} else if (subcommand === "stats") {
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
8439
|
+
const aggregate = await refreshShardAggregate();
|
|
8440
|
+
const statsMode = commandParts[1];
|
|
8441
|
+
if (statsMode === "all") {
|
|
8442
|
+
cmdCtx.ui.notify(buildAllStatsOutput(aggregate), "info");
|
|
8443
|
+
} else if (statsMode === "contributors") {
|
|
8444
|
+
cmdCtx.ui.notify(buildContributorsStatsOutput(aggregate, model, currentSessionHashSet ? currentSessionHash : undefined), "info");
|
|
8445
|
+
} else if (statsMode) {
|
|
8446
|
+
cmdCtx.ui.notify("Usage: /cache-optimizer stats [all|contributors]", "info");
|
|
8447
|
+
} else {
|
|
8448
|
+
const sessionModels = currentSessionHashSet ? aggregate.bySession[currentSessionHash] ?? {} : {};
|
|
8449
|
+
cmdCtx.ui.notify(buildSessionStatsOutput(sessionModels, model, aggregate.modelRefsByKey), "info");
|
|
7932
8450
|
}
|
|
7933
|
-
const adapter = selectAdapterForModel(model);
|
|
7934
|
-
const sk = model ? sessionModelKey(model) : undefined;
|
|
7935
|
-
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
7936
|
-
const samples = sk ? getRecentSamples(sk) : [];
|
|
7937
|
-
const output = buildStatsOutput(model, adapter, statsState, samples);
|
|
7938
|
-
cmdCtx.ui.notify(output, "info");
|
|
7939
8451
|
} else if (subcommand === "config") {
|
|
7940
8452
|
const configKey = commandParts[1];
|
|
7941
8453
|
const requestedMode = commandParts[2];
|
|
@@ -7998,7 +8510,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7998
8510
|
// Reset local footer stats for the effective active model. If the
|
|
7999
8511
|
// selected model is a virtual router and the protocol exposes a live
|
|
8000
8512
|
// route, this clears the real upstream bucket, not the router shell.
|
|
8001
|
-
resetStatsForModel(model);
|
|
8513
|
+
await resetStatsForModel(model);
|
|
8002
8514
|
|
|
8003
8515
|
// Persist immediately.
|
|
8004
8516
|
await flushPersistCacheStats(cmdCtx);
|
|
@@ -8348,7 +8860,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8348
8860
|
"Enable — Turn on runtime optimizations",
|
|
8349
8861
|
"Disable — Turn off runtime optimizations",
|
|
8350
8862
|
"Doctor — Show cache configuration",
|
|
8351
|
-
"Stats — Show
|
|
8863
|
+
"Stats — Show current-session model statistics",
|
|
8352
8864
|
"Compat — Show compat suggestion",
|
|
8353
8865
|
"Fix — Auto-fix compat issues (writes models.json)",
|
|
8354
8866
|
"Footer mode — Choose total, session, or process stats",
|
|
@@ -8369,12 +8881,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
8369
8881
|
} else if (choice === menuOptions[5]) {
|
|
8370
8882
|
await handleCacheOptimizerCommand("fix", cmdCtx);
|
|
8371
8883
|
} else if (choice === menuOptions[6]) {
|
|
8372
|
-
const modeOptions = ["
|
|
8884
|
+
const modeOptions = ["session — Current Pi conversation session (default)", "total — All local sessions today", "process — Current extension instance only", "Cancel"];
|
|
8373
8885
|
const modeChoice = await cmdCtx.ui.select("Footer cache stats mode", modeOptions);
|
|
8374
8886
|
const nextMode = modeChoice === modeOptions[0]
|
|
8375
|
-
? "
|
|
8887
|
+
? "session"
|
|
8376
8888
|
: modeChoice === modeOptions[1]
|
|
8377
|
-
? "
|
|
8889
|
+
? "total"
|
|
8378
8890
|
: modeChoice === modeOptions[2]
|
|
8379
8891
|
? "process"
|
|
8380
8892
|
: undefined;
|
|
@@ -8392,7 +8904,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
8392
8904
|
diagnosis.push(" enable — Enable prompt/cache optimizations for this Pi process");
|
|
8393
8905
|
diagnosis.push(" disable — Disable prompt/cache optimizations for this Pi process");
|
|
8394
8906
|
diagnosis.push(" doctor — Show current model/provider/api/baseUrl/compat and low-hit diagnosis");
|
|
8395
|
-
diagnosis.push(" stats — Show
|
|
8907
|
+
diagnosis.push(" stats — Show detailed statistics for every model in the current session");
|
|
8908
|
+
diagnosis.push(" stats all — Show detailed totals for every model across all local sessions");
|
|
8909
|
+
diagnosis.push(" stats contributors — Show per-session contributors for the active model");
|
|
8396
8910
|
diagnosis.push(" compat — Show compat suggestion with edit location");
|
|
8397
8911
|
diagnosis.push(" config footer-mode total|session|process — Persist the footer stats mode");
|
|
8398
8912
|
diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
|