smartcompact 0.1.1 → 0.1.4

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
@@ -6,13 +6,16 @@
6
6
  [![GitHub stars](https://img.shields.io/github/stars/coffeecoproject/SmartCompact?style=social)](https://github.com/coffeecoproject/SmartCompact/stargazers)
7
7
  [![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/coffeecoproject/SmartCompact/blob/main/LICENSE)
8
8
 
9
- **SmartCompact lets long coding-agent sessions compact earlier, right after a piece of work is finished.** On 18–60 consecutive coding tasks it cut cost by **7–44%** with no loss in hidden acceptance tests; the longer the session, the more it saves. In the longest runs (60 tasks, native sessions reaching 820k–960k tokens) the model missed 6 of 180 recall questions about details mentioned in passing much earlier ([test report](https://github.com/coffeecoproject/SmartCompact/blob/main/docs/TEST-REPORT.md), in Chinese).
9
+ <p align="center"><a href="https://github.com/coffeecoproject/SmartCompact/raw/main/docs/media/smartcompact-en.mp4"><img src="https://raw.githubusercontent.com/coffeecoproject/SmartCompact/main/docs/media/cover-en.jpg" alt="SmartCompact in 90 seconds (video)" width="100%"></a></p>
10
+ <p align="center"><sub>▶ Watch the 90-second explainer (click the image to play)</sub></p>
11
+
12
+ **SmartCompact lets long coding-agent sessions compact earlier, at a pause after a piece of work is finished.** On 18–60 consecutive coding tasks it cut cost by **7–44%** with no loss in hidden acceptance tests; the longer the session, the more it saves. In the longest runs (60 tasks, native sessions reaching 820k–960k tokens) the model missed 6 of 180 recall questions about details mentioned in passing much earlier ([how it was tested](https://github.com/coffeecoproject/SmartCompact/blob/main/docs/EVALUATION.md)).
10
13
 
11
14
  Every request re-reads the whole context, so a long context makes every step expensive. Native auto-compaction waits until the context is nearly full (close to 1M tokens in Claude Code, up to 800k in Codex if configured), and it ignores timing, so it often lands in the middle of a task. SmartCompact works with Claude Code (CLI and desktop app), [Pi](https://github.com/earendil-works/pi) and the Codex CLI:
12
15
 
13
- - **Compact at the right moment.** Once the context passes the **allow line** (200k tokens by default), it waits for a piece of work to finish: a commit, tests passing, a task completed.
14
- - **Never in the middle of work.** It does not compact on its own mid-task, nor while tests or the build are failing, errors keep repeating, or a background job is running.
15
- - **A fallback.** If no good moment comes and the context passes the **grace line** (allow line × 1.3), it compacts at the end of a turn.
16
+ - **Compact at a good moment.** Once the context passes the **allow line** (200k tokens by default), it compacts when a turn ends, if a piece of work has been finished since the last compaction (a commit, tests passing, a task completed).
17
+ - **Never in the middle of work.** It only decides when a turn ends, never mid-turn, and it waits while tests or the build are failing or errors keep repeating.
18
+ - **No limit of its own.** If no piece of work gets finished, the agent is usually deep in one large task, which is when a large context pays off. SmartCompact leaves it alone, and the platform's native compaction applies as it would without the plugin.
16
19
  - **A progress ledger bridges the gap.** Files changed, the latest commit and test result, and decisions the agent recorded are handed back to the model after compaction.
17
20
  - **No pointless compactions.** It skips compaction when it would not free enough space.
18
21
 
@@ -27,7 +30,7 @@ The same codebase and 18–30 consecutive development tasks, where later tasks c
27
30
  | Claude Code (Haiku 5.5, 30 tasks) | close to 1M | −7% (noisy) | 30/30 both | 90/90 both |
28
31
  | Claude Code (Haiku 5.5, 60 tasks) | close to 1M | **−44%** | native 59–60/60, SmartCompact 60/60 | native 180/180, SmartCompact 174/180 |
29
32
 
30
- Cost is converted to cached-input-token equivalents using each provider's price ratios. The later native compaction kicks in and the faster a session grows, the more it saves. Below 200k SmartCompact does nothing, so it costs nothing either. After a compaction the model spends extra requests getting familiar with the code again; that is included in the cost above. These are small samples on one codebase and one kind of task; the [report](https://github.com/coffeecoproject/SmartCompact/blob/main/docs/TEST-REPORT.md) has the method, the data and the limitations.
33
+ Cost is converted to cached-input-token equivalents using each provider's price ratios. The later native compaction kicks in and the faster a session grows, the more it saves. Below 200k SmartCompact does nothing, so it costs nothing either. After a compaction the model spends extra requests getting familiar with the code again; that is included in the cost above. These are small samples on one codebase and one kind of task; the [evaluation](https://github.com/coffeecoproject/SmartCompact/blob/main/docs/EVALUATION.md) has the method, the data and the limitations.
31
34
 
32
35
  ## Install
33
36
 
@@ -38,7 +41,7 @@ Cost is converted to cached-input-token equivalents using each provider's price
38
41
  /plugin install smartcompact@smartcompact
39
42
  ```
40
43
 
41
- It takes effect in new sessions. The plugin moves the native auto-compaction to about the grace line; `/smartcompact pause` returns the current session to native behavior.
44
+ It takes effect in new sessions. `/smartcompact pause` turns it off for the current session.
42
45
 
43
46
  **Pi**
44
47
 
@@ -51,45 +54,45 @@ With Pi's default settings, SmartCompact compacts at a finished piece of work wh
51
54
  **Codex CLI** (0.162 or later, Node.js 22.18 or later)
52
55
 
53
56
  ```bash
54
- git clone https://github.com/coffeecoproject/SmartCompact.git
55
- cd SmartCompact
56
- node scripts/codex.ts install
57
+ codex plugin marketplace add coffeecoproject/SmartCompact
58
+ codex plugin add smartcompact@smartcompact
57
59
  ```
58
60
 
59
- The script only appends one marked block of hooks to `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`), backing up the file first. Trust the 4 hooks when Codex asks on its next start. Run install again after upgrading Node or moving the repository.
61
+ Trust the plugin's 4 hooks when Codex asks on its next start. The hooks find a Node.js 22.18+ on their own (`PATH`, nvm, Homebrew; or set `SMARTCOMPACT_NODE`); without one they do nothing. To update, run `codex plugin marketplace upgrade smartcompact` and then the `codex plugin add` command again. If you installed an earlier version with `node scripts/codex.ts install`, run `node scripts/codex.ts uninstall` in that clone once, so the hooks do not run twice.
60
62
 
61
- Codex hooks cannot postpone a compaction in the middle of a turn, so SmartCompact decides when a turn ends and, if it should compact, starts the compaction through Codex's shared background server once the session is idle (the interactive UI uses it by default). `codex exec` does not use the shared server by default, and the Codex App (ChatGPT.app) currently cannot; there SmartCompact only hands back the progress ledger, and compaction follows Codex's native rules. For a fallback there, add Codex's own `model_post_turn_compact_threshold_percent` at the top of `config.toml`: it compacts after a turn ends once the context passes that percentage of the usable window (31 is about 260k for an 872k window; see the test report).
63
+ Codex hooks cannot postpone a compaction in the middle of a turn, so SmartCompact decides when a turn ends and, if it should compact, starts the compaction through Codex's shared background server once the session is idle (the interactive UI uses it by default). `codex exec` does not use the shared server by default, and the Codex App (ChatGPT.app) currently cannot; there SmartCompact only hands back the progress ledger, and compaction follows Codex's native rules. If you want compaction to come earlier there, add Codex's own `model_post_turn_compact_threshold_percent` at the top of `config.toml`: it compacts after a turn ends once the context passes that percentage of the usable window (31 is about 260k for an 872k window; see the test report).
62
64
 
63
65
  ## Settings
64
66
 
65
67
  | Setting | Default | Meaning |
66
68
  |---|---|---|
67
- | Allow line | 200000 | Above it, compact right after a piece of work is finished |
68
- | Grace factor | 1.3 | Above allow line × factor, stop waiting for a finished piece of work |
69
+ | Allow line | 200000 | Above it, compact when a turn ends once a piece of work has been finished |
69
70
  | Minimum freed | 60000 | Skip compaction if it is expected to free less than this |
70
71
 
71
- - Claude Code: the plugin options `allowLineTokens`, `graceFactor` and `minFreedTokens`; leave them empty for the defaults.
72
- - Pi: the flags `--smartcompact-allow-line`, `--smartcompact-grace` and `--smartcompact-min-freed`.
73
- - Codex: the environment variables `SMARTCOMPACT_ALLOW_LINE`, `SMARTCOMPACT_GRACE` and `SMARTCOMPACT_MIN_FREED`.
72
+ - Claude Code: the plugin options `allowLineTokens` and `minFreedTokens`; leave them empty for the defaults.
73
+ - Pi: the flags `--smartcompact-allow-line` and `--smartcompact-min-freed`.
74
+ - Codex: the environment variables `SMARTCOMPACT_ALLOW_LINE` and `SMARTCOMPACT_MIN_FREED`.
74
75
 
75
76
  ## Seeing what it did
76
77
 
77
- - Claude Code and Pi: `/smartcompact` (or `/smartcompact report`) shows the three lines, the finished-work signals it saw and a cost estimate; `/smartcompact pause` and `resume` turn it off and on for the session. The model can also call a `checkpoint` tool to record decisions, which come back with the ledger after compaction.
78
- - Codex: `node adapters/codex/report.ts <session ID>`; records are kept in `~/.codex/smartcompact/`.
78
+ - Claude Code and Pi: `/smartcompact` (or `/smartcompact report`) shows the allow line, the native compaction point, the finished-work signals it saw and a cost estimate; `/smartcompact pause` and `resume` turn it off and on for the session. The model can also call a `checkpoint` tool to record decisions, which come back with the ledger after compaction.
79
+ - Codex: `node ~/.codex/plugins/cache/smartcompact/smartcompact/<version>/adapters/codex/report.ts <session ID>` (Node.js 22.18+); records are kept in `~/.codex/smartcompact/`.
79
80
 
80
81
  ## FAQ
81
82
 
82
- **How does it know a piece of work is finished?** From the commands the agent runs. A commit counts if `git commit` actually ran and the tool call did not fail. A test command piped into something else (`| tail`) without `pipefail` does not count, since its exit code cannot be trusted.
83
+ **How does it know a piece of work is finished?** From the commands the agent runs. Any finished piece of work since the last compaction counts, not only one in the current turn: the end of a turn is already a pause, so waiting for the next commit would mostly just cost more. A commit counts if `git commit` actually ran and the tool call did not fail. A test command piped into something else (`| tail`) without `pipefail` does not count, since its exit code cannot be trusted.
83
84
 
84
85
  **Will the model forget things after compaction?** Sometimes, for details mentioned in passing. The ledger only records files, commits, tests and decisions the model chose to record. Details the user mentions in passing are kept by asking the compaction summary to keep every specific fact the user stated. On Pi this clearly reduced losses, but it is not a guarantee.
85
86
 
86
- **Anything specific to Claude Code?** SmartCompact's fallback point is close to the native compaction point it moves. If it cannot recognize a finished piece of work for a long time, native compaction may still fire mid-turn. SmartCompact moves the native compaction point by setting `CLAUDE_CODE_AUTO_COMPACT_WINDOW` in the session, and processes started from that session inherit it: a `claude -p` or script the agent runs will compact at about 260k as well. Unset the variable for such processes if that matters.
87
+ **Does it change Claude Code's own settings?** No. Native auto-compaction stays where it is (close to 1M). Versions up to 0.1.2 moved it to about 260k by setting `CLAUDE_CODE_AUTO_COMPACT_WINDOW` in the session (which child processes inherited) and also compacted there without a finished piece of work. 0.1.3 removed both: a long task is no longer cut off at a fixed size. A session opened on 0.1.2 or earlier keeps the old setting until you start a new one.
88
+
89
+ **Does it help with a smaller context window?** Only when native compaction comes well after 200k. With a 200k window, native compaction comes first (about 167k in Claude Code) and SmartCompact never acts. On Codex's default 272k window, native compaction is at about 245k, which leaves only 200k–245k for SmartCompact, so it saves little unless `model_auto_compact_token_limit` is set higher. The allow line can be lowered (`allowLineTokens`, `--smartcompact-allow-line`, `SMARTCOMPACT_ALLOW_LINE`), but lower lines have not been evaluated.
87
90
 
88
91
  **How do I remove it?**
89
92
 
90
93
  - Claude Code: `/plugin uninstall smartcompact@smartcompact`.
91
94
  - Pi: `pi remove npm:smartcompact`.
92
- - Codex: `node scripts/codex.ts uninstall` in the repository.
95
+ - Codex: `codex plugin remove smartcompact@smartcompact`.
93
96
 
94
97
  ## Development
95
98
 
@@ -102,7 +105,7 @@ This runs the structure checks, the core dependency boundary check and all tests
102
105
  - `plugin/`: the Claude Code plugin. `plugin/src/core/` holds the platform-independent decisions, signals, ledger and cost estimate that every adapter reuses.
103
106
  - `adapters/pi/`, `adapters/codex/`: the Pi and Codex adapters.
104
107
  - `eval/`: the A/B evaluation scripts (one each for Claude, Pi and Codex); the task format is described in `eval/day.py`. The tasks and hidden tests come from an internal project and are not published.
105
- - `DESIGN.md`: design notes; `docs/TEST-REPORT.md`: the test report (both in Chinese).
108
+ - `docs/EVALUATION.md`: how the A/B runs were done and their results. `DESIGN.md`: design notes and `docs/TEST-REPORT.md`: the full test report (both in Chinese).
106
109
 
107
110
  ## License
108
111
 
package/README.zh-CN.md CHANGED
@@ -6,13 +6,16 @@
6
6
  [![GitHub stars](https://img.shields.io/github/stars/coffeecoproject/SmartCompact?style=social)](https://github.com/coffeecoproject/SmartCompact/stargazers)
7
7
  [![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/coffeecoproject/SmartCompact/blob/main/LICENSE)
8
8
 
9
- **SmartCompact 让编程 agent 的长会话早点压缩,挑在刚做完一件事的时候。** 在 18~60 个连续开发任务上,成本降低 **7%~44%**,隐藏验收测试没有损失,会话越长省得越多。最长的一组(60 个任务,原生组上下文到 82 万~96 万)里,很早之前随口提到的细节,180 道记忆题少答对 6 道([测试报告](https://github.com/coffeecoproject/SmartCompact/blob/main/docs/TEST-REPORT.md))。
9
+ <p align="center"><a href="https://github.com/coffeecoproject/SmartCompact/raw/main/docs/media/smartcompact-zh.mp4"><img src="https://raw.githubusercontent.com/coffeecoproject/SmartCompact/main/docs/media/cover-zh.jpg" alt="90 秒看懂 SmartCompact(视频)" width="100%"></a></p>
10
+ <p align="center"><sub>▶ 90 秒动画介绍(点击图片播放)</sub></p>
11
+
12
+ **SmartCompact 让编程 agent 的长会话早点压缩,挑在做完一件事之后的停顿处。** 在 18~60 个连续开发任务上,成本降低 **7%~44%**,隐藏验收测试没有损失,会话越长省得越多。最长的一组(60 个任务,原生组上下文到 82 万~96 万)里,很早之前随口提到的细节,180 道记忆题少答对 6 道([测试报告](https://github.com/coffeecoproject/SmartCompact/blob/main/docs/TEST-REPORT.md))。
10
13
 
11
14
  agent 每发一次请求,都要把整段上下文重新读一遍,上下文越长,每一步越贵。原生的自动压缩要等到快满了才触发(Claude Code 接近 100 万,Codex 可以配到 80 万),而且不看时机,常常发生在一件事做到一半的时候。SmartCompact 支持 Claude Code(终端和桌面 App)、[Pi](https://github.com/earendil-works/pi) 和 Codex 命令行:
12
15
 
13
- - **挑对时机压缩。** 上下文超过**允许压缩线**(默认 20 万)以后,等到刚做完一件事(一次提交、测试刚通过、任务完成)再压缩。
14
- - **不在做到一半时压缩。** 不在一件事做到一半时主动压缩;测试失败、构建失败、连续报错或后台任务还在跑时也不压缩。
15
- - **有兜底。** 一直等不到合适时机、上下文超过**宽限线**(允许压缩线 × 1.3)时,在一轮结束时压缩。
16
+ - **挑对时机压缩。** 上下文超过**允许压缩线**(默认 20 万)以后,只要上次压缩以来做完过一件事(一次提交、测试通过、任务完成),就在这一轮结束时压缩。
17
+ - **不在做到一半时压缩。** 只在一轮结束时判断,从不在一轮中间主动压缩;测试失败、构建失败或连续报错时先等着。
18
+ - **不设自己的上限。** 一直没做完一件事,多半是在干一件大活,这时正需要大上下文。插件不去打断,压缩按平台原生规则来,和没装插件时一样。
16
19
  - **用进度笔记接上。** 改过的文件、最近的提交和测试结果、记下的决定,压缩后补回给模型。
17
20
  - **不做白费的压缩。** 预计腾不出足够空间时不压缩,避免刚压缩完又压缩。
18
21
 
@@ -38,7 +41,7 @@ agent 每发一次请求,都要把整段上下文重新读一遍,上下文
38
41
  /plugin install smartcompact@smartcompact
39
42
  ```
40
43
 
41
- 装好后新开会话生效。插件会把原生自动压缩线调到宽限线附近,`/smartcompact pause` 可以让当前会话恢复原生行为。
44
+ 装好后新开会话生效。`/smartcompact pause` 可以暂停当前会话的插件。
42
45
 
43
46
  **Pi**
44
47
 
@@ -51,45 +54,45 @@ Pi 默认配置下,插件在一轮结束(`agent_settled`)时挑刚做完
51
54
  **Codex 命令行**(0.162 及以上,Node.js 22.18 及以上)
52
55
 
53
56
  ```bash
54
- git clone https://github.com/coffeecoproject/SmartCompact.git
55
- cd SmartCompact
56
- node scripts/codex.ts install
57
+ codex plugin marketplace add coffeecoproject/SmartCompact
58
+ codex plugin add smartcompact@smartcompact
57
59
  ```
58
60
 
59
- 安装脚本只在 `~/.codex/config.toml`(或 `$CODEX_HOME/config.toml`)末尾写入一段带标记的 hook 配置,改之前备份原文件。下次启动 Codex 时按提示信任这 4 个 hook。升级 Node 或移动仓库后重新运行一次 install。
61
+ 下次启动 Codex 时按提示信任插件的 4 个 hook。hook 会自己找 22.18 及以上的 Node.js(PATH、nvm、Homebrew,也可以用 `SMARTCOMPACT_NODE` 指定),找不到就什么都不做。更新时运行 `codex plugin marketplace upgrade smartcompact`,再执行一次上面的 `codex plugin add`。以前用 `node scripts/codex.ts install` 装过的,先在那份仓库里运行一次 `node scripts/codex.ts uninstall`,避免 hook 运行两遍。
60
62
 
61
- Codex 的 hook 不能推迟一轮中间的压缩,所以插件在一轮结束时判断,需要压缩就通过 Codex 的共享后台服务(交互界面默认会用它)在会话空闲时发起。`codex exec` 默认不走共享后台服务,Codex App(ChatGPT.app)目前走不了,这时插件只补回进度笔记,压缩仍按 Codex 原生规则。想给它们也加个兜底,可以在 `config.toml` 顶部加上 Codex 自带的 `model_post_turn_compact_threshold_percent`:上下文超过可用窗口的这个百分比时,在一轮结束后压缩(窗口 87.2 万时设 31,约 26 万;详见测试报告)。
63
+ Codex 的 hook 不能推迟一轮中间的压缩,所以插件在一轮结束时判断,需要压缩就通过 Codex 的共享后台服务(交互界面默认会用它)在会话空闲时发起。`codex exec` 默认不走共享后台服务,Codex App(ChatGPT.app)目前走不了,这时插件只补回进度笔记,压缩仍按 Codex 原生规则。想让它们早点压缩,可以在 `config.toml` 顶部加上 Codex 自带的 `model_post_turn_compact_threshold_percent`:上下文超过可用窗口的这个百分比时,在一轮结束后压缩(窗口 87.2 万时设 31,约 26 万;详见测试报告)。
62
64
 
63
65
  ## 参数
64
66
 
65
67
  | 参数 | 默认 | 含义 |
66
68
  |---|---|---|
67
- | 允许压缩线 | 200000 | 超过它且刚做完一件事时压缩 |
68
- | 宽限倍数 | 1.3 | 超过 允许压缩线 × 倍数 时不再等刚做完一件事 |
69
+ | 允许压缩线 | 200000 | 超过它、做完过一件事后,在一轮结束时压缩 |
69
70
  | 最少腾出空间 | 60000 | 预计腾出的空间少于它时不压缩 |
70
71
 
71
- - Claude Code:插件配置项 `allowLineTokens`、`graceFactor`、`minFreedTokens`,留空用默认值。
72
- - Pi:命令行开关 `--smartcompact-allow-line`、`--smartcompact-grace`、`--smartcompact-min-freed`。
73
- - Codex:环境变量 `SMARTCOMPACT_ALLOW_LINE`、`SMARTCOMPACT_GRACE`、`SMARTCOMPACT_MIN_FREED`。
72
+ - Claude Code:插件配置项 `allowLineTokens`、`minFreedTokens`,留空用默认值。
73
+ - Pi:命令行开关 `--smartcompact-allow-line`、`--smartcompact-min-freed`。
74
+ - Codex:环境变量 `SMARTCOMPACT_ALLOW_LINE`、`SMARTCOMPACT_MIN_FREED`。
74
75
 
75
76
  ## 查看效果
76
77
 
77
- - Claude Code 和 Pi:`/smartcompact`(或 `/smartcompact report`)查看三条线、识别到的阶段节点和成本估算;`/smartcompact pause` / `resume` 暂停或恢复当前会话。模型还可以调用 `checkpoint` 工具记下决定,压缩后和进度笔记一起补回。
78
- - Codex:`node adapters/codex/report.ts <会话 ID>`,记录在 `~/.codex/smartcompact/`。
78
+ - Claude Code 和 Pi:`/smartcompact`(或 `/smartcompact report`)查看允许压缩线、原生压缩点、识别到的阶段节点和成本估算;`/smartcompact pause` / `resume` 暂停或恢复当前会话。模型还可以调用 `checkpoint` 工具记下决定,压缩后和进度笔记一起补回。
79
+ - Codex:`node ~/.codex/plugins/cache/smartcompact/smartcompact/<版本>/adapters/codex/report.ts <会话 ID>`(Node.js 22.18 及以上),记录在 `~/.codex/smartcompact/`。
79
80
 
80
81
  ## 常见问题
81
82
 
82
- **怎么判断"刚做完一件事"?** 靠识别 agent 执行的命令。提交只要真的执行了 `git commit` 且工具调用没报错就算;测试命令接了管道(`| tail`)又没开 `pipefail` 时退出码不可信,不算测试结果。
83
+ **怎么判断"做完一件事"?** 靠识别 agent 执行的命令。看的是上次压缩以来有没有做完过一件事,不要求正好在这一轮:一轮结束本身就是停顿点,非要等下一次提交,多半只是多花钱。提交只要真的执行了 `git commit` 且工具调用没报错就算;测试命令接了管道(`| tail`)又没开 `pipefail` 时退出码不可信,不算测试结果。
83
84
 
84
85
  **压缩后模型会不会忘事?** 随口提到的细节有时会丢。进度笔记只记文件、提交、测试和模型主动记下的决定。用户在对话里随口交代的细节,靠压缩时要求摘要原样保留用户说过的具体信息。Pi 上这样做明显减少了丢失,但不能保证不丢。
85
86
 
86
- **Claude Code 上有什么要注意的?** 插件的兜底压缩点和它调整后的原生压缩点接近;长时间认不出刚做完一件事时,原生压缩仍可能在一轮中间触发。插件是通过在会话里设置 `CLAUDE_CODE_AUTO_COMPACT_WINDOW` 来调整原生压缩点的,从这个会话里启动的进程会继承它:agent 运行的 `claude -p` 或脚本里启动的 Claude,也会在 26 万左右压缩。需要的话,启动这类进程前去掉这个变量。
87
+ **会改 Claude Code 自己的设置吗?** 不会,原生自动压缩点保持原样(接近 100 万)。0.1.2 及以前的版本会在会话里设置 `CLAUDE_CODE_AUTO_COMPACT_WINDOW`,把原生压缩点挪到约 26 万(子进程也会继承),并且到了那里不等做完一件事也压缩。0.1.3 去掉了这两点:大活不会再在固定大小处被打断。升级前就开着的会话还留着旧设置,新开一个会话就没有了。
88
+
89
+ **上下文窗口小的模型有用吗?** 只有原生压缩点比 20 万高出一截时才有用。窗口只有 20 万时,原生先压(Claude Code 约 16.7 万),插件根本不会出手。Codex 默认窗口 27.2 万,原生约 24.5 万压,插件只有 20 万~24.5 万这一小段可用,省得不多,除非把 `model_auto_compact_token_limit` 调高。允许压缩线可以调低(`allowLineTokens`、`--smartcompact-allow-line`、`SMARTCOMPACT_ALLOW_LINE`),但调低后的效果没有评测过。
87
90
 
88
91
  **怎么卸载?**
89
92
 
90
93
  - Claude Code:`/plugin uninstall smartcompact@smartcompact`。
91
94
  - Pi:`pi remove npm:smartcompact`。
92
- - Codex:在仓库目录里运行 `node scripts/codex.ts uninstall`。
95
+ - Codex:`codex plugin remove smartcompact@smartcompact`。
93
96
 
94
97
  ## 开发
95
98
 
@@ -102,7 +105,7 @@ sh scripts/check.sh
102
105
  - `plugin/`:Claude Code 插件;`plugin/src/core/` 是和平台无关的决策、信号、进度笔记和成本估算,其他适配都复用它。
103
106
  - `adapters/pi/`、`adapters/codex/`:Pi 和 Codex 的适配。
104
107
  - `eval/`:对照评测脚本(Claude、Pi、Codex 各一个),任务格式见 `eval/day.py`;评测用的题目和隐藏测试来自内部项目,没有放进仓库。
105
- - `DESIGN.md`:设计说明;`docs/TEST-REPORT.md`:测试报告。
108
+ - `DESIGN.md`:设计说明;`docs/TEST-REPORT.md`:测试报告;`docs/EVALUATION.md`:对照评测的英文说明。
106
109
 
107
110
  ## 许可证
108
111
 
@@ -1,8 +1,7 @@
1
1
  // SmartCompact 的 pi 扩展:把 pi 的事件接到 core 的决策上。
2
- // 和 Claude 版的区别:插件改不了 pi 的原生压缩线。
3
- // - 兜底线 = pi 配置的原生压缩点(窗口 − reserveTokens),上限 = 窗口 − pi 默认预留。
4
- // - 默认配置下两者相同,插件只在 agent_settled(不会再自动继续时)挑阶段节点主动压缩;
5
- // 用户把 reserveTokens 调大、让原生压缩提前时,插件在提前的那个点按工作状态决定是否取消。
2
+ // - 插件在 agent_settled(不会再自动继续时)挑做完一件事的时机主动压缩;等不到就交给 pi 原生压缩。
3
+ // - 上限 = 窗口 − pi 默认预留。用户把 reserveTokens 调大、让原生压缩提前时,
4
+ // 插件在提前的那个点按工作状态决定是否取消,最晚到上限放行。
6
5
 
7
6
  import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
8
7
  import { SettingsManager } from '@earendil-works/pi-coding-agent'
@@ -15,6 +14,7 @@ import { countDecisions, renderReport } from '../../plugin/src/core/report.ts'
15
14
  import {
16
15
  type DecisionLogEntry,
17
16
  expectedAfterTokens,
17
+ growthSinceMilestone,
18
18
  INITIAL_SESSION_STATE,
19
19
  parseState,
20
20
  type SessionState,
@@ -41,7 +41,6 @@ const nudgeText = (tokens: number): string =>
41
41
 
42
42
  export default function smartCompact(pi: ExtensionAPI) {
43
43
  pi.registerFlag('smartcompact-allow-line', { description: 'SmartCompact 允许压缩线(token)', type: 'string' })
44
- pi.registerFlag('smartcompact-grace', { description: 'SmartCompact 宽限倍数', type: 'string' })
45
44
  pi.registerFlag('smartcompact-min-freed', { description: 'SmartCompact 最少腾出空间(token)', type: 'string' })
46
45
 
47
46
  let state: SessionState = INITIAL_SESSION_STATE
@@ -64,7 +63,6 @@ export default function smartCompact(pi: ExtensionAPI) {
64
63
  const params = (ctx: ExtensionContext) =>
65
64
  resolveParams({
66
65
  allowLineTokens: Number(pi.getFlag('smartcompact-allow-line')) || undefined,
67
- graceFactor: Number(pi.getFlag('smartcompact-grace')) || undefined,
68
66
  minFreedTokens: Number(pi.getFlag('smartcompact-min-freed')) || undefined,
69
67
  defaultAfterTokens: compactionSettings(ctx).keepRecent + PI_AFTER_OVERHEAD,
70
68
  })
@@ -73,21 +71,17 @@ export default function smartCompact(pi: ExtensionAPI) {
73
71
  const usage = ctx.getContextUsage()
74
72
  if (!usage) return null
75
73
  const { reserve } = compactionSettings(ctx)
76
- return {
77
- allow: params(ctx).allowLineTokens,
78
- midTurn: usage.contextWindow - reserve,
79
- ceiling: usage.contextWindow - Math.min(reserve, PI_DEFAULT_RESERVE),
80
- adjustsNative: false,
81
- }
74
+ return { allow: params(ctx).allowLineTokens, ceiling: usage.contextWindow - Math.min(reserve, PI_DEFAULT_RESERVE) }
82
75
  }
83
76
 
84
77
  const tokensOf = (ctx: ExtensionContext): number | undefined => ctx.getContextUsage()?.tokens ?? undefined
85
78
 
86
- // 成本估算对比的是"没有插件时":pi 原生会在它配置的压缩点(兜底线)压缩,而不是等到真正的极限。
79
+ // 成本估算对比的是"没有插件时":pi 原生会在它配置的压缩点(窗口 − reserveTokens)压缩,而不是等到真正的极限。
87
80
  const sample = (ctx: ExtensionContext, current: Lines | null) => {
88
81
  const tokens = tokensOf(ctx)
82
+ const window = ctx.getContextUsage()?.contextWindow
89
83
  const model = `${ctx.model?.provider ?? ''} ${ctx.model?.id ?? ''}`
90
- if (tokens !== undefined && current) state = withSample(state, tokens, current.midTurn, pricesFor(model))
84
+ if (tokens !== undefined && current && window) state = withSample(state, tokens, window - compactionSettings(ctx).reserve, pricesFor(model))
91
85
  return tokens
92
86
  }
93
87
 
@@ -191,7 +185,8 @@ export default function smartCompact(pi: ExtensionAPI) {
191
185
  { tokens, lines: current, work: state.work, expectedAfterTokens: expectedAfterTokens(state, params(ctx).defaultAfterTokens), paused: state.paused },
192
186
  params(ctx),
193
187
  )
194
- if (decision.action !== 'none') log({ event: 'turn-end', action: decision.action, reason: decision.reason, tokens })
188
+ const sinceMilestoneTokens = decision.action === 'compact' ? growthSinceMilestone(state.work, tokens) : undefined
189
+ if (decision.action !== 'none') log({ event: 'turn-end', action: decision.action, reason: decision.reason, tokens, sinceMilestoneTokens })
195
190
  if (decision.action !== 'compact' || !ctx.isIdle() || ctx.hasPendingMessages()) return
196
191
  ours = true
197
192
  ctx.compact({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartcompact",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Compact long agent sessions earlier, at a finished piece of work, to cut cost without losing results (Claude Code, pi, Codex)",
6
6
  "license": "MIT",
@@ -0,0 +1,35 @@
1
+ # SmartCompact for Claude Code
2
+
3
+ SmartCompact compacts long Claude Code sessions earlier, at a pause after a piece of work is finished, so every later request re-reads a smaller context. Once the context passes 200k tokens, it compacts at the end of a turn if a commit, passing tests or a completed task happened since the last compaction. It waits while tests or the build are failing or errors repeat. It sets no limit of its own: if no piece of work gets finished, the session keeps its context and Claude Code's native auto-compaction applies as usual. A short progress ledger (files changed, latest commit and test result, recorded decisions) is handed back to the model after compaction.
4
+
5
+ In a 60-task A/B run on Claude Haiku it cut estimated cost by 44% with no loss in hidden acceptance tests; the model missed 6 of 180 recall questions about details mentioned in passing long before. Method and data: [docs/EVALUATION.md](https://github.com/coffeecoproject/SmartCompact/blob/main/docs/EVALUATION.md).
6
+
7
+ ## What it runs, reads and writes
8
+
9
+ Everything runs locally inside Claude Code. **The plugin makes no network requests, starts no processes, and does not read or change environment variables or Claude Code's settings.**
10
+
11
+ - **Reads**: the session's token usage; the tool name, input and success of each tool call, to recognize work: file paths from Edit/Write, Bash command text (to recognize `git commit` and test or build commands, and whether they failed), the `[branch hash] message` line from a commit's output, TodoWrite items and task events. It reads no credentials or tokens and no file contents, and nothing it reads leaves the machine.
12
+ - **Writes**: only the plugin's own storage (`$.store`): per-session state and the progress ledger (file paths, the latest commit line, the latest test command and result, in-progress task names, notes recorded with the `checkpoint` tool) and a short decision log. Old session records are pruned automatically.
13
+
14
+ ### What each hook does
15
+
16
+ All hooks live in one module, `hooks/register.ts`.
17
+
18
+ - **Session start**: records the context window (for the cost estimate) and registers the `checkpoint` tool and the `/smartcompact` command.
19
+ - **`/smartcompact` command**: answers only its own command (`report`, `pause`, `resume`).
20
+ - **Tool calls**: answers calls to its own `checkpoint` tool by recording the note. For every other tool it only looks at the result to recognize commits, tests, builds and edited files, and passes the result through unchanged.
21
+ - **Task created / completed**: keeps the list of in-progress task names for the ledger.
22
+ - **Prompt submit**: adds context to the turn, never changes the user's message. After a compaction it adds the progress ledger; once per cycle, when the context nears the allow line, it adds a one-line reminder to record progress with `checkpoint`.
23
+ - **Turn complete**: decides whether to compact. When it should, it compacts the session. Where a session cannot compact directly (headless sessions such as the desktop app), it runs the `/compact` command itself with SmartCompact's compaction instructions; it runs no other commands.
24
+ - **Compaction**: never blocks or changes a compaction itself. After any compaction of the main session it appends the progress ledger to the compacted context. Subagent compactions are left alone.
25
+
26
+ ## Use
27
+
28
+ - Install: `/plugin marketplace add coffeecoproject/SmartCompact`, then `/plugin install smartcompact@smartcompact`. It takes effect in new sessions.
29
+ - `/smartcompact` (or `/smartcompact report`) shows the lines, recognized milestones and a cost estimate; `/smartcompact pause` and `/smartcompact resume` turn it off and on for the session.
30
+ - Settings (optional, empty means default): `allowLineTokens` (200000), `minFreedTokens` (60000).
31
+ - Remove: `/plugin uninstall smartcompact@smartcompact`.
32
+
33
+ Full documentation, Pi and Codex versions: https://github.com/coffeecoproject/SmartCompact
34
+
35
+ License: MIT.
@@ -1,4 +1,4 @@
1
- // 两个决策函数:一轮结束时要不要主动压缩;一轮进行中原生自动压缩来问时放不放行。
1
+ // 两个决策函数:一轮结束时要不要主动压缩;一轮进行中原生自动压缩来问时放不放行(只有原生压缩点能提前的平台用到,目前是 pi)。
2
2
  // 读不到用量时一律不干预,按原生行为来。
3
3
 
4
4
  import type { Lines } from './lines.ts'
@@ -18,7 +18,7 @@ export type DecisionInput = {
18
18
  export type TurnEndDecision =
19
19
  | { action: 'none'; reason: 'paused' | 'no-reading' | 'below-line' }
20
20
  | { action: 'wait'; reason: HardSignal | 'insufficient-benefit' | 'waiting-for-milestone' }
21
- | { action: 'compact'; reason: MilestoneSource | 'past-grace' }
21
+ | { action: 'compact'; reason: MilestoneSource }
22
22
 
23
23
  export type MidTurnDecision =
24
24
  | { action: 'allow'; reason: 'paused' | 'no-reading' | 'at-ceiling' | 'default' }
@@ -37,7 +37,7 @@ export function decideAtTurnEnd(input: DecisionInput, params: Params): TurnEndDe
37
37
  if (hard) return { action: 'wait', reason: hard }
38
38
  if (!freesEnough(tokens, input, params)) return { action: 'wait', reason: 'insufficient-benefit' }
39
39
  if (work.milestone) return { action: 'compact', reason: work.milestone }
40
- if (tokens >= lines.midTurn) return { action: 'compact', reason: 'past-grace' }
40
+ // 不设兜底线:一直等不到做完一件事,多半是在做一件大活,正需要大上下文;交给原生在上限处压缩。
41
41
  return { action: 'wait', reason: 'waiting-for-milestone' }
42
42
  }
43
43
 
@@ -1,31 +1,7 @@
1
- // 三条线:允许压缩线 L、轮中兜底线 M、上限 C(原生压缩点)。
2
-
3
- import type { Params } from './params.ts'
1
+ // 两条线:允许压缩线 L(超过它,做完一件事时压缩才划算)、上限 C(原生压缩点)。
2
+ // 插件不设自己的兜底线:一直等不到做完一件事,就交给原生在上限处压缩,和没装插件时一样。
4
3
 
5
4
  export type Lines = {
6
5
  allow: number
7
- midTurn: number
8
6
  ceiling: number
9
- /** false 表示窗口本身放不下 L 和 M,插件不调整原生压缩线,只负责进度笔记 */
10
- adjustsNative: boolean
11
- }
12
-
13
- /** CLAUDE_CODE_AUTO_COMPACT_WINDOW 允许的最小值 */
14
- export const MIN_NATIVE_SETTING = 100_000
15
-
16
- /**
17
- * @param nativeSettingTokens 插件调整之前,原生压缩线的设定值(窗口或用户自己的配置)
18
- */
19
- export function planLines(nativeSettingTokens: number, params: Params): Lines {
20
- const ceiling = nativeSettingTokens - params.nativeBufferTokens
21
- const allow = params.allowLineTokens
22
- // 原生压缩线设定值有下限,M 不会低于它对应的触发点,否则算出的 M 与原生实际行为不符。
23
- const midTurn = Math.max(Math.round(allow * params.graceFactor), MIN_NATIVE_SETTING - params.nativeBufferTokens)
24
- if (midTurn >= ceiling) return { allow, midTurn: ceiling, ceiling, adjustsNative: false }
25
- return { allow, midTurn, ceiling, adjustsNative: true }
26
- }
27
-
28
- /** 让原生自动压缩从 M 开始尝试,需要写入的压缩线设定值。 */
29
- export function nativeSettingFor(lines: Lines, params: Params): number {
30
- return lines.midTurn + params.nativeBufferTokens
31
7
  }
@@ -3,13 +3,11 @@
3
3
  export type Params = {
4
4
  /** 允许压缩线 L:一轮结束时超过它才考虑压缩 */
5
5
  allowLineTokens: number
6
- /** 轮中兜底线 M = L × graceFactor */
7
- graceFactor: number
8
6
  /** 一次压缩至少要腾出的空间,不够就不压,防止连续压缩 */
9
7
  minFreedTokens: number
10
8
  /** 还没实测到压缩后大小时,用这个估计值 */
11
9
  defaultAfterTokens: number
12
- /** 原生触发点 ≈ 压缩线设定值 − 这个值(实测约 33k) */
10
+ /** Claude 原生触发点 ≈ 窗口 − 这个值(实测约 33k) */
13
11
  nativeBufferTokens: number
14
12
  /** 用量读数可能落后于最新的工具结果,接近上限时留出的余量 */
15
13
  ceilingMarginTokens: number
@@ -23,7 +21,6 @@ export type Params = {
23
21
 
24
22
  export const DEFAULT_PARAMS: Params = Object.freeze({
25
23
  allowLineTokens: 200_000,
26
- graceFactor: 1.3,
27
24
  minFreedTokens: 60_000,
28
25
  defaultAfterTokens: 50_000,
29
26
  nativeBufferTokens: 33_000,
@@ -35,7 +32,6 @@ export const DEFAULT_PARAMS: Params = Object.freeze({
35
32
 
36
33
  const MINIMUMS: Record<keyof Params, number> = {
37
34
  allowLineTokens: 1,
38
- graceFactor: 1,
39
35
  minFreedTokens: 0,
40
36
  defaultAfterTokens: 0,
41
37
  nativeBufferTokens: 0,
@@ -29,10 +29,10 @@ export function renderReport(input: {
29
29
  const seen = state.seen
30
30
  return [
31
31
  `SmartCompact${state.paused ? '(已暂停)' : ''}`,
32
- `- 当前用量 ${k(tokens)};允许压缩线 ${k(lines.allow)},兜底线 ${k(lines.midTurn)},上限 ${k(lines.ceiling)}${lines.adjustsNative ? '' : '(原生压缩线未调整)'}`,
32
+ `- 当前用量 ${k(tokens)};允许压缩线 ${k(lines.allow)},原生压缩点 ${k(lines.ceiling)}`,
33
33
  `- 本会话压缩 ${state.compactions} 次;最近一次压缩后约 ${k(state.measuredAfterTokens)}`,
34
34
  `- 本会话决策:插件压缩 ${(counts['compacted:plugin'] ?? 0) + (counts['compacted:plugin-queued'] ?? 0)},自动压缩放行 ${counts['compacted:auto'] ?? 0},手动压缩 ${counts['compacted:manual'] ?? 0},轮中否决 ${counts['mid-turn:veto'] ?? 0},轮末等待 ${counts['turn-end:wait'] ?? 0}`,
35
- `- 信号:提交 ${seen.commit},测试通过 ${seen['tests-passed']},任务完成 ${seen['task-completed']},checkpoint 完成 ${seen.checkpoint},后台任务 ${seen.background}`,
35
+ `- 信号:提交 ${seen.commit},测试通过 ${seen['tests-passed']},任务完成 ${seen['task-completed']},checkpoint 完成 ${seen.checkpoint}`,
36
36
  `- 当前状态:${state.work.milestone ? `已到阶段节点(${state.work.milestone})` : '等待阶段节点'}${state.work.testsFailing ? ',测试失败中' : ''}${state.work.buildFailing ? ',构建失败中' : ''}`,
37
37
  ratio === undefined
38
38
  ? '- 成本估算:还没有读数'
@@ -9,19 +9,19 @@ export type Signal =
9
9
  | { kind: 'task-completed' }
10
10
  | { kind: 'checkpoint'; milestone: boolean }
11
11
  | { kind: 'tool-result'; isError: boolean }
12
- | { kind: 'background'; running: number }
13
12
 
14
13
  export type MilestoneSource = 'commit' | 'tests-passed' | 'task-completed' | 'checkpoint'
15
14
 
16
- export type HardSignal = 'tests-failing' | 'build-failing' | 'error-streak' | 'background-running'
15
+ export type HardSignal = 'tests-failing' | 'build-failing' | 'error-streak'
17
16
 
18
17
  export type WorkState = {
19
18
  /** 自上次压缩以来最近的阶段节点 */
20
19
  milestone: MilestoneSource | null
20
+ /** 最近的阶段节点出现时的上下文用量,用来看它到压缩时隔了多远 */
21
+ milestoneTokens?: number
21
22
  testsFailing: boolean
22
23
  buildFailing: boolean
23
24
  errorStreak: number
24
- backgroundRunning: number
25
25
  }
26
26
 
27
27
  export const INITIAL_WORK_STATE: WorkState = Object.freeze({
@@ -29,7 +29,6 @@ export const INITIAL_WORK_STATE: WorkState = Object.freeze({
29
29
  testsFailing: false,
30
30
  buildFailing: false,
31
31
  errorStreak: 0,
32
- backgroundRunning: 0,
33
32
  })
34
33
 
35
34
  /** 这个信号代表哪种阶段节点,不是阶段节点时为 null。 */
@@ -49,7 +48,9 @@ export function milestoneOf(signal: Signal): MilestoneSource | null {
49
48
  }
50
49
 
51
50
  export function applySignal(state: WorkState, signal: Signal): WorkState {
52
- const next = { ...state, milestone: milestoneOf(signal) ?? state.milestone }
51
+ const milestone = milestoneOf(signal)
52
+ // 失败之后又做完了一件事(比如提交、checkpoint),那段失败就算过去了,不再挡着压缩。
53
+ const next = milestone ? { ...state, milestone, testsFailing: false, buildFailing: false } : state
53
54
  switch (signal.kind) {
54
55
  case 'tests':
55
56
  return { ...next, testsFailing: !signal.passed }
@@ -57,16 +58,14 @@ export function applySignal(state: WorkState, signal: Signal): WorkState {
57
58
  return { ...next, buildFailing: !signal.passed }
58
59
  case 'tool-result':
59
60
  return { ...next, errorStreak: signal.isError ? state.errorStreak + 1 : 0 }
60
- case 'background':
61
- return { ...next, backgroundRunning: Math.max(0, signal.running) }
62
61
  default:
63
62
  return next
64
63
  }
65
64
  }
66
65
 
67
- /** 压缩后清空阶段节点;失败、后台任务这些仍然成立的事实保留。 */
66
+ /** 压缩后清空阶段节点;失败这些仍然成立的事实保留。 */
68
67
  export function afterCompaction(state: WorkState): WorkState {
69
- return { ...state, milestone: null }
68
+ return { ...state, milestone: null, milestoneTokens: undefined }
70
69
  }
71
70
 
72
71
  /** 表明工作做到一半的硬信号,没有则返回 null。 */
@@ -74,6 +73,5 @@ export function hardSignal(state: WorkState, params: Params): HardSignal | null
74
73
  if (state.testsFailing) return 'tests-failing'
75
74
  if (state.buildFailing) return 'build-failing'
76
75
  if (state.errorStreak >= params.errorStreakLimit) return 'error-streak'
77
- if (state.backgroundRunning > 0) return 'background-running'
78
76
  return null
79
77
  }
@@ -4,7 +4,7 @@ import { EMPTY_LEDGER, type Ledger } from './ledger.ts'
4
4
  import { afterCompaction, applySignal, INITIAL_WORK_STATE, milestoneOf, type MilestoneSource, type Signal, type WorkState } from './signals.ts'
5
5
  import { EMPTY_TALLY, type Prices, recordCompaction, recordSample, type UsageTally } from './usage.ts'
6
6
 
7
- export type SignalsSeen = Record<MilestoneSource | 'background', number>
7
+ export type SignalsSeen = Record<MilestoneSource, number>
8
8
 
9
9
  export type SessionState = {
10
10
  work: WorkState
@@ -29,7 +29,7 @@ export const INITIAL_SESSION_STATE: SessionState = Object.freeze({
29
29
  paused: false,
30
30
  notesUpdated: false,
31
31
  nudged: false,
32
- seen: Object.freeze({ commit: 0, 'tests-passed': 0, 'task-completed': 0, checkpoint: 0, background: 0 }),
32
+ seen: Object.freeze({ commit: 0, 'tests-passed': 0, 'task-completed': 0, checkpoint: 0 }),
33
33
  usage: EMPTY_TALLY,
34
34
  }) as SessionState
35
35
 
@@ -45,15 +45,21 @@ export function expectedAfterTokens(state: SessionState, defaultAfterTokens: num
45
45
  return state.measuredAfterTokens ?? defaultAfterTokens
46
46
  }
47
47
 
48
- /** 应用信号,同时更新信号计数。 */
48
+ /** 应用信号,同时更新信号计数;出现阶段节点时记下当时的上下文用量(最近一次读数)。 */
49
49
  export function withSignals(state: SessionState, signals: readonly Signal[]): SessionState {
50
50
  const seen = { ...state.seen }
51
+ let work = signals.reduce(applySignal, state.work)
51
52
  for (const signal of signals) {
52
53
  const milestone = milestoneOf(signal)
53
54
  if (milestone) seen[milestone] += 1
54
- if (signal.kind === 'background' && signal.running > 0) seen.background += 1
55
55
  }
56
- return { ...state, work: signals.reduce(applySignal, state.work), seen }
56
+ if (signals.some(signal => milestoneOf(signal))) work = { ...work, milestoneTokens: state.usage.lastTokens }
57
+ return { ...state, work, seen }
58
+ }
59
+
60
+ /** 最近的阶段节点之后上下文又涨了多少;没有记录时为 undefined。 */
61
+ export function growthSinceMilestone(work: WorkState, tokens: number | undefined): number | undefined {
62
+ return tokens === undefined || work.milestoneTokens === undefined ? undefined : tokens - work.milestoneTokens
57
63
  }
58
64
 
59
65
  /** 记一次用量读数:压缩后的第一次读数同时记为实测的压缩后大小。 */
@@ -87,6 +93,8 @@ export type DecisionLogEntry = {
87
93
  tokens?: number
88
94
  tokensBefore?: number
89
95
  tokensAfter?: number
96
+ /** 决定压缩时,距离最近的阶段节点上下文涨了多少 */
97
+ sinceMilestoneTokens?: number
90
98
  trigger?: string
91
99
  detail?: string
92
100
  }