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 +11 -9
- package/README.zh-CN.md +12 -10
- package/index.ts +55 -36
- package/package.json +3 -3
- package/src/stats-store.ts +17 -6
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.
|
|
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
|
|
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
|
|
142
|
-
| `total` | Aggregate
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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`(默认) |
|
|
142
|
-
| `total` |
|
|
143
|
-
| `process` | 仅显示当前 extension instance
|
|
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.
|
|
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.
|
|
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.
|
|
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`
|
|
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
|
-
|
|
446
|
+
let instanceId = randomUUID();
|
|
447
447
|
const instanceStartedAt = Date.now();
|
|
448
|
-
|
|
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(
|
|
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 =
|
|
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:
|
|
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(
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
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
|
|
841
|
-
await
|
|
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 =
|
|
1092
|
-
strippedPrompt
|
|
1093
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
41
|
-
"@earendil-works/pi-server": "1.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"
|
package/src/stats-store.ts
CHANGED
|
@@ -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
|
-
|
|
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] =
|
|
390
|
-
result.totalsByModel[key] =
|
|
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);
|