@andares/pi-toolkits 0.9.7 → 0.10.0

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
@@ -10,12 +10,12 @@
10
10
  [![Pi package gallery](https://img.shields.io/badge/pi.dev-package%20gallery-6f42c1)](https://pi.dev/packages/@andares/pi-toolkits)
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
12
  [![Package Manager](https://img.shields.io/badge/package%20manager-pnpm-orange?logo=pnpm)](https://pnpm.io)
13
- [![pi >= 1.0](https://img.shields.io/badge/pi-%3E%3D1.0.0%20%3C1.12.0-blueviolet)](https://github.com/earendil-works/pi)
13
+ [![pi >= 0.99](https://img.shields.io/badge/pi-%3E%3D0.99.0-blueviolet)](https://github.com/earendil-works/pi)
14
14
  [![GitHub](https://img.shields.io/badge/github-andares%2Fpi--toolkits-181717?logo=github)](https://github.com/andares/pi-toolkits)
15
15
 
16
16
  In a nutshell: seven ready-to-use capabilities — **read-only consult mode** `ask`, **session-scoped boundary permission** `ask-perm`, **model favorites** `favorites`, **prompt stash** `stash`, **third-party code review** `third-review`, **dedicated compaction model** `compact-model` and **session startup automation** `autostart` — with more to come.
17
17
 
18
- **Requirements**: pi `>=1.0.0 <1.12.0` — the four most recently released minor lines (1.0.x / 1.1.x / 1.2.x / 1.3.x) are supported best-effort, and the upper bound encodes the forward probe: on every host release we check up to **8 minor lines** beyond the newest one (now 1.4 … 1.11) and never claim past that cap. **Measured natural compatibility: 1.3.0** (the newest release at the time of writing — nothing newer exists to probe yet). A single command, `pnpm check:compat`, derives that version set from the registry and runs, per version, in a sandbox under the system temp dir with the whole `@earendil-works/*` family pinned to the version under test: install → `tsc --noEmit` → full test suite → keybinding audit → hash-compare of the host `dist` files this package patches or mirrors. Results over 1.0.0 → 1.3.0 (all 8 published versions): typecheck, 473 tests and the keybinding audit pass on every one; the tracked host files drift only in `core/agent-session.js` (1.3.0 changed the tool-restore path; `cycleModel` untouched), `core/keybindings.js` and `core/compaction/utils.js` (both changed in 1.2.0, table semantics for our keys unchanged), and `extensions/types.d.ts` (additive) — `ModelSelectorComponent`, `core/compaction/compaction.js` and `core/usage-totals.js` are byte-identical across the whole window. Smoke-loading on real pi 1.3.0 produces no warnings. Note: pi ≥ 0.99 hosts require host-provided packages to be declared in `peerDependencies` with a `"*"` range — `@earendil-works/pi-tui` and `typebox` (the `scratchpad` tool builds a TypeBox schema) are declared that way; an installed copy in `dependencies` would make pi warn.
18
+ **Requirements**: pi `>=0.99.0`. Every published pi release from 0.99.0 through 1.4.0 has been verified with the full gate (install → `tsc --noEmit` → full test suite → keybinding audit → hash compare of the host files this package patches), each in a sandbox with the whole `@earendil-works/*` family pinned to the version under test. Newer releases are verified as they ship and the floor moves only when the measurement says so; `pnpm check:compat` reproduces the result. Note: pi ≥ 0.99 hosts require host-provided packages to be declared in `peerDependencies` with a `"*"` range — `@earendil-works/pi-tui` and `typebox` (the `scratchpad` tool builds a TypeBox schema) are declared that way; an installed copy in `dependencies` would make pi warn.
19
19
 
20
20
  ---
21
21
 
@@ -69,7 +69,7 @@ After installing, verify in any pi session:
69
69
  | Feature | Entry point | One-line description |
70
70
  | --- | --- | --- |
71
71
  | 🛡️ **ask** read-only consult mode | `/ask` | enter consult mode: `write`/`edit`/`powershell` hard-disabled, `bash` runs in a read-only sandbox, `grep`/`find`/`ls` activated, and `scratchpad` allowed to write throwaway notes into the session's temp directory |
72
- | 🔐 **permission** session-scoped boundary checks | `/ask-perm` | four in/out × read/write modes, off by default and never persisted; outside writes ask, credentials are denied, destructive commands always ask; composes with `/ask` |
72
+ | 🔐 **permission** session-scoped boundary checks | `/ask-perm` | four in/out × read/write modes, off by default and never persisted; outside writes ask, credentials are denied, destructive commands always ask; composes with `/ask`; a capped JSONL approval log feeds later rule mining |
73
73
  | ⭐ **favorites** model favorites | `ctrl+A` / `ctrl+J` inside `/model` | favorite the models you use most (marked in bold bright yellow), `ctrl+p` cycles only among favorites |
74
74
  | 📥 **stash** prompt stash | `ctrl+alt+y` | a stash slot for one prompt: press to swap, press twice to stash and clear the input box |
75
75
  | 🔎 **review** third-party code review | `/third-review` | summon the configured review model to check + fix the task just finished, focusing on "is this round's requirement implemented / is anything missing / is anything wrong" |
@@ -104,7 +104,7 @@ Enter with `/ask` (press it again to exit). Meant for the case where you only wa
104
104
 
105
105
  <a name="permission"></a>
106
106
 
107
- `/ask-perm` turns on boundary checks for the **current session only**. Every piece of state — the on/off flag, the four modes, the session grants — lives in the extension's memory. Nothing is written to disk, the feature is off by default, and it leaves no status behind when disabled. Toggling it on can never silently follow you into the next session.
107
+ `/ask-perm` turns on boundary checks for the **current session only**. Every piece of state — the on/off flag, the four modes, the session grants — lives in the extension's memory and is never persisted: the feature is off by default and leaves no status behind when disabled. Toggling it on can never silently follow you into the next session. Separately, decisions worth keeping are appended to a capped local **approval log** for later rule mining (see below); it is write-only telemetry and is never consulted while deciding.
108
108
 
109
109
  **Enable**
110
110
 
@@ -137,9 +137,36 @@ Repeating the argument form with the modes already in force is a **no-op**: no s
137
137
  - **credentials are denied in every mode** — `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.pi/agent/auth*.json`, `.env` files (templates excepted). Without this, `out read = allow` would hand out `~/.ssh/id_rsa`.
138
138
  - **destructive commands always ask** — `rm` of `/`, the home directory, or the working directory; `git reset --hard`; `git clean -f`; `git push --force`; package `publish`; `dd` to a raw device; `mkfs*`; shutdown/reboot; `chmod -R 777`; anything through `sudo`. Scope alone cannot express "destructive".
139
139
 
140
+ **Approval log** (write-only telemetry, on by default)
141
+
142
+ Every decision worth keeping becomes one JSON object per line (JSONL) in `~/.pi/agent/approval-log/audit-NNNNNN.jsonl`. The policy never reads the log: deleting it or making the directory read-only changes no verdict.
143
+
144
+ - **Scope**: blocks from `/ask` mode are approval decisions too, so they are recorded as well (with the gate name and the model-facing reason).
145
+ - **What is recorded**: interactive asks (the exact dialog lines, the bash decomposition, the chosen answer, the grant and its latency), automatic denials and blocks, session-grant allows, `/ask-perm` mode changes, and one `session` summary with call/ask/block counters. Plain mode-allowed `read`/`grep` calls are skipped by default so the ring buffer is not buried in noise; `PI_APPROVAL_LOG=all` also logs plain mode-allowed calls, `PI_APPROVAL_LOG=off` disables the log.
146
+ - **Rotation**: 1000 records **or** 8 MiB per file, at most 10 files; creating an 11th deletes the lowest-numbered one. Typical retention is 15–30 MB (hard ceiling 80 MB) and roughly the last 10,000 records.
147
+ - **Fidelity**: the four modes, session roots, tool call, every command unit (including `opaque` units and parser notes), each target's canonical path / effect / in-or-out scope, the deciding rule layer, the dialog lines, the answer and the resulting grant. `write`/`edit` file **contents are never logged** (only path and byte count); bearer tokens, `token=`/`password=`, URL credentials and known key prefixes are redacted.
148
+ - **Failure and privacy**: directory `0700`, files `0600`, local only, safe to delete at any time. A logging failure is reported once and swallowed — the verdict path never fails because the log is unavailable. `PI_APPROVAL_LOG_DIR` relocates the directory.
149
+
150
+ ```bash
151
+ # focus on the prompts that became session grants
152
+ jq -r 'select(.event=="ask" and .answer.choice=="allow-session")
153
+ | [.ts,.tool,.verdict.target,.input.command] | @tsv' \
154
+ ~/.pi/agent/approval-log/*.jsonl
155
+
156
+ # target roots × answers — the shortest path to a "safe temp dir" rule
157
+ jq -r 'select(.event=="ask") | [.verdict.grantRoot,.answer.choice] | @tsv' \
158
+ ~/.pi/agent/approval-log/*.jsonl | sort | uniq -c | sort -rn
159
+
160
+ # parser gaps (opaque units) worth teaching units.ts
161
+ jq -r 'select(.event=="ask") | .units[] | select(.opaque) | [.command,.note] | @tsv' \
162
+ ~/.pi/agent/approval-log/*.jsonl | sort | uniq -c | sort -rn
163
+ ```
164
+
165
+ See the repository's `pnpm approval-report` for a read-only summary (answer mix, target roots, rule candidates, opaque gaps, playwright patterns).
166
+
140
167
  **Composition with `/ask`**: the two features are independent gates on one bus. Each answers only its own question — ask: "does this mutate?", permission: "does this cross the boundary?" — and either can block. Ask mode still denies credential reads, and permission still applies while the read-only sandbox is active. Neither feature needs to know the other exists. The one exception is ask's `scratchpad`: that tool is self-jailed (relative names only, one temp directory), so ask marks it while active and permission skips the boundary dialog that would otherwise fire for an "outside write" on every note.
141
168
 
142
- **Enable it for every session** (the state itself is never persisted — this is the opt-in):
169
+ **Enable it for every session** (the state itself is never persisted — this is the opt-in; the audit log is independent and on by default):
143
170
 
144
171
  ```toml
145
172
  # <project>/.pi/autostart.toml, or <agent-dir>/autostart.toml for all projects
@@ -147,7 +174,7 @@ Repeating the argument form with the modes already in force is a **no-op**: no s
147
174
  run = ["/ask-perm allow allow allow ask"]
148
175
  ```
149
176
 
150
- > **Implementation notes**: a new session resets the state on `session_start` (a repeated emission for the same session keeps it); the footer shows `perm i:A/A o:A/ask` while on (`A` allow, `a` ask, `D` deny). Subagent sessions start with their own state and do not inherit the parent's mode — when child sessions must be gated too, enable it through the project's autostart file. POSIX hosts (Linux / macOS / WSL) are the tested target; native Windows Git-Bash path spellings are approximated, not verified. bash decomposition uses `shell-quote`; unknown variables expand to a sentinel that makes the unit ask instead of silently becoming an empty path. Added directories are read from pi-add-dir's session entry (`add-dir:state`). Heuristic, not a sandbox: project-internal destructive commands are covered by the fixed table, not by a sandbox. One known blind spot: a *read* command's bare filename operand (`cat notes.txt`) is not collected as a target, so with `out read = ask` only absolute-path-shaped read operands are questioned — reads never write, so the four modes' write side is unaffected.
177
+ > **Implementation notes**: a new session resets the state on `session_start` (a repeated emission for the same session keeps it); the footer shows `perm i:A/A o:A/ask` while on (`A` allow, `a` ask, `D` deny). Subagent sessions start with their own state and do not inherit the parent's mode — when child sessions must be gated too, enable it through the project's autostart file. The approval log is appended by the gate bus (asks/blocks/session summaries) and by the permission gate (mode changes and session-grant allows); the writer is lazy, keeps no state across processes, and never participates in a decision. POSIX hosts (Linux / macOS / WSL) are the tested target; native Windows Git-Bash path spellings are approximated, not verified. bash decomposition uses `shell-quote`; unknown variables expand to a sentinel that makes the unit ask instead of silently becoming an empty path. Added directories are read from pi-add-dir's session entry (`add-dir:state`). Heuristic, not a sandbox: project-internal destructive commands are covered by the fixed table, not by a sandbox. One known blind spot: a *read* command's bare filename operand (`cat notes.txt`) is not collected as a target, so with `out read = ask` only absolute-path-shaped read operands are questioned — reads never write, so the four modes' write side is unaffected.
151
178
 
152
179
  ---
153
180
 
@@ -255,11 +282,11 @@ pi's `/compact` and auto-compact summarize the context **with the current sessio
255
282
  | auto-compact threshold reached | summarized by the compaction model |
256
283
  | context overflow recovery | summarized by the compaction model, retry behavior unchanged |
257
284
 
258
- **Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim — copied from 0.87.1, byte-identical across every published version of the supported window, 1.0.0 → 1.3.0 — and only the executing model changes; the conversation serializer itself is imported from the host, so it follows whatever the running host does, e.g. 1.2.0 stops feeding assistant thinking back into the summarization request):
285
+ **Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim — copied from 0.87.1, byte-identical across 1.1.0 → 1.4.0 apart from the two 1.4.0 fixes adopted here: summaries request the model's output limit instead of a fixed fraction of `reserveTokens`, and `/compact <instructions>` also reaches the split-turn prefix — and only the executing model changes; the conversation serializer itself is imported from the host, so it follows whatever the running host does, e.g. 1.2.0 stops feeding assistant thinking back into the summarization request):
259
286
 
260
287
  - **Summary format** — the structured Goal / Progress / Key Decisions checkpoint; when a summary already exists, the iterative-update prompt is used so information carries over instead of being lost
261
288
  - **Split behavior** — the `keepRecentTokens` retention policy, and `firstKeptEntryId` / `tokensBefore` passed back unchanged
262
- - **Output budget** — the same formula as the built-in (history segment 0.8×, split-prefix segment 0.5× of `reserveTokens`, then capped by the model's max output)
289
+ - **Output budget** — summaries ask for the model's own output limit, exactly like the built-in since 1.4.0 (#9512: a fixed 0.8×/0.5× `reserveTokens` cap made long reasoning fail the summary with "generation hit the token cap"); `reserveTokens` only decides when compaction starts
263
290
  - **File-list tail** — `<read-files>` / `<modified-files>` tags are appended to the summary as usual, with details maintained in the same shape
264
291
  - **Usage accounting** — the compaction model's tokens/cost are counted in the session statistics
265
292
  - **Cancellable** — `Esc` during compaction cancels cleanly (the signal is passed through to the underlying call)
@@ -336,7 +363,7 @@ An **identical command line** declared by both sources runs once: a duplicate is
336
363
 
337
364
  **Key constraint: extension commands only**. Commands registered via `registerCommand` such as `/add-dir` (pi-add-dir) and `/cd` (pi-cd) work; built-in commands (`/compact`, `/reload`, `/resume`, …) are dispatched in the TUI layer and cannot be triggered from the extension layer; skill commands (`/skill:name`) are an expansion mechanism rather than an execution mechanism. The target extension you depend on (e.g. pi-add-dir) must be installed first.
338
365
 
339
- > **Implementation notes**: execution uses `sendUserMessage(cmd, { expandPromptTemplates: true })` — that option routes `/cmd args` into command dispatch instead of the model prompt path (the wrapper defaults to `false`; `sendUserMessage` and `_tryExecuteExtensionCommand` are byte-identical across 1.0.0–1.3.0, see the host-compatibility discipline in AGENTS.md). Pre-validation uses `pi.getCommands()` (accepting only `source: "extension"`). Depends on `smol-toml` (zero-dependency, ~10KB, TOML 1.0).
366
+ > **Implementation notes**: execution uses `sendUserMessage(cmd, { expandPromptTemplates: true })` — that option routes `/cmd args` into command dispatch instead of the model prompt path (the wrapper defaults to `false`; `sendUserMessage` and `_tryExecuteExtensionCommand` are byte-identical across 1.1.0–1.4.0, see the host-compatibility discipline in AGENTS.md). Pre-validation uses `pi.getCommands()` (accepting only `source: "extension"`). Depends on `smol-toml` (zero-dependency, ~10KB, TOML 1.0).
340
367
 
341
368
  ---
342
369
 
@@ -352,7 +379,8 @@ pnpm typecheck # tsc --noEmit
352
379
  pnpm test # vitest (all feature unit tests)
353
380
 
354
381
  pnpm check:keybindings # audit this plugin's keys against the host's own dispatch table
355
- pnpm check:compat # verify the declared pi range: last 4 minor lines + 8-line forward probe
382
+ pnpm check:compat # mandatory 4-line window + backward probe (4 more lines) → public floor
383
+ pnpm approval-report # summarize the local approval log (answer mix, targets, rule candidates)
356
384
  ```
357
385
 
358
386
  ### Project structure
@@ -363,6 +391,7 @@ src/
363
391
  ├── lib/ # shared infrastructure (tool-set snapshot/restore,
364
392
  │ # gate bus composing tool_call policies, jailed-tool
365
393
  │ # registry the boundary gate consults,
394
+ │ # rolling JSONL approval log,
366
395
  │ # model-key resolution shared by review & compact)
367
396
  └── features/ # feature modules — one directory per feature
368
397
  ├── ask/ # ask mode: command + state machine, bash sandbox,
@@ -377,7 +406,7 @@ src/
377
406
  │ # persisted store, tests
378
407
  ├── permission/ # session-scoped boundary permission: modes, panel,
379
408
  │ # bash units, policy, session grants, prompt,
380
- │ # tests
409
+ │ # audit payload, tests
381
410
  ├── review/ # third-party review: input/command triggers,
382
411
  │ # review-model config store, prompt, tests
383
412
  └── stash/ # prompt stash: hotkey state machine + tests
@@ -414,7 +443,7 @@ PRs and issues welcome. A few conventions:
414
443
  - **Built-in patches** (favorites) must stay **version-guarded and idempotent** — verify the target prototype members exist before patching, guard against double-apply, degrade with a `console.warn` instead of breaking the extension
415
444
  - **Built-in prompt sync** (compact) — the summarization prompts in `src/features/compact/prompt.ts` are copied verbatim from pi's built-in compaction; when upgrading the pi dependency, diff `dist/core/compaction/compaction.js` (and `utils.js`) and update the strings + assembly helpers to match, keeping takeover output byte-compatible with stock
416
445
  - **Keybindings** — before picking a new shortcut, check pi defaults + reserved keys, terminal control chars, and OS/terminal-emulator grabs; prefer `ctrl+alt+<letter>` for cross-platform safety. Do not stop at "it works on my machine in the main screen": in `fullscreen` mode (pi's default since 1.0) the alt-screen layer consumes `tui.altScreen.*` **before** dialogs see the key, and on Windows/WSL several of those bindings differ from the Linux defaults. Run `pnpm check:keybindings [candidate...]` (and `pnpm check:keybindings --all` after a pi upgrade) — it probes the installed host's resolved keybinding table with the host's own `matchesKey`. A key free in the host table can still be taken at the machine layer: Windows Terminal binds `ctrl+v` (paste) and `ctrl+shift+f` (find), and third-party Windows tools commonly grab `ctrl+alt+<letter>` (a screenshot tool once took `ctrl+alt+f`) — so try the key for real on the target machine before shipping it.
417
- - **Host compatibility** — the peer range is a measured statement, not a wish: the four most recently released pi minor lines are supported best-effort, the upper bound encodes the forward probe cap (newest line + 8 minor lines), and the README annotation must name the highest version actually verified. Re-run `pnpm check:compat` whenever the host dependency moves and read its verdict block (it fails if the declared range drifts from the policy). See the pi 宿主兼容 section of AGENTS.md; keybinding collisions found during an upgrade are reported to the maintainer for a decision, never re-keyed silently.
446
+ - **Host compatibility** — the peer range is a measured statement, not a wish, and it only points backwards: the four most recently released pi minor lines are a mandatory window (fix our code, never raise the floor), below it we probe up to four older lines to find where compatibility actually ends, and the single number users see is the resulting floor (`pi >= <floor>`, no upper bound). Re-run `pnpm check:compat` whenever the host dependency moves and read its verdict block (it fails if the declared floor drifts from the measured one). See the pi 宿主兼容 section of AGENTS.md; keybinding collisions found during an upgrade are reported to the maintainer for a decision, never re-keyed silently.
418
447
  - **Quality gate** — `pnpm typecheck && pnpm test` must pass before submitting
419
448
 
420
449
  ---
package/README.zh-CN.md CHANGED
@@ -10,12 +10,12 @@
10
10
  [![Pi package gallery](https://img.shields.io/badge/pi.dev-package%20gallery-6f42c1)](https://pi.dev/packages/@andares/pi-toolkits)
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
12
  [![Package Manager](https://img.shields.io/badge/package%20manager-pnpm-orange?logo=pnpm)](https://pnpm.io)
13
- [![pi >= 1.0](https://img.shields.io/badge/pi-%3E%3D1.0.0%20%3C1.12.0-blueviolet)](https://github.com/earendil-works/pi)
13
+ [![pi >= 0.99](https://img.shields.io/badge/pi-%3E%3D0.99.0-blueviolet)](https://github.com/earendil-works/pi)
14
14
  [![GitHub](https://img.shields.io/badge/github-andares%2Fpi--toolkits-181717?logo=github)](https://github.com/andares/pi-toolkits)
15
15
 
16
16
  一句话介绍:目前包含七个开箱即用的功能——**只读咨询模式** `ask`、**会话级边界权限** `ask-perm`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`、**会话启动自动化** `autostart`,后续持续追加。
17
17
 
18
- **版本要求**:pi `>=1.0.0 <1.12.0` —— 最近 4 个中版本线(1.0.x / 1.1.x / 1.2.x / 1.3.x)尽力支持,上界编码「向后探针」的硬上限:宿主每发一版,我们最多向后查 **8 条中版本线**(当前 1.4 … 1.11),再晚不宣称。**实测自然兼容最高 1.3.0**(写此文时的最新版本,更新的还没发布、无物可探)。一条命令 `pnpm check:compat` 从 registry 推导目标集合,逐版本在系统临时目录沙箱中(把整个 `@earendil-works/*` 家族钉在待测版本)跑:安装 → `tsc --noEmit` → 全量测试 → 键位审计 → 对 patch/mirror 的宿主 `dist` 文件做哈希比对。1.0.0 → 1.3.0 全部 8 个已发布版本:typecheck、473 个测试、键位审计全绿;被跟踪的宿主文件仅在 `core/agent-session.js`(1.3.0 改了 tool-restore 路径,`cycleModel` 未动)、`core/keybindings.js` 与 `core/compaction/utils.js`(二者都是 1.2.0 变的,我们按键的解析结果不变)、`extensions/types.d.ts`(纯增量)上有差异 —— `ModelSelectorComponent`、`core/compaction/compaction.js` 与 `core/usage-totals.js` 在窗口内字节级不变。真实 pi 1.3.0 冒烟加载无警告。注:pi ≥ 0.99 的宿主要求「宿主提供的包」一律声明为 peerDependencies `"*"` —— `@earendil-works/pi-tui` 与 `typebox`(ask 的 `scratchpad` 用它构造参数 schema)都按此声明;若放进 `dependencies`,pi 会告警。
18
+ **版本要求**:pi `>=0.99.0`。0.99.0 到 1.4.0 的每一个已发布版本都用完整门禁实测过(install → `tsc --noEmit` → 全量测试 → 键位审计 → 对 patch 目标宿主文件做哈希比对),每个版本都在沙箱中把整个 `@earendil-works/*` 家族钉在同一版本。更新的版本随发布逐个验证,下限只在实测结果允许时移动;`pnpm check:compat` 可复现该结论。注:pi ≥ 0.99 的宿主要求「宿主提供的包」一律声明为 peerDependencies `"*"` —— `@earendil-works/pi-tui` 与 `typebox`(ask 的 `scratchpad` 用它构造参数 schema)都按此声明;若放进 `dependencies`,pi 会告警。
19
19
 
20
20
  ---
21
21
 
@@ -69,7 +69,7 @@ pi -e ./src/index.ts
69
69
  | 功能 | 入口 | 一句话说明 |
70
70
  | --- | --- | --- |
71
71
  | 🛡️ **ask** 只读咨询模式 | `/ask` | 进入咨询模式:`write`/`edit`/`powershell` 硬禁用,`bash` 只读沙箱,临时启用 `grep`/`find`/`ls`,并开放 `scratchpad` 把草稿写进会话临时目录 |
72
- | 🔐 **permission** 会话级边界权限 | `/ask-perm` | 项目内/外 × 读/写四格模式,默认关闭且从不落盘;项目外写询问、凭据路径硬拒、破坏性命令必问;与 `/ask` 并行组合 |
72
+ | 🔐 **permission** 会话级边界权限 | `/ask-perm` | 项目内/外 × 读/写四格模式,默认关闭且从不落盘;项目外写询问、凭据路径硬拒、破坏性命令必问;与 `/ask` 并行组合;另有滚动 JSONL 审批日志供事后挖规则 |
73
73
  | ⭐ **favorites** 模型收藏 | `/model` 内 `ctrl+A` / `ctrl+J` | 收藏常用模型(加粗亮黄标记),`ctrl+p` 仅在收藏间循环 |
74
74
  | 📥 **stash** 提示词暂存 | `ctrl+alt+y` | 一段提示词的暂存槽,按键交换 / 连按两次暂存并清空输入框 |
75
75
  | 🔎 **review** 第三方代码评审 | `/third-review` | 召唤配置的评审模型,对刚完成的任务做查+修,重点检查「本轮需求实现 / 是否有遗漏 / 是否有错误」 |
@@ -104,7 +104,7 @@ pi -e ./src/index.ts
104
104
 
105
105
  <a name="permission"></a>
106
106
 
107
- `/ask-perm` 只在**当前会话**开启边界检查。开关状态、四项模式、会话授权全部只存在扩展内存里:不落盘、默认关闭、关闭后不留任何状态。开启一次不会悄悄跟到下一个会话。
107
+ `/ask-perm` 只在**当前会话**开启边界检查。开关状态、四项模式、会话授权全部只存在扩展内存里:权限**状态**不落盘、默认关闭、关闭后不留任何状态。开启一次不会悄悄跟到下一个会话。另外,值得留档的判定会追加到本地**审批日志**(见下),供事后挖规则用;它是只写的遥测数据,判定时永远不会被读取。
108
108
 
109
109
  **开启**
110
110
 
@@ -137,9 +137,36 @@ pi -e ./src/index.ts
137
137
  - **凭据路径任何模式都拒绝** —— `~/.ssh`、`~/.gnupg`、`~/.aws`、`~/.pi/agent/auth*.json`、`.env` 文件(模板除外)。没有这一条,默认的「项目外读放行」就等于把 `~/.ssh/id_rsa` 交出去
138
138
  - **破坏性命令永远询问** —— 对 `/`、家目录或工作目录的 `rm`;`git reset --hard`;`git clean -f`;`git push --force`;包 `publish`;`dd` 写裸设备;`mkfs*`;关机/重启;`chmod -R 777`;任何经 `sudo` 的命令。作用域本身表达不了「破坏性」
139
139
 
140
+ **审批日志**(只写遥测,默认开启)
141
+
142
+ 每个值得留档的判定都会写成一行 JSON(JSONL),落在 `~/.pi/agent/approval-log/audit-NNNNNN.jsonl`。策略层从不读它:删掉日志或把目录改成只读,不会改变任何判定结果。
143
+
144
+ - **范围**:`/ask` 模式的拦截同样算审批决定,也会记录(带闸门名与模型可见理由)。
145
+ - **记录什么**:交互弹窗(用户看到的原始行、bash 拆解结果、答复、授权与决策耗时)、自动拒绝/拦截、会话授权放行、`/ask-perm` 模式切换,以及每个会话一条 `session` 摘要(调用/弹窗/拦截计数)。普通自动放行的 `read`/`grep` 默认不记,避免把环形缓冲淹在噪声里;`PI_APPROVAL_LOG=all` 连普通自动放行也记,`PI_APPROVAL_LOG=off` 关闭日志。
146
+ - **滚动**:每文件 1000 条**或** 8 MiB,最多 10 个文件;创建第 11 个时删掉序号最小的那个。典型体积 15–30 MB(硬上限 80 MB),约保留最近 1 万条判定。
147
+ - **保真度**:四格模式、会话根目录、工具调用、每个命令单元(含 `opaque` 单元与解析器备注)、每个目标规范化后的路径/读或写/项目内或外、命中哪条规则层、弹窗原文、答复与产生的授权。`write`/`edit` 的文件**正文永不记录**(只记路径与字节数);bearer token、`token=`/`password=`、URL 内嵌凭据与已知密钥前缀会脱敏。
148
+ - **失败与隐私**:目录 `0700`、文件 `0600`,只在本机,可随时删除。日志故障只警告一次并被吞掉——判定链路不会因为日志不可用而失败。`PI_APPROVAL_LOG_DIR` 可改目录。
149
+
150
+ ```bash
151
+ # 只看"本会话允许"的弹窗
152
+ jq -r 'select(.event=="ask" and .answer.choice=="allow-session")
153
+ | [.ts,.tool,.verdict.target,.input.command] | @tsv' \
154
+ ~/.pi/agent/approval-log/*.jsonl
155
+
156
+ # 目标根目录 × 答复 —— 找「临时目录直接放行」规则的最短路径
157
+ jq -r 'select(.event=="ask") | [.verdict.grantRoot,.answer.choice] | @tsv' \
158
+ ~/.pi/agent/approval-log/*.jsonl | sort | uniq -c | sort -rn
159
+
160
+ # 解析不了的单元:定位 units.ts 的缺口
161
+ jq -r 'select(.event=="ask") | .units[] | select(.opaque) | [.command,.note] | @tsv' \
162
+ ~/.pi/agent/approval-log/*.jsonl | sort | uniq -c | sort -rn
163
+ ```
164
+
165
+ 仓库内可用 `pnpm approval-report` 生成只读汇总(答复分布、目标根目录、候选规则、opaque 缺口、playwright 模式)。
166
+
140
167
  **与 `/ask` 的组合**:两者是同一总线上的独立闸门,各自只回答自己的问题 —— ask 回答「会不会产生变更」,permission 回答「有没有越过边界」,任一拦截都生效。所以 ask 模式下凭据仍然读不到,permission 在只读沙箱下也照常工作;两个功能互不需要知道对方存在。
141
168
 
142
- **每个会话自动开启**(状态本身不落盘,这就是持久化的替身):
169
+ **每个会话自动开启**(状态本身不落盘,这就是持久化的替身;审批日志独立于它、默认开启):
143
170
 
144
171
  ```toml
145
172
  # <项目>/.pi/autostart.toml;想对所有项目生效则用 <agent-dir>/autostart.toml
@@ -147,7 +174,7 @@ pi -e ./src/index.ts
147
174
  run = ["/ask-perm allow allow allow ask"]
148
175
  ```
149
176
 
150
- > **实现说明**:新会话在 `session_start` 重置状态(同一会话的重复 emission 保持状态);开启时 footer 显示 `perm i:A/A o:A/ask`(`A` allow、`a` ask、`D` deny)。子代理会话有自己的状态、不继承父会话的模式;需要子会话也受控时,通过项目 autostart 再开启一次。实测目标是 POSIX 宿主(Linux/macOS/WSL);原生 Windows Git-Bash 的路径拼写只做了近似,未验证。bash 拆解使用 `shell-quote`;未知变量会展开成哨兵而不是空字符串,使该命令转为询问而不是被悄悄当成空路径。add-dir 目录从 pi-add-dir 的会话条目 `add-dir:state` 读取。启发式,不是沙箱:项目内的破坏性操作由固定表兜底,而不是靠沙箱。已知盲区一处:**读命令的裸文件名参数**(`cat notes.txt`)不被收集为目标,所以把 `out read` 调成 `ask` 时只有绝对路径形状的读目标会被问——读本身不会写,四格模式的写侧不受影响。
177
+ > **实现说明**:新会话在 `session_start` 重置状态(同一会话的重复 emission 保持状态);开启时 footer 显示 `perm i:A/A o:A/ask`(`A` allow、`a` ask、`D` deny)。子代理会话有自己的状态、不继承父会话的模式;需要子会话也受控时,通过项目 autostart 再开启一次。审批日志由闸门总线(弹窗/拦截/会话摘要)与 permission 闸门(模式切换、会话授权放行)追加;写入器懒初始化、不跨进程保存状态,也从不参与判定。实测目标是 POSIX 宿主(Linux/macOS/WSL);原生 Windows Git-Bash 的路径拼写只做了近似,未验证。bash 拆解使用 `shell-quote`;未知变量会展开成哨兵而不是空字符串,使该命令转为询问而不是被悄悄当成空路径。add-dir 目录从 pi-add-dir 的会话条目 `add-dir:state` 读取。启发式,不是沙箱:项目内的破坏性操作由固定表兜底,而不是靠沙箱。已知盲区一处:**读命令的裸文件名参数**(`cat notes.txt`)不被收集为目标,所以把 `out read` 调成 `ask` 时只有绝对路径形状的读目标会被问——读本身不会写,四格模式的写侧不受影响。
151
178
 
152
179
  ---
153
180
 
@@ -255,11 +282,11 @@ pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文
255
282
  | auto-compact 阈值触发 | 压缩模型总结 |
256
283
  | context overflow 恢复 | 压缩模型总结,重试行为不变 |
257
284
 
258
- **接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,复制自 0.87.1、在支持区间内全部已发布版本(1.0.0 → 1.3.0)字节级不变,只换执行模型;对话序列化函数(`serializeConversation`)是直接从宿主导入的,因此跟随宿主行为 —— 例如 1.2.0 起不再把 assistant thinking 回填进摘要请求):
285
+ **接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,复制自 0.87.1、在 1.1.0 → 1.4.0 内字节级不变 —— 除了此处采纳的两项 1.4.0 修正:摘要请求使用模型自身的输出上限而非 `reserveTokens` 的固定比例,以及 `/compact <instructions>` 也会进入 split-turn 前缀;只换执行模型;对话序列化函数(`serializeConversation`)是直接从宿主导入的,因此跟随宿主行为 —— 例如 1.2.0 起不再把 assistant thinking 回填进摘要请求):
259
286
 
260
287
  - **摘要格式** —— Goal / Progress / Key Decisions 结构化 checkpoint;已有摘要时用迭代更新版 prompt,信息延续不丢
261
288
  - **切分行为** —— 保留最近 `keepRecentTokens` 的策略、`firstKeptEntryId` / `tokensBefore` 原样回传,不动
262
- - **输出预算** —— 与内建同公式(历史段 0.8×、切分前缀段 0.5× `reserveTokens`,再按模型输出上限收敛)
289
+ - **输出预算** —— 摘要请求使用模型自身的输出上限,与 1.4.0 起的内建一致(#9512:过去按 0.8×/0.5× `reserveTokens` 设上限,长思考会把摘要顶到 "generation hit the token cap" 而失败);`reserveTokens` 只决定何时开始压缩
263
290
  - **文件清单尾部** —— `<read-files>` / `<modified-files>` 标签照常拼在摘要尾部,details 同形状维护
264
291
  - **用量统计** —— 压缩模型的 token/cost 计入 session 统计
265
292
  - **可取消** —— 压缩进行中 `Esc` 干净取消(信号透传到底层调用)
@@ -336,7 +363,7 @@ run = [
336
363
 
337
364
  **关键约束:只支持扩展命令**。`/add-dir`(pi-add-dir)、`/cd`(pi-cd)等 `registerCommand` 注册的命令可以;内置命令(`/compact`、`/reload`、`/resume`…)在 TUI 层分发,扩展层触发不了;skill 命令(`/skill:name`)是展开机制而非执行机制。依赖的目标扩展(如 pi-add-dir)需要先安装。
338
365
 
339
- > **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,`sendUserMessage` 与 `_tryExecuteExtensionCommand` 在 1.0.0–1.3.0 全区间字节级一致,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
366
+ > **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,`sendUserMessage` 与 `_tryExecuteExtensionCommand` 在 1.1.0–1.4.0 全区间字节级一致,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
340
367
 
341
368
  ---
342
369
 
@@ -352,7 +379,8 @@ pnpm typecheck # tsc --noEmit
352
379
  pnpm test # vitest(全部功能单元测试)
353
380
 
354
381
  pnpm check:keybindings # 用宿主自己的分发表审计本插件的按键
355
- pnpm check:compat # 校验声明的 pi 区间:最近 4 条中版本线 + 向后 8 条线探针
382
+ pnpm check:compat # 校验声明的 pi 下限:强制 4 条线窗口 + 向后 4 条线探针 → public floor
383
+ pnpm approval-report # 汇总本地审批日志(答复分布、目标目录、候选规则)
356
384
  ```
357
385
 
358
386
  ### 项目结构
@@ -363,6 +391,7 @@ src/
363
391
  ├── lib/ # 共享基础设施(工具集快照/恢复、
364
392
  │ # 组合 tool_call 策略的闸门总线、
365
393
  │ # 边界闸门会查询的自封闭工具登记表、
394
+ │ # 滚动 JSONL 审批日志、
366
395
  │ # review 与 compact 共用的模型 key 解析)
367
396
  └── features/ # 功能模块 —— 一个功能一个目录
368
397
  ├── ask/ # ask 模式:命令 + 状态机、bash 沙箱、
@@ -375,7 +404,8 @@ src/
375
404
  ├── favorites/ # 模型收藏:选择器与循环 patch、
376
405
  │ # 持久化存储、测试
377
406
  ├── permission/ # 会话级边界权限:模式、面板、bash 拆解、
378
- │ # 策略、会话授权、确认对话框、测试
407
+ │ # 策略、会话授权、确认对话框、
408
+ │ # 审计 payload、测试
379
409
  ├── review/ # 第三方评审:输入/指令触发、
380
410
  │ # 评审模型配置存储、提示词、测试
381
411
  └── stash/ # 提示词暂存:热键状态机 + 测试
@@ -412,7 +442,7 @@ pnpm release:github --audit # 列出 npm 版本 vs GitHub Release vs 远端 t
412
442
  - **内建 patch**(favorites)必须保持**版本守卫 + 幂等** —— 打 patch 前先确认目标原型成员存在,防止重复应用,失败时用 `console.warn` 降级而不是把扩展搞挂
413
443
  - **内建提示词同步**(compact) —— `src/features/compact/prompt.ts` 里的总结 prompt 逐字抄自 pi 内建压缩;升级 pi 依赖时 diff `dist/core/compaction/compaction.js`(及 `utils.js`),同步字符串与组装辅助函数,保持接管输出与内建字节级一致
414
444
  - **键位** —— 定新快捷键前先查 pi 默认键位 + 保留键、终端控制字符、OS/终端模拟器占用;跨平台安全优先选 `ctrl+alt+<字母>`。**不得只验证「主屏下我这台机器能用」**:自 pi 1.0 起默认 `fullscreen`,alt-screen 层在对话框看到按键**之前**就消费 `tui.altScreen.*`,且 Windows/WSL 上多个绑定与 Linux 默认值不同。用 `pnpm check:keybindings [候选键...]` 核查(pi 升级后跑 `pnpm check:keybindings --all`),它用宿主自己的 `matchesKey` 探测已安装宿主的**已解析**键位表。**宿主表空闲 ≠ 机器层空闲**:Windows Terminal 默认占 `ctrl+v`(粘贴)与 `ctrl+shift+f`(查找),第三方 Windows 工具常绑 `ctrl+alt+<字母>`(截图工具占过 `ctrl+alt+f`)——定键后必须在真机试按,确认按键真的到得了应用。
415
- - **宿主兼容** —— peer 区间是实测结论而非愿望:最近 4 个已发布中版本线尽力支持,上界编码「向后探 8 条中版本线」的上限,README 必须标注**实测支持到的最高版本**。宿主依赖一动就重跑 `pnpm check:compat`,读它的 verdict 段(区间与策略不一致会直接判失败)。详见 AGENTS.md「pi 宿主兼容」;升级中发现的键位冲突**上报崔总决策**,不得顺手换键。
445
+ - **宿主兼容** —— peer 区间是实测结论而非愿望,而且只往回指:最近 4 个已发布中版本线是**强制窗口**(不兼容就改我们的代码,绝不抬下限),窗口之下再向后探最多 4 条线找出兼容实际停在哪里,对使用者只展示一个数:由此得到的**下限**(`pi >= <floor>`,无上界)。宿主依赖一动就重跑 `pnpm check:compat`,读它的 verdict 段(声明下限与实测不一致会直接判失败)。详见 AGENTS.md「pi 宿主兼容」;升级中发现的键位冲突**上报崔总决策**,不得顺手换键。
416
446
  - **质量门禁** —— 提交前必须通过 `pnpm typecheck && pnpm test`
417
447
 
418
448
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andares/pi-toolkits",
3
- "version": "0.9.7",
3
+ "version": "0.10.0",
4
4
  "description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart, and a dedicated compaction model",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "boundary"
34
34
  ],
35
35
  "peerDependencies": {
36
- "@earendil-works/pi-coding-agent": ">=1.0.0 <1.12.0",
36
+ "@earendil-works/pi-coding-agent": ">=0.99.0",
37
37
  "@earendil-works/pi-tui": "*",
38
38
  "typebox": "*"
39
39
  },
@@ -43,9 +43,9 @@
43
43
  "smol-toml": "^1.9.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@earendil-works/pi-ai": "^1.3.0",
47
- "@earendil-works/pi-coding-agent": "^1.3.0",
48
- "@earendil-works/pi-tui": "^1.3.0",
46
+ "@earendil-works/pi-ai": "^1.4.0",
47
+ "@earendil-works/pi-coding-agent": "^1.4.0",
48
+ "@earendil-works/pi-tui": "^1.4.0",
49
49
  "@types/node": "^26.6.2",
50
50
  "typebox": "^1.3.27",
51
51
  "typescript": "^5.6.0",
@@ -61,6 +61,7 @@
61
61
  "test": "vitest run",
62
62
  "check:keybindings": "node ./scripts/check-keybindings.mjs",
63
63
  "check:compat": "node ./scripts/check-compat.mjs",
64
+ "approval-report": "node ./scripts/approval-report.mjs",
64
65
  "release:github": "node ./scripts/release-github.mjs",
65
66
  "release": "node ./scripts/publish.mjs",
66
67
  "tag-current": "node ./scripts/tag-current.mjs"
@@ -2,7 +2,7 @@
2
2
  * ask feature — actionable rejection messages.
3
3
  *
4
4
  * The `tool_call` hook's `reason` is delivered to the model as the tool result
5
- * (verified against pi 1.3.0: `extensions/runner.js` returns the `{block,
5
+ * (verified against pi 1.4.0: `extensions/runner.js` returns the `{block,
6
6
  * reason}` result unchanged and pi-agent-core's `agent-loop.js` turns it into
7
7
  * `createErrorToolResult(reason)`; both paths are byte-identical to 1.2.0), so
8
8
  * it is the best place to name the nearest allowed alternative and stop the model
@@ -48,7 +48,7 @@ export const RUNNABLE_REASONS: ReadonlySet<string> = new Set([
48
48
  * `-r` (`--resume`), `--session <path>` and `--fork <path>` all open an
49
49
  * existing session *before* the first runtime exists, and that runtime is
50
50
  * created without an explicit session_start event, so extensions see pi's
51
- * default reason — `startup` — not `resume` (verified on pi 1.0.0–1.3.0:
51
+ * default reason — `startup` — not `resume` (verified on pi 1.1.0–1.4.0:
52
52
  * `main.js` builds the initial session manager from those flags and calls
53
53
  * `createAgentSessionRuntime` with no `sessionStartEvent`;
54
54
  * `agent-session.js` defaults it to `{ reason: "startup" }`). `resume` is only
@@ -27,7 +27,7 @@
27
27
  * that flag is what routes "/cmd args" through _tryExecuteExtensionCommand
28
28
  * instead of the model prompt path. The wrapper defaults it to false in this
29
29
  * package (each byte-identical across every published version of the supported
30
- * window, 1.0.0 → 1.3.0; see AGENTS.md host-compat discipline for the version
30
+ * window, 1.1.0 → 1.4.0; see AGENTS.md host-compat discipline for the version
31
31
  * matrix).
32
32
  *
33
33
  * Only extension commands can run this way. Built-in commands (/compact,
@@ -418,27 +418,25 @@ describe("session_before_compact handler", () => {
418
418
  .calls[0] as [
419
419
  AvailableModel,
420
420
  unknown,
421
- { maxTokens: number; signal: AbortSignal },
421
+ { maxTokens?: number; signal: AbortSignal },
422
422
  ];
423
423
  expect(modelKey(model)).toBe("google/gemini-2.5-flash");
424
- // 0.8 × reserveTokens(16000) = 12800, under the model cap (8192).
425
- expect(options.maxTokens).toBe(8192);
424
+ // pi 1.4.0 (#9512): no fixed output cap — the request omits maxTokens
425
+ // so the provider uses the model's own output limit.
426
+ expect(options).not.toHaveProperty("maxTokens");
426
427
  expect(options.signal).toBe(event.signal);
427
428
  });
428
429
 
429
- it("clamps the budget by reserveTokens when the model cap is larger", async () => {
430
+ it("does not derive an output budget from reserveTokens", async () => {
430
431
  getCompactStore().setModel("google/gemini-2.5-flash");
431
432
  const { handler } = registeredHandler();
432
433
  const bigCapModel = { ...MODELS[0], maxTokens: 65536 };
433
434
  const { ctx, complete } = fakeCtx({ models: [bigCapModel] });
434
435
  await handler(fakeEvent() as never, ctx);
435
436
  const [, , options] = (complete as ReturnType<typeof vi.fn>).mock
436
- .calls[0] as [
437
- AvailableModel,
438
- unknown,
439
- { maxTokens: number },
440
- ];
441
- expect(options.maxTokens).toBe(12800);
437
+ .calls[0] as [AvailableModel, unknown, { maxTokens?: number }];
438
+ // reserveTokens (16000) used to cap this at 12800; the cap is gone.
439
+ expect(options).not.toHaveProperty("maxTokens");
442
440
  });
443
441
 
444
442
  it("falls back with a warning on an empty summary", async () => {
@@ -568,6 +566,31 @@ describe("session_before_compact handler", () => {
568
566
  expect(prompts[0]).toContain("Additional focus: 保留命令输出细节");
569
567
  });
570
568
 
569
+ it("passes customInstructions to the split-turn prefix summary too", async () => {
570
+ getCompactStore().setModel("google/gemini-2.5-flash");
571
+ const { handler } = registeredHandler();
572
+ const prompts: string[] = [];
573
+ const { ctx } = fakeCtx({
574
+ complete: vi.fn(async (_model, context) => {
575
+ prompts.push(
576
+ (context.messages[0]?.content as Array<{ text?: string }>)[0]?.text ??
577
+ "",
578
+ );
579
+ return fakeResponse("## Goal\n- x");
580
+ }),
581
+ });
582
+ const event = fakeEvent({
583
+ messagesToSummarize: [],
584
+ turnPrefixMessages: [userMessage("prefix msg")],
585
+ isSplitTurn: true,
586
+ });
587
+ Object.assign(event, { customInstructions: "保留命令输出细节" });
588
+ await handler(event as never, ctx);
589
+ expect(prompts).toHaveLength(1);
590
+ expect(prompts[0]).toContain("# Instructions");
591
+ expect(prompts[0]).toContain("Additional focus: 保留命令输出细节");
592
+ });
593
+
571
594
  it("switches to the update prompt when a previous summary exists", async () => {
572
595
  getCompactStore().setModel("google/gemini-2.5-flash");
573
596
  const { handler } = registeredHandler();
@@ -9,13 +9,15 @@
9
9
  * - takeover point: the `session_before_compact` extension event, which pi
10
10
  * emits for ALL three trigger reasons (manual / threshold / overflow)
11
11
  * before running its own compaction
12
- * - prompts, message assembly, split-turn handling, output budgets, and the
13
- * summary file-list tail are verbatim pi built-in behavior (copied from
14
- * 0.87.1, byte-identical across every published version of the supported
15
- * window, 1.0.0 → 1.3.0) — only the executing model differs. The one
16
- * exception is `serializeConversation`, which is imported from the host
17
- * instead of mirrored, so it tracks whatever the running host does
18
- * (1.2.0 stopped feeding assistant thinking into the request).
12
+ * - prompts, message assembly, split-turn handling, summary budgets, and
13
+ * the summary file-list tail are verbatim pi built-in behavior (copied
14
+ * from 0.87.1; `core/compaction/compaction.js` stayed byte-identical
15
+ * 1.0.0 → 1.3.0, and 1.4.0 changed only the summary output budget and
16
+ * the split-turn focus handling mirrored below) — only the executing
17
+ * model differs. The one exception is `serializeConversation`, which is
18
+ * imported from the host instead of mirrored, so it tracks whatever the
19
+ * running host does (1.2.0 stopped feeding assistant thinking into the
20
+ * request).
19
21
  * - lossless fallback: unset config, unavailable model, empty/failed
20
22
  * summary, or user abort → return undefined and pi's default compaction
21
23
  * (session model) runs untouched. The extension can never make
@@ -75,7 +77,6 @@ async function completeSummary(
75
77
  ctx: ExtensionContext,
76
78
  model: AvailableModel,
77
79
  promptText: string,
78
- maxTokens: number,
79
80
  signal: AbortSignal,
80
81
  ): Promise<{ text: string; usage: CompleteUsage }> {
81
82
  const response = await ctx.modelRegistry.complete(
@@ -91,7 +92,10 @@ async function completeSummary(
91
92
  ],
92
93
  },
93
94
  {
94
- maxTokens,
95
+ // No `maxTokens`: since pi 1.4.0 (#9512) the built-in compaction asks
96
+ // for the model's own output limit instead of a fixed fraction of
97
+ // reserveTokens, so a long reasoning block can no longer eat the cap
98
+ // and fail the summary with "generation hit the token cap".
95
99
  signal,
96
100
  cacheRetention: "none",
97
101
  sessionId: uuidv7(),
@@ -118,30 +122,26 @@ async function completeSummary(
118
122
  return { text, usage: response.usage };
119
123
  }
120
124
 
121
- /**
122
- * pi's output-budget formula (compaction.js, 0.87.1 → 1.3.0 unchanged): a
123
- * fraction of reserveTokens clamped by the model's own output cap.
124
- */
125
- function summarizationBudget(
126
- fraction: number,
127
- reserveTokens: number,
128
- modelMaxTokens: number,
129
- ): number {
130
- return Math.min(
131
- Math.floor(fraction * reserveTokens),
132
- modelMaxTokens > 0 ? modelMaxTokens : Number.POSITIVE_INFINITY,
133
- );
134
- }
135
-
136
125
  /**
137
126
  * Run the full compaction summarization with the configured model,
138
- * replicating pi's compact() (0.87.1, byte-identical across the supported
139
- * 1.0.0 → 1.3.0 window):
127
+ * replicating pi's compact() as of 1.4.0 (byte-identical to 0.87.1's shape
128
+ * apart from the two 1.4.0 fixes adopted for the whole window, see below):
140
129
  * history summary (first-connection or iterative-update prompt, custom
141
130
  * focus appended), plus a separate turn-prefix checkpoint when the cut point
142
131
  * splits a turn, merged into one summary with the file-list tail. Returns
143
132
  * undefined on any failure after notifying (pi default compaction then takes
144
133
  * over).
134
+ *
135
+ * 1.4.0 changed two things this mirror covers, and we follow 1.4.0 for every
136
+ * host in the supported window (1.1.0 → 1.4.0) because both are bug fixes
137
+ * rather than behavior we could sensibly keep per version:
138
+ * - summaries no longer cap maxTokens at 0.8/0.5 × reserveTokens; the
139
+ * request omits maxTokens so the provider uses the model's output limit
140
+ * (#9512 — long reasoning made summaries fail with "generation hit the
141
+ * token cap"). `reserveTokens` now only decides when compaction starts.
142
+ * - `/compact <instructions>` also reaches the split-turn prefix summary
143
+ * (#11058; before 1.4.0 the focus was dropped when the split turn was the
144
+ * only new content).
145
145
  */
146
146
  async function runCompactModelCompaction(
147
147
  ctx: ExtensionContext,
@@ -157,7 +157,6 @@ async function runCompactModelCompaction(
157
157
  firstKeptEntryId,
158
158
  previousSummary,
159
159
  fileOps,
160
- settings,
161
160
  } = preparation;
162
161
 
163
162
  try {
@@ -177,11 +176,6 @@ async function runCompactModelCompaction(
177
176
  previousSummary,
178
177
  customInstructions,
179
178
  ),
180
- summarizationBudget(
181
- 0.8,
182
- settings.reserveTokens,
183
- model.maxTokens,
184
- ),
185
179
  signal,
186
180
  );
187
181
  historyText = history.text;
@@ -192,8 +186,8 @@ async function runCompactModelCompaction(
192
186
  model,
193
187
  buildTurnPrefixPromptText(
194
188
  serializeConversation(convertToLlm(turnPrefixMessages)),
189
+ customInstructions,
195
190
  ),
196
- summarizationBudget(0.5, settings.reserveTokens, model.maxTokens),
197
191
  signal,
198
192
  );
199
193
  summaryText = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefix.text}`;
@@ -212,7 +206,6 @@ async function runCompactModelCompaction(
212
206
  previousSummary,
213
207
  customInstructions,
214
208
  ),
215
- summarizationBudget(0.8, settings.reserveTokens, model.maxTokens),
216
209
  signal,
217
210
  );
218
211
  summaryText = history.text;