pi-cache-optimizer 2.8.17 → 2.8.19

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
@@ -62,7 +62,7 @@ Run `/reload` in Pi after install/update/remove so extension hooks refresh.
62
62
 
63
63
  On Pi 0.79.7 and newer, `pi update` updates Pi itself only. To update installed Pi packages such as this extension, run `pi update --extensions` (packages only) or `pi update --all` (Pi + packages).
64
64
 
65
- This extension requires Pi 0.82+ and is validated against Pi 1.0.0. It uses the official Pi package types directly for type-checking, along with extension hooks, `getAgentDir()`, and prompt options shared by those versions; it does not depend on Pi 0.83+ APIs such as `ctx.scopedModels` or the bundled TypeBox 1.3 aliases. Native virtual model support and codemode nested-call coalescing activate only on Pi 0.99+ hosts that produce them; older hosts keep the previous behavior. The local development baseline uses Node.js 22.19.0 or newer, matching Pi 1.0.0's engine requirement.
65
+ This extension requires Pi 0.82+ and is validated against Pi 1.0.2. It uses the official Pi package types directly for type-checking, along with extension hooks, `getAgentDir()`, and prompt options shared by those versions; it does not depend on Pi 0.83+ APIs such as `ctx.scopedModels` or the bundled TypeBox 1.3 aliases. Native virtual model support and codemode nested-call coalescing activate only on Pi 0.99+ hosts that produce them; older hosts keep the previous behavior. The local development baseline uses Node.js 22.19.0 or newer, matching Pi 1.0.2's engine requirement.
66
66
 
67
67
  ## Commands
68
68
 
@@ -110,6 +110,8 @@ Persistent feature settings are managed under the Pi agent directory by the nati
110
110
  /cache-optimizer config reset
