# wj-pi-auto-compact

`wj-pi-auto-compact` 是一个面向 [Pi](https://github.com/earendil-works/pi-mono) 的上下文自动压缩扩展。当当前模型的上下文使用率达到指定百分比后，扩展会在合适的生命周期节点调用 Pi 公共扩展 API 触发压缩；如果逻辑任务仍在执行，压缩成功后会尝试继续该任务。

扩展可以独立运行，不依赖、导入或探测任何子代理扩展。其他有状态扩展可以按需接入通用协调协议，在压缩前后暂停并恢复自己的会话状态。

## 功能

- **按百分比触发**：基于 `ctx.getContextUsage().percent` 判断，默认阈值为 `90%`。
- **区分任务状态**：最终答复结束后只执行空闲压缩；工具任务尚未完成时，压缩成功后尝试继续同一逻辑任务。
- **避免重复压缩**：识别 Pi 已经发起的 `threshold` 或 `overflow` 原生压缩，不再重复调用 `ctx.compact()`。
- **保守处理并发事件**：外部手动压缩、来源不明确的压缩记录和 `Already compacted` 错误不会被误判为本次压缩成功。
- **扩展间协调屏障**：通过 Pi EventBus 提供 `discover`、`prepare` 和 `complete` 三阶段协调协议。
- **严格配置校验**：拒绝非法 UTF-8、非法 JSON、未知字段、错误类型和越界阈值。
- **运行时兼容门禁**：Pi 版本不满足要求时不注册压缩处理器，并在宿主支持 extension UI 时显示一次警告。
- **适配多种运行模式**：TUI、RPC、JSON 和 print 模式下均执行相同的压缩逻辑。

本扩展不会注册命令、工具、快捷键、widget 或设置界面。安装并启用后，正常使用 Pi 即可。

## 运行要求

| 项目 | 最低要求 |
| --- | --- |
| Node.js | `22.19.0` |
| Pi | `>=0.84.1`，包名为 `@earendil-works/pi-coding-agent`；已验证至 `0.84.2` |
| Git | 从 Git 仓库安装时需要 |

扩展从 Pi 公开导出的 `VERSION` 获取实际版本，并使用严格的 SemVer 规则检查 `>=0.84.1`。该范围按 `semver` 默认规则排除预发布版本，例如 `0.84.1-beta.1` 不会通过检查。

> Pi 扩展以当前用户权限运行，可以访问本机文件和进程。安装任何第三方扩展前都应先检查其源码。

## 安装

### 用户级安装

对当前用户的所有 Pi 项目启用：

```bash
pi install npm:wj-pi-auto-compact
```

Pi 会克隆仓库、安装生产依赖，并把包写入用户级设置。

### 项目级安装

只为当前项目启用：

```bash
pi install npm:wj-pi-auto-compact -l
```

项目级安装会写入当前项目的 `.pi/settings.json`。其他使用者信任该项目后，Pi 会自动安装缺失的包。

### 临时试用

只在本次 Pi 进程中加载，不修改持久设置：

```bash
pi -e npm:wj-pi-auto-compact
```

固定到标签或提交的包不会被 `pi update --extensions` 自动移动到其他版本。

### 管理安装

```bash
# 查看已安装的 Pi 包
pi list

# 更新本扩展
pi update --extension npm:wj-pi-auto-compact

# 删除用户级安装
pi remove npm:wj-pi-auto-compact

# 删除当前项目的安装
pi remove npm:wj-pi-auto-compact -l
```

## 使用方法

安装完成后启动 Pi，不需要执行额外命令：

```bash
pi
```

默认情况下，扩展会在某个 turn 结束且上下文使用率达到或超过 `90%` 时记录一次阈值命中，并根据任务状态选择处理方式：

| 场景 | 行为 |
| --- | --- |
| 助手已经给出最终答复 | 等待真实的 `agent_settled`，压缩成功后保持会话空闲，不创建新 turn |
| 助手刚完成工具调用，且 Pi 随后开始了真实的新 turn | 建立协调屏障并中断当前 run；Pi 静止后执行压缩，成功时尝试注入一次用户可见的 continuation |
| 工具调用后没有出现后继 `turn_start` | 按已结束任务处理，只压缩，不猜测任务需要继续 |
| 使用率低于阈值，或 Pi 无法提供百分比 | 不触发压缩，也不自行估算 token 百分比 |

TUI 或支持 extension UI 的 RPC 宿主会在开始自动压缩时显示一次信息通知，其中包含触发时的上下文使用率、配置阈值、已用 token、上下文窗口 token 和阈值对应的 token 数。示例：

```text
Context usage is 95% (95,000 / 100,000 tokens). Auto-compaction threshold is 90% (90,000 tokens). Preparing to compact.
```

JSON、print 或其他 `ctx.hasUI === false` 的宿主不会增加替代文本输出。

## 配置

配置文件固定为：

```text
<用户主目录>/.pi/agent/wj-pi-auto-compact.json
```

这是用户级配置，目前不支持项目级配置、环境变量或命令行覆盖。文件不存在时直接使用默认值，无需手动创建。

完整配置示例：

```json
{
  "enabled": true,
  "maxContextPercent": 90,
  "customInstructions": "请优先保留当前任务目标、已完成的修改、未解决的问题、关键文件路径和下一步计划。"
}
```

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | 是否启用本扩展的自动触发逻辑 |
| `maxContextPercent` | `number` | `90` | 触发阈值，必须是有限数值，且 `0 < value <= 100`；允许小数 |
| `customInstructions` | `string` | 未配置 | 传给 Pi 原生压缩的补充指令；空字符串或全空白字符串不产生附加指令 |

三个字段都可以省略，省略时使用对应默认值。例如 `{}` 等价于不配置自定义指令的默认配置。

### 配置校验

配置必须满足以下条件：

- 文件内容是有效 UTF-8。
- 根值是 JSON 对象，不能是数组、字符串或 `null`。
- 只能包含 `enabled`、`maxContextPercent` 和 `customInstructions`。
- 每个字段的类型和值域必须正确。

`customInstructions` 会透传给 Pi 的 `ctx.compact({ customInstructions })`，作为摘要时的补充指令；Pi 仍然使用自己的默认摘要提示词，不会被本字段完全替换。该配置只影响本扩展触发的自动压缩，不影响 Pi 原生的 threshold/overflow 自动压缩。

配置文件存在但校验失败时，扩展会禁用当前扩展实例，而不是带着部分配置继续运行。宿主支持 extension UI 时会显示一次具体原因。修正配置后，在 Pi 中执行 `/reload` 或重新启动 Pi；配置只在扩展加载时读取，不会在每个 turn 中重复读取。

## 运行机制

一次典型事务的流程如下：

```text
turn_end 检查使用率
  -> 根据任务状态等待 agent_end 或后继 turn_start
  -> discover / prepare 协调参与者
  -> 等待 agent_settled
  -> 复用 Pi 原生压缩，或调用 ctx.compact()
  -> complete 协调参与者
  -> 必要时发送用户可见的 continuation
```

### 空闲压缩

最终答复结束后达到阈值时，扩展会等待 Pi 的真实 `agent_settled` 再触发压缩。成功后不会发送 continuation，也不会创建额外的模型轮次。

### 工具任务续跑

扩展不会仅凭助手消息中存在 `toolCall` 就认定任务尚未结束。只有成功的工具 turn 后又观察到真实的 `turn_start`，才会建立协调屏障并调用 `ctx.abort()` 中断当前 run。

压缩和协调都成功后，扩展通过 `pi.sendMessage()` 发送一次对用户和模型均可见的 continuation：

```text
[wj-pi-auto-compact/v1]
Auto-compaction completed. Continue the interrupted task from the compacted context.
```

该消息使用 `display: true` 和 `triggerTurn: true`，并通过 `details.coordinationRequestId` 关联本次协调事务。它继续的是同一逻辑任务，但 Pi 会创建一个新的物理 turn。

扩展会在 5 秒内等待该消息对应的真实 `message_start`。超时只表示“启动未确认”，不会证明消息没有入队，也不会反向发送错误补偿；迟到的 `message_start` 仍可完成本地匹配。只有 `sendMessage()` 同步抛错时，扩展才能确定 continuation 未提交。

### 与 Pi 原生压缩的关系

本扩展不会关闭或替换 Pi 自带的 threshold/overflow 自动压缩。两者的配置彼此独立：

- `maxContextPercent` 控制本扩展何时准备压缩。
- `customInstructions` 只用于本扩展最终调用 `ctx.compact()` 的自动压缩请求；缺失或为空时使用 Pi 默认摘要指令。
- Pi 的 `reserveTokens`、`keepRecentTokens` 等设置仍控制 Pi 自身的压缩策略和摘要内容。
- 当前事务中观察到同一 branch 上明确的 `threshold` 或 `overflow` `session_compact` 后，本扩展认为原生压缩已经完成，不再调用 `ctx.compact()`。
- `overflow` 事件带有 `willRetry: true` 时，Pi 已承诺原生重试，本扩展不会再发送 continuation。
- Pi 已尝试原生压缩但失败或取消时，本扩展不会额外包一层重试。
- 用户或其他扩展触发的 manual compaction 不会被当成本事务的成功证据。

压缩摘要、进度指示、取消处理、重试次数和退避逻辑均复用 Pi 的原生实现。

## 协调协议

扩展通过 Pi EventBus 公开协议 `wj-pi-auto-compact/coordination/v1`，供其他有状态扩展在压缩前后建立屏障。独立运行时不需要任何参与者。

| 阶段 | 请求事件 | 响应事件 | 用途 |
| --- | --- | --- | --- |
| 发现 | `.../discover` | `.../discovered` | 查找当前 session 中需要屏障的参与者 |
| 准备 | `.../prepare` | `.../prepared` | 要求固定参与者暂停或保存状态 |
| 完成 | `.../complete` | `.../completed` | 通知压缩结果并释放屏障 |

事件完整前缀为 `wj-pi-auto-compact/coordination/v1/`。协议只传递协议版本、请求 ID、不透明的参与者 ID、屏障标记、业务确认和压缩结果，不传递其他扩展的任务身份或运行时领域数据。

关键语义：

- 每个事务都会重新发现参与者，`session_start` 的预发现结果只用于预热。
- 发现窗口为 100 毫秒；prepare 和 complete 的业务确认期限分别为 12 秒。
- 只有 `requiresBarrier: true` 的参与者会进入本次固定成员集合。
- EventBus 的底层投递成功不能代替 `prepared` 或 `completed` 业务确认。
- prepare 被拒绝、超时或状态不确定时，会向全部目标发送幂等的 `not_started` 以释放潜在屏障。
- complete 采用全体确认语义；任一固定参与者未确认时，不会发送 continuation，并向全体成员补发 `not_started`。
- `succeeded` 只表示压缩已经成功，不保证 continuation 已经交付或模型最终执行成功。

协议类型、解析规则和常量见 [`src/coordination-protocol.ts`](src/coordination-protocol.ts)，客户端事务实现见 [`src/coordination-client.ts`](src/coordination-client.ts)。

## 许可证

本项目使用 [MIT License](LICENSE)，Copyright (c) 2026 WJ。

## 已知限制

这些限制来自 Pi 当前公开扩展 API，扩展只能保守处理，无法完全消除：

- manual `session_before_compact` 和 `session_compact` 没有请求 ID。本扩展只能结合当前 branch、非 manual 原因和本次 `ctx.compact()` 的专属回调判断归属。
- `ctx.compact()` 没有公开取消句柄或 Promise。正常 session 中，本扩展会持续等待本次调用的 `onComplete` 或 `onError`；session shutdown/reload 只能停止本地等待并释放协调事务，不能保证底层摘要工作已经停止。
- `pi.sendMessage()` 的公开返回类型是 `void`，没有投递回调或撤销 API。匹配的 `message_start` 只能证明 Pi 已经开始处理，不能保证后续 provider 请求或模型 turn 最终成功。
