compressed-context 0.1.0 → 0.2.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 +59 -51
- package/package.json +64 -64
- package/src/compression/active-summary-cache.ts +50 -50
- package/src/compression/adopt.ts +57 -57
- package/src/compression/auto.ts +386 -372
- package/src/compression/babel.ts +85 -85
- package/src/compression/block.ts +25 -25
- package/src/compression/cache-history.ts +75 -75
- package/src/compression/compaction.ts +189 -189
- package/src/compression/decompress-tool.ts +74 -74
- package/src/compression/decompress.ts +292 -292
- package/src/compression/deleted-sessions.ts +24 -24
- package/src/compression/distill.ts +220 -220
- package/src/compression/events.ts +275 -275
- package/src/compression/markdown.ts +53 -53
- package/src/compression/normalize.ts +114 -114
- package/src/compression/render.ts +70 -70
- package/src/compression/schema.ts +131 -84
- package/src/compression/select.ts +123 -123
- package/src/compression/session-message.ts +112 -112
- package/src/compression/settings-rpc.ts +123 -111
- package/src/compression/settings.ts +175 -175
- package/src/compression/shape.ts +75 -0
- package/src/compression/state.ts +248 -248
- package/src/compression/summarize.ts +534 -595
- package/src/compression/summary.ts +73 -73
- package/src/compression/tool.ts +184 -142
- package/src/compression/transcript-store.ts +258 -258
- package/src/compression/transcript.ts +63 -63
- package/src/compression/validate.ts +81 -81
- package/src/i18n/en.ts +93 -93
- package/src/i18n/index.ts +28 -28
- package/src/i18n/types.ts +89 -89
- package/src/i18n/zh.ts +93 -93
- package/src/index.ts +116 -108
- package/src/shared/blocks.ts +17 -17
- package/src/shared/cache.ts +199 -199
- package/src/shared/format.ts +23 -23
- package/src/shared/media.ts +184 -184
- package/src/shared/parse.ts +25 -25
- package/src/shared/parts.ts +72 -72
- package/src/shared/rpc.ts +145 -145
- package/src/shared/scan.ts +32 -32
- package/src/shared/settings.ts +466 -471
- package/src/shared/stats.ts +142 -142
- package/src/shared/tokens.ts +259 -259
- package/src/tui/chart/cache-chart.ts +237 -237
- package/src/tui/chart/color.ts +58 -58
- package/src/tui/index.tsx +109 -111
- package/src/tui/panel/cache.tsx +203 -203
- package/src/tui/panel/data.ts +105 -105
- package/src/tui/panel/index.tsx +176 -176
- package/src/tui/panel/summaries.tsx +203 -192
- package/src/tui/settings/dialogs.tsx +215 -215
- package/src/tui/settings/options.ts +238 -238
- package/src/tui/settings.tsx +418 -418
- package/src/tui/types.ts +4 -4
package/README.md
CHANGED
|
@@ -1,89 +1,97 @@
|
|
|
1
1
|
# compressed-context
|
|
2
2
|
|
|
3
|
-
OpenCode
|
|
3
|
+
OpenCode 的上下文压缩插件。它会将较早的对话历史折叠成独立的摘要块,让长会话在有限的上下文窗口里继续跑下去。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **手动压缩**:模型可主动调用 `compress` 工具,在任意时刻打检查点
|
|
9
|
-
- **分层摘要**:T1 压缩当前对话流;T2 / T3 合并相邻摘要,层级单调递减
|
|
10
|
-
- **解压**:`decompress` 还原某个块覆盖的原始消息,可只还原下一层或展开到原文
|
|
11
|
-
- **缓存友好**:压缩请求保持前缀不变,尽量减少缓存未命中
|
|
12
|
-
- **摘要格式**:Markdown(默认)或 Babel(首行 `T=标题|G=目标`,正文自由高密度)
|
|
13
|
-
- **主动提醒**:上下文增长超过设定值时,在请求尾部追加一次提醒,让模型自己决定何时压缩
|
|
14
|
-
- **TUI 面板**:查看摘要块、缓存命中率、上下文构成,设置项在插件设置里直接改
|
|
7
|
+
## 架构与工作原理
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
插件接管发给模型的上下文:把较早的对话折叠成摘要块,让长会话继续跑下去:
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
```text
|
|
12
|
+
原始上下文:
|
|
13
|
+
[ 系统提示词 ][ 早期历史对话 ............................. ][ 最近活跃对话 ]
|
|
14
|
+
↓ 触发压缩 (T1)
|
|
15
|
+
一级压缩后:
|
|
16
|
+
[ 系统提示词 ][ T1 摘要块 #1 ][ T1 摘要块 #2 ] ............ [ 最近活跃对话 ]
|
|
17
|
+
↓ 摘要堆积达到阈值 (T2/T3)
|
|
18
|
+
分层合并后:
|
|
19
|
+
[ 系统提示词 ][ T2 合并摘要 (块#1+#2) ][ T1 摘要块 #3 ] .... [ 最近活跃对话 ]
|
|
20
|
+
```
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
- **递归分层折叠**:
|
|
23
|
+
- **T1 级**:将溢出的原始对话流折叠为自包含检查点(原始消息移入持久化存储,释放窗口)。
|
|
24
|
+
- **T2 / T3 级**:当摘要块自身占用接近预算上限时,自动在背景对相邻摘要进行高阶合并(`T1 + T1 → T2`,`T2 + T2 → T3`)。
|
|
25
|
+
- **无损按需回溯**:模型可在需要时通过 `decompress` 按块还原历史,支持只解压下一层级摘要或直接恢复原始对话流。
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
## 核心特性
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
- **自动压缩**:上下文达到阈值时自动触发(工具调用循环中的子步骤同样生效)。
|
|
30
|
+
- **主动压缩**:向模型暴露 `compress` 工具,允许模型在阶段性任务完成后主动打检查点。
|
|
31
|
+
- **多级合并**:T1 负责压缩原始对话;当摘要本身积攒过多时,会自动向上合并为 T2、T3 高阶摘要。
|
|
32
|
+
- **按需解压**:模型可通过 `decompress` 工具还原指定摘要块(支持逐层展开或直接恢复为原始对话)。
|
|
33
|
+
- **缓存友好**:压缩请求尽量保持前缀不变,避免破坏 Prompt Cache。
|
|
34
|
+
- **双摘要格式**:支持标准 `markdown`(默认)和极致压缩的 `babel` 格式(首行 `T=标题|G=目标`,正文为高密度非自然语言)。
|
|
35
|
+
- **超长提醒**:上下文增长超过设定阈值时,在请求尾部追加轻量提醒,引导模型自主决定压缩时机。
|
|
36
|
+
- **TUI 面板**:直观查看摘要块、缓存命中率与上下文构成,支持在面板内直接修改配置。
|
|
37
|
+
|
|
38
|
+
## 安装与配置
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
插件通过 npm 分发,OpenCode 启动时会自动安装。
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
由于本插件会接管上下文管理,**建议同时关闭 OpenCode 原生的自动压缩**,避免两套策略打架。在全局配置 `~/.config/opencode/opencode.json`(或项目级 `opencode.json`)中加入:
|
|
33
43
|
|
|
34
44
|
```json
|
|
35
45
|
{
|
|
36
|
-
"plugin": ["
|
|
46
|
+
"plugin": ["compressed-context"],
|
|
47
|
+
"compaction": { "auto": false }
|
|
37
48
|
}
|
|
38
49
|
```
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
插件会接管上下文压缩,建议关掉 OpenCode 原生自动压缩,避免两套策略互相干扰:
|
|
51
|
+
**本地开发调试**:如果你拉取了源码自行修改,直接填入本地仓库的绝对路径即可:
|
|
43
52
|
|
|
44
53
|
```json
|
|
45
54
|
{
|
|
46
|
-
"plugin": ["compressed-context"],
|
|
55
|
+
"plugin": ["/absolute/path/to/compressed-context"],
|
|
47
56
|
"compaction": { "auto": false }
|
|
48
57
|
}
|
|
49
58
|
```
|
|
50
59
|
|
|
51
60
|
## 配置项
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
所有配置均可直接在 TUI 的插件设置面板中修改,或通过配置文件写入:
|
|
54
63
|
|
|
55
|
-
|
|
|
56
|
-
|
|
57
|
-
| `contextLimit`
|
|
58
|
-
| `triggerPercent`
|
|
59
|
-
| `autoCompress`
|
|
60
|
-
| `summaryBlockPercent`
|
|
61
|
-
| `summaryTotalPercent`
|
|
62
|
-
| `summaryFormat`
|
|
63
|
-
| `summaryLanguage`
|
|
64
|
-
| `nudgeGrowthTokens`
|
|
65
|
-
| `enableCompressTool`
|
|
66
|
-
| `enableDecompressTool` |
|
|
64
|
+
| 配置项 | 默认值 | 说明 |
|
|
65
|
+
| ---------------------- | ------------------ | -------------------------------------------------------------- |
|
|
66
|
+
| `contextLimit` | `"auto"` | 上下文窗口上限。填具体数字时,实际取该值与模型原生窗口的较小值 |
|
|
67
|
+
| `triggerPercent` | `80` | 上下文占用达到百分之多少时触发自动压缩 |
|
|
68
|
+
| `autoCompress` | `true` | 是否开启自动压缩 |
|
|
69
|
+
| `summaryBlockPercent` | `10` | 单个摘要块的 Token 预算(占窗口的百分比) |
|
|
70
|
+
| `summaryTotalPercent` | `50` | 所有摘要的总 Token 预算占比,超出后触发 T2 / T3 合并 |
|
|
71
|
+
| `summaryFormat` | `"markdown"` | 摘要格式:`markdown` 或 `babel` |
|
|
72
|
+
| `summaryLanguage` | `"auto"` | 摘要语言(`babel` 格式下忽略此项) |
|
|
73
|
+
| `nudgeGrowthTokens` | `50000` | 触发“主动压缩提醒”的 Token 增长阈值,设为 `0` 则关闭 |
|
|
74
|
+
| `enableCompressTool` | `true` | 是否允许模型调用 `compress` 工具 |
|
|
75
|
+
| `enableDecompressTool` | `true` | 是否允许模型调用 `decompress` 工具 |
|
|
67
76
|
|
|
68
|
-
##
|
|
77
|
+
## 本地开发
|
|
69
78
|
|
|
70
79
|
```bash
|
|
71
80
|
pnpm install
|
|
72
|
-
pnpm check
|
|
73
|
-
bun scripts/e2e-nudge.mjs #
|
|
74
|
-
bun scripts/e2e-tool-loop.mjs #
|
|
81
|
+
pnpm check # 运行 typecheck + lint + 单测 + TUI 测试
|
|
82
|
+
bun scripts/e2e-nudge.mjs # E2E 测试:主动提醒(隔离环境 + Mock Provider)
|
|
83
|
+
bun scripts/e2e-tool-loop.mjs # E2E 测试:工具循环内的自动压缩
|
|
75
84
|
```
|
|
76
85
|
|
|
77
|
-
##
|
|
86
|
+
## 效果参考
|
|
78
87
|
|
|
79
|
-
在 Terminal-Bench 的 `schemelike-metacircular-eval`
|
|
80
|
-
压在 93,560 并让该次运行通过验证。**只跑了一次,波动很大,只能当参考**,详见
|
|
81
|
-
[docs/benchmark.md](docs/benchmark.md)。
|
|
88
|
+
在 Terminal-Bench 的 `schemelike-metacircular-eval` 任务上做过初步实测:在 100k 窗口限制下,插件成功将单步上下文峰值压在 93,560 tokens 并顺利通过验证。由于单次测试存在随机波动,数据仅供参考,详细记录见 [docs/benchmark.md](docs/benchmark.md)。
|
|
82
89
|
|
|
83
|
-
##
|
|
90
|
+
## 致谢与灵感
|
|
84
91
|
|
|
85
|
-
[
|
|
92
|
+
- [billion-context](https://github.com/ranxianglei/billion-context)
|
|
93
|
+
- [Large Language Models Do Not Always Need Readable Language](https://arxiv.org/abs/2606.19857) —— Babel 摘要格式(BabelTele)的灵感来源
|
|
86
94
|
|
|
87
|
-
##
|
|
95
|
+
## 协议
|
|
88
96
|
|
|
89
|
-
|
|
97
|
+
[MIT](LICENSE) © 1cyberlangke1
|
package/package.json
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "compressed-context",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OpenCode 上下文管理插件:按需压缩、自包含摘要",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"author": "1cyberlangke1",
|
|
8
|
-
"homepage": "https://github.com/1cyberlangke1/compressed-context#readme",
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/1cyberlangke1/compressed-context.git"
|
|
12
|
-
},
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/1cyberlangke1/compressed-context/issues"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"opencode",
|
|
18
|
-
"plugin",
|
|
19
|
-
"context",
|
|
20
|
-
"compression",
|
|
21
|
-
"summarization",
|
|
22
|
-
"llm"
|
|
23
|
-
],
|
|
24
|
-
"exports": {
|
|
25
|
-
".": "./src/index.ts",
|
|
26
|
-
"./tui": "./src/tui/index.tsx"
|
|
27
|
-
},
|
|
28
|
-
"files": [
|
|
29
|
-
"src"
|
|
30
|
-
],
|
|
31
|
-
"peerDependencies": {
|
|
32
|
-
"@opencode/plugin": "^2.0.32",
|
|
33
|
-
"@opentui/core": ">=0.5.17",
|
|
34
|
-
"@opentui/solid": ">=0.5.17",
|
|
35
|
-
"solid-js": ">=1.9.0"
|
|
36
|
-
},
|
|
37
|
-
"scripts": {
|
|
38
|
-
"typecheck": "tsc --noEmit",
|
|
39
|
-
"lint": "eslint .",
|
|
40
|
-
"check": "pnpm typecheck && pnpm lint && pnpm test && pnpm test:tui",
|
|
41
|
-
"test": "vitest run",
|
|
42
|
-
"test:tui": "cd tui-test && bun test",
|
|
43
|
-
"test:e2e:nudge": "bun scripts/e2e-nudge.mjs",
|
|
44
|
-
"test:e2e:tool-loop": "bun scripts/e2e-tool-loop.mjs",
|
|
45
|
-
"test:watch": "vitest"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@eslint/js": "^10.0.1",
|
|
49
|
-
"@opencode/plugin": "^2.
|
|
50
|
-
"@opencode/theme": "2.
|
|
51
|
-
"@opentui/core": "0.
|
|
52
|
-
"@opentui/solid": "0.
|
|
53
|
-
"@types/node": "^26.6.5",
|
|
54
|
-
"eslint": "^10.13.0",
|
|
55
|
-
"solid-js": "1.9.15",
|
|
56
|
-
"typescript": "^6.0.3",
|
|
57
|
-
"typescript-eslint": "^8.71.2",
|
|
58
|
-
"vitest": "^5.0.5"
|
|
59
|
-
},
|
|
60
|
-
"dependencies": {
|
|
61
|
-
"effect": "4.0.1",
|
|
62
|
-
"jsonrepair": "^3.15.0"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "compressed-context",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "OpenCode 上下文管理插件:按需压缩、自包含摘要",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "1cyberlangke1",
|
|
8
|
+
"homepage": "https://github.com/1cyberlangke1/compressed-context#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/1cyberlangke1/compressed-context.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/1cyberlangke1/compressed-context/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"opencode",
|
|
18
|
+
"plugin",
|
|
19
|
+
"context",
|
|
20
|
+
"compression",
|
|
21
|
+
"summarization",
|
|
22
|
+
"llm"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.ts",
|
|
26
|
+
"./tui": "./src/tui/index.tsx"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@opencode/plugin": "^2.0.32",
|
|
33
|
+
"@opentui/core": ">=0.5.17",
|
|
34
|
+
"@opentui/solid": ">=0.5.17",
|
|
35
|
+
"solid-js": ">=1.9.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"lint": "eslint .",
|
|
40
|
+
"check": "pnpm typecheck && pnpm lint && pnpm test && pnpm test:tui",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:tui": "cd tui-test && bun test",
|
|
43
|
+
"test:e2e:nudge": "bun scripts/e2e-nudge.mjs",
|
|
44
|
+
"test:e2e:tool-loop": "bun scripts/e2e-tool-loop.mjs",
|
|
45
|
+
"test:watch": "vitest"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^10.0.1",
|
|
49
|
+
"@opencode/plugin": "^2.1.2",
|
|
50
|
+
"@opencode/theme": "2.1.2",
|
|
51
|
+
"@opentui/core": "0.6.6",
|
|
52
|
+
"@opentui/solid": "0.6.6",
|
|
53
|
+
"@types/node": "^26.6.5",
|
|
54
|
+
"eslint": "^10.13.0",
|
|
55
|
+
"solid-js": "1.9.15",
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"typescript-eslint": "^8.71.2",
|
|
58
|
+
"vitest": "^5.0.5"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"effect": "4.0.1",
|
|
62
|
+
"jsonrepair": "^3.15.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import { renderAll, type RenderOptions } from "./render.ts"
|
|
2
|
-
import type { State } from "./state.ts"
|
|
3
|
-
|
|
4
|
-
interface Entry {
|
|
5
|
-
readonly revision: string
|
|
6
|
-
readonly text: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const LIMIT = 32
|
|
10
|
-
const cache = new Map<string, Entry>()
|
|
11
|
-
|
|
12
|
-
/** 输入:状态 + 渲染选项;输出:只由块身份决定的 revision。 */
|
|
13
|
-
function revisionOf(state: State, options: RenderOptions): string {
|
|
14
|
-
return JSON.stringify({
|
|
15
|
-
blocks: state.blocks.map((block) => [
|
|
16
|
-
block.id,
|
|
17
|
-
block.tier,
|
|
18
|
-
block.order,
|
|
19
|
-
block.at,
|
|
20
|
-
block.sourceBlockIds ?? [],
|
|
21
|
-
]),
|
|
22
|
-
pending: state.pending?.id ?? null,
|
|
23
|
-
options,
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** 输入:sessionID + 状态 + 渲染选项;输出:活跃摘要文本。 */
|
|
28
|
-
export function renderActiveSummary(
|
|
29
|
-
sessionID: string,
|
|
30
|
-
state: State,
|
|
31
|
-
options: RenderOptions,
|
|
32
|
-
): string {
|
|
33
|
-
const revision = revisionOf(state, options)
|
|
34
|
-
const hit = cache.get(sessionID)
|
|
35
|
-
if (hit?.revision === revision) return hit.text
|
|
36
|
-
const text = renderAll(state.blocks, state.pending, options)
|
|
37
|
-
cache.delete(sessionID)
|
|
38
|
-
cache.set(sessionID, { revision, text })
|
|
39
|
-
while (cache.size > LIMIT) {
|
|
40
|
-
const oldest = cache.keys().next().value
|
|
41
|
-
if (oldest === undefined) break
|
|
42
|
-
cache.delete(oldest)
|
|
43
|
-
}
|
|
44
|
-
return text
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 输入:sessionID;输出:无;预期:会话删除时丢掉活跃摘要缓存。 */
|
|
48
|
-
export function clearActiveSummary(sessionID: string): void {
|
|
49
|
-
cache.delete(sessionID)
|
|
50
|
-
}
|
|
1
|
+
import { renderAll, type RenderOptions } from "./render.ts"
|
|
2
|
+
import type { State } from "./state.ts"
|
|
3
|
+
|
|
4
|
+
interface Entry {
|
|
5
|
+
readonly revision: string
|
|
6
|
+
readonly text: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const LIMIT = 32
|
|
10
|
+
const cache = new Map<string, Entry>()
|
|
11
|
+
|
|
12
|
+
/** 输入:状态 + 渲染选项;输出:只由块身份决定的 revision。 */
|
|
13
|
+
function revisionOf(state: State, options: RenderOptions): string {
|
|
14
|
+
return JSON.stringify({
|
|
15
|
+
blocks: state.blocks.map((block) => [
|
|
16
|
+
block.id,
|
|
17
|
+
block.tier,
|
|
18
|
+
block.order,
|
|
19
|
+
block.at,
|
|
20
|
+
block.sourceBlockIds ?? [],
|
|
21
|
+
]),
|
|
22
|
+
pending: state.pending?.id ?? null,
|
|
23
|
+
options,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 输入:sessionID + 状态 + 渲染选项;输出:活跃摘要文本。 */
|
|
28
|
+
export function renderActiveSummary(
|
|
29
|
+
sessionID: string,
|
|
30
|
+
state: State,
|
|
31
|
+
options: RenderOptions,
|
|
32
|
+
): string {
|
|
33
|
+
const revision = revisionOf(state, options)
|
|
34
|
+
const hit = cache.get(sessionID)
|
|
35
|
+
if (hit?.revision === revision) return hit.text
|
|
36
|
+
const text = renderAll(state.blocks, state.pending, options)
|
|
37
|
+
cache.delete(sessionID)
|
|
38
|
+
cache.set(sessionID, { revision, text })
|
|
39
|
+
while (cache.size > LIMIT) {
|
|
40
|
+
const oldest = cache.keys().next().value
|
|
41
|
+
if (oldest === undefined) break
|
|
42
|
+
cache.delete(oldest)
|
|
43
|
+
}
|
|
44
|
+
return text
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 输入:sessionID;输出:无;预期:会话删除时丢掉活跃摘要缓存。 */
|
|
48
|
+
export function clearActiveSummary(sessionID: string): void {
|
|
49
|
+
cache.delete(sessionID)
|
|
50
|
+
}
|
package/src/compression/adopt.ts
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode/plugin"
|
|
2
|
-
|
|
3
|
-
import { createT1Block } from "./block.ts"
|
|
4
|
-
import { toMarkdownSummary } from "./markdown.ts"
|
|
5
|
-
import { isCheckpoint, messageText, type MessageLike } from "./select.ts"
|
|
6
|
-
import { loadState, saveState } from "./state.ts"
|
|
7
|
-
|
|
8
|
-
/** 原生 checkpoint 里抽出的摘要与 recent 上下文。 */
|
|
9
|
-
export interface NativeCheckpoint {
|
|
10
|
-
readonly summary: string
|
|
11
|
-
readonly recent?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 输入:OpenCode checkpoint 文本。
|
|
16
|
-
* 输出:`<summary>` / `<recent-context>` 内容;不是 checkpoint 时 undefined。
|
|
17
|
-
* 预期:只做只读解析,不改原文。
|
|
18
|
-
*/
|
|
19
|
-
export function extractCheckpoint(text: string): NativeCheckpoint | undefined {
|
|
20
|
-
if (!text.trimStart().startsWith("<conversation-checkpoint>")) return undefined
|
|
21
|
-
const summary = /<summary>\n?([\s\S]*?)\n?<\/summary>/.exec(text)?.[1]?.trim()
|
|
22
|
-
if (summary === undefined || summary.length === 0) return undefined
|
|
23
|
-
const recent = /<recent-context>\n?([\s\S]*?)\n?<\/recent-context>/.exec(text)?.[1]?.trim()
|
|
24
|
-
return recent === undefined || recent.length === 0 ? { summary } : { summary, recent }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 输入:插件上下文 + sessionID + 当前请求消息。
|
|
29
|
-
* 输出:是否收养了原生摘要。
|
|
30
|
-
* 预期:仅当插件还没有任何块时,把 OpenCode 自己的 checkpoint 摘要当作 b1:T1;
|
|
31
|
-
* 它没有原始 transcript,fromId/toId 指向 checkpoint 消息本身。
|
|
32
|
-
*/
|
|
33
|
-
export async function adoptNativeCheckpoint(
|
|
34
|
-
ctx: Plugin.Context,
|
|
35
|
-
sessionID: string,
|
|
36
|
-
messages: readonly MessageLike[],
|
|
37
|
-
): Promise<boolean> {
|
|
38
|
-
const state = await loadState(ctx.storage, sessionID)
|
|
39
|
-
if (state.blocks.length > 0 || state.pending !== undefined || state.terminal !== undefined) return false
|
|
40
|
-
|
|
41
|
-
const checkpoint = messages.findLast((message) => isCheckpoint(message))
|
|
42
|
-
if (checkpoint === undefined) return false
|
|
43
|
-
const parsed = extractCheckpoint(messageText(checkpoint))
|
|
44
|
-
if (parsed === undefined) return false
|
|
45
|
-
|
|
46
|
-
const summary = toMarkdownSummary(parsed.summary)
|
|
47
|
-
const block = createT1Block({
|
|
48
|
-
blocks: [],
|
|
49
|
-
summary,
|
|
50
|
-
// 原生摘要没有原始 transcript;空范围让 decompress full 返回 unavailable,而不是伪造原文。
|
|
51
|
-
fromId: "",
|
|
52
|
-
toId: "",
|
|
53
|
-
sourceChars: parsed.summary.length,
|
|
54
|
-
})
|
|
55
|
-
await saveState(ctx.storage, sessionID, { blocks: [block], attempts: 0 })
|
|
56
|
-
return true
|
|
57
|
-
}
|
|
1
|
+
import type { Plugin } from "@opencode/plugin"
|
|
2
|
+
|
|
3
|
+
import { createT1Block } from "./block.ts"
|
|
4
|
+
import { toMarkdownSummary } from "./markdown.ts"
|
|
5
|
+
import { isCheckpoint, messageText, type MessageLike } from "./select.ts"
|
|
6
|
+
import { loadState, saveState } from "./state.ts"
|
|
7
|
+
|
|
8
|
+
/** 原生 checkpoint 里抽出的摘要与 recent 上下文。 */
|
|
9
|
+
export interface NativeCheckpoint {
|
|
10
|
+
readonly summary: string
|
|
11
|
+
readonly recent?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 输入:OpenCode checkpoint 文本。
|
|
16
|
+
* 输出:`<summary>` / `<recent-context>` 内容;不是 checkpoint 时 undefined。
|
|
17
|
+
* 预期:只做只读解析,不改原文。
|
|
18
|
+
*/
|
|
19
|
+
export function extractCheckpoint(text: string): NativeCheckpoint | undefined {
|
|
20
|
+
if (!text.trimStart().startsWith("<conversation-checkpoint>")) return undefined
|
|
21
|
+
const summary = /<summary>\n?([\s\S]*?)\n?<\/summary>/.exec(text)?.[1]?.trim()
|
|
22
|
+
if (summary === undefined || summary.length === 0) return undefined
|
|
23
|
+
const recent = /<recent-context>\n?([\s\S]*?)\n?<\/recent-context>/.exec(text)?.[1]?.trim()
|
|
24
|
+
return recent === undefined || recent.length === 0 ? { summary } : { summary, recent }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 输入:插件上下文 + sessionID + 当前请求消息。
|
|
29
|
+
* 输出:是否收养了原生摘要。
|
|
30
|
+
* 预期:仅当插件还没有任何块时,把 OpenCode 自己的 checkpoint 摘要当作 b1:T1;
|
|
31
|
+
* 它没有原始 transcript,fromId/toId 指向 checkpoint 消息本身。
|
|
32
|
+
*/
|
|
33
|
+
export async function adoptNativeCheckpoint(
|
|
34
|
+
ctx: Plugin.Context,
|
|
35
|
+
sessionID: string,
|
|
36
|
+
messages: readonly MessageLike[],
|
|
37
|
+
): Promise<boolean> {
|
|
38
|
+
const state = await loadState(ctx.storage, sessionID)
|
|
39
|
+
if (state.blocks.length > 0 || state.pending !== undefined || state.terminal !== undefined) return false
|
|
40
|
+
|
|
41
|
+
const checkpoint = messages.findLast((message) => isCheckpoint(message))
|
|
42
|
+
if (checkpoint === undefined) return false
|
|
43
|
+
const parsed = extractCheckpoint(messageText(checkpoint))
|
|
44
|
+
if (parsed === undefined) return false
|
|
45
|
+
|
|
46
|
+
const summary = toMarkdownSummary(parsed.summary)
|
|
47
|
+
const block = createT1Block({
|
|
48
|
+
blocks: [],
|
|
49
|
+
summary,
|
|
50
|
+
// 原生摘要没有原始 transcript;空范围让 decompress full 返回 unavailable,而不是伪造原文。
|
|
51
|
+
fromId: "",
|
|
52
|
+
toId: "",
|
|
53
|
+
sourceChars: parsed.summary.length,
|
|
54
|
+
})
|
|
55
|
+
await saveState(ctx.storage, sessionID, { blocks: [block], attempts: 0 })
|
|
56
|
+
return true
|
|
57
|
+
}
|