111
111
  ```
112
112
 
113
+ `openai-codex-responses` keeps Pi's native skill index even when skill compression is enabled. Real task-level benchmarks with `openai-codex/gpt-6-luna` showed that the shorter Markdown skill index could increase the model's tool-call trajectory and total task cost; Codex therefore keeps the native Pi XML skill list while still benefiting from the optimizer's other cache/runtime features.
114
+
113
115
  `config reset` reads the latest disk configuration under the shared transaction lock and removes only persistent feature overrides, preserving the latest footer mode, model-specific prompt-cache-key settings, and file permissions. Invalid JSON/schema, symlinks, non-regular targets, and concurrent manual changes are refused rather than overwritten. These commands do not modify shell startup files or `PI_CACHE_RETENTION`; `enable` and `disable` remain current-process runtime switches.
114
116
 
115
117
  ## Opt-in deterministic tool ordering
@@ -130,17 +132,17 @@ The fixture verifier reports numeric tool-order changes and confirms cache-marke
130
132
 
131
133
  ## Footer cache stats mode
132
134
 
133
- Current versions store stats under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory. Each loaded extension instance owns one UUID-named shard and writes it through temp-file + atomic rename. 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.
135
+ 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.
134
136
 
135
- 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 48 hours; young corrupt files, valid current-day shards, live old instances, and symlink targets remain protected.
137
+ 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.
136
138
 
137
139
  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:
138
140
 
139
141
  | Value | Effect |
140
142
  |---|---|
141
- | `session` (default) | Aggregate today's shards carrying the current hashed Pi conversation session id and exact provider/model. A reload creates a new instance shard but remains in the same session scope. |
142
- | `total` | Aggregate today's valid local shards for the exact provider/model across sessions, including child Pi agents that load this extension and share the same agent directory. |
143
- | `process` | Show only counters collected by the current extension instance. It starts at `0/0` after Pi restart or extension reload. |
143
+ | `session` (default) | Aggregate all retained shards carrying the current hashed Pi conversation session id and exact provider/model, including across local-day boundaries. A reload creates a new instance shard but remains in the same lifetime session scope. |
144
+ | `total` | Aggregate the current local day's valid shards for the exact provider/model across sessions, including child Pi agents that load this extension and share the same agent directory. |
145
+ | `process` | Show only today's counters collected by the current extension instance. It starts at `0/0` at local midnight, after Pi restart, or extension reload. |
144
146
 
145
147
  Persistent command configuration takes precedence over the environment variable:
146
148
 
@@ -154,7 +156,7 @@ The explicit setting is stored in `pi-cache-optimizer-config.json` under Pi's ag
154
156
 
155
157
  ## Per-model `prompt_cache_key` opt-out
156
158
 
157
- Some OpenAI-compatible Completions endpoints reject `prompt_cache_key` with HTTP 400 even though the same field is valid for other providers. Pi 1.0.0 owns the key for Responses/Codex transports; this extension's key opt-out and fallback apply only to `openai-completions`. Pi 1.0.0 has no native `supportsPromptCacheKey` compat field; do **not** add that unknown field to `models.json`. `supportsLongCacheRetention` is not an equivalent switch and should not be used for this purpose.
159
+ Some OpenAI-compatible Completions endpoints reject `prompt_cache_key` with HTTP 400 even though the same field is valid for other providers. Pi 1.0.2 owns the key for Responses/Codex transports; this extension's key opt-out and fallback apply only to `openai-completions`. Pi 1.0.2 has no native `supportsPromptCacheKey` compat field; do **not** add that unknown field to `models.json`. `supportsLongCacheRetention` is not an equivalent switch and should not be used for this purpose.
158
160
 
159
161
  When the extension observes an explicit field-level `prompt_cache_key` unsupported error for the exact provider/model, ordinary `/cache-optimizer fix` offers a confirmed model-scoped repair. Value-validation failures and conditional restrictions such as “not allowed when temperature is set” do not qualify. If concurrent responses from different models cannot be correlated because Pi provides no request ID, header-only evidence is ignored unless the finalized assistant message supplies exact provider/model identity. If you already know that the endpoint rejects the field, use the explicit command:
160
162
 
@@ -194,7 +196,7 @@ Notes:
194
196
 
195
197
  - `sendSessionAffinityHeaders: true` is the safe default when your proxy supports sticky routing.
196
198
  - `supportsLongCacheRetention: true` is optional. Add it only when the endpoint explicitly supports OpenAI long prompt cache retention.
197
- - Do not add `supportsPromptCacheKey` to `models.json`: Pi 1.0.0 does not define that compat field. Use `/cache-optimizer fix prompt-cache-key` to store an exact provider/model omit rule in the extension-owned config; it removes both key spellings, including a key supplied by Pi.
199
+ - Do not add `supportsPromptCacheKey` to `models.json`: Pi 1.0.2 does not define that compat field. Use `/cache-optimizer fix prompt-cache-key` to store an exact provider/model omit rule in the extension-owned config; it removes both key spellings, including a key supplied by Pi.
198
200
  - If you see `400 Unsupported parameter: prompt_cache_retention`, remove/avoid `supportsLongCacheRetention` for that channel. Keep `sendSessionAffinityHeaders` if supported. The extension detects the explicit error from response headers or the finalized assistant error message and strips the parameter from subsequent requests in the current process.
199
201
  - Use `/cache-optimizer compat` or `/cache-optimizer doctor` to see model-specific advice.
200
202
  - DeepSeek model names select the `DS cache` adapter only; they do not prove a reasoning wire protocol. Generic cache/routing advice remains active for absent or non-DeepSeek formats. DeepSeek replay advice is shown only when effective `compat.thinkingFormat: "deepseek"` is explicitly configured; it never treats `thinkingFormat` as a missing fix key.
@@ -362,7 +364,7 @@ Rollback creates a new access-mode-preserving backup and uses atomic replacement
362
364
 
363
365
  ## Footer stats
364
366
 
365
- Stats are read-only local counters stored as UUID-owned shards under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory (custom agent dirs use `PI_CODING_AGENT_DIR`). Shards contain only dates, opaque session hashes, exact provider/model counters, reset epochs, and process lifecycle metadata — no API keys, prompts, payloads, headers, responses, or model output. The footer defaults to the current conversation session; `total` aggregates all valid current-day shards for the exact provider/model, and `process` shows only the current extension instance. Footer mode configuration is stored separately in `pi-cache-optimizer-config.json`. Upgrading from the old v6 shared stats files deletes those local counters instead of migrating them.
367
+ Stats are read-only local counters stored as UUID-owned shards under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory (custom agent dirs use `PI_CODING_AGENT_DIR`). Shards contain only dates, opaque session hashes, exact provider/model counters, reset epochs, and process lifecycle metadata — no API keys, prompts, payloads, headers, responses, or model output. The footer defaults to the current conversation session and retains that session's counters across local-day boundaries; `total` aggregates only current-day shards, and `process` shows only the current extension instance. Footer mode configuration is stored separately in `pi-cache-optimizer-config.json`. Upgrading from the old v6 shared stats files deletes those local counters instead of migrating them.
366
368
 
367
369
  Pi 0.79+ also includes a built-in footer `CH` marker for the latest prompt cache hit rate. This extension complements that marker with persisted provider/model counters plus proxy compat diagnostics.
368
370
 
package/README.zh-CN.md CHANGED
@@ -62,7 +62,7 @@ pi remove npm:pi-deepseek-cache-optimizer && pi install npm:pi-cache-optimizer
62
62
 
63
63
  Pi 0.79.7 及之后,`pi update` 默认只更新 Pi 本体。若要更新已安装的 Pi package(包括本扩展),请运行 `pi update --extensions`(只更新 packages)或 `pi update --all`(Pi 与 packages 一起更新)。
64
64
 
65
- 本扩展要求 Pi 0.82+,并已使用 Pi 1.0.0 验证。TypeScript 校验直接使用官方 Pi package 类型,同时只使用这些版本共有的 extension hooks、`getAgentDir()` 和 prompt options;不依赖 Pi 0.83+ 专有 API(例如 `ctx.scopedModels` 或 bundled TypeBox 1.3 aliases)。原生虚拟模型支持与 codemode 嵌套工具调用合并只在会产生它们的 Pi 0.99+ 上生效,较早版本保持原有行为。本地开发基线需要 Node.js 22.19.0 或更高版本,与 Pi 1.0.0 的 engine 要求一致。
65
+ 本扩展要求 Pi 0.82+,并已使用 Pi 1.0.2 验证。TypeScript 校验直接使用官方 Pi package 类型,同时只使用这些版本共有的 extension hooks、`getAgentDir()` 和 prompt options;不依赖 Pi 0.83+ 专有 API(例如 `ctx.scopedModels` 或 bundled TypeBox 1.3 aliases)。原生虚拟模型支持与 codemode 嵌套工具调用合并只在会产生它们的 Pi 0.99+ 上生效,较早版本保持原有行为。本地开发基线需要 Node.js 22.19.0 或更高版本,与 Pi 1.0.2 的 engine 要求一致。
66
66
 
67
67
  ## 命令
68
68
 
@@ -110,6 +110,8 @@ Doctor 显示的 endpoint URL 会移除用户名/密码、查询参数和 fragme
110
110
  /cache-optimizer config reset
111
111
  ```
