pi-cache-optimizer 2.6.20 → 2.6.22

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 (2) hide show
  1. package/index.ts +141 -30
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -14,6 +14,12 @@ type CacheRetentionEnvSnapshot = {
14
14
 
15
15
  const PI_CACHE_RETENTION_ENV = "PI_CACHE_RETENTION";
16
16
  const LONG_CACHE_RETENTION_VALUE = "long";
17
+ const PI_CACHE_RETENTION_BASELINE_SYMBOL = Symbol.for("pi.cache.optimizer.retention-baseline.v1");
18
+
19
+ type CacheRetentionBaselineV1 = {
20
+ version: 1;
21
+ snapshot: CacheRetentionEnvSnapshot;
22
+ };
17
23
 
18
24
  function captureCacheRetentionEnv(env: MutableEnv = process.env): CacheRetentionEnvSnapshot {
19
25
  return {
@@ -36,7 +42,28 @@ function restoreCacheRetentionEnv(snapshot: CacheRetentionEnvSnapshot, env: Muta
36
42
  }
37
43
  }
38
44
 
39
- const STARTUP_CACHE_RETENTION_ENV = captureCacheRetentionEnv();
45
+ function isCacheRetentionBaselineV1(value: unknown): value is CacheRetentionBaselineV1 {
46
+ if (typeof value !== "object" || value === null) return false;
47
+ const record = value as { version?: unknown; snapshot?: unknown };
48
+ if (record.version !== 1 || typeof record.snapshot !== "object" || record.snapshot === null) return false;
49
+ const snapshot = record.snapshot as { wasSet?: unknown; value?: unknown };
50
+ return typeof snapshot.wasSet === "boolean" &&
51
+ (snapshot.value === undefined || typeof snapshot.value === "string");
52
+ }
53
+
54
+ function getOrCaptureCacheRetentionBaseline(
55
+ env: MutableEnv = process.env,
56
+ globals: Record<symbol, unknown> = globalThis as Record<symbol, unknown>,
57
+ ): CacheRetentionEnvSnapshot {
58
+ const existing = globals[PI_CACHE_RETENTION_BASELINE_SYMBOL];
59
+ if (isCacheRetentionBaselineV1(existing)) return { ...existing.snapshot };
60
+
61
+ const snapshot = captureCacheRetentionEnv(env);
62
+ globals[PI_CACHE_RETENTION_BASELINE_SYMBOL] = { version: 1, snapshot: { ...snapshot } };
63
+ return snapshot;
64
+ }
65
+
66
+ const STARTUP_CACHE_RETENTION_ENV = getOrCaptureCacheRetentionBaseline();
40
67
 
41
68
  /**
42
69
  * Pi Cache Optimizer (formerly pi-deepseek-cache-optimizer)
@@ -76,6 +103,7 @@ const NO_SKILL_COMPRESSION_ENV = "PI_CACHE_OPTIMIZER_NO_SKILL_COMPRESSION";
76
103
  const NO_PROMPT_REWRITE_ENV = "PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE";
77
104
  const PI_ROUTING_REGISTRY_SYMBOL = Symbol.for("pi.routing.registry.v1");
78
105
  const PI_CACHE_HINTS_SYMBOL = Symbol.for("pi.cache.hints.v1");
106
+ const PI_CACHE_HINTS_OWNER_SYMBOL = Symbol.for("pi.cache.optimizer.hints-owner.v1");
79
107
 
80
108
  let runtimeOptimizerEnabled = true;
81
109
 
@@ -921,16 +949,29 @@ function getCacheHintsService(): PiCacheHintsV1 | undefined {
921
949
  return isCacheHintsServiceV1(candidate) ? candidate : undefined;
922
950
  }
923
951
 
924
- function installCacheHintsService(service: PiCacheHintsV1): () => void {
952
+ function markOptimizerOwnedCacheHintsService(service: PiCacheHintsV1): PiCacheHintsV1 {
953
+ (service as PiCacheHintsV1 & Record<symbol, unknown>)[PI_CACHE_HINTS_OWNER_SYMBOL] = true;
954
+ return service;
955
+ }
956
+
957
+ function isOptimizerOwnedCacheHintsService(value: unknown): boolean {
958
+ return typeof value === "object" && value !== null &&
959
+ (value as Record<symbol, unknown>)[PI_CACHE_HINTS_OWNER_SYMBOL] === true;
960
+ }
961
+
962
+ function installCacheHintsService(
963
+ service: PiCacheHintsV1,
964
+ options: { discardPrevious?: (value: unknown) => boolean } = {},
965
+ ): () => void {
925
966
  const globals = getProtocolGlobal();
926
967
  const previous = globals[PI_CACHE_HINTS_SYMBOL];
927
968
  globals[PI_CACHE_HINTS_SYMBOL] = service;
928
969
  return () => {
929
970
  if (globals[PI_CACHE_HINTS_SYMBOL] !== service) return;
930
- if (previous === undefined) {
931
- delete globals[PI_CACHE_HINTS_SYMBOL];
932
- } else {
971
+ if (previous !== undefined && !options.discardPrevious?.(previous)) {
933
972
  globals[PI_CACHE_HINTS_SYMBOL] = previous;
973
+ } else {
974
+ delete globals[PI_CACHE_HINTS_SYMBOL];
934
975
  }
935
976
  };
936
977
  }
@@ -1794,10 +1835,21 @@ function modelFromAssistantMessage(message: unknown, fallback: PiModel | undefin
1794
1835
  const api = firstNonEmptyString(record.api, fallback?.api) ?? "";
1795
1836
  if (!id || !provider) return fallback;
1796
1837
 
1838
+ const fallbackName = isNonEmptyString(fallback?.name) ? fallback.name : undefined;
1839
+ const preservesFallbackIdentity =
1840
+ !isVirtualRoutingModel(fallback) &&
1841
+ provider === fallback?.provider &&
1842
+ id === fallback?.id &&
1843
+ fallbackName !== undefined;
1844
+
1797
1845
  return {
1798
1846
  ...(fallback ?? {}),
1799
1847
  id,
1800
- name: id,
1848
+ // Direct providers such as kimi-coding may echo only a short model id
1849
+ // (`k3`) while the active model name (`Kimi K3`) carries the adapter token.
1850
+ // Preserve that display name only when response and fallback identities are
1851
+ // exactly the same; routed/different identities keep message-local naming.
1852
+ name: preservesFallbackIdentity ? fallbackName : id,
1801
1853
  provider,
1802
1854
  api,
1803
1855
  baseUrl: fallback?.baseUrl ?? "",
@@ -3908,6 +3960,15 @@ function mergeCacheSessions(
3908
3960
  return sessions;
3909
3961
  }
3910
3962
 
3963
+ function createSerializedAsyncRunner(): <T>(operation: () => Promise<T>) => Promise<T> {
3964
+ let tail: Promise<unknown> = Promise.resolve();
3965
+ return <T>(operation: () => Promise<T>): Promise<T> => {
3966
+ const result = tail.then(operation);
3967
+ tail = result.catch(() => undefined);
3968
+ return result;
3969
+ };
3970
+ }
3971
+
3911
3972
  function mergeCacheTotals(
3912
3973
  existingTotalsByModel: Record<string, CacheStats>,
3913
3974
  stateTotalsByModel: Record<string, CacheStats>,
@@ -5954,6 +6015,7 @@ export const __internals_for_tests = {
5954
6015
  getAssistantMessageModelTokenValues,
5955
6016
  getCompat,
5956
6017
  modelKey,
6018
+ modelFromAssistantMessage,
5957
6019
  consolidateDirectProviderStatsModel,
5958
6020
  // Platform-friendly path helpers
5959
6021
  getAgentDirDisplayPath,
@@ -6013,10 +6075,15 @@ export const __internals_for_tests = {
6013
6075
  isVirtualRoutingModel,
6014
6076
  installCacheHintsService,
6015
6077
  getCacheHintsService,
6078
+ markOptimizerOwnedCacheHintsService,
6079
+ isOptimizerOwnedCacheHintsService,
6080
+ getOrCaptureCacheRetentionBaseline,
6081
+ PI_CACHE_RETENTION_BASELINE_SYMBOL,
6016
6082
  // Persistence helpers (for reload/reset tests)
6017
6083
  mergeCacheSessions,
6018
6084
  mergeCacheTotals,
6019
6085
  mergeLastRoutedModels,
6086
+ createSerializedAsyncRunner,
6020
6087
  writePersistedCacheStats,
6021
6088
  readPersistedCacheStats,
6022
6089
  STATE_FILE_PATH,
@@ -6066,7 +6133,9 @@ export default function (pi: ExtensionAPI) {
6066
6133
  let lastStatusText: string | undefined;
6067
6134
  let persistenceWarningShown = false;
6068
6135
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
6136
+ const enqueuePersist = createSerializedAsyncRunner();
6069
6137
  let replacePersistedTotalsOnNextWrite = false;
6138
+ let forceReplaceTotalsAfterFailure = false;
6070
6139
  const pendingDeletedTotalModelKeys = new Set<string>();
6071
6140
  let integrityNotificationShown = false;
6072
6141
  let currentSessionId = "";
@@ -6088,7 +6157,7 @@ export default function (pi: ExtensionAPI) {
6088
6157
  }
6089
6158
  }
6090
6159
 
6091
- const uninstallCacheHintsService = installCacheHintsService({
6160
+ const uninstallCacheHintsService = installCacheHintsService(markOptimizerOwnedCacheHintsService({
6092
6161
  version: 1,
6093
6162
  getHints(input: PiCacheHintsInput): PiCacheHintsOutput | undefined {
6094
6163
  if (!runtimeOptimizerEnabled || isEnabledEnv(process.env[NO_PROMPT_REWRITE_ENV])) return undefined;
@@ -6107,8 +6176,7 @@ export default function (pi: ExtensionAPI) {
6107
6176
  cacheRetention: hint.cacheRetention,
6108
6177
  };
6109
6178
  },
6110
- });
6111
- void uninstallCacheHintsService;
6179
+ }), { discardPrevious: isOptimizerOwnedCacheHintsService });
6112
6180
 
6113
6181
  /**
6114
6182
  * Build a session-scoped stats key from the current session hash + model key.
@@ -6231,28 +6299,59 @@ export default function (pi: ExtensionAPI) {
6231
6299
  lastStatusText = undefined;
6232
6300
  }
6233
6301
 
6234
- async function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
6235
- try {
6236
- await writePersistedCacheStats(
6237
- getCacheStatsState(),
6238
- currentSessionHashSet ? currentSessionHash : undefined,
6239
- {
6240
- deleteModelKeys: Array.from(pendingDeletedTotalModelKeys),
6241
- replaceTotals: replacePersistedTotalsOnNextWrite,
6242
- },
6243
- );
6244
- pendingDeletedTotalModelKeys.clear();
6245
- replacePersistedTotalsOnNextWrite = false;
6246
- } catch (error) {
6247
- console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
6248
- if (!persistenceWarningShown) {
6249
- persistenceWarningShown = true;
6250
- ctx?.ui.notify(
6251
- `${LOG_PREFIX}: failed to persist footer stats; using in-memory stats for this process.`,
6252
- "warning",
6253
- );
6254
- }
6302
+ function snapshotCacheStatsState(): CacheStatsState {
6303
+ const state = getCacheStatsState();
6304
+ const statsByModel = Object.fromEntries(
6305
+ Object.entries(state.statsByModel).map(([key, stats]) => [key, { ...stats }]),
6306
+ );
6307
+ const totalsByModel = Object.fromEntries(
6308
+ Object.entries(state.totalsByModel).map(([key, stats]) => [key, { ...stats }]),
6309
+ );
6310
+ const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
6311
+ for (const id of CACHE_PROVIDER_IDS) {
6312
+ const stats = state.legacyFamily[id];
6313
+ if (stats) legacyFamily[id] = { ...stats };
6255
6314
  }
6315
+ const lastRoutedModelBySession = state.lastRoutedModelBySession
6316
+ ? Object.fromEntries(
6317
+ Object.entries(state.lastRoutedModelBySession).map(([key, model]) => [key, { ...model }]),
6318
+ )
6319
+ : undefined;
6320
+ return { statsByModel, totalsByModel, legacyFamily, lastRoutedModelBySession };
6321
+ }
6322
+
6323
+ function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
6324
+ const state = snapshotCacheStatsState();
6325
+ const sessionHash = currentSessionHashSet ? currentSessionHash : undefined;
6326
+ const deleteModelKeys = Array.from(pendingDeletedTotalModelKeys);
6327
+ const replaceTotals = replacePersistedTotalsOnNextWrite;
6328
+ // This queued write owns the current intents. Later mutations can enqueue
6329
+ // independent intents without an earlier completion clearing them.
6330
+ pendingDeletedTotalModelKeys.clear();
6331
+ replacePersistedTotalsOnNextWrite = false;
6332
+
6333
+ return enqueuePersist(async () => {
6334
+ const recoverFromEarlierFailure = forceReplaceTotalsAfterFailure;
6335
+ try {
6336
+ await writePersistedCacheStats(state, sessionHash, {
6337
+ deleteModelKeys,
6338
+ replaceTotals: replaceTotals || recoverFromEarlierFailure,
6339
+ });
6340
+ if (recoverFromEarlierFailure) forceReplaceTotalsAfterFailure = false;
6341
+ } catch (error) {
6342
+ // The next queued write must replace totals authoritatively so a failed
6343
+ // reset/delete cannot merge stale persisted counters back into state.
6344
+ forceReplaceTotalsAfterFailure = true;
6345
+ console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
6346
+ if (!persistenceWarningShown) {
6347
+ persistenceWarningShown = true;
6348
+ ctx?.ui.notify(
6349
+ `${LOG_PREFIX}: failed to persist footer stats; using in-memory stats for this process.`,
6350
+ "warning",
6351
+ );
6352
+ }
6353
+ }
6354
+ });
6256
6355
  }
6257
6356
 
6258
6357
  /** Schedule a debounced persist. Coalesces rapid message_end writes
@@ -6542,10 +6641,22 @@ export default function (pi: ExtensionAPI) {
6542
6641
  }
6543
6642
 
6544
6643
  pi.on("session_start", async (event, ctx) => {
6644
+ if (runtimeOptimizerEnabled) requestLongCacheRetention();
6545
6645
  await restoreCacheStats(event.reason, ctx);
6546
6646
  await publishStatus(ctx);
6547
6647
  });
6548
6648
 
6649
+ pi.on("session_shutdown", async (_event, ctx) => {
6650
+ try {
6651
+ await flushPersistCacheStats(ctx);
6652
+ } finally {
6653
+ latestCacheHint = undefined;
6654
+ delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
6655
+ uninstallCacheHintsService();
6656
+ restoreCacheRetentionEnv(STARTUP_CACHE_RETENTION_ENV);
6657
+ }
6658
+ });
6659
+
6549
6660
  pi.on("model_select", async (event, ctx) => {
6550
6661
  if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(event.model, ctx) ?? event.model, ctx, warnedModels);
6551
6662
  await publishStatus(ctx, event.model);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.6.20",
3
+ "version": "2.6.22",
4
4
  "description": "Improve Pi prompt/KV cache hit rates with stable prompts, OpenAI-compatible cache keys, proxy compat warnings, and footer cache stats.",
5
5
  "keywords": [
6
6
  "pi-package",