pi-cache-optimizer 2.6.21 → 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 +128 -29
  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
  }
@@ -3919,6 +3960,15 @@ function mergeCacheSessions(
3919
3960
  return sessions;
3920
3961
  }
3921
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
+
3922
3972
  function mergeCacheTotals(
3923
3973
  existingTotalsByModel: Record<string, CacheStats>,
3924
3974
  stateTotalsByModel: Record<string, CacheStats>,
@@ -6025,10 +6075,15 @@ export const __internals_for_tests = {
6025
6075
  isVirtualRoutingModel,
6026
6076
  installCacheHintsService,
6027
6077
  getCacheHintsService,
6078
+ markOptimizerOwnedCacheHintsService,
6079
+ isOptimizerOwnedCacheHintsService,
6080
+ getOrCaptureCacheRetentionBaseline,
6081
+ PI_CACHE_RETENTION_BASELINE_SYMBOL,
6028
6082
  // Persistence helpers (for reload/reset tests)
6029
6083
  mergeCacheSessions,
6030
6084
  mergeCacheTotals,
6031
6085
  mergeLastRoutedModels,
6086
+ createSerializedAsyncRunner,
6032
6087
  writePersistedCacheStats,
6033
6088
  readPersistedCacheStats,
6034
6089
  STATE_FILE_PATH,
@@ -6078,7 +6133,9 @@ export default function (pi: ExtensionAPI) {
6078
6133
  let lastStatusText: string | undefined;
6079
6134
  let persistenceWarningShown = false;
6080
6135
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
6136
+ const enqueuePersist = createSerializedAsyncRunner();
6081
6137
  let replacePersistedTotalsOnNextWrite = false;
6138
+ let forceReplaceTotalsAfterFailure = false;
6082
6139
  const pendingDeletedTotalModelKeys = new Set<string>();
6083
6140
  let integrityNotificationShown = false;
6084
6141
  let currentSessionId = "";
@@ -6100,7 +6157,7 @@ export default function (pi: ExtensionAPI) {
6100
6157
  }
6101
6158
  }
6102
6159
 
6103
- const uninstallCacheHintsService = installCacheHintsService({
6160
+ const uninstallCacheHintsService = installCacheHintsService(markOptimizerOwnedCacheHintsService({
6104
6161
  version: 1,
6105
6162
  getHints(input: PiCacheHintsInput): PiCacheHintsOutput | undefined {
6106
6163
  if (!runtimeOptimizerEnabled || isEnabledEnv(process.env[NO_PROMPT_REWRITE_ENV])) return undefined;
@@ -6119,8 +6176,7 @@ export default function (pi: ExtensionAPI) {
6119
6176
  cacheRetention: hint.cacheRetention,
6120
6177
  };
6121
6178
  },
6122
- });
6123
- void uninstallCacheHintsService;
6179
+ }), { discardPrevious: isOptimizerOwnedCacheHintsService });
6124
6180
 
6125
6181
  /**
6126
6182
  * Build a session-scoped stats key from the current session hash + model key.
@@ -6243,28 +6299,59 @@ export default function (pi: ExtensionAPI) {
6243
6299
  lastStatusText = undefined;
6244
6300
  }
6245
6301
 
6246
- async function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
6247
- try {
6248
- await writePersistedCacheStats(
6249
- getCacheStatsState(),
6250
- currentSessionHashSet ? currentSessionHash : undefined,
6251
- {
6252
- deleteModelKeys: Array.from(pendingDeletedTotalModelKeys),
6253
- replaceTotals: replacePersistedTotalsOnNextWrite,
6254
- },
6255
- );
6256
- pendingDeletedTotalModelKeys.clear();
6257
- replacePersistedTotalsOnNextWrite = false;
6258
- } catch (error) {
6259
- console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
6260
- if (!persistenceWarningShown) {
6261
- persistenceWarningShown = true;
6262
- ctx?.ui.notify(
6263
- `${LOG_PREFIX}: failed to persist footer stats; using in-memory stats for this process.`,
6264
- "warning",
6265
- );
6266
- }
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 };
6267
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
+ });
6268
6355
  }
6269
6356
 
6270
6357
  /** Schedule a debounced persist. Coalesces rapid message_end writes
@@ -6554,10 +6641,22 @@ export default function (pi: ExtensionAPI) {
6554
6641
  }
6555
6642
 
6556
6643
  pi.on("session_start", async (event, ctx) => {
6644
+ if (runtimeOptimizerEnabled) requestLongCacheRetention();
6557
6645
  await restoreCacheStats(event.reason, ctx);
6558
6646
  await publishStatus(ctx);
6559
6647
  });
6560
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
+
6561
6660
  pi.on("model_select", async (event, ctx) => {
6562
6661
  if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(event.model, ctx) ?? event.model, ctx, warnedModels);
6563
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.21",
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",