112
112
 
113
+ 对于 `openai-codex-responses`,即使开启 skill compression,也会保留 Pi 原生 skill index。真实 `openai-codex/gpt-6-luna` 任务级 benchmark 表明,较短的 Markdown skill index 虽然减少了单次 prompt,但可能增加模型的工具调用轨迹和最终任务总成本;因此 Codex 保留 Pi 原生 XML skill 列表,同时继续使用本扩展的其他缓存与运行时优化。
114
+
113
115
  `config reset` 在共享事务锁内读取最新磁盘配置,只移除持久化 feature 覆盖,保留最新 footer mode、按模型配置的 prompt-cache-key 设置和文件权限。JSON/schema 无效、symlink、非普通文件或并发手动修改时会拒绝覆盖。这些命令不会修改 shell 启动文件或 `PI_CACHE_RETENTION`;`enable` 和 `disable` 仍然是当前进程的运行时开关。
114
116
 
115
117
  ## Opt-in 确定性工具排序
@@ -130,17 +132,17 @@ bun .trellis/tasks/09-03-context-epoch-tool-ordering/verify.ts
130
132
 
131
133
  ## Footer 缓存统计模式
132
134
 
133
- 当前版本把统计保存在 Pi agent 目录的 `pi-cache-optimizer-stats.d/shards/` 下。每个已加载的 extension instance 独占一个 UUID 命名 shard,并通过临时文件 + 原子 rename 写入,因此父/子/并行 Pi 进程不会互相覆盖。旧 v6 单文件统计在升级时直接删除,本地 footer 计数从零开始;不会影响上游 provider 的实际缓存。
135
+ 当前版本把统计保存在 Pi agent 目录的 `pi-cache-optimizer-stats.d/shards/` 下。每个已加载的 extension instance 独占一个当天的活跃 UUID shard,并通过临时文件 + 原子 rename 写入;本地日期切换时关闭旧 shard、创建新的 UUID,保留 session 历史,因此父/子/并行 Pi 进程不会互相覆盖。旧 v6 单文件统计在升级时直接删除,本地 footer 计数从零开始;不会影响上游 provider 的实际缓存。
134
136
 
135
- 即使等待响应时切换到同一 provider 的另一个模型,响应统计仍归属于实际请求的 catalog/request-local 模型。旧 transport 的响应别名只根据请求快照处理,不使用新选择的模型;footer 继续显示当前选择的 direct 模型。损坏 JSON/schema 的旧 shard 按文件修改时间在 48 小时后清理;较新的损坏文件、有效的当天 shard、仍存活的旧 instance 和 symlink 目标均受保护。
137
+ 即使等待响应时切换到同一 provider 的另一个模型,响应统计仍归属于实际请求的 catalog/request-local 模型。旧 transport 的响应别名只根据请求快照处理,不使用新选择的模型;footer 继续显示当前选择的 direct 模型。损坏 JSON/schema 的旧 shard 按文件修改时间在两个月后清理;较新的损坏文件、有效的当天 shard、仍存活的旧 instance 和 symlink 目标均受保护。
136
138
 
137
139
  Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 provider/model 时污染当前窗口。可以通过命令或环境变量切换显示范围:
138
140
 
139
141
  | 值 | 作用 |
140
142
  |---|---|
