pi-cache-optimizer 2.8.2 → 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.
Files changed (4) hide show
  1. package/README.md +24 -18
  2. package/README.zh-CN.md +24 -17
  3. package/index.ts +915 -435
  4. package/package.json +4 -4
package/index.ts CHANGED
@@ -1,9 +1,15 @@
1
- import { createHash } from "node:crypto";
2
- import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
- import { 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
- import { getAgentDir, type BuildSystemPromptOptions, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ getAgentDir,
8
+ type BuildSystemPromptOptions,
9
+ type ExtensionAPI,
10
+ type ExtensionCommandContext,
11
+ type ExtensionContext,
12
+ } from "@earendil-works/pi-coding-agent";
7
13
 
8
14
  type MutableEnv = Record<string, string | undefined>;
9
15
 
@@ -85,6 +91,7 @@ const STARTUP_CACHE_RETENTION_ENV = getOrCaptureCacheRetentionBaseline();
85
91
  requestLongCacheRetention();
86
92
 
87
93
  type PiModel = NonNullable<ExtensionContext["model"]>;
94
+ type ModelIdentity = Pick<PiModel, "provider" | "id">;
88
95
  type UnknownRecord = Record<string, unknown>;
89
96
  type CacheProviderId = "deepseek" | "openai" | "claude" | "gemini";
90
97
 
@@ -95,7 +102,19 @@ const STATUS_KEY = "pi-cache-stats";
95
102
  const STATE_DIR = getAgentDir();
96
103
  const STATE_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-stats.json");
97
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");
98
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;
99
118
  const CACHE_PROVIDER_IDS: CacheProviderId[] = ["deepseek", "openai", "claude", "gemini"];
100
119
  const OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_OPENAI_CACHE_KEY";
101
120
  const NO_OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_NO_OPENAI_CACHE_KEY";
@@ -372,6 +391,46 @@ type OptimizedSystemPrompt = {
372
391
  * Contains only numeric counters and booleans — never message content, prompts,
373
392
  * payloads, headers, API keys, or model outputs.
374
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
+
375
434
  type CacheUsageSample = {
376
435
  timestamp: number;
377
436
  hit: boolean;
@@ -1164,6 +1223,7 @@ const CACHE_OPTIMIZER_COMMANDS = [
1164
1223
  ] as const;
1165
1224
  const CACHE_OPTIMIZER_CONFIG_ARGUMENTS = ["footer-mode"] as const;
1166
1225
  const CACHE_OPTIMIZER_FOOTER_MODES = ["total", "session", "process"] as const;
1226
+ const CACHE_OPTIMIZER_STATS_ARGUMENTS = ["all", "contributors"] as const;
1167
1227
 
1168
1228
  function filterCommandCompletionItems(
1169
1229
  values: readonly string[],
@@ -1202,9 +1262,18 @@ function getCacheOptimizerArgumentCompletions(argumentPrefix: string): CommandCo
1202
1262
  if (subcommandPrefix === "config") {
1203
1263
  return filterCommandCompletionItems(CACHE_OPTIMIZER_CONFIG_ARGUMENTS, "", "config");
1204
1264
  }
1265
+ if (subcommandPrefix === "stats") {
1266
+ return filterCommandCompletionItems(CACHE_OPTIMIZER_STATS_ARGUMENTS, "", "stats");
1267
+ }
1205
1268
  return filterCommandCompletionItems(CACHE_OPTIMIZER_COMMANDS, parts[0]);
1206
1269
  }
1207
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
+
1208
1277
  if (parts[0].toLowerCase() !== "config") return null;
1209
1278
 
1210
1279
  if (parts.length === 2) {
@@ -1260,7 +1329,7 @@ function resolveFooterStatsMode(
1260
1329
  ): { mode: FooterStatsMode; source: FooterStatsModeSource } {
1261
1330
  if (configuredMode) return { mode: configuredMode, source: "config" };
1262
1331
  const envMode = parseFooterStatsMode(env[FOOTER_MODE_ENV]);
1263
- return envMode ? { mode: envMode, source: "env" } : { mode: "total", source: "default" };
1332
+ return envMode ? { mode: envMode, source: "env" } : { mode: "session", source: "default" };
1264
1333
  }
1265
1334
 
1266
1335
  function footerStatsMode(
@@ -2088,7 +2157,7 @@ function isAyaLikeAssistantMessage(message: unknown, model: PiModel | undefined)
2088
2157
 
2089
2158
  // ── Model key ──────────────────────────────────────────────────────
2090
2159
 
2091
- function modelKey(model: PiModel): string {
2160
+ function modelKey(model: ModelIdentity): string {
2092
2161
  return `${model.provider}/${model.id}`;
2093
2162
  }
2094
2163
 
@@ -2461,12 +2530,8 @@ function getPromptCacheRetentionUnsupportedHint(): string {
2461
2530
  return "If this channel returns `400 Unsupported parameter: prompt_cache_retention`, remove/avoid `supportsLongCacheRetention`; this extension does not write that field directly, but Pi may send it when long retention is requested and compat says the proxy supports it.";
2462
2531
  }
2463
2532
 
2464
- function hasPromptCacheRetentionUnsupportedSignal(headers: Record<string, string> | undefined): boolean {
2465
- if (!headers) return false;
2466
-
2467
- const normalized = Object.entries(headers)
2468
- .map(([key, value]) => `${lower(key)}: ${lower(value)}`)
2469
- .join("\n");
2533
+ function hasPromptCacheRetentionUnsupportedText(value: unknown): boolean {
2534
+ const normalized = lower(value);
2470
2535
  if (!normalized.includes("prompt_cache_retention")) return false;
2471
2536
 
2472
2537
  return [
@@ -2478,10 +2543,24 @@ function hasPromptCacheRetentionUnsupportedSignal(headers: Record<string, string
2478
2543
  "extra inputs",
2479
2544
  "not permitted",
2480
2545
  "unrecognized",
2481
- "bad request",
2482
2546
  ].some((needle) => normalized.includes(needle));
2483
2547
  }
2484
2548
 
2549
+ function hasPromptCacheRetentionUnsupportedSignal(headers: Record<string, string> | undefined): boolean {
2550
+ if (!headers) return false;
2551
+ return hasPromptCacheRetentionUnsupportedText(
2552
+ Object.entries(headers)
2553
+ .map(([key, value]) => `${key}: ${value}`)
2554
+ .join("\n"),
2555
+ );
2556
+ }
2557
+
2558
+ function hasPromptCacheRetentionUnsupportedErrorMessage(message: unknown): boolean {
2559
+ const record = getAssistantRecord(message);
2560
+ return record?.stopReason === "error" &&
2561
+ hasPromptCacheRetentionUnsupportedText(record.errorMessage);
2562
+ }
2563
+
2485
2564
  type CompatAdvicePlacement = {
2486
2565
  providerLabel?: string;
2487
2566
  modelId?: string;
@@ -3823,6 +3902,87 @@ function formatRecentTrendSummary(samples: CacheUsageSample[], maxCount: number)
3823
3902
  /**
3824
3903
  * Build the output for `/cache-optimizer stats`.
3825
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
+
3826
3986
  function buildStatsOutput(model: PiModel | undefined, adapter: CacheProviderAdapter | undefined, stats: CacheStats | undefined, recentSamples: CacheUsageSample[]): string {
3827
3987
  const lines: string[] = [];
3828
3988
 
@@ -4231,8 +4391,8 @@ function filterRestorableStatsForSession(
4231
4391
  * `restoreCacheStats` can migrate them on the next load before the session
4232
4392
  * id is known.
4233
4393
  *
4234
- * Pure function (no I/O) — suitable for unit tests without touching the real
4235
- * state file at `~/.pi/agent/pi-cache-optimizer-stats.json`.
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.
4236
4396
  */
4237
4397
  function mergeCacheSessions(
4238
4398
  existingSessions: Record<string, Record<string, CacheStats>>,
@@ -4392,7 +4552,304 @@ async function writePersistedCacheStats(
4392
4552
  await rename(tempPath, STATE_FILE_PATH);
4393
4553
  }
4394
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
+ }
4395
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
+ }
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
+ }
4396
4853
 
4397
4854
  function isCompatCheckApplicable(model: PiModel): boolean {
4398
4855
  return isOpenAICompatibleProxyApi(model.api) && !isOfficialOpenAIBaseUrl(model) && !isPiBuiltInLlamaCppModel(model);
@@ -4405,6 +4862,16 @@ function isPromptCacheRetention400Applicable(model: PiModel): boolean {
4405
4862
  getCompat(model).supportsLongCacheRetention === true;
4406
4863
  }
4407
4864
 
4865
+ function isExplicitPromptCacheRetentionUnsupportedApplicable(model: PiModel): boolean {
4866
+ // A finalized assistant error with an explicit unsupported-parameter signal
4867
+ // proves that prompt_cache_retention reached this provider/model. Do not
4868
+ // require compat inherited from the active fallback model: router shells may
4869
+ // have no upstream compat metadata when no live routing registry is present.
4870
+ return isOpenAICompatibleApi(model.api) &&
4871
+ !isOfficialOpenAIBaseUrl(model) &&
4872
+ !isPiBuiltInLlamaCppModel(model);
4873
+ }
4874
+
4408
4875
  /**
4409
4876
  * Whether the 403 sendSessionAffinityHeaders diagnostic applies to a model.
4410
4877
  *
@@ -6528,18 +6995,121 @@ function formatCompatKeysForInsertion(compatKeys: Record<string, unknown>): stri
6528
6995
  }
6529
6996
 
6530
6997
  /**
6531
- * Generate the timestamp string for backup filename.
6532
- * Format: YYYYMMDDTHHMMSSZ (UTC)
6998
+ * Generate a unique UTC timestamp component for backup filenames.
6999
+ * Milliseconds, process id, and an in-process sequence prevent collisions.
6533
7000
  */
6534
- function backupTimestamp(): string {
6535
- const now = new Date();
7001
+ let backupSequence = 0;
7002
+
7003
+ function backupTimestamp(now: Date = new Date()): string {
6536
7004
  const y = now.getUTCFullYear();
6537
- const m = String(now.getUTCMonth() + 1).padStart(2, '0');
6538
- const d = String(now.getUTCDate()).padStart(2, '0');
6539
- const h = String(now.getUTCHours()).padStart(2, '0');
6540
- const min = String(now.getUTCMinutes()).padStart(2, '0');
6541
- const s = String(now.getUTCSeconds()).padStart(2, '0');
6542
- return `${y}${m}${d}T${h}${min}${s}Z`;
7005
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
7006
+ const d = String(now.getUTCDate()).padStart(2, "0");
7007
+ const h = String(now.getUTCHours()).padStart(2, "0");
7008
+ const min = String(now.getUTCMinutes()).padStart(2, "0");
7009
+ const s = String(now.getUTCSeconds()).padStart(2, "0");
7010
+ const ms = String(now.getUTCMilliseconds()).padStart(3, "0");
7011
+ return `${y}${m}${d}T${h}${min}${s}${ms}Z-${process.pid}-${backupSequence++}`;
7012
+ }
7013
+
7014
+ function uniqueTempPath(targetPath: string, purpose: string): string {
7015
+ return `${targetPath}.${process.pid}.${Date.now()}.${backupSequence++}.${purpose}.tmp`;
7016
+ }
7017
+
7018
+ async function atomicReplaceTextFilePreservingMode(
7019
+ targetPath: string,
7020
+ content: string,
7021
+ mode: number,
7022
+ purpose: string,
7023
+ ): Promise<void> {
7024
+ const tempPath = uniqueTempPath(targetPath, purpose);
7025
+ try {
7026
+ await writeFile(tempPath, content, { encoding: "utf8", mode });
7027
+ await chmod(tempPath, mode);
7028
+ await rename(tempPath, targetPath);
7029
+ } catch (error) {
7030
+ try {
7031
+ await unlink(tempPath);
7032
+ } catch (cleanupError) {
7033
+ if (getErrorCode(cleanupError) !== "ENOENT") {
7034
+ console.warn(`${LOG_PREFIX}: failed to remove temporary models.json file`, cleanupError);
7035
+ }
7036
+ }
7037
+ throw error;
7038
+ }
7039
+ }
7040
+
7041
+ async function atomicRestoreFileFromBackup(
7042
+ backupPath: string,
7043
+ targetPath: string,
7044
+ mode: number,
7045
+ ): Promise<void> {
7046
+ const tempPath = uniqueTempPath(targetPath, "restore");
7047
+ try {
7048
+ await copyFile(backupPath, tempPath, fsConstants.COPYFILE_EXCL);
7049
+ await chmod(tempPath, mode);
7050
+ await rename(tempPath, targetPath);
7051
+ } catch (error) {
7052
+ try {
7053
+ await unlink(tempPath);
7054
+ } catch (cleanupError) {
7055
+ if (getErrorCode(cleanupError) !== "ENOENT") {
7056
+ console.warn(`${LOG_PREFIX}: failed to remove temporary restore file`, cleanupError);
7057
+ }
7058
+ }
7059
+ throw error;
7060
+ }
7061
+ }
7062
+
7063
+ type ModelsJsonFixTransactionResult =
7064
+ | { ok: true }
7065
+ | { ok: false; postCheckError: string };
7066
+
7067
+ async function applyModelsJsonFixTransaction(
7068
+ modifiedText: string,
7069
+ backupPath: string,
7070
+ validateWrittenText: (writtenText: string) => string | null,
7071
+ ): Promise<ModelsJsonFixTransactionResult> {
7072
+ const originalMode = (await stat(MODELS_JSON_PATH)).mode & 0o7777;
7073
+ await copyFile(MODELS_JSON_PATH, backupPath, fsConstants.COPYFILE_EXCL);
7074
+ await chmod(backupPath, originalMode);
7075
+
7076
+ let targetReplaced = false;
7077
+ try {
7078
+ await atomicReplaceTextFilePreservingMode(
7079
+ MODELS_JSON_PATH,
7080
+ modifiedText,
7081
+ originalMode,
7082
+ "fix",
7083
+ );
7084
+ targetReplaced = true;
7085
+
7086
+ const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
7087
+ const postCheckError = validateWrittenText(writtenText);
7088
+ const writtenMode = (await stat(MODELS_JSON_PATH)).mode & 0o7777;
7089
+ const effectiveError = postCheckError ?? (
7090
+ writtenMode === originalMode
7091
+ ? null
7092
+ : `models.json access mode changed from ${originalMode.toString(8)} to ${writtenMode.toString(8)}`
7093
+ );
7094
+ if (effectiveError !== null) {
7095
+ await atomicRestoreFileFromBackup(backupPath, MODELS_JSON_PATH, originalMode);
7096
+ return { ok: false, postCheckError: effectiveError };
7097
+ }
7098
+
7099
+ return { ok: true };
7100
+ } catch (error) {
7101
+ if (targetReplaced) {
7102
+ try {
7103
+ await atomicRestoreFileFromBackup(backupPath, MODELS_JSON_PATH, originalMode);
7104
+ } catch (restoreError) {
7105
+ throw new AggregateError(
7106
+ [error, restoreError],
7107
+ "models.json fix failed and the atomic backup restore also failed",
7108
+ );
7109
+ }
7110
+ }
7111
+ throw error;
7112
+ }
6543
7113
  }
6544
7114
 
6545
7115
  // Internal helpers exported only so the task verification script
@@ -6593,6 +7163,7 @@ export const __internals_for_tests = {
6593
7163
  isSessionAffinity403Applicable,
6594
7164
  isOpenAISdkHeader403Applicable,
6595
7165
  hasPromptCacheRetentionUnsupportedSignal,
7166
+ hasPromptCacheRetentionUnsupportedErrorMessage,
6596
7167
  // Non-GPT OpenAI-compatible model detection
6597
7168
  isKimiLikeModel,
6598
7169
  isKimiLikeAssistantMessage,
@@ -6745,6 +7316,10 @@ export const __internals_for_tests = {
6745
7316
  // Recent sample / stats output / diagnosis helpers
6746
7317
  MAX_RECENT_SAMPLES,
6747
7318
  buildStatsOutput,
7319
+ buildSessionStatsOutput,
7320
+ buildAllStatsOutput,
7321
+ buildContributorsStatsOutput,
7322
+ formatCompactStats,
6748
7323
  buildLowHitDiagnosis,
6749
7324
  formatRecentTrendSummary,
6750
7325
  formatHitRatio,
@@ -6792,8 +7367,23 @@ export const __internals_for_tests = {
6792
7367
  createSerializedAsyncRunner,
6793
7368
  writePersistedCacheStats,
6794
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,
6795
7382
  STATE_FILE_PATH,
6796
7383
  LEGACY_STATE_FILE_PATH,
7384
+ SHARD_STATE_DIR,
7385
+ SHARD_FILES_DIR,
7386
+ SHARD_GLOBAL_EPOCH_PATH,
6797
7387
  STATE_DIR,
6798
7388
  // JSONC surgical edit helpers
6799
7389
  MODELS_JSON_PATH,
@@ -6816,6 +7406,9 @@ export const __internals_for_tests = {
6816
7406
  deepEqualIgnoringKeys,
6817
7407
  formatCompatKeysForInsertion,
6818
7408
  backupTimestamp,
7409
+ atomicReplaceTextFilePreservingMode,
7410
+ atomicRestoreFileFromBackup,
7411
+ applyModelsJsonFixTransaction,
6819
7412
  // Fix suggestion builder
6820
7413
  buildFixSuggestion,
6821
7414
  // Adaptive thinking compat helpers
@@ -6847,17 +7440,25 @@ export default function (pi: ExtensionAPI) {
6847
7440
  let lastStatusText: string | undefined;
6848
7441
  let persistenceWarningShown = false;
6849
7442
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
7443
+ let shardRefreshTimer: ReturnType<typeof setTimeout> | null = null;
7444
+ let shardWatcher: ReturnType<typeof watch> | undefined;
6850
7445
  const enqueuePersist = createSerializedAsyncRunner();
6851
- let replacePersistedTotalsOnNextWrite = false;
6852
- let forceReplaceTotalsAfterFailure = false;
6853
- const pendingDeletedTotalModelKeys = new Set<string>();
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");
6854
7453
  let integrityNotificationShown = false;
6855
7454
  let currentSessionId = "";
6856
7455
  let currentSessionHash = "";
6857
7456
  let currentSessionHashSet = false;
6858
7457
  let lastActualRoutedModel: PersistedRoutedModelRef | undefined;
6859
7458
  let latestCacheHint: PiCacheHintSnapshot | undefined;
7459
+ let shardCreatedAt = Date.now();
6860
7460
  const PERSIST_DEBOUNCE_MS = 2000;
7461
+ const SHARD_REFRESH_DEBOUNCE_MS = 250;
6861
7462
 
6862
7463
  function buildObservedRuntimeFixSuggestion(model: PiModel): FixSuggestion | undefined {
6863
7464
  const key = modelKey(model);
@@ -6972,17 +7573,102 @@ export default function (pi: ExtensionAPI) {
6972
7573
  recentSamplesByModelKey.clear();
6973
7574
  }
6974
7575
 
6975
- function getCacheStatsState(): CacheStatsState {
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
+ }
6976
7642
  return {
6977
- statsByModel: cacheStatsByModel,
6978
- totalsByModel: cacheStatsTotalsByModel,
6979
- legacyFamily: cacheStatsLegacyFamily,
6980
- ...(currentSessionHashSet && lastActualRoutedModel
6981
- ? { lastRoutedModelBySession: { [currentSessionHash]: lastActualRoutedModel } }
6982
- : {}),
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 } } : {}),
6983
7658
  };
6984
7659
  }
6985
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
+
6986
7672
  /** Look up visible cumulative stats for a model, falling back to legacy family. */
6987
7673
  function getStatsForModel(model: PiModel | undefined, adapter: CacheProviderAdapter): CacheStats {
6988
7674
  if (model) {
@@ -7032,79 +7718,41 @@ export default function (pi: ExtensionAPI) {
7032
7718
  return created;
7033
7719
  }
7034
7720
 
7035
- function resetStatsForModel(model: PiModel): void {
7721
+ async function resetStatsForModel(model: PiModel): Promise<void> {
7036
7722
  const displayKey = modelKey(model);
7723
+ const nextEpoch = await advanceModelStatsEpoch(displayKey);
7724
+ modelEpochByKey.set(displayKey, nextEpoch);
7725
+ delete cacheStatsProcessByModel[displayKey];
7726
+ delete cacheStatsTotalsByModel[displayKey];
7037
7727
  for (const key of Object.keys(cacheStatsByModel)) {
7038
7728
  if (modelKeyFromSessionScoped(key) === displayKey) delete cacheStatsByModel[key];
7039
7729
  }
7040
- delete cacheStatsTotalsByModel[displayKey];
7041
- delete cacheStatsProcessByModel[displayKey];
7042
- pendingDeletedTotalModelKeys.add(displayKey);
7043
7730
  for (const key of Array.from(recentSamplesByModelKey.keys())) {
7044
7731
  if (modelKeyFromSessionScoped(key) === displayKey) recentSamplesByModelKey.delete(key);
7045
7732
  }
7046
7733
  lastStatusText = undefined;
7047
7734
  }
7048
7735
 
7049
- function resetCurrentSessionStats(): void {
7050
- const prefix = `${currentSessionHash || "_nosession"}:`;
7051
- for (const key of Object.keys(cacheStatsByModel)) {
7052
- if (key.startsWith(prefix)) delete cacheStatsByModel[key];
7053
- }
7054
- cacheStatsProcessByModel = {};
7736
+ async function resetCurrentSessionStats(): Promise<void> {
7737
+ currentGlobalEpoch = await advanceGlobalStatsEpoch();
7738
+ cacheStatsByModel = {};
7055
7739
  cacheStatsTotalsByModel = {};
7056
- replacePersistedTotalsOnNextWrite = true;
7057
- for (const key of Array.from(recentSamplesByModelKey.keys())) {
7058
- if (key.startsWith(prefix)) recentSamplesByModelKey.delete(key);
7059
- }
7740
+ cacheStatsProcessByModel = {};
7741
+ modelEpochByKey.clear();
7742
+ modelApiByKey.clear();
7743
+ modelNameByKey.clear();
7744
+ clearRecentSamples();
7060
7745
  lastActualRoutedModel = undefined;
7061
7746
  lastStatusText = undefined;
7062
7747
  }
7063
7748
 
7064
- function snapshotCacheStatsState(): CacheStatsState {
7065
- const state = getCacheStatsState();
7066
- const statsByModel = Object.fromEntries(
7067
- Object.entries(state.statsByModel).map(([key, stats]) => [key, { ...stats }]),
7068
- );
7069
- const totalsByModel = Object.fromEntries(
7070
- Object.entries(state.totalsByModel).map(([key, stats]) => [key, { ...stats }]),
7071
- );
7072
- const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
7073
- for (const id of CACHE_PROVIDER_IDS) {
7074
- const stats = state.legacyFamily[id];
7075
- if (stats) legacyFamily[id] = { ...stats };
7076
- }
7077
- const lastRoutedModelBySession = state.lastRoutedModelBySession
7078
- ? Object.fromEntries(
7079
- Object.entries(state.lastRoutedModelBySession).map(([key, model]) => [key, { ...model }]),
7080
- )
7081
- : undefined;
7082
- return { statsByModel, totalsByModel, legacyFamily, lastRoutedModelBySession };
7083
- }
7084
-
7085
- function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
7086
- const state = snapshotCacheStatsState();
7087
- const sessionHash = currentSessionHashSet ? currentSessionHash : undefined;
7088
- const deleteModelKeys = Array.from(pendingDeletedTotalModelKeys);
7089
- const replaceTotals = replacePersistedTotalsOnNextWrite;
7090
- // This queued write owns the current intents. Later mutations can enqueue
7091
- // independent intents without an earlier completion clearing them.
7092
- pendingDeletedTotalModelKeys.clear();
7093
- replacePersistedTotalsOnNextWrite = false;
7094
-
7749
+ function persistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
7750
+ const shard = buildCurrentStatsShard(lifecycleState);
7095
7751
  return enqueuePersist(async () => {
7096
- const recoverFromEarlierFailure = forceReplaceTotalsAfterFailure;
7097
7752
  try {
7098
- await writePersistedCacheStats(state, sessionHash, {
7099
- deleteModelKeys,
7100
- replaceTotals: replaceTotals || recoverFromEarlierFailure,
7101
- });
7102
- if (recoverFromEarlierFailure) forceReplaceTotalsAfterFailure = false;
7753
+ await writeStatsShardV7(instanceShardPath, shard);
7103
7754
  } catch (error) {
7104
- // The next queued write must replace totals authoritatively so a failed
7105
- // reset/delete cannot merge stale persisted counters back into state.
7106
- forceReplaceTotalsAfterFailure = true;
7107
- console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
7755
+ console.warn(`${LOG_PREFIX}: failed to persist cache stats shard`, error);
7108
7756
  if (!persistenceWarningShown) {
7109
7757
  persistenceWarningShown = true;
7110
7758
  ctx?.ui.notify(
@@ -7116,61 +7764,37 @@ export default function (pi: ExtensionAPI) {
7116
7764
  });
7117
7765
  }
7118
7766
 
7119
- /** Schedule a debounced persist. Coalesces rapid message_end writes
7120
- * into a single disk write after PERSIST_DEBOUNCE_MS of silence.
7121
- * In-memory stats remain instantly up-to-date for the footer; only
7122
- * the on-disk persistence is delayed. */
7123
7767
  function schedulePersistCacheStats(ctx?: ExtensionContext): void {
7124
7768
  if (persistTimer !== null) clearTimeout(persistTimer);
7125
7769
  persistTimer = setTimeout(() => {
7126
7770
  persistTimer = null;
7127
- persistCacheStats(ctx).catch((err) => {
7128
- console.warn(`${LOG_PREFIX}: debounced persist failed`, err);
7129
- });
7771
+ void persistCacheStats(ctx).then(() => refreshShardAggregate()).catch(() => undefined);
7130
7772
  }, PERSIST_DEBOUNCE_MS);
7773
+ persistTimer.unref?.();
7131
7774
  }
7132
7775
 
7133
- /** Flush any pending debounced persist immediately (cancels timer + writes).
7134
- * Used on reload and day-rollover where immediate durability matters. */
7135
- async function flushPersistCacheStats(ctx?: ExtensionContext): Promise<void> {
7776
+ async function flushPersistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
7136
7777
  if (persistTimer !== null) {
7137
7778
  clearTimeout(persistTimer);
7138
7779
  persistTimer = null;
7139
7780
  }
7140
- await persistCacheStats(ctx);
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();
7141
7787
  }
7142
7788
 
7143
7789
  async function rollOverStatsIfNeeded(ctx?: ExtensionContext): Promise<void> {
7144
7790
  const day = currentLocalDay();
7145
7791
  let changed = false;
7146
-
7147
- // Roll over per-model entries.
7148
- for (const key of Object.keys(cacheStatsByModel)) {
7149
- const stats = cacheStatsByModel[key];
7150
- if (stats && stats.day !== day) {
7151
- cacheStatsByModel[key] = emptyCacheStats(day);
7152
- changed = true;
7153
- }
7154
- }
7155
-
7156
- // Roll over cumulative provider/model totals.
7157
7792
  for (const key of Object.keys(cacheStatsProcessByModel)) {
7158
- const stats = cacheStatsProcessByModel[key];
7159
- if (stats && stats.day !== day) {
7793
+ if (cacheStatsProcessByModel[key]?.day !== day) {
7160
7794
  cacheStatsProcessByModel[key] = emptyCacheStats(day);
7161
7795
  changed = true;
7162
7796
  }
7163
7797
  }
7164
-
7165
- for (const key of Object.keys(cacheStatsTotalsByModel)) {
7166
- const stats = cacheStatsTotalsByModel[key];
7167
- if (stats && stats.day !== day) {
7168
- cacheStatsTotalsByModel[key] = emptyCacheStats(day);
7169
- changed = true;
7170
- }
7171
- }
7172
-
7173
- // Roll over legacy family entries.
7174
7798
  for (const id of CACHE_PROVIDER_IDS) {
7175
7799
  const stats = cacheStatsLegacyFamily[id];
7176
7800
  if (stats && stats.day !== day) {
@@ -7178,57 +7802,37 @@ export default function (pi: ExtensionAPI) {
7178
7802
  changed = true;
7179
7803
  }
7180
7804
  }
7181
-
7182
7805
  if (changed) {
7806
+ shardCreatedAt = Date.now();
7183
7807
  lastStatusText = undefined;
7184
- await persistCacheStats(ctx);
7808
+ await ensureCurrentEpochs();
7809
+ await flushPersistCacheStats(ctx);
7185
7810
  }
7186
7811
  }
7187
7812
 
7188
7813
  async function restoreCacheStats(reason: string, ctx: ExtensionContext): Promise<void> {
7189
7814
  syncSessionHash(ctx);
7190
-
7815
+ lastStatusText = undefined;
7816
+ cacheStatsProcessByModel = {};
7817
+ cacheStatsLegacyFamily = emptyAllCacheStats();
7818
+ modelEpochByKey.clear();
7819
+ modelApiByKey.clear();
7820
+ modelNameByKey.clear();
7821
+ currentGlobalEpoch = await readGlobalStatsEpoch();
7191
7822
  if (reason === "reload") {
7192
- // /reload: preserve session-scoped stats (same session hash).
7193
- // Pi extension reload creates a fresh closure, so cacheStatsByModel
7194
- // starts empty. Read persisted data and filter for current session.
7195
- lastStatusText = undefined;
7196
7823
  lastPromptIntegrityWarningAt = 0;
7197
7824
  integrityNotificationShown = false;
7198
7825
  clearRecentSamples();
7199
-
7200
- const persisted = await readPersistedCacheStats();
7201
- cacheStatsByModel = filterRestorableStatsForSession(
7202
- persisted,
7203
- currentSessionHashSet ? currentSessionHash : undefined,
7204
- );
7205
- cacheStatsProcessByModel = {};
7206
- cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
7207
- cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
7208
- lastActualRoutedModel = currentSessionHashSet
7209
- ? persisted?.lastRoutedModelBySession?.[currentSessionHash]
7210
- : undefined;
7211
-
7212
- await rollOverStatsIfNeeded(ctx);
7213
- return;
7214
7826
  }
7215
-
7216
- // First load / process start: read persisted stats and filter for
7217
- // this session's entries. If the session hash is unavailable, start
7218
- // fresh instead of loading all persisted session buckets.
7219
- const persisted = await readPersistedCacheStats();
7220
- cacheStatsByModel = filterRestorableStatsForSession(
7221
- persisted,
7222
- currentSessionHashSet ? currentSessionHash : undefined,
7223
- );
7224
- cacheStatsProcessByModel = {};
7225
- cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
7226
- cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
7227
- lastActualRoutedModel = currentSessionHashSet
7228
- ? persisted?.lastRoutedModelBySession?.[currentSessionHash]
7229
- : undefined;
7230
- lastStatusText = undefined;
7231
- 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();
7232
7836
  }
7233
7837
 
7234
7838
  async function publishStatus(ctx: ExtensionContext, model: PiModel | undefined = ctx.model): Promise<void> {
@@ -7273,8 +7877,9 @@ export default function (pi: ExtensionAPI) {
7273
7877
  }
7274
7878
 
7275
7879
  if (adapter) {
7276
- // Footer mode defaults to daily provider/model totals. Users may select
7277
- // current-session counters through persistent command config or the env var.
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.
7278
7883
  const stats = displayModel
7279
7884
  ? selectFooterStatsForModel(mode, sessionHash, cacheStatsByModel, cacheStatsTotalsByModel, displayModel, cacheStatsProcessByModel)
7280
7885
  : undefined;
@@ -7358,13 +7963,35 @@ export default function (pi: ExtensionAPI) {
7358
7963
  pi.on("session_start", async (event, ctx) => {
7359
7964
  if (runtimeOptimizerEnabled) requestLongCacheRetention();
7360
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();
7361
7984
  await publishStatus(ctx);
7362
7985
  });
7363
7986
 
7364
7987
  pi.on("session_shutdown", async (_event, ctx) => {
7365
7988
  try {
7366
- await flushPersistCacheStats(ctx);
7989
+ await flushPersistCacheStats(ctx, "closed");
7367
7990
  } finally {
7991
+ if (shardRefreshTimer !== null) clearTimeout(shardRefreshTimer);
7992
+ shardRefreshTimer = null;
7993
+ shardWatcher?.close();
7994
+ shardWatcher = undefined;
7368
7995
  latestCacheHint = undefined;
7369
7996
  delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
7370
7997
  uninstallCacheHintsService();
@@ -7374,6 +8001,7 @@ export default function (pi: ExtensionAPI) {
7374
8001
 
7375
8002
  pi.on("model_select", async (event, ctx) => {
7376
8003
  if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(event.model, ctx) ?? event.model, ctx, warnedModels);
8004
+ await refreshShardAggregate();
7377
8005
  await publishStatus(ctx, event.model);
7378
8006
  });
7379
8007
 
@@ -7615,6 +8243,31 @@ export default function (pi: ExtensionAPI) {
7615
8243
  syncSessionHash(ctx);
7616
8244
  const msgRecord = asRecord(event.message);
7617
8245
 
8246
+ // Some providers expose an HTTP 400 error body only through the finalized
8247
+ // assistant error message; after_provider_response may contain the status
8248
+ // and no diagnostic response headers. Record the same model-scoped
8249
+ // prompt_cache_retention fallback from that authoritative message identity.
8250
+ if (runtimeOptimizerEnabled && hasPromptCacheRetentionUnsupportedErrorMessage(event.message)) {
8251
+ const fallbackModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
8252
+ const messageModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
8253
+ const errorModel = messageModel
8254
+ ? findModelInRegistry(ctx.modelRegistry, messageModel.provider, messageModel.id) ?? messageModel
8255
+ : undefined;
8256
+ if (errorModel && isExplicitPromptCacheRetentionUnsupportedApplicable(errorModel)) {
8257
+ const key = modelKey(errorModel);
8258
+ promptCacheRetention400Models.add(key);
8259
+ if (!warnedPromptCacheRetention400Models.has(key)) {
8260
+ warnedPromptCacheRetention400Models.add(key);
8261
+ ctx.ui.notify(
8262
+ `⚠️ ${LOG_PREFIX}: ${key} rejected prompt_cache_retention. ` +
8263
+ getPromptCacheRetentionUnsupportedHint() +
8264
+ ` Run /cache-optimizer doctor for the exact edit location.`,
8265
+ "warning",
8266
+ );
8267
+ }
8268
+ }
8269
+ }
8270
+
7618
8271
  // Record only Anthropic's explicit mixed-TTL ordering error. This is a
7619
8272
  // non-retryable 400 in Pi 0.82.1, so the fallback applies to the next
7620
8273
  // subsequent request (and to a retry only if another layer initiates one).
@@ -7703,6 +8356,20 @@ export default function (pi: ExtensionAPI) {
7703
8356
  // Update session, process-local, and cumulative buckets for the actual
7704
8357
  // routed model. The process bucket is intentionally never persisted.
7705
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);
7706
8373
  const sk = sessionModelKey(statsModel);
7707
8374
  addUsageToCacheStats(getOrCreateStatsByModelKey(sk), usage);
7708
8375
  addUsageToCacheStats(getOrCreateProcessStatsForModel(statsModel), usage);
@@ -7712,6 +8379,7 @@ export default function (pi: ExtensionAPI) {
7712
8379
  }
7713
8380
 
7714
8381
  schedulePersistCacheStats(ctx);
8382
+ await refreshShardAggregate();
7715
8383
  await publishStatus(ctx, statsModel);
7716
8384
  });
7717
8385
 
@@ -7722,36 +8390,37 @@ export default function (pi: ExtensionAPI) {
7722
8390
  // disable — disable runtime prompt/cache optimizations for this process
7723
8391
  // doctor — show current model/provider/api/baseUrl/compat status
7724
8392
  // with low-hit diagnosis
7725
- // stats — show active model stats bucket, recent trend, usage
8393
+ // stats — show detailed current-session models; `stats all` shows all local shards
7726
8394
  // compat — show compat suggestion with file path
7727
8395
  // config footer-mode total|session|process — persist footer mode override
7728
8396
  // fix — auto-fix compat issues (writes models.json, requires UI)
7729
8397
  // reset — reset current provider/model footer stats bucket (local only)
7730
8398
  // (no args) — interactive menu (with UI) or help summary
7731
8399
  // ────────────────────────────────────────────────────────────────
7732
- pi.registerCommand("cache-optimizer", {
7733
- description: "Configure and diagnose Pi cache behavior",
7734
- getArgumentCompletions: getCacheOptimizerArgumentCompletions,
7735
- handler: async (args: string, cmdCtx) => {
7736
- syncSessionHash(cmdCtx);
7737
- const selectedModel = cmdCtx.model;
7738
- const model = resolveRouteModel(selectedModel, cmdCtx as unknown as ExtensionContext) ?? selectedModel;
7739
- const commandParts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
7740
- const subcommand = commandParts[0] || "help";
8400
+ async function handleCacheOptimizerCommand(
8401
+ args: string,
8402
+ cmdCtx: ExtensionCommandContext,
8403
+ ): Promise<void> {
8404
+ syncSessionHash(cmdCtx);
8405
+ const selectedModel = cmdCtx.model;
8406
+ const model = resolveRouteModel(selectedModel, cmdCtx) ?? selectedModel;
8407
+ const commandParts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
8408
+ const subcommand = commandParts[0] || "help";
7741
8409
 
7742
8410
  if (subcommand === "enable") {
7743
8411
  setRuntimeOptimizerEnabled(true);
7744
- resetCurrentSessionStats();
7745
- await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
7746
- await publishStatus(cmdCtx as unknown as ExtensionContext, model);
8412
+ await resetCurrentSessionStats();
8413
+ await flushPersistCacheStats(cmdCtx);
8414
+ await publishStatus(cmdCtx, model);
7747
8415
  cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
7748
8416
  } else if (subcommand === "disable") {
7749
8417
  setRuntimeOptimizerEnabled(false);
7750
- resetCurrentSessionStats();
7751
- await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
7752
- await publishStatus(cmdCtx as unknown as ExtensionContext, model);
8418
+ await resetCurrentSessionStats();
8419
+ await flushPersistCacheStats(cmdCtx);
8420
+ await publishStatus(cmdCtx, model);
7753
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");
7754
8422
  } else if (subcommand === "doctor") {
8423
+ await refreshShardAggregate();
7755
8424
  if (!model) {
7756
8425
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
7757
8426
  return;
@@ -7767,16 +8436,18 @@ export default function (pi: ExtensionAPI) {
7767
8436
  : diagnosis;
7768
8437
  cmdCtx.ui.notify(fullDiagnosis, "info");
7769
8438
  } else if (subcommand === "stats") {
7770
- if (!model) {
7771
- cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
7772
- return;
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");
7773
8450
  }
7774
- const adapter = selectAdapterForModel(model);
7775
- const sk = model ? sessionModelKey(model) : undefined;
7776
- const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
7777
- const samples = sk ? getRecentSamples(sk) : [];
7778
- const output = buildStatsOutput(model, adapter, statsState, samples);
7779
- cmdCtx.ui.notify(output, "info");
7780
8451
  } else if (subcommand === "config") {
7781
8452
  const configKey = commandParts[1];
7782
8453
  const requestedMode = commandParts[2];
@@ -7795,7 +8466,7 @@ export default function (pi: ExtensionAPI) {
7795
8466
  await writePersistedFooterMode(nextMode);
7796
8467
  persistedFooterStatsMode = nextMode;
7797
8468
  lastStatusText = undefined;
7798
- await publishStatus(cmdCtx as unknown as ExtensionContext, model);
8469
+ await publishStatus(cmdCtx, model);
7799
8470
  const resolved = resolveFooterStatsMode(persistedFooterStatsMode);
7800
8471
  cmdCtx.ui.notify(
7801
8472
  `✅ Footer mode set to ${resolved.mode}. Persistent config overrides ${FOOTER_MODE_ENV}.`,
@@ -7839,13 +8510,13 @@ export default function (pi: ExtensionAPI) {
7839
8510
  // Reset local footer stats for the effective active model. If the
7840
8511
  // selected model is a virtual router and the protocol exposes a live
7841
8512
  // route, this clears the real upstream bucket, not the router shell.
7842
- resetStatsForModel(model);
8513
+ await resetStatsForModel(model);
7843
8514
 
7844
8515
  // Persist immediately.
7845
- await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
8516
+ await flushPersistCacheStats(cmdCtx);
7846
8517
 
7847
8518
  // Update footer to show 0/0.
7848
- await publishStatus(cmdCtx as unknown as ExtensionContext, model);
8519
+ await publishStatus(cmdCtx, model);
7849
8520
 
7850
8521
  cmdCtx.ui.notify(
7851
8522
  `✅ Reset local footer cache stats for "${displayKey}". ` +
@@ -7998,20 +8669,20 @@ export default function (pi: ExtensionAPI) {
7998
8669
  );
7999
8670
  if (confirmed) {
8000
8671
  try {
8001
- await copyFile(MODELS_JSON_PATH, backupPath);
8002
- const tempPath = `${MODELS_JSON_PATH}.${process.pid}.${Date.now()}.fix.tmp`;
8003
- await writeFile(tempPath, plan.modifiedText, "utf8");
8004
- await rename(tempPath, MODELS_JSON_PATH);
8005
-
8006
- const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
8007
- const postErr = selfCheckMissingEntryInsertion(
8008
- originalText, writtenText,
8009
- suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
8672
+ const result = await applyModelsJsonFixTransaction(
8673
+ plan.modifiedText,
8674
+ backupPath,
8675
+ (writtenText) => selfCheckMissingEntryInsertion(
8676
+ originalText,
8677
+ writtenText,
8678
+ suggestion.providerLabel,
8679
+ suggestion.modelId,
8680
+ suggestion.compatKeys,
8681
+ ),
8010
8682
  );
8011
- if (postErr !== null) {
8012
- await copyFile(backupPath, MODELS_JSON_PATH);
8683
+ if ("postCheckError" in result) {
8013
8684
  cmdCtx.ui.notify(
8014
- `❌ Post-write self-check failed: ${postErr}\n` +
8685
+ `❌ Post-write self-check failed: ${result.postCheckError}\n` +
8015
8686
  `The backup at ${backupPath} has been restored. No changes applied.`,
8016
8687
  "error",
8017
8688
  );
@@ -8025,7 +8696,8 @@ export default function (pi: ExtensionAPI) {
8025
8696
  );
8026
8697
  } catch (e) {
8027
8698
  cmdCtx.ui.notify(
8028
- `❌ Write failed: ${e instanceof Error ? e.message : String(e)}`,
8699
+ `❌ Write failed: ${e instanceof Error ? e.message : String(e)}\n` +
8700
+ `Backup may be at: ${backupPath}`,
8029
8701
  "error",
8030
8702
  );
8031
8703
  }
@@ -8147,22 +8819,21 @@ export default function (pi: ExtensionAPI) {
8147
8819
 
8148
8820
  // Write: backup → temp + rename → self-check again
8149
8821
  try {
8150
- // Backup
8151
- await copyFile(MODELS_JSON_PATH, backupPath);
8152
-
8153
- // Atomic write
8154
- const tempPath = `${MODELS_JSON_PATH}.${process.pid}.${Date.now()}.fix.tmp`;
8155
- await writeFile(tempPath, modifiedText, "utf8");
8156
- await rename(tempPath, MODELS_JSON_PATH);
8157
-
8158
- // Post-write self-check (read back)
8159
- const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
8160
- const postCheckError = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, decision.placement);
8161
- if (postCheckError !== null) {
8162
- // Restore from backup
8163
- await copyFile(backupPath, MODELS_JSON_PATH);
8822
+ const result = await applyModelsJsonFixTransaction(
8823
+ modifiedText,
8824
+ backupPath,
8825
+ (writtenText) => selfCheckFix(
8826
+ originalText,
8827
+ writtenText,
8828
+ suggestion.providerLabel,
8829
+ suggestion.modelId,
8830
+ suggestion.compatKeys,
8831
+ decision.placement,
8832
+ ),
8833
+ );
8834
+ if ("postCheckError" in result) {
8164
8835
  cmdCtx.ui.notify(
8165
- `❌ Post-write self-check failed: ${postCheckError}\n` +
8836
+ `❌ Post-write self-check failed: ${result.postCheckError}\n` +
8166
8837
  `The backup at ${backupPath} has been restored. No changes applied.`,
8167
8838
  "error",
8168
8839
  );
@@ -8189,7 +8860,7 @@ export default function (pi: ExtensionAPI) {
8189
8860
  "Enable — Turn on runtime optimizations",
8190
8861
  "Disable — Turn off runtime optimizations",
8191
8862
  "Doctor — Show cache configuration",
8192
- "Stats — Show cache stats and trend",
8863
+ "Stats — Show current-session model statistics",
8193
8864
  "Compat — Show compat suggestion",
8194
8865
  "Fix — Auto-fix compat issues (writes models.json)",
8195
8866
  "Footer mode — Choose total, session, or process stats",
@@ -8198,228 +8869,30 @@ export default function (pi: ExtensionAPI) {
8198
8869
  ];
8199
8870
  const choice = await cmdCtx.ui.select("Cache Optimizer", menuOptions);
8200
8871
  if (choice === menuOptions[0]) {
8201
- setRuntimeOptimizerEnabled(true);
8202
- resetCurrentSessionStats();
8203
- await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
8204
- await publishStatus(cmdCtx as unknown as ExtensionContext, model);
8205
- cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
8872
+ await handleCacheOptimizerCommand("enable", cmdCtx);
8206
8873
  } else if (choice === menuOptions[1]) {
8207
- setRuntimeOptimizerEnabled(false);
8208
- resetCurrentSessionStats();
8209
- await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
8210
- await publishStatus(cmdCtx as unknown as ExtensionContext, model);
8211
- 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");
8874
+ await handleCacheOptimizerCommand("disable", cmdCtx);
8212
8875
  } else if (choice === menuOptions[2]) {
8213
- if (!model) {
8214
- cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
8215
- } else {
8216
- const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), anthropicTtlOrderError: anthropicTtlOrderErrorModels.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
8217
- const adapter = selectAdapterForModel(model);
8218
- const sk = model ? sessionModelKey(model) : undefined;
8219
- const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
8220
- const samples = sk ? getRecentSamples(sk) : [];
8221
- const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
8222
- const fullDiagnosis = lowHitLines.length > 0
8223
- ? diagnosis + "\n" + lowHitLines.join("\n")
8224
- : diagnosis;
8225
- cmdCtx.ui.notify(fullDiagnosis, "info");
8226
- }
8876
+ await handleCacheOptimizerCommand("doctor", cmdCtx);
8227
8877
  } else if (choice === menuOptions[3]) {
8228
- if (!model) {
8229
- cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
8230
- } else {
8231
- const adapter = selectAdapterForModel(model);
8232
- const sk = model ? sessionModelKey(model) : undefined;
8233
- const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
8234
- const samples = sk ? getRecentSamples(sk) : [];
8235
- const output = buildStatsOutput(model, adapter, statsState, samples);
8236
- cmdCtx.ui.notify(output, "info");
8237
- }
8878
+ await handleCacheOptimizerCommand("stats", cmdCtx);
8238
8879
  } else if (choice === menuOptions[4]) {
8239
- if (!model) {
8240
- cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
8241
- } else {
8242
- const compatResult = buildCompatDiagnosis(model);
8243
- if (compatResult) {
8244
- cmdCtx.ui.notify(compatResult, "warning");
8245
- } else {
8246
- cmdCtx.ui.notify(
8247
- isAdaptiveThinkingCompatApplicable(model) || isDeepSeekCompatCheckApplicable(model) || isCompatCheckApplicable(model)
8248
- ? "✅ Compat fully configured."
8249
- : getCompatCheckNotApplicableLines(model).join("\n"),
8250
- "info",
8251
- );
8252
- }
8253
- }
8880
+ await handleCacheOptimizerCommand("compat", cmdCtx);
8254
8881
  } else if (choice === menuOptions[5]) {
8255
- // Fix — auto-fix compat issues
8256
- if (!model) {
8257
- cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
8258
- return;
8259
- }
8260
- const suggestion = buildCommandFixSuggestion(model);
8261
- if (!suggestion) {
8262
- const key = modelKey(model);
8263
- cmdCtx.ui.notify(`✅ Nothing to fix for "${key}". Compat already configured.`, "info");
8264
- return;
8265
- }
8266
-
8267
- // Read models.json
8268
- let originalText: string;
8269
- try {
8270
- originalText = await readFile(MODELS_JSON_PATH, "utf8");
8271
- } catch {
8272
- cmdCtx.ui.notify(`❌ Could not read ${MODELS_JSON_PATH}. File may not exist.`, "error");
8273
- return;
8274
- }
8275
-
8276
- const location = locateModelInJsonc(originalText, suggestion.providerLabel, suggestion.modelId);
8277
- const menuDecision = location
8278
- ? chooseFixPlacement(
8279
- originalText,
8280
- location,
8281
- suggestion.compatKeys,
8282
- suggestion.providerLabel,
8283
- suggestion.forceModelLevel,
8284
- )
8285
- : undefined;
8286
- const overridePlan = location
8287
- ? undefined
8288
- : composeModelOverrideInsertion(
8289
- originalText,
8290
- suggestion.providerLabel,
8291
- suggestion.modelId,
8292
- suggestion.compatKeys,
8293
- );
8294
- if (!location && !overridePlan) {
8295
- cmdCtx.ui.notify(
8296
- `❌ Could not safely locate a modelOverrides insertion point in ${getModelsJsonDisplayPath()}.\n` +
8297
- `Manual edit required:\n${formatMissingEntryManualSnippet(suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys)}\n` +
8298
- `Then run /reload.`,
8299
- "warning",
8300
- );
8301
- return;
8302
- }
8303
- const modifiedText = location && menuDecision
8304
- ? composeFixInsertion(originalText, location, suggestion.compatKeys, menuDecision.placement)
8305
- : overridePlan!.modifiedText;
8306
- const checkError = location && menuDecision
8307
- ? selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement)
8308
- : selfCheckMissingEntryInsertion(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
8309
- if (checkError !== null) {
8310
- cmdCtx.ui.notify(`❌ Self-check failed: ${checkError}\nNo changes made.`, "error");
8311
- return;
8312
- }
8313
-
8314
- const keysPreview = JSON.stringify(suggestion.compatKeys, null, 2);
8315
- const ts = backupTimestamp();
8316
- const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
8317
-
8318
- const menuLocationDesc = overridePlan?.placementLabel ?? (menuDecision?.placement === "provider"
8319
- ? `providers["${suggestion.providerLabel}"] -> compat (provider level)`
8320
- : menuDecision?.placement === "modelOverride"
8321
- ? `providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat`
8322
- : `providers["${suggestion.providerLabel}"] -> models -> "${suggestion.modelId}" -> compat (model level)`);
8323
- const menuScopeRiskLine = menuDecision?.placement === "provider"
8324
- ? ` 1. This change applies to ALL ${location?.allModelIds.length || 1} model(s) in the "${suggestion.providerLabel}" provider, across all sessions.`
8325
- : ` 1. This change affects ALL sessions using the "${suggestion.providerLabel}" provider/channel (scoped to model "${suggestion.modelId}").`;
8326
-
8327
- const previewLines = [
8328
- `📝 Preview of changes to ${getModelsJsonDisplayPath()}:`,
8329
- `Location: ${menuLocationDesc}`,
8330
- `Placement: ${menuDecision ? `${menuDecision.placement} level — ${menuDecision.reason}` : "modelOverrides — built-in/API-login-safe model override"}`,
8331
- `Compat JSON to write:`,
8332
- keysPreview,
8333
- ``,
8334
- `⚠️ Risk notice:`,
8335
- menuScopeRiskLine,
8336
- ` 2. A timestamped backup will be written to: ${backupPath}`,
8337
- ` 3. You must restart Pi / run /reload for the change to take effect.`,
8338
- ` 4. If the file contains comments, verify the result after write.`,
8339
- ``,
8340
- `Apply these changes?`,
8341
- ];
8342
-
8343
- const confirmed = await cmdCtx.ui.confirm("Cache Optimizer — Fix", previewLines.join("\n"));
8344
- if (!confirmed) {
8345
- cmdCtx.ui.notify("No changes were made. Canceled by user.", "info");
8346
- return;
8347
- }
8348
-
8349
- try {
8350
- await copyFile(MODELS_JSON_PATH, backupPath);
8351
- const tempPath = `${MODELS_JSON_PATH}.${process.pid}.${Date.now()}.fix.tmp`;
8352
- await writeFile(tempPath, modifiedText, "utf8");
8353
- await rename(tempPath, MODELS_JSON_PATH);
8354
-
8355
- const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
8356
- const postCheck = location && menuDecision
8357
- ? selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement)
8358
- : selfCheckMissingEntryInsertion(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
8359
- if (postCheck !== null) {
8360
- await copyFile(backupPath, MODELS_JSON_PATH);
8361
- cmdCtx.ui.notify(`❌ Post-write check failed: ${postCheck}\nBackup restored.`, "error");
8362
- return;
8363
- }
8364
-
8365
- cmdCtx.ui.notify(
8366
- `✅ Fix applied to ${getModelsJsonDisplayPath()}.` +
8367
- `\nBackup: ${backupPath}` +
8368
- `\nRun /reload or restart Pi for the change to take effect.`,
8369
- "info",
8370
- );
8371
- } catch (writeError) {
8372
- cmdCtx.ui.notify(
8373
- `❌ Write failed: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
8374
- "error",
8375
- );
8376
- }
8882
+ await handleCacheOptimizerCommand("fix", cmdCtx);
8377
8883
  } else if (choice === menuOptions[6]) {
8378
- const modeOptions = ["total — Daily cumulative totals (default)", "session — Current Pi conversation session", "process — Current Pi process only", "Cancel"];
8884
+ const modeOptions = ["session — Current Pi conversation session (default)", "total — All local sessions today", "process — Current extension instance only", "Cancel"];
8379
8885
  const modeChoice = await cmdCtx.ui.select("Footer cache stats mode", modeOptions);
8380
8886
  const nextMode = modeChoice === modeOptions[0]
8381
- ? "total"
8887
+ ? "session"
8382
8888
  : modeChoice === modeOptions[1]
8383
- ? "session"
8889
+ ? "total"
8384
8890
  : modeChoice === modeOptions[2]
8385
8891
  ? "process"
8386
8892
  : undefined;
8387
- if (nextMode) {
8388
- try {
8389
- await writePersistedFooterMode(nextMode);
8390
- persistedFooterStatsMode = nextMode;
8391
- lastStatusText = undefined;
8392
- await publishStatus(cmdCtx as unknown as ExtensionContext, model);
8393
- cmdCtx.ui.notify(
8394
- `✅ Footer mode set to ${nextMode}. Persistent config overrides ${FOOTER_MODE_ENV}.`,
8395
- "info",
8396
- );
8397
- } catch (error) {
8398
- cmdCtx.ui.notify(
8399
- `❌ Could not update footer mode config: ${error instanceof Error ? error.message : String(error)}`,
8400
- "error",
8401
- );
8402
- }
8403
- }
8893
+ if (nextMode) await handleCacheOptimizerCommand(`config footer-mode ${nextMode}`, cmdCtx);
8404
8894
  } else if (choice === menuOptions[7]) {
8405
- if (!model) {
8406
- cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
8407
- } else {
8408
- const adapter = selectAdapterForModel(model);
8409
- if (!adapter) {
8410
- cmdCtx.ui.notify("ℹ️ Active model does not match a cache adapter. No stats to reset.", "info");
8411
- } else {
8412
- const displayKey = modelKey(model);
8413
- resetStatsForModel(model);
8414
- await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
8415
- await publishStatus(cmdCtx as unknown as ExtensionContext, model);
8416
- cmdCtx.ui.notify(
8417
- `✅ Reset local footer cache stats for "${displayKey}". ` +
8418
- "Upstream provider prompt cache was not modified.",
8419
- "info",
8420
- );
8421
- }
8422
- }
8895
+ await handleCacheOptimizerCommand("reset", cmdCtx);
8423
8896
  }
8424
8897
  // choice === "cancel" or undefined → no action
8425
8898
  return;
@@ -8431,7 +8904,9 @@ export default function (pi: ExtensionAPI) {
8431
8904
  diagnosis.push(" enable — Enable prompt/cache optimizations for this Pi process");
8432
8905
  diagnosis.push(" disable — Disable prompt/cache optimizations for this Pi process");
8433
8906
  diagnosis.push(" doctor — Show current model/provider/api/baseUrl/compat and low-hit diagnosis");
8434
- diagnosis.push(" stats — Show active model stats bucket and recent trend");
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");
8435
8910
  diagnosis.push(" compat — Show compat suggestion with edit location");
8436
8911
  diagnosis.push(" config footer-mode total|session|process — Persist the footer stats mode");
8437
8912
  diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
@@ -8459,6 +8934,11 @@ export default function (pi: ExtensionAPI) {
8459
8934
  }
8460
8935
  cmdCtx.ui.notify(diagnosis.join("\n"), "info");
8461
8936
  }
8462
- },
8937
+ }
8938
+
8939
+ pi.registerCommand("cache-optimizer", {
8940
+ description: "Configure and diagnose Pi cache behavior",
8941
+ getArgumentCompletions: getCacheOptimizerArgumentCompletions,
8942
+ handler: handleCacheOptimizerCommand,
8463
8943
  });
8464
8944
  }