pi-cache-optimizer 2.8.27 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -137,6 +137,10 @@ The fixture verifier reports numeric tool-order changes and confirms cache-marke
137
137
 
138
138
  Current versions store stats under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory. Each loaded extension instance owns one active UUID-named daily shard and writes it through temp-file + atomic rename; at local midnight it closes the old shard and starts a new UUID without erasing session history. This prevents parent/child/parallel Pi processes from overwriting one another. Upgrading from the old v6 single-file format deletes the old local stats files and starts footer counters from zero; upstream provider caches are not affected.
139
139
 
140
+ Since 2.9.0, maintenance (at most every six hours, under the shared maintenance lock, in the background) folds closed past-day shards, and active ones whose process has been gone for over 24 hours, into one archive snapshot under `pi-cache-optimizer-stats.d/archive/`, then deletes the originals. The shard directory therefore only holds roughly today's shards, so startup and refresh cost no longer grows with the 60-day retention window. Archived counters keep their reset epochs, so `reset` hides them immediately, and they expire on the same 60-day window. The archive records which shards it absorbed, so a crash mid-compaction never double counts. Older versions ignore the archive: if you downgrade or run an older version alongside, its `session` footer shows only history that has not been archived yet; current-day `total` and `process` counters are unaffected.
141
+
142
+ After 2.9.0, each snapshot is an immutable, compact generation file `archive/archive.<generation>.json` (about a fifth of the size of 2.9.0's single `archive.json`). A compaction publishes generation N+1 only if nobody else did since it read generation N (an exclusive hard link, atomic on Windows and POSIX), and deletes raw shards only when its generation is the newest; the maintenance lock is also refreshed while a pass runs. A process that stalls past the stale-lock window (for example a sleeping laptop) therefore can never overwrite a newer archive and lose counters. Readers use the newest generation, and the previous one is kept until the next publish. A 2.9.0 `archive.json` is still read, and the next compaction migrates it and replaces it with a directory of the same name. A 2.9.0 process that is still running then treats the archive as unreadable: it no longer compacts or deletes anything, and its `session` footer shows only unarchived history, as above. If a 2.9.0 process ever writes `archive.json` again after the migration (only possible if that directory is removed), the file is never counted, to rule out double counting; it is set aside as `archive.legacy-<time>.json`, and the shards that process archived into it are missing from the footer. On a filesystem without hard links (FAT/exFAT), compaction logs a warning and leaves raw shards in place.
143
+
140
144
  Response stats stay with the dispatched catalog/request-local model even if you switch to another model on the same provider while waiting. Legacy response aliases use the request snapshot, not the new selection; the footer continues showing the selected direct model. Old corrupt JSON/schema shards expire by file modification time after two months; young corrupt files, valid current-day shards, live old instances, and symlink targets remain protected.
141
145
 
142
146
  The footer defaults to `session`, which reflects the current Pi conversation rather than another parallel Pi terminal using the same provider/model. Use either the command or environment variable to select the scope:
package/README.zh-CN.md CHANGED
@@ -137,6 +137,10 @@ bun .trellis/tasks/09-03-context-epoch-tool-ordering/verify.ts
137
137
 
138
138
  当前版本把统计保存在 Pi agent 目录的 `pi-cache-optimizer-stats.d/shards/` 下。每个已加载的 extension instance 独占一个当天的活跃 UUID shard,并通过临时文件 + 原子 rename 写入;本地日期切换时关闭旧 shard、创建新的 UUID,保留 session 历史,因此父/子/并行 Pi 进程不会互相覆盖。旧 v6 单文件统计在升级时直接删除,本地 footer 计数从零开始;不会影响上游 provider 的实际缓存。
139
139
 
140
+ 从 2.9.0 起,维护任务会把已关闭的往日 shard,以及进程已退出超过 24 小时的活跃 shard,合并进 `pi-cache-optimizer-stats.d/archive/` 下的一份归档快照,再删除原文件。维护最多每 6 小时一次,在共享维护锁下后台运行。这样 shard 目录里基本只剩当天的 shard,启动和刷新的开销不再随 60 天保留期增长。归档计数保留 reset epoch,`reset` 立即生效,并按同样的 60 天窗口过期。归档会记录已吸收的 shard,所以合并中途崩溃也不会重复计数。旧版本不会读取归档:如果降级或与旧版本同时运行,旧版的 `session` footer 只显示尚未归档的历史;当天的 `total` 和 `process` 计数不受影响。
141
+
142
+ 2.9.0 之后的版本把每份快照保存为不可变的紧凑代号文件 `archive/archive.<generation>.json`,大小约为 2.9.0 单个 `archive.json` 的五分之一。压缩读到第 N 代后,只有在期间没有别人发布过的情况下才能发布第 N+1 代(用独占的硬链接实现,在 Windows 和 POSIX 上都是原子的),而且只有自己发布的代号仍是最新时才删除原始 shard;压缩运行期间还会刷新维护锁的时间戳。因此,一个停顿超过锁过期时间的进程(比如笔记本休眠)醒来后,再也不会覆盖更新的归档、造成计数丢失。读取端使用最新的代号,上一代保留到下一次发布。2.9.0 的 `archive.json` 仍然会被读取,下一次压缩会迁移它,并用一个同名目录取而代之。仍在运行的 2.9.0 进程随后会把归档视为不可读:它不再压缩或删除任何文件,它的 `session` footer 只显示尚未归档的历史,与上面所说一致。如果迁移之后 2.9.0 又写出了 `archive.json`(只有在这个目录被删除时才可能),为了杜绝重复计数,这个文件永远不会被计入,而是改名为 `archive.legacy-<time>.json` 留存,那个进程归档进去的 shard 将不会出现在 footer 里。在不支持硬链接的文件系统(FAT/exFAT)上,压缩会输出警告,原始 shard 保持原样。
143
+
140
144
  即使等待响应时切换到同一 provider 的另一个模型,响应统计仍归属于实际请求的 catalog/request-local 模型。旧 transport 的响应别名只根据请求快照处理,不使用新选择的模型;footer 继续显示当前选择的 direct 模型。损坏 JSON/schema 的旧 shard 按文件修改时间在两个月后清理;较新的损坏文件、有效的当天 shard、仍存活的旧 instance 和 symlink 目标均受保护。
141
145
 
142
146
  Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 provider/model 时污染当前窗口。可以通过命令或环境变量切换显示范围:
package/index.ts CHANGED
@@ -13,7 +13,8 @@ import { atomicCreateTextFileNoReplace, atomicReplaceTextFilePreservingMode, ato
13
13
  import { getAssistantMessageModelTokenValues, getModelIdNameTokenValues, isAdaptiveGenerationModel, isKimiCodingAdaptiveModel } from "./src/model-detect.ts";
14
14
  import { findLastExactModelDefinition, mergeCacheCompat, resolveEffectiveCompatFromConfig } from "./src/compat-config.ts";
15
15
  import { analyzeModelsJsonForMissingEntry, applyModelsJsonFixTransaction, chooseFixPlacement, composeFixInsertion, composeMissingEntryInsertion, composeModelOverrideInsertion, composeModelsJsonReceiptRollback, composeProviderAffinityInsertion, createModelsJsonFixReceipt, createRollbackBackupPath, decideFixPlacement, formatCompatKeysForInsertion, hasExplicitLongRetentionOptInFromConfig, hasReceiptReasoningProtocolChange, isActionableModelsJsonFixReceipt, locateModelInJsonc, markModelsJsonFixReceiptRolledBack, parseModelsJsonFixReceipt, prepareModelsJsonRollback, readModelsJsonFixReceipt, readModelsJsonFixReceiptSnapshot, receiptBackupPath, resolveExplicitCompatValue, selfCheckFix, selfCheckMissingEntryInsertion, validateModelsJsonRollback, writeModelsJsonFixReceipt } from "./src/models-json-fix.ts";
16
- import { type CacheProviderId, type CacheStats, LEGACY_STATE_FILE_PATH, type PersistedRoutedModelRef, type PersistedStatsShardV7, SHARD_FILES_DIR, SHARD_GLOBAL_EPOCH_PATH, SHARD_STATE_DIR, STATE_FILE_PATH, type ShardAggregate, type UsageSnapshot, addUsageToCacheStats, advanceGlobalStatsEpoch, advanceModelStatsEpoch, aggregateStatsShardsV7, cleanupStatsShardsV7, cloneCacheStats, mapWithConcurrency, currentLocalDay, emptyAllCacheStats, emptyCacheStats, initialEpoch, loadStatsShardAggregateV7, maybeCleanupStatsShardsV7, modelEpochPath, parseCacheStats, parsePersistedRoutedModelRef, parsePersistedStatsShardV7, readGlobalStatsEpoch, readModelStatsEpoch, readValidStatsShardsV7, removeLegacyStatsFiles, writeStatsShardV7 } from "./src/stats-store.ts";
16
+ import { SHARD_ARCHIVE_PATH, aggregateStatsWithArchiveV7, compactStatsShardsV7, encodeStatsArchiveV2, listStatsArchiveGenerations, maybeRunStatsMaintenance, parseStatsArchiveV2, readStatsArchive, readStatsArchiveView, StatsArchiveView, statsArchiveGenerationPath } from "./src/stats-archive.ts";
17
+ import { type CacheProviderId, type CacheStats, LEGACY_STATE_FILE_PATH, acquireStatsMaintenanceLock, holdsStatsMaintenanceLock, releaseStatsMaintenanceLock, touchStatsMaintenanceLock, type PersistedRoutedModelRef, type PersistedStatsShardV7, SHARD_FILES_DIR, SHARD_GLOBAL_EPOCH_PATH, SHARD_RETENTION_MS, SHARD_STATE_DIR, STATE_FILE_PATH, type ShardAggregate, type UsageSnapshot, addUsageToCacheStats, advanceGlobalStatsEpoch, advanceModelStatsEpoch, aggregateStatsShardsV7, cleanupStatsShardsV7, cloneCacheStats, mapWithConcurrency, currentLocalDay, emptyAllCacheStats, emptyCacheStats, initialEpoch, loadStatsShardAggregateV7, modelEpochPath, parseCacheStats, parsePersistedRoutedModelRef, parsePersistedStatsShardV7, readGlobalStatsEpoch, readModelStatsEpoch, readValidStatsShardsV7, removeLegacyStatsFiles, writeStatsShardV7 } from "./src/stats-store.ts";
17
18
  import { PI_VIRTUAL_MODEL_API, getCompat, isAnthropicMessagesApi, isKimiCodingEmptySignatureModel, isKnownThirdPartyOpenAIEndpoint, isMistralConversationsApi, isNativeVirtualModel, isOfficialOpenAIBaseUrl, isOpenAICompatibleApi, isOpenAICompatibleProxyApi, isOpenAIFamilyAssistantMessage, isOpenAIFamilyModel, isOpenAIFamilyToken, isPiBuiltInLlamaCppModel, isResponsesPromptRewriteBypassApi, isRoutedFallbackModel, isValidModelsConfigForEffectiveCompat, modelKey, readEffectiveCompatConfig } from "./src/model-identity.ts";
18
19
  import { CONFIG_FILE_PATH, VIRTUAL_REWRITE_ENV, FOOTER_MODE_ENV, TOOL_ORDER_ENV, featureEnabled, footerStatsMode, isEnabledEnv, isToolOrderEnabled, parseFooterStatsMode, parsePersistedCacheOptimizerConfig, readPersistedCacheOptimizerConfig, resolveFooterStatsMode, runtimeOptimizerEnabled, setPersistedCacheOptimizerConfig, shouldInjectOpenAIPromptCacheKey, writePersistedCacheOptimizerConfig, writePersistedFeature, writePersistedFooterMode } from "./src/config.ts";
19
20
  import { decideZeroPriceWarming, readCacheWarmingMode } from "./src/cache-warming.ts";
@@ -323,6 +324,22 @@ export const __internals_for_tests = {
323
324
  aggregateStatsShardsV7,
324
325
  loadStatsShardAggregateV7,
325
326
  cleanupStatsShardsV7,
327
+ compactStatsShardsV7,
328
+ maybeRunStatsMaintenance,
329
+ readStatsArchive,
330
+ readStatsArchiveView,
331
+ aggregateStatsWithArchiveV7,
332
+ encodeStatsArchiveV2,
333
+ parseStatsArchiveV2,
334
+ listStatsArchiveGenerations,
335
+ statsArchiveGenerationPath,
336
+ StatsArchiveView,
337
+ touchStatsMaintenanceLock,
338
+ acquireStatsMaintenanceLock,
339
+ holdsStatsMaintenanceLock,
340
+ releaseStatsMaintenanceLock,
341
+ SHARD_ARCHIVE_PATH,
342
+ SHARD_RETENTION_MS,
326
343
  mapWithConcurrency,
327
344
  removeLegacyStatsFiles,
328
345
  readGlobalStatsEpoch,
@@ -599,15 +616,34 @@ export default function (pi: ExtensionAPI) {
599
616
  }
600
617
 
601
618
  async function refreshShardAggregate(): Promise<ShardAggregate> {
602
- const persistedShards = await readValidStatsShardsV7();
619
+ // Order matters: compaction publishes the archive before deleting the
620
+ // shards it absorbed, so reading the directory first and the archive
621
+ // second always sees each absorbed shard in at least one of them. Reading
622
+ // them concurrently could pair an old archive with a post-delete listing
623
+ // and drop those counters (and the last routed model) for this refresh.
624
+ const rawShards = await readValidStatsShardsV7();
625
+ const archive = await readStatsArchiveView();
626
+ // Shards folded into the archive are counted there. Ignore their raw file
627
+ // (a crash may leave it behind) and this process's in-memory retired copy.
628
+ for (const id of archive.absorbedIds) retiredShards.delete(id);
629
+ const persistedShards = rawShards.filter((shard) => !archive.absorbedIds.has(shard.instanceId));
603
630
  for (const shard of persistedShards) retiredShards.delete(shard.instanceId);
604
631
  const shards = persistedShards.filter((shard) => shard.instanceId !== instanceId);
605
632
  shards.push(...retiredShards.values());
606
- if (currentSessionHashSet) shards.push(buildCurrentStatsShard());
633
+ const trailing = currentSessionHashSet ? [buildCurrentStatsShard()] : [];
607
634
  // Keep lifetime session footer data separate from daily totals/commands.
608
- // null explicitly means all retained days; undefined invokes the default day.
609
- const aggregate = await aggregateStatsShardsV7(shards);
610
- const sessionAggregate = await aggregateStatsShardsV7(shards, null);
635
+ // Both equal aggregating [...shards, ...archive.shards, ...trailing]
636
+ // (issue #28 review H): the current day only sees archive entries of that
637
+ // day (normally none: the archive holds past days), and the all-days view
638
+ // merges the archive's pre-reduced per-(session, model) entries instead
639
+ // of re-aggregating every archived bucket. The all-days view is only read
640
+ // for the current session (cacheStatsByModel lookups and the last routed
641
+ // model), so it is built for that session alone; rebuilding every
642
+ // archived session's entries cost more than the rest of the refresh.
643
+ const aggregate = await aggregateStatsWithArchiveV7(shards, archive, trailing, currentLocalDay());
644
+ const sessionAggregate = await aggregateStatsWithArchiveV7(
645
+ shards, archive, trailing, null, new Set(currentSessionHashSet ? [currentSessionHash] : []),
646
+ );
611
647
 
612
648
  // A reset from another process can advance epochs while this instance is
613
649
  // idle. Adopt those epochs during every aggregate refresh, not only when a
@@ -885,7 +921,7 @@ export default function (pi: ExtensionAPI) {
885
921
  // no second directory pass is needed. Cleanup also warms the parsed-shard
886
922
  // cache, so the scan below only re-stats files.
887
923
  try {
888
- await maybeCleanupStatsShardsV7();
924
+ await maybeRunStatsMaintenance();
889
925
  } catch (error) {
890
926
  console.warn(`${LOG_PREFIX}: stats shard maintenance failed`, error);
891
927
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.8.27",
3
+ "version": "2.9.1",
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",