141
- | `session`(默认) | 聚合今天携带当前 hashed Pi conversation session id 且精确 provider/model 相同的 shard。Extension reload 会产生新 instance shard,但仍属于同一 session 范围。 |
142
- | `total` | 聚合今天同一精确 provider/model 的全部有效本地 shard,包括加载本扩展并共享同一 agent 目录的子 Pi agent。 |
143
- | `process` | 仅显示当前 extension instance 采集的统计。Pi 重启或 extension reload 后从 `0/0` 开始。 |
143
+ | `session`(默认) | 聚合所有携带当前 hashed Pi conversation session id 且精确 provider/model 相同的保留 shard,跨越本地日期仍会累计。Extension reload 会产生新 instance shard,但仍属于同一长期 session 范围。 |
144
+ | `total` | 聚合当前本地日期同一精确 provider/model 的全部有效本地 shard,包括加载本扩展并共享同一 agent 目录的子 Pi agent。 |
145
+ | `process` | 仅显示当前 extension instance 当天采集的统计。本地日期切换、Pi 重启或 extension reload 后从 `0/0` 开始。 |
144
146
 
145
147
  持久命令配置优先于环境变量:
146
148
 
@@ -154,7 +156,7 @@ Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 prov
154
156
 
155
157
  ## 按模型关闭 `prompt_cache_key`
156
158
 
157
- 某些 OpenAI-compatible Completions endpoint 会因 `prompt_cache_key` 返回 HTTP 400,但同一个字段对其他 provider 可能有效。Pi 1.0+ 已负责 Responses/Codex transport 的 key;本扩展的 fallback 和 opt-out 只作用于 `openai-completions`。Pi 1.0.0 没有原生的 `supportsPromptCacheKey` compat 字段,**不要**把这个未知字段加入 `models.json`。`supportsLongCacheRetention` 也不是等价开关,不应借用来实现此目的。
159
+ 某些 OpenAI-compatible Completions endpoint 会因 `prompt_cache_key` 返回 HTTP 400,但同一个字段对其他 provider 可能有效。Pi 1.0+ 已负责 Responses/Codex transport 的 key;本扩展的 fallback 和 opt-out 只作用于 `openai-completions`。Pi 1.0.2 没有原生的 `supportsPromptCacheKey` compat 字段,**不要**把这个未知字段加入 `models.json`。`supportsLongCacheRetention` 也不是等价开关,不应借用来实现此目的。
158
160
 
159
161
  当扩展观察到精确 provider/model 对 `prompt_cache_key` 的明确字段级拒绝后,普通 `/cache-optimizer fix` 会提供经过确认的模型级修复。参数值校验失败,以及“设置 temperature 时不允许”这类条件限制都不构成证据。若不同模型的响应并发交错,而 Pi 又没有提供 request ID,扩展会忽略无法安全关联的 response-header 证据;只有最终 assistant message 提供精确 provider/model 身份时才恢复归因。如果你已经确定 endpoint 不支持该字段,可以主动执行:
160
162
 
@@ -168,7 +170,7 @@ Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 prov
168
170
 
169
171
  LiteLLM / OneAPI / NewAPI / 类 OpenRouter 渠道等第三方 `openai-completions` 代理,常会把同一个 session 分散到多个上游后端,导致 provider 侧 prompt cache 被拆散。
170
172
 
171
- Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention` 的模型兼容性;本扩展不按 provider 名称增加特殊分支,而是结合 `models.json` 与 runtime model,按精确 provider/model 解析有效 compat。Pi 0.81+ 也内置了使用 OpenAI-shaped transport 的 `llama.cpp` provider。本扩展只对第三方 `openai-completions` 保留 session-id key fallback;Pi 1.0+ 已负责 Responses/Codex transport 的 key。只有符合 Pi 内置 provider 明确 compat 指纹的模型会跳过通用 proxy 路由 / session-affinity 建议;仅复用 `llama.cpp` id 的自定义或覆盖 provider 仍按普通 OpenAI-compatible 渠道处理。`prompt_cache_retention` 继续遵循统一安全规则:仅官方 OpenAI 或 `models.json` 中有效配置为 `supportsLongCacheRetention: true` 时保留,否则发送前移除。Pi 1.0.0 没有原生的 `supportsPromptCacheKey` compat 字段,因此仅对 `openai-completions` 按模型关闭 key 的策略保存在本扩展的 `pi-cache-optimizer-config.json` 中,而不是 `models.json`。扩展配置独立于 Pi 的 compat 优先级,只影响其中列出的精确 provider/model。
173
+ Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention` 的模型兼容性;本扩展不按 provider 名称增加特殊分支,而是结合 `models.json` 与 runtime model,按精确 provider/model 解析有效 compat。Pi 0.81+ 也内置了使用 OpenAI-shaped transport 的 `llama.cpp` provider。本扩展只对第三方 `openai-completions` 保留 session-id key fallback;Pi 1.0+ 已负责 Responses/Codex transport 的 key。只有符合 Pi 内置 provider 明确 compat 指纹的模型会跳过通用 proxy 路由 / session-affinity 建议;仅复用 `llama.cpp` id 的自定义或覆盖 provider 仍按普通 OpenAI-compatible 渠道处理。`prompt_cache_retention` 继续遵循统一安全规则:仅官方 OpenAI 或 `models.json` 中有效配置为 `supportsLongCacheRetention: true` 时保留,否则发送前移除。Pi 1.0.2 没有原生的 `supportsPromptCacheKey` compat 字段,因此仅对 `openai-completions` 按模型关闭 key 的策略保存在本扩展的 `pi-cache-optimizer-config.json` 中,而不是 `models.json`。扩展配置独立于 Pi 的 compat 优先级,只影响其中列出的精确 provider/model。
172
174
 
