@criogaid/pi-codex-compaction 0.3.0 → 0.4.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/CHANGELOG.md +8 -0
- package/README.md +108 -63
- package/package.json +1 -1
- package/src/checkpoint.ts +14 -4
- package/src/index.ts +11 -1
- package/src/remote.ts +3 -0
- package/src/request-snapshot.ts +105 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## 未发布
|
|
4
4
|
|
|
5
|
+
## 0.4.0
|
|
6
|
+
|
|
7
|
+
- 更新压缩历史的恢复提示,明确使用当前包 `@criogaid/pi-codex-compaction`。保留旧会话摘要的精确识别与消息编辑保护,无需迁移会话文件。
|
|
8
|
+
|
|
9
|
+
- 压缩请求在会话、模型、历史、工具及设置匹配时,复用普通请求中观察到的系统指令、工具声明、缓存标识、思考与服务等级等选项。请求仍由 Pi 生成和发送,不沿用旧响应 ID、输出上限或传输开关。
|
|
10
|
+
|
|
11
|
+
- 重写 README:按“V2 默认启用、失败退回 Pi 原生压缩、独立摘要模型只接管原生压缩”说明压缩流程,补充流程图、示例和徽章。
|
|
12
|
+
|
|
5
13
|
## 0.3.0
|
|
6
14
|
|
|
7
15
|
- npm 发布包名改为 `@criogaid/pi-codex-compaction`。新增标签触发的发布流程,通过版本核对、类型检查、测试和打包检查后发布,并提供源码来源证明;现有配置路径和会话格式保持不变。
|
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
# pi-codex-compaction
|
|
1
|
+
# @criogaid/pi-codex-compaction
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/Criogaid/pi-codex-compaction/actions/workflows/ci.yml) [](https://www.npmjs.com/package/@criogaid/pi-codex-compaction) [](https://www.npmjs.com/package/@criogaid/pi-codex-compaction) [](LICENSE)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
让 [Pi](https://github.com/earendil-works/pi) 在 Codex 模型上使用 Codex Remote Compaction V2 压缩聊天记录,失败时自动退回 Pi 原生压缩,并且可以为原生压缩单独指定一个摘要模型。
|
|
6
|
+
|
|
7
|
+
- **装上即用:** 对 Codex 模型默认启用 V2 远程压缩,无需任何配置。
|
|
8
|
+
- **失败自动退回:** 当前模型不支持 V2,或 V2 请求失败时,改用 Pi 原生的文字摘要压缩,对话不会中断。
|
|
9
|
+
- **独立摘要模型(可选):** 退回 Pi 原生压缩时,可以使用你指定的模型和思考等级生成摘要。它只接管这一步,不影响 V2 远程压缩,也不改变当前对话模型。
|
|
10
|
+
|
|
11
|
+
[安装](#安装) · [工作方式](#工作方式) · [独立摘要模型](#独立摘要模型) · [配置](#配置) · [自定义网关](#自定义网关) · [注意事项](#注意事项)
|
|
6
12
|
|
|
7
13
|
## 安装
|
|
8
14
|
|
|
@@ -18,58 +24,85 @@ pi install npm:@criogaid/pi-codex-compaction
|
|
|
18
24
|
pi install git:github.com/Criogaid/pi-codex-compaction
|
|
19
25
|
```
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## 开始使用
|
|
27
|
+
> [!NOTE]
|
|
28
|
+
> 请安装带 `@criogaid` 作用域的包。不带作用域的 `pi-codex-compaction` 属于另一个仓库。
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
使用 Pi 自带的 `openai-codex` 服务商和官方地址时,安装后无需配置。手动执行 `/compact`、Pi 自动压缩,以及聊天记录超出模型容量后触发的压缩,都会经过本扩展。
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
## 工作方式
|
|
28
33
|
|
|
29
34
|
```text
|
|
30
|
-
|
|
35
|
+
触发压缩(/compact、自动压缩、超出上下文)
|
|
36
|
+
|
|
|
37
|
+
v
|
|
38
|
+
V2 开启,且当前模型支持 V2? -- 否 ------------------+
|
|
39
|
+
| 是 |
|
|
40
|
+
v |
|
|
41
|
+
用当前对话模型请求 V2 远程压缩 |
|
|
42
|
+
| |
|
|
43
|
+
v |
|
|
44
|
+
成功? -- 是 --> 完成 |
|
|
45
|
+
| 否 |
|
|
46
|
+
v |
|
|
47
|
+
Pi 原生压缩(文字摘要) <----------------------------+
|
|
48
|
+
|
|
|
49
|
+
+-- 已启用独立摘要模型 --> 用指定模型和思考等级生成摘要
|
|
50
|
+
| 失败则停止本次压缩
|
|
51
|
+
|
|
|
52
|
+
+-- 未启用 -------------> 用当前对话模型生成摘要
|
|
31
53
|
```
|
|
32
54
|
|
|
33
|
-
|
|
55
|
+
| 情况 | 压缩方式 | 使用的模型 |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| V2 开启,模型支持 V2,请求成功 | V2 远程压缩 | 当前对话模型 |
|
|
58
|
+
| V2 请求失败 | Pi 原生压缩 | 独立摘要模型,未启用时为当前对话模型 |
|
|
59
|
+
| 模型不支持 V2,或 V2 已关闭 | Pi 原生压缩 | 独立摘要模型,未启用时为当前对话模型 |
|
|
34
60
|
|
|
35
|
-
|
|
36
|
-
| --- | --- |
|
|
37
|
-
| Remote Compaction V2 | 是否优先尝试远程压缩,默认 On |
|
|
38
|
-
| Use separate summary model | 是否使用独立模型生成文字摘要,默认 Off |
|
|
39
|
-
| Summary model and thinking level | 选择摘要模型及其思考等级 |
|
|
61
|
+
压缩完成后继续原来的对话,当前对话模型和思考等级保持不变。V2 失败、改用独立摘要模型时,Pi 会显示提示及实际使用的模型。
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
`/compact` 后附加的要求(例如“重点保留尚未完成的任务”)只对 Pi 原生压缩生效。V2 不接受这些要求,扩展会提示它们被忽略。
|
|
42
64
|
|
|
43
|
-
|
|
65
|
+
## 独立摘要模型
|
|
44
66
|
|
|
45
|
-
|
|
67
|
+
假设你平时用 `gpt-6-astra` 聊天,希望退回原生压缩时改用 `gpt-6.1-sol` 生成摘要:
|
|
46
68
|
|
|
47
|
-
|
|
69
|
+
1. 执行 `/codex-compaction` 打开设置菜单。
|
|
70
|
+
2. 在 `Summary model and thinking level` 中选择 `gpt-6.1-sol` 及其思考等级。
|
|
71
|
+
3. 打开 `Use separate summary model`。
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
之后的压缩过程如下:
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| V2 关闭,或模型不支持协议 | 直接用文字摘要压缩 |
|
|
75
|
+
- `gpt-6-astra` 支持 V2 时,仍由 `gpt-6-astra` 完成 V2 远程压缩,不经过 `gpt-6.1-sol`。
|
|
76
|
+
- V2 失败或不可用时,由 `gpt-6.1-sol` 生成文字摘要。
|
|
77
|
+
- 压缩结束后继续使用 `gpt-6-astra` 对话。
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
如果想始终用 `gpt-6.1-sol` 生成文字摘要,再关闭 `Remote Compaction V2`。
|
|
57
80
|
|
|
58
|
-
|
|
81
|
+
独立摘要模型的登录凭据沿用 Pi 的配置。选择虚拟模型时,Pi 按它的配置路由到实际发送请求的模型。
|
|
59
82
|
|
|
60
|
-
|
|
83
|
+
> [!IMPORTANT]
|
|
84
|
+
> 已启用的独立摘要模型不可用或请求失败时,本次压缩会停止,不会悄悄改用当前对话模型。摘要为空、生成被中断或因长度限制被截断时,也会停止压缩。
|
|
61
85
|
|
|
62
|
-
##
|
|
86
|
+
## 配置
|
|
63
87
|
|
|
64
|
-
|
|
88
|
+
### 设置菜单
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
执行 `/codex-compaction`:
|
|
91
|
+
|
|
92
|
+
| 设置 | 默认 | 用途 |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `Remote Compaction V2` | On | 是否优先尝试 V2 远程压缩 |
|
|
95
|
+
| `Use separate summary model` | Off | 退回原生压缩时是否使用独立摘要模型 |
|
|
96
|
+
| `Summary model and thinking level` | 未选择 | 独立摘要模型及其思考等级 |
|
|
69
97
|
|
|
70
|
-
|
|
98
|
+
- 终端菜单中按回车或空格切换开关,修改立即保存,光标停留在原来的行。选中一项时,列表下方会说明它的用途及与其他设置的关系。
|
|
99
|
+
- 模型可以按服务商(Provider)、ID 或名称搜索;取消选择不会保存。
|
|
100
|
+
- 关闭 `Use separate summary model` 会保留已选模型,之后重新打开即可使用。
|
|
101
|
+
- 通过 RPC 使用 Pi 的客户端会在选择框标题中看到何时使用文字摘要以及当前选用的模型,每次操作后退出菜单。
|
|
71
102
|
|
|
72
|
-
|
|
103
|
+
### 配置文件
|
|
104
|
+
|
|
105
|
+
设置保存在 `~/.pi/agent/extensions/pi-codex-compaction/config.json`。设置了 `PI_CODING_AGENT_DIR` 时,改用该目录下的 `extensions/pi-codex-compaction/config.json`。菜单会在保存时创建文件;非交互模式下可以直接编辑。
|
|
73
106
|
|
|
74
107
|
```json
|
|
75
108
|
{
|
|
@@ -79,28 +112,38 @@ pi install git:github.com/Criogaid/pi-codex-compaction
|
|
|
79
112
|
},
|
|
80
113
|
"fallback": {
|
|
81
114
|
"enabled": true,
|
|
82
|
-
"provider": "
|
|
83
|
-
"model": "
|
|
115
|
+
"provider": "openai-codex",
|
|
116
|
+
"model": "gpt-6.1-sol",
|
|
84
117
|
"thinkingLevel": "high"
|
|
85
118
|
}
|
|
86
119
|
}
|
|
87
120
|
```
|
|
88
121
|
|
|
89
|
-
|
|
122
|
+
| 字段 | 对应菜单项 | 说明 |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `remoteCompaction.enabled` | `Remote Compaction V2` | 省略 `remoteCompaction` 时默认开启 |
|
|
125
|
+
| `fallback.enabled` | `Use separate summary model` | 旧配置省略此字段时按开启处理 |
|
|
126
|
+
| `fallback.provider`、`fallback.model`、`fallback.thinkingLevel` | `Summary model and thinking level` | 使用 Pi 中的实际 ID 和该模型支持的思考等级;三项须同时提供或同时省略 |
|
|
90
127
|
|
|
91
|
-
|
|
128
|
+
省略整个 `fallback` 表示未指定独立摘要模型。配置文件不保存 API key,模型和认证须先在 Pi 中配置好。
|
|
92
129
|
|
|
93
|
-
|
|
130
|
+
<details>
|
|
131
|
+
<summary><strong>读取时机与校验规则</strong></summary>
|
|
94
132
|
|
|
95
|
-
|
|
133
|
+
- 配置文件须为 UTF-8 JSON,大小不超过 16 KiB。
|
|
134
|
+
- 每次压缩开始时读取一次配置,压缩途中的修改在下次生效。
|
|
135
|
+
- 文件无法读取、JSON 格式错误或 V2 设置无效时,停止压缩。
|
|
136
|
+
- `fallback` 部分只在需要原生压缩时校验,因此它填错不会影响成功的 V2 压缩。
|
|
137
|
+
- `fallback` 无效时菜单显示 `Invalid`。你仍可切换 V2,已填写的内容会原样保留;重新选择模型即可修复,之后需要打开 `Use separate summary model` 才会使用它。
|
|
138
|
+
- 打开菜单后配置文件被其他程序修改时,保存会失败;重新打开菜单再操作即可。
|
|
96
139
|
|
|
97
|
-
|
|
140
|
+
</details>
|
|
98
141
|
|
|
99
142
|
## 自定义网关
|
|
100
143
|
|
|
101
|
-
|
|
144
|
+
默认只有 Pi 自带的 `openai-codex` 服务商(官方地址)会尝试 V2。其他网关须本身支持 Codex Remote Compaction V2,仅兼容普通 Responses API 不够;模型的 API 类型须为 `openai-responses` 或 `openai-codex-responses`。
|
|
102
145
|
|
|
103
|
-
在 Pi 的 `models.json` 中为模型添加 `compat.remoteCompaction
|
|
146
|
+
在 Pi 的 `models.json` 中为模型添加 `compat.remoteCompaction`:
|
|
104
147
|
|
|
105
148
|
```json
|
|
106
149
|
{
|
|
@@ -126,23 +169,19 @@ pi install git:github.com/Criogaid/pi-codex-compaction
|
|
|
126
169
|
|
|
127
170
|
替换地址和模型 ID,并设置 `CUSTOM_CODEX_API_KEY` 环境变量。这个例子请求 `https://gateway.example.com/v1/responses`。
|
|
128
171
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
## 使用前需要了解
|
|
132
|
-
|
|
133
|
-
关闭 V2 后改用文字摘要压缩,不影响已压缩的聊天记录。
|
|
172
|
+
网关的压缩请求使用其他路径时,可以在 `remoteCompaction` 内增加 `endpoint`。它的协议、主机和端口须与 Pi 认证后实际使用的 `baseUrl` 一致,且不能包含用户名、密码、查询参数(`?`)或片段(`#`)。
|
|
134
173
|
|
|
135
|
-
|
|
174
|
+
## 注意事项
|
|
136
175
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
176
|
+
- **V2 压缩结果绑定服务商。** V2 压缩后的旧记录以加密数据保存,只有原服务能读取,扩展无法还原成文字。重新打开会话或创建分支时可以继续使用,但须保持服务商、API 类型和请求地址不变。换到其他服务后,原生压缩只能根据 Pi 仍可读取的摘要和消息生成,无法包含这部分加密记录。
|
|
177
|
+
- **同一服务内切换模型。** 扩展会继续发送已压缩的记录;如果新模型不接受,需要切回原模型。V2 协议仍属实验性质。
|
|
178
|
+
- **关闭 V2 不影响已有记录。** 已经用 V2 压缩的记录仍会照常发送。
|
|
179
|
+
- **加载顺序。** 压缩后 Pi 会保留一部分近期消息。如果它们被其他扩展改写,本扩展可能无法确认其与已压缩记录对应,从而停止使用那部分记录。搭配会改写聊天内容的扩展时,请把本包放在 Pi 的 `packages` 列表靠前的位置。
|
|
180
|
+
- **缓存。** 会话、模型、历史前缀、工具和设置未变时,压缩请求会复用最近一次普通请求中观察到的系统指令、工具定义、缓存标识和思考等选项。关闭或隐藏的工具不会因为压缩重新出现在这份工具声明中。记录只覆盖本扩展收到的请求内容,之后执行的扩展仍可能改写它;服务端也自行决定是否使用缓存,因此不保证缓存命中或费用下降。
|
|
142
181
|
|
|
143
182
|
## 开发
|
|
144
183
|
|
|
145
|
-
使用 Node 24
|
|
184
|
+
使用 Node 24 或更新版本,在仓库根目录运行:
|
|
146
185
|
|
|
147
186
|
```bash
|
|
148
187
|
npm ci --ignore-scripts
|
|
@@ -151,21 +190,27 @@ npm test
|
|
|
151
190
|
npm run pack:check
|
|
152
191
|
```
|
|
153
192
|
|
|
154
|
-
|
|
193
|
+
本地加载使用 `pi install .`。Pi 不会替本地包安装依赖,须先完成上面的安装步骤。
|
|
194
|
+
|
|
195
|
+
运行代码在 `src/`,入口为 `src/index.ts`,Pi 直接加载 TypeScript 源码;测试在 `tests/`,编译结果写入 Git 忽略的 `dist/`。打包检查同时验证发布文件清单和 Pi 的实际入口加载。[CI](.github/workflows/ci.yml) 在 Ubuntu 24.04 和 Windows 上使用 Node 24 执行验证。变更记录见 [CHANGELOG.md](CHANGELOG.md),维护规则见 [AGENTS.md](AGENTS.md)。
|
|
155
196
|
|
|
156
|
-
|
|
197
|
+
<details>
|
|
198
|
+
<summary><strong>发布流程</strong></summary>
|
|
157
199
|
|
|
158
|
-
[
|
|
200
|
+
[Publish](.github/workflows/publish.yml) 在推送 `v*` 标签时运行。标签须与 `package.json` 的版本一致,例如版本 `0.4.0` 对应标签 `v0.4.0`。目前只发布正式版本,不接受 `-beta`、`-rc` 等预发布后缀。
|
|
159
201
|
|
|
160
|
-
|
|
202
|
+
首次发布前,在仓库的 **Settings → Secrets and variables → Actions** 中添加 `NPM_TOKEN`。按 [npm 文档](https://docs.npmjs.com/creating-and-viewing-access-tokens)创建 granular access token,授予 `@criogaid` 作用域的发布权限(Read and write / publish and stage),并启用 Bypass two-factor authentication。包创建后,可以将 token 权限缩小到该包。
|
|
161
203
|
|
|
162
|
-
|
|
204
|
+
准备版本时:
|
|
163
205
|
|
|
164
|
-
|
|
206
|
+
1. 运行 `npm version <版本号> --no-git-tag-version`,同步更新 `package.json` 和 `package-lock.json`。
|
|
207
|
+
2. 将 CHANGELOG 中“未发布”的改动整理到对应版本下。
|
|
208
|
+
3. 运行上面的验证命令,通过后提交。
|
|
209
|
+
4. 为该提交创建并推送 `v<版本号>` 标签。
|
|
165
210
|
|
|
166
|
-
|
|
211
|
+
工作流会核对版本,重新安装依赖,执行类型检查、测试和打包检查,全部通过后发布公开 npm 包,并附带可追溯到源码提交和工作流的来源证明(provenance)。普通分支推送不会发布;已发布的 npm 版本不能覆盖,后续修改须使用新版本号。
|
|
167
212
|
|
|
168
|
-
|
|
213
|
+
</details>
|
|
169
214
|
|
|
170
215
|
## 许可证
|
|
171
216
|
|
package/package.json
CHANGED
package/src/checkpoint.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
|
|
21
21
|
export const CHECKPOINT_KIND = "pi-codex-compaction";
|
|
22
22
|
export const CHECKPOINT_VERSION = 1;
|
|
23
|
+
const EXTENSION_PACKAGE = "@criogaid/pi-codex-compaction";
|
|
23
24
|
|
|
24
25
|
export interface CodexCheckpointDetails extends ProviderIdentity {
|
|
25
26
|
kind: typeof CHECKPOINT_KIND;
|
|
@@ -62,12 +63,21 @@ export function fingerprintMessage(message: AgentMessage): string {
|
|
|
62
63
|
export function checkpointMarker(checkpointId: string): string {
|
|
63
64
|
return [
|
|
64
65
|
`[PI_CODEX_REMOTE_CHECKPOINT:${checkpointId}]`,
|
|
65
|
-
"
|
|
66
|
-
|
|
66
|
+
"The compressed chat history could not be loaded. Do not infer missing details.",
|
|
67
|
+
`Ask the user to enable ${EXTENSION_PACKAGE} and reconnect to the model service that created it.`,
|
|
67
68
|
].join(" ");
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
export function fallbackSummary(checkpointId: string): string {
|
|
72
|
+
return [
|
|
73
|
+
`Earlier chat history was compressed by Codex Remote Compaction V2 (checkpoint ${checkpointId}).`,
|
|
74
|
+
`To use it, keep ${EXTENSION_PACKAGE} enabled and connected to the original model service.`,
|
|
75
|
+
"If it cannot be loaded, only Pi's retained recent messages are available; do not guess missing details.",
|
|
76
|
+
].join(" ");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The persisted v1 summary identifies checkpoints written before the package was renamed.
|
|
80
|
+
function legacyFallbackSummary(checkpointId: string): string {
|
|
71
81
|
return [
|
|
72
82
|
`Codex Remote Compaction V2 checkpoint ${checkpointId} stores the older history opaquely.`,
|
|
73
83
|
"Full replay requires @oipsanthony/pi-codex-compaction and the original provider endpoint and model.",
|
|
@@ -202,9 +212,9 @@ export function projectCheckpointContext(
|
|
|
202
212
|
messages: readonly AgentMessage[],
|
|
203
213
|
details: CodexCheckpointDetails,
|
|
204
214
|
): AgentMessage[] | undefined {
|
|
205
|
-
const
|
|
215
|
+
const summaries = new Set([fallbackSummary(details.checkpointId), legacyFallbackSummary(details.checkpointId)]);
|
|
206
216
|
const summaryIndex = messages.findIndex(
|
|
207
|
-
(message) => message.role === "compactionSummary" && message.summary
|
|
217
|
+
(message) => message.role === "compactionSummary" && summaries.has(message.summary),
|
|
208
218
|
);
|
|
209
219
|
if (summaryIndex < 0) return undefined;
|
|
210
220
|
const keptStart = summaryIndex + 1;
|
package/src/index.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { requestRemoteCompaction } from "./remote.js";
|
|
|
35
35
|
import { requestFallbackCompaction } from "./fallback.js";
|
|
36
36
|
import { COMPACTION_SETTINGS_RELATIVE_PATH, loadCompactionSettings, type CompactionConfiguration } from "./fallback-settings.js";
|
|
37
37
|
import { registerCompactionCommand } from "./fallback-command.js";
|
|
38
|
-
import { compactionRequest, RequestSnapshotTracker, type RequestSnapshots } from "./request-snapshot.js";
|
|
38
|
+
import { compactionRequest, providerRequestFor, RequestSnapshotTracker, type ProviderRequestInputs, type RequestSnapshots } from "./request-snapshot.js";
|
|
39
39
|
|
|
40
40
|
const STATUS_KEY = "codex-compaction";
|
|
41
41
|
const COMPLETION_ENTRY_TYPE = "pi-codex-compaction-completed";
|
|
@@ -75,6 +75,13 @@ function activeTools(pi: ExtensionAPI): Tool[] {
|
|
|
75
75
|
}));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function providerRequestInputs(pi: ExtensionAPI, ctx: ExtensionContext): ProviderRequestInputs {
|
|
79
|
+
return {
|
|
80
|
+
systemPrompt: ctx.getSystemPrompt(), thinkingLevel: pi.getThinkingLevel(), settings: pi.getSettings(),
|
|
81
|
+
activeTools: pi.getActiveTools(), tools: pi.getAllTools(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
function canonicalMessages(ctx: ExtensionContext): AgentMessage[] {
|
|
79
86
|
const branch = ctx.sessionManager.getBranch();
|
|
80
87
|
return buildSessionContext(branch, branch.at(-1)?.id ?? null).messages;
|
|
@@ -198,6 +205,7 @@ async function compactRemotely(
|
|
|
198
205
|
ctx.ui.notify("Codex Remote Compaction V2 does not accept custom instructions; they are ignored.", "warning");
|
|
199
206
|
}
|
|
200
207
|
const current = projectedCurrentMessages(event, supported.identity);
|
|
208
|
+
const inputs = providerRequestInputs(pi, ctx);
|
|
201
209
|
const request = compactionRequest(snapshots, sessionId, supported, current.messages, {
|
|
202
210
|
blockImages: settings.images?.blockImages ?? false,
|
|
203
211
|
systemPrompt: () => ctx.getSystemPrompt(),
|
|
@@ -208,6 +216,7 @@ async function compactRemotely(
|
|
|
208
216
|
model: supported.model,
|
|
209
217
|
context: request.context,
|
|
210
218
|
userItemOrigins: userItemOrigins(request.messages),
|
|
219
|
+
providerRequest: providerRequestFor(snapshots, sessionId, supported, current.messages, inputs),
|
|
211
220
|
reasoning,
|
|
212
221
|
sessionId,
|
|
213
222
|
thinkingBudgets: settings.thinkingBudgets,
|
|
@@ -341,6 +350,7 @@ export function createCodexCompactionExtension(
|
|
|
341
350
|
capableModel(ctx.model),
|
|
342
351
|
() => canonicalMessages(ctx),
|
|
343
352
|
() => ctx.getSystemPrompt(),
|
|
353
|
+
() => ({ payload: payload ?? event.payload, inputs: providerRequestInputs(pi, ctx) }),
|
|
344
354
|
);
|
|
345
355
|
return payload;
|
|
346
356
|
});
|
package/src/remote.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { trimToolOutputsToContextWindow } from "./context-window.js";
|
|
|
7
7
|
import { estimateImages, type ImageEstimates } from "./image-budget.js";
|
|
8
8
|
import { contextUserItems, type UserItemOrigin } from "./retention-input.js";
|
|
9
9
|
import { CodexCompactionProtocolError, createCompactionCollector, isObject, type JsonObject, prepareRemoteCompactionPayload } from "./protocol.js";
|
|
10
|
+
import { applyProviderRequest, type ProviderRequestSnapshot } from "./request-snapshot.js";
|
|
10
11
|
|
|
11
12
|
const REMOTE_COMPACTION_FEATURE = "remote_compaction_v2";
|
|
12
13
|
const REQUEST_TIMEOUT_MS = 300_000;
|
|
@@ -27,6 +28,7 @@ export interface RemoteCompactionRequest {
|
|
|
27
28
|
/** Pi origins of the context's user messages, used to align provider user items with Pi roles. */
|
|
28
29
|
userItemOrigins?: readonly UserItemOrigin[];
|
|
29
30
|
priorCheckpoint?: { identity: ProviderIdentity; marker: string; replacementHistory: readonly JsonObject[] };
|
|
31
|
+
providerRequest?: ProviderRequestSnapshot;
|
|
30
32
|
onPrepared?: () => void;
|
|
31
33
|
fetch?: typeof globalThis.fetch;
|
|
32
34
|
}
|
|
@@ -108,6 +110,7 @@ export async function requestRemoteCompaction(request: RemoteCompactionRequest):
|
|
|
108
110
|
if (prior && !sameBackend(prior, identity)) {
|
|
109
111
|
throw new CodexCompactionProtocolError("The active opaque checkpoint belongs to a different resolved provider backend");
|
|
110
112
|
}
|
|
113
|
+
if (isObject(payload)) payload = applyProviderRequest(payload, resolved, request.providerRequest);
|
|
111
114
|
const contextItems = contextUserItems(isObject(payload) ? payload.input : undefined, request.userItemOrigins);
|
|
112
115
|
const payloadItems = isObject(payload) && Array.isArray(payload.input) ? payload.input.filter(isObject) : [];
|
|
113
116
|
const estimates = await estimateImages([...payloadItems, ...request.priorCheckpoint?.replacementHistory ?? []], request.signal);
|
package/src/request-snapshot.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Own ordinary-request prompt and context snapshots that let compaction reuse Pi's projected request prefix,
|
|
2
2
|
// and build compaction's provider context the way Pi 0.99 builds an ordinary request.
|
|
3
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import { getCurrentSystemMessage, getSystemMessageText, type Context, type Message, type Tool } from "@earendil-works/pi-ai";
|
|
5
|
-
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { convertToLlm, type ExtensionAPI, type ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { sameBackend, sameModel, type CapableModel, type ProviderIdentity } from "./capability.js";
|
|
7
7
|
import { type CodexCheckpointDetails, fingerprintMessage, projectCheckpointRequest, withoutSystemMessages } from "./checkpoint.js";
|
|
8
|
+
import { isObject, type JsonObject } from "./protocol.js";
|
|
8
9
|
|
|
9
10
|
const BLOCKED_IMAGE_TEXT = "Image reading is disabled.";
|
|
10
11
|
|
|
@@ -92,14 +93,17 @@ export function captureContextSnapshot(
|
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
export function reuseContextSnapshot(messages: AgentMessage[], snapshot: ContextSnapshot | undefined): AgentMessage[] {
|
|
97
|
-
if (!snapshot) return messages;
|
|
96
|
+
function matchingSource(messages: readonly AgentMessage[], snapshot: Pick<ContextSnapshot, "sourceFingerprints">): AgentMessage[] | undefined {
|
|
98
97
|
const source = requestSource(messages);
|
|
99
|
-
|
|
98
|
+
return source.length < snapshot.sourceFingerprints.length || snapshot.sourceFingerprints.some(
|
|
100
99
|
(fingerprint, index) => fingerprintMessage(source[index]) !== fingerprint,
|
|
101
|
-
)
|
|
102
|
-
|
|
100
|
+
) ? undefined : source;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Reuse the projected request while its source is an unchanged prefix, then append newer messages. */
|
|
104
|
+
export function reuseContextSnapshot(messages: AgentMessage[], snapshot: ContextSnapshot | undefined): AgentMessage[] {
|
|
105
|
+
const source = snapshot && matchingSource(messages, snapshot);
|
|
106
|
+
return source ? [...structuredClone(snapshot.messages), ...source.slice(snapshot.sourceFingerprints.length)] : messages;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
/** Pi 0.99 applies per-run prompt overrides after context hooks by collapsing system messages into one head. */
|
|
@@ -110,10 +114,95 @@ export function applyPromptOverride(messages: Message[], override: PromptOverrid
|
|
|
110
114
|
return [{ ...declarations, content: override.text }, ...withoutSystemMessages(messages)];
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
/** Public Pi state that can change declarations or request parameters without changing the conversation. */
|
|
118
|
+
export interface ProviderRequestInputs {
|
|
119
|
+
readonly systemPrompt: string;
|
|
120
|
+
readonly thinkingLevel: ThinkingLevel;
|
|
121
|
+
readonly settings: ReturnType<ExtensionAPI["getSettings"]>;
|
|
122
|
+
readonly activeTools: readonly string[];
|
|
123
|
+
readonly tools: readonly ToolInfo[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface ProviderRequestFields {
|
|
127
|
+
readonly instructions?: string;
|
|
128
|
+
readonly tools?: readonly JsonObject[];
|
|
129
|
+
readonly reasoning?: JsonObject;
|
|
130
|
+
readonly prompt_cache_key?: string;
|
|
131
|
+
readonly prompt_cache_retention?: string;
|
|
132
|
+
readonly service_tier?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ProviderRequestSnapshot extends SnapshotScope {
|
|
136
|
+
readonly sourceFingerprints: readonly string[];
|
|
137
|
+
readonly inputsKey: string;
|
|
138
|
+
readonly fields: ProviderRequestFields;
|
|
139
|
+
readonly verbosity?: string;
|
|
140
|
+
readonly prefix: readonly JsonObject[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function declarationPrefix(input: readonly JsonObject[]): readonly JsonObject[] {
|
|
144
|
+
const end = input.findIndex((item) => item.type !== "additional_tools" &&
|
|
145
|
+
!((item.type === undefined || item.type === "message") && (item.role === "system" || item.role === "developer")));
|
|
146
|
+
return end < 0 ? input : input.slice(0, end);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function requestInputsKey(target: CapableModel, inputs: ProviderRequestInputs): string {
|
|
150
|
+
return JSON.stringify({ model: target.model, ...inputs });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function captureProviderRequest(
|
|
154
|
+
context: ContextSnapshot, target: CapableModel, payload: unknown, inputs: ProviderRequestInputs,
|
|
155
|
+
): ProviderRequestSnapshot | undefined {
|
|
156
|
+
if (!isObject(payload) || payload.model !== target.model.id ||
|
|
157
|
+
!Array.isArray(payload.input) || !payload.input.every(isObject)) return undefined;
|
|
158
|
+
const { instructions, tools, reasoning, prompt_cache_key, prompt_cache_retention, service_tier, text } = payload;
|
|
159
|
+
const verbosity = isObject(text) ? text.verbosity : undefined;
|
|
160
|
+
if ((instructions !== undefined && typeof instructions !== "string") ||
|
|
161
|
+
(tools !== undefined && (!Array.isArray(tools) || !tools.every(isObject))) ||
|
|
162
|
+
(reasoning !== undefined && !isObject(reasoning)) ||
|
|
163
|
+
(prompt_cache_key !== undefined && typeof prompt_cache_key !== "string") ||
|
|
164
|
+
(prompt_cache_retention !== undefined && typeof prompt_cache_retention !== "string") ||
|
|
165
|
+
(service_tier !== undefined && typeof service_tier !== "string") ||
|
|
166
|
+
(text !== undefined && !isObject(text)) || (verbosity !== undefined && typeof verbosity !== "string")) return undefined;
|
|
167
|
+
return {
|
|
168
|
+
sessionId: context.sessionId, identity: context.identity, sourceFingerprints: context.sourceFingerprints,
|
|
169
|
+
inputsKey: requestInputsKey(target, inputs),
|
|
170
|
+
fields: structuredClone({ instructions, tools, reasoning, prompt_cache_key, prompt_cache_retention, service_tier }),
|
|
171
|
+
verbosity, prefix: structuredClone(declarationPrefix(payload.input)),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Reuse wire declarations only with an unchanged source prefix and request configuration. */
|
|
176
|
+
export function providerRequestFor(
|
|
177
|
+
snapshots: RequestSnapshots, sessionId: string, target: CapableModel, current: readonly AgentMessage[], inputs: ProviderRequestInputs,
|
|
178
|
+
): ProviderRequestSnapshot | undefined {
|
|
179
|
+
const snapshot = snapshotFor(snapshots.providerRequest, sessionId, target);
|
|
180
|
+
return snapshot && matchingSource(current, snapshot) &&
|
|
181
|
+
snapshot.inputsKey === requestInputsKey(target, inputs) ? snapshot : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Replace only declarations and cache-related fields; Pi still owns the current input, transport, and output limits. */
|
|
185
|
+
export function applyProviderRequest(
|
|
186
|
+
payload: JsonObject, target: CapableModel, snapshot: ProviderRequestSnapshot | undefined,
|
|
187
|
+
): JsonObject {
|
|
188
|
+
if (!snapshot || !sameBackend(snapshot.identity, target.identity) || !sameModel(snapshot.identity, target.model) ||
|
|
189
|
+
payload.model !== target.model.id || !Array.isArray(payload.input) || !payload.input.every(isObject)) return payload;
|
|
190
|
+
const updated: JsonObject = { ...payload, ...structuredClone(snapshot.fields),
|
|
191
|
+
input: [...structuredClone(snapshot.prefix), ...payload.input.slice(declarationPrefix(payload.input).length)] };
|
|
192
|
+
if (isObject(payload.text) || snapshot.verbosity !== undefined) {
|
|
193
|
+
const text = { ...(isObject(payload.text) ? payload.text : {}) };
|
|
194
|
+
delete text.verbosity;
|
|
195
|
+
if (snapshot.verbosity !== undefined) text.verbosity = snapshot.verbosity;
|
|
196
|
+
updated.text = Object.keys(text).length ? text : undefined;
|
|
197
|
+
}
|
|
198
|
+
return updated;
|
|
199
|
+
}
|
|
200
|
+
|
|
113
201
|
/** The snapshots that the latest ordinary request left for compaction. */
|
|
114
202
|
export interface RequestSnapshots {
|
|
115
203
|
readonly promptOverride?: PromptOverride;
|
|
116
204
|
readonly context?: ContextSnapshot;
|
|
205
|
+
readonly providerRequest?: ProviderRequestSnapshot;
|
|
117
206
|
}
|
|
118
207
|
|
|
119
208
|
interface TrackedSnapshots {
|
|
@@ -121,6 +210,7 @@ interface TrackedSnapshots {
|
|
|
121
210
|
pendingSource?: { readonly sessionId: string; readonly fingerprints: readonly string[] };
|
|
122
211
|
pendingContext?: ContextSnapshot;
|
|
123
212
|
context?: ContextSnapshot;
|
|
213
|
+
providerRequest?: ProviderRequestSnapshot;
|
|
124
214
|
}
|
|
125
215
|
|
|
126
216
|
/**
|
|
@@ -172,10 +262,17 @@ export class RequestSnapshotTracker {
|
|
|
172
262
|
target: CapableModel | undefined,
|
|
173
263
|
canonical: () => AgentMessage[],
|
|
174
264
|
systemPrompt: () => string,
|
|
265
|
+
observation: () => { readonly payload: unknown; readonly inputs: ProviderRequestInputs },
|
|
175
266
|
): void {
|
|
176
267
|
this.state.promptOverride = target && capturePromptOverride(canonical(), sessionId, target, systemPrompt());
|
|
177
268
|
// Keep the pending snapshot for retries that prepare a payload without running context hooks again.
|
|
178
269
|
this.state.context = this.state.pendingContext;
|
|
270
|
+
this.state.providerRequest = undefined;
|
|
271
|
+
const context = target && snapshotFor(this.state.context, sessionId, target);
|
|
272
|
+
if (context && target) {
|
|
273
|
+
const { payload, inputs } = observation();
|
|
274
|
+
this.state.providerRequest = captureProviderRequest(context, target, payload, inputs);
|
|
275
|
+
}
|
|
179
276
|
}
|
|
180
277
|
}
|
|
181
278
|
|