173
175
  对真正的代理,建议先启用 session affinity:
174
176
 
@@ -194,7 +196,7 @@ Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention`
194
196
 
195
197
  - `sendSessionAffinityHeaders: true` 是安全默认项,前提是你的代理支持 sticky routing。
196
198
  - `supportsLongCacheRetention: true` 是可选项。只有 endpoint 明确支持 OpenAI long prompt cache retention 时才添加。
197
- - 不要把 `supportsPromptCacheKey` 加入 `models.json`:Pi 1.0.0 没有定义这个 compat 字段。请使用 `/cache-optimizer fix prompt-cache-key` 在扩展自有配置中保存精确 provider/model 的 omit 规则;它会移除两种 key 写法,包括 Pi 提供的 key。
199
+ - 不要把 `supportsPromptCacheKey` 加入 `models.json`:Pi 1.0.2 没有定义这个 compat 字段。请使用 `/cache-optimizer fix prompt-cache-key` 在扩展自有配置中保存精确 provider/model 的 omit 规则;它会移除两种 key 写法,包括 Pi 提供的 key。
198
200
  - 如果出现 `400 Unsupported parameter: prompt_cache_retention`,请为该渠道移除 / 避免 `supportsLongCacheRetention`;如支持,可保留 `sendSessionAffinityHeaders`。扩展会从响应头或最终 assistant error message 中识别这条明确错误,并在当前进程的后续请求中移除该参数。
199
201
  - 使用 `/cache-optimizer compat` 或 `/cache-optimizer doctor` 查看当前模型的具体建议。
200
202
  - DeepSeek 模型名只用于选择 `DS cache` adapter,不能证明 reasoning wire protocol。缺少或使用非 DeepSeek format 时仍保留通用缓存 / 路由建议;只有 effective `compat.thinkingFormat: "deepseek"` 被明确配置时,才显示 DeepSeek replay 建议,且不会把 `thinkingFormat` 列为缺失修复项。
@@ -361,7 +363,7 @@ Rollback 会创建新的、保留访问权限的备份并使用原子替换。
361
363
 
362
364
  ## Footer 统计
363
365
 
364
- 统计是只读本地计数,保存在 Pi agent 目录的 UUID shard(`pi-cache-optimizer-stats.d/shards/`;自定义 agent 目录使用 `PI_CODING_AGENT_DIR`)。Shard 只包含日期、opaque session hash、精确 provider/model 计数、reset epoch 和进程生命周期元数据,不包含 API key、prompt、payload、headers、响应或模型输出。Footer 默认显示当前 conversation session;`total` 聚合同一精确 provider/model 的所有有效本地 shard,`process` 只显示当前 extension instance。旧 v6 共享统计文件升级时直接删除,不迁移旧计数。
366
+ 统计是只读本地计数,保存在 Pi agent 目录的 UUID shard(`pi-cache-optimizer-stats.d/shards/`;自定义 agent 目录使用 `PI_CODING_AGENT_DIR`)。Shard 只包含日期、opaque session hash、精确 provider/model 计数、reset epoch 和进程生命周期元数据,不包含 API key、prompt、payload、headers、响应或模型输出。Footer 默认显示当前 conversation session,并跨本地日期累计该 session;`total` 只聚合当前本地日期的同一精确 provider/model shard,`process` 只显示当前 extension instance。旧 v6 共享统计文件升级时直接删除,不迁移旧计数。
365
367
 
366
368
  Pi 0.79+ 已内置 footer `CH` 标记,用于显示最近一次 prompt cache hit rate。本扩展在此基础上补充持久化的 provider/model 计数,以及代理 compat 诊断。
367
369
 
package/index.ts CHANGED
@@ -443,9 +443,13 @@ export default function (pi: ExtensionAPI) {
443
443
  let shardRefreshTimer: ReturnType<typeof setTimeout> | null = null;
444
444
  let shardWatcher: ReturnType<typeof watch> | undefined;
445
445
  const enqueuePersist = createSerializedAsyncRunner();
446
- const instanceId = randomUUID();
446
+ let instanceId = randomUUID();
447
447
  const instanceStartedAt = Date.now();
448
- const instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
448
+ let instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
449
+ let shardDay = currentLocalDay();
450
+ let rolloverPromise: Promise<void> | undefined;
451
+ // Failed day-close writes remain visible in memory until persisted or reload.
452
+ const retiredShards = new Map<string, PersistedStatsShardV7>();
449
453
  const modelApiByKey = new Map<string, string>();
450
454
  const modelNameByKey = new Map<string, string>();
451
455
  const modelEpochByKey = new Map<string, string>();
@@ -607,9 +611,14 @@ export default function (pi: ExtensionAPI) {
607
611
 
608
612
  async function refreshShardAggregate(): Promise<ShardAggregate> {
609
613
  const persistedShards = await readValidStatsShardsV7();
614
+ for (const shard of persistedShards) retiredShards.delete(shard.instanceId);
610
615
  const shards = persistedShards.filter((shard) => shard.instanceId !== instanceId);
616
+ shards.push(...retiredShards.values());
611
617
  if (currentSessionHashSet) shards.push(buildCurrentStatsShard());
618
+ // Keep lifetime session footer data separate from daily totals/commands.
619
+ // null explicitly means all retained days; undefined invokes the default day.
612
620
  const aggregate = await aggregateStatsShardsV7(shards);
621
+ const sessionAggregate = await aggregateStatsShardsV7(shards, null);
613
622
 
614
623
  // A reset from another process can advance epochs while this instance is
615
624
  // idle. Adopt those epochs during every aggregate refresh, not only when a
@@ -636,26 +645,19 @@ export default function (pi: ExtensionAPI) {
636
645
  }
637
646
 
638
647
  cacheStatsByModel = {};
639
- for (const [sessionHash, models] of Object.entries(aggregate.bySession)) {
648
+ for (const [sessionHash, models] of Object.entries(sessionAggregate.bySession)) {
640
649
  for (const [key, stats] of Object.entries(models)) {
641
650
  cacheStatsByModel[`${sessionHash}:${key}`] = stats;
642
651
  }
643
652
  }
644
653
  cacheStatsTotalsByModel = aggregate.totalsByModel;
645
654
  if (currentSessionHashSet) {
646
- lastActualRoutedModel = aggregate.lastRoutedModelBySession[currentSessionHash];
655
+ lastActualRoutedModel = sessionAggregate.lastRoutedModelBySession[currentSessionHash];
647
656
  }
648
657
  lastStatusText = undefined;
649
658
  return aggregate;
650
659
  }
651
660
 
652
- async function ensureCurrentEpochs(): Promise<void> {
653
- currentGlobalEpoch = await readGlobalStatsEpoch();
654
- for (const key of Object.keys(cacheStatsProcessByModel)) {
655
- modelEpochByKey.set(key, await readModelStatsEpoch(key));
656
- }
657
- }
658
-
659
661
  function buildCurrentStatsShard(state: "active" | "closed" = "active"): PersistedStatsShardV7 {
660
662
  const now = Date.now();
661
663
  const models: PersistedStatsShardV7["models"] = {};
@@ -683,7 +685,7 @@ export default function (pi: ExtensionAPI) {
683
685
  updatedAt: now,
684
686
  ...(state === "closed" ? { closedAt: now } : {}),
685
687
  },
686
- day: currentLocalDay(),
688
+ day: shardDay,
687
689
  globalEpoch: currentGlobalEpoch,
688
690
  models,
689
691
  ...(lastActualRoutedModel ? { lastRoutedModel: { ...lastActualRoutedModel } } : {}),
@@ -780,9 +782,10 @@ export default function (pi: ExtensionAPI) {
780
782
 
781
783
  function persistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
782
784
  const shard = buildCurrentStatsShard(lifecycleState);
785
+ const path = instanceShardPath;
783
786
  return enqueuePersist(async () => {
784
787
  try {
785
- await writeStatsShardV7(instanceShardPath, shard);
788
+ await writeStatsShardV7(path, shard);
786
789
  } catch (error) {
787
790
  console.warn(`${LOG_PREFIX}: failed to persist cache stats shard`, error);
788
791
  if (!persistenceWarningShown) {
@@ -810,6 +813,7 @@ export default function (pi: ExtensionAPI) {
810
813
  clearTimeout(persistTimer);
811
814
  persistTimer = null;
812
815
  }
816
+ await rollOverStatsIfNeeded(ctx);
813
817
  // Adopt resets performed by another Pi process before capturing the shard
814
818
  // snapshot. Otherwise shutdown/command flush could rewrite stale local
815
819
  // counters under an old epoch even though aggregation already hid them.
@@ -819,27 +823,31 @@ export default function (pi: ExtensionAPI) {
819
823
  }
820
824
 
821
825
  async function rollOverStatsIfNeeded(ctx?: ExtensionContext): Promise<void> {
826
+ if (rolloverPromise) return rolloverPromise;
822
827
  const day = currentLocalDay();
823
- let changed = false;
824
- for (const key of Object.keys(cacheStatsProcessByModel)) {
825
- if (cacheStatsProcessByModel[key]?.day !== day) {
826
- cacheStatsProcessByModel[key] = emptyCacheStats(day);
827
- changed = true;
828
- }
829
- }
830
- for (const id of CACHE_PROVIDER_IDS) {
831
- const stats = cacheStatsLegacyFamily[id];
832
- if (stats && stats.day !== day) {
833
- cacheStatsLegacyFamily[id] = emptyCacheStats(day);
834
- changed = true;
828
+ if (shardDay === day) return;
829
+ rolloverPromise = (async () => {
830
+ if (persistTimer !== null) {
831
+ clearTimeout(persistTimer);
832
+ persistTimer = null;
835
833
  }
836
- }
837
- if (changed) {
834
+ // Preserve yesterday under its original UUID/day before clearing daily
835
+ // counters. Each day owns a new shard so total never includes yesterday.
836
+ await refreshShardAggregate();
837
+ await persistCacheStats(ctx, "closed");
838
+ retiredShards.set(instanceId, buildCurrentStatsShard("closed"));
839
+ instanceId = randomUUID();
840
+ instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
841
+ shardDay = day;
838
842
  shardCreatedAt = Date.now();
843
+ cacheStatsProcessByModel = {};
844
+ cacheStatsLegacyFamily = emptyAllCacheStats();
839
845
  lastStatusText = undefined;
840
- await ensureCurrentEpochs();
841
- await flushPersistCacheStats(ctx);
842
- }
846
+ await persistCacheStats(ctx);
847
+ await refreshShardAggregate();
848
+ })();
849
+ try { await rolloverPromise; }
850
+ finally { rolloverPromise = undefined; }
843
851
  }
844
852
 
845
853
  async function restoreCacheStats(reason: string, ctx: ExtensionContext): Promise<void> {
@@ -1072,9 +1080,16 @@ export default function (pi: ExtensionAPI) {
1072
1080
  return {};
1073
1081
  }
1074
1082
 
1083
+ // OpenAI Codex/Responses is unusually sensitive to skill-index shape: real
1084
+ // TourStory task benchmarks showed the compressed Markdown index could
1085
+ // lengthen the Pi tool trajectory enough to increase total task cost even
1086
+ // though each request became shorter. Keep Pi's native skill block for the
1087
+ // Codex transport; other providers retain the compression optimization.
1088
+ const codexSkillCompressionBypass = model?.api === "openai-codex-responses";
1089
+
1075
1090
  // Skill compression first, as a section edit when Pi supports it (see
1076
1091
  // compressSkillsViaSection); the string path below is the fallback.
1077
- const skillsViaSection = compressSkillsViaSection(event);
1092
+ const skillsViaSection = codexSkillCompressionBypass ? false : compressSkillsViaSection(event);
1078
1093
 
1079
1094
  // Strip per-turn churn from <session-overview>.
1080
1095
  // Removing RECENT COMMITS, Working directory status, and
@@ -1088,14 +1103,18 @@ export default function (pi: ExtensionAPI) {
1088
1103
  // (Pi < 0.86, or a forced prompt from an earlier handler). Deterministic from the same `event.systemPromptOptions.skills`,
1089
1104
  // so cache stability is unchanged. No-op if opted out, below
1090
1105
  // SKILL_COMPRESSION_MIN_COUNT, or if pi emitted a format we don't recognize.
1091
- const compressedPrompt = compressSkillsInSystemPrompt(
1092
- strippedPrompt,
1093
- event.systemPromptOptions,
1094
- );
1106
+ const compressedPrompt = codexSkillCompressionBypass
1107
+ ? strippedPrompt
1108
+ : compressSkillsInSystemPrompt(
1109
+ strippedPrompt,
1110
+ event.systemPromptOptions,
1111
+ );
1095
1112
  // With a section edit and nothing to strip, Pi renders the compressed prompt
1096
1113
  // itself and no forced prompt is returned.
1097
1114
  recordSkillCompressionOutcome(
1098
- skillsViaSection
1115
+ codexSkillCompressionBypass
1116
+ ? { applied: false, reason: "OpenAI Codex uses Pi's native skill index to preserve task trajectory", visibleSkills, at: Date.now() }
1117
+ : skillsViaSection
1099
1118
  ? { applied: "section", visibleSkills, at: Date.now() }
1100
1119
  : compressedPrompt !== strippedPrompt
1101
1120
  ? { applied: "string", visibleSkills, at: Date.now() }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.8.17",
3
+ "version": "2.8.19",
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",
@@ -37,8 +37,8 @@
37
37
  "check:modules": "node scripts/check-module-graph.mjs"
38
38
  },
39
39
  "devDependencies": {
40
- "@earendil-works/pi-coding-agent": "^1.0.0",
41
- "@earendil-works/pi-server": "1.0.0",
40
+ "@earendil-works/pi-coding-agent": "^1.0.2",
41
+ "@earendil-works/pi-server": "1.0.2",
42
42
  "@types/node": "^22.0.0",
43
43
  "jiti": "^2.7.0",
44
44
  "typescript": "^5.8.3"
@@ -26,7 +26,9 @@ export const SHARD_CLEANUP_LOCK_PATH = join(SHARD_MAINTENANCE_DIR, "cleanup.lock
26
26
 
27
27
  export const SHARD_CLEANUP_MARKER_PATH = join(SHARD_MAINTENANCE_DIR, "last-cleanup");
28
28
 
29
- export const SHARD_RETENTION_MS = 48 * 60 * 60 * 1000;
29
+ // Retain closed and inactive shards for roughly two months so session-scoped
30
+ // statistics can survive long-running conversations without meaningful disk use.
31
+ export const SHARD_RETENTION_MS = 60 * 24 * 60 * 60 * 1000;
30
32
 
31
33
  export const SHARD_TEMP_RETENTION_MS = 24 * 60 * 60 * 1000;
32
34
 
@@ -188,6 +190,15 @@ export function mergeCacheStatsForTotal(existing: CacheStats | undefined, incomi
188
190
  return existing;
189
191
  }
190
192
 
193
+ /** Merge counters while retaining historical days for an explicitly session-scoped view. */
194
+ export function mergeStatsAcrossDays(existing: CacheStats | undefined, incoming: CacheStats, day: string | null): CacheStats {
195
+ if (day !== null) return mergeCacheStatsForTotal(existing, incoming);
196
+ if (!existing) return cloneCacheStats(incoming);
197
+ addCacheStatsTotals(existing, incoming);
198
+ existing.day = incoming.day > existing.day ? incoming.day : existing.day;
199
+ return existing;
200
+ }
201
+
191
202
  export function parsePersistedRoutedModelRef(value: unknown): PersistedRoutedModelRef | undefined {
192
203
  const record = asRecord(value);
193
204
  const provider = record?.provider;
@@ -352,7 +363,7 @@ export async function readValidStatsShardsV7(directory: string = SHARD_FILES_DIR
352
363
 
353
364
  export async function aggregateStatsShardsV7(
354
365
  shards: PersistedStatsShardV7[],
355
- day: string = currentLocalDay(),
366
+ day: string | null = currentLocalDay(),
356
367
  ): Promise<ShardAggregate> {
357
368
  const globalEpoch = await readGlobalStatsEpoch();
358
369
  const modelEpochs = new Map<string, string>();
@@ -371,7 +382,7 @@ export async function aggregateStatsShardsV7(
371
382
  const modelSessions = new Map<string, Set<string>>();
372
383
 
373
384
  for (const shard of shards) {
374
- if (shard.day !== day || shard.globalEpoch !== globalEpoch) continue;
385
+ if ((day !== null && shard.day !== day) || shard.globalEpoch !== globalEpoch) continue;
375
386
  let contributed = false;
376
387
  if (shard.lastRoutedModel && (routedUpdatedAt.get(shard.sessionHash) ?? -1) < shard.lifecycle.updatedAt) {
377
388
  routedUpdatedAt.set(shard.sessionHash, shard.lifecycle.updatedAt);
@@ -383,11 +394,11 @@ export async function aggregateStatsShardsV7(
383
394
  epoch = await readModelStatsEpoch(key);
384
395
  modelEpochs.set(key, epoch);
385
396
  }
386
- if (entry.modelEpoch !== epoch || entry.stats.day !== day) continue;
397
+ if (entry.modelEpoch !== epoch || (day !== null && entry.stats.day !== day)) continue;
387
398
  contributed = true;
388
399
  const sessionModels = result.bySession[shard.sessionHash] ??= {};
389
- sessionModels[key] = mergeCacheStatsForTotal(sessionModels[key], entry.stats);
390
- result.totalsByModel[key] = mergeCacheStatsForTotal(result.totalsByModel[key], entry.stats);
400
+ sessionModels[key] = mergeStatsAcrossDays(sessionModels[key], entry.stats, day);
401
+ result.totalsByModel[key] = mergeStatsAcrossDays(result.totalsByModel[key], entry.stats, day);
391
402
  result.instancesByModel[key] = (result.instancesByModel[key] ?? 0) + 1;
392
403
  if ((modelRefUpdatedAt.get(key) ?? -1) < shard.lifecycle.updatedAt) {
393
404
  modelRefUpdatedAt.set(key, shard.lifecycle.updatedAt);