dsh-compact-relay 0.3.2 → 0.3.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/CHANGELOG.md +26 -0
- package/README.en.md +8 -4
- package/README.md +8 -4
- package/ROADMAP.md +30 -3
- package/TUTORIAL.md +3 -2
- package/lib/actions/better-compact.js +24 -72
- package/lib/actions/compact-run.js +76 -0
- package/lib/actions/compress-hint.js +16 -7
- package/lib/actions/handoff-docs.js +9 -3
- package/lib/actions/handoff.js +5 -22
- package/lib/actions/settings.js +1 -4
- package/lib/actions/state.js +2 -1
- package/lib/actions/support.js +76 -0
- package/lib/actions/test-compact.js +12 -41
- package/lib/adapters/http.js +2 -1
- package/lib/adapters/prompt.js +72 -0
- package/lib/adapters/tool.js +12 -8
- package/lib/client.js +424 -96
- package/lib/config.js +1 -2
- package/lib/core/compress-cue.js +96 -0
- package/lib/core/compress-hint.js +1 -1
- package/lib/core/errors.js +0 -1
- package/lib/core/handoff-spec.js +1 -1
- package/lib/core/markdown-lite.js +178 -0
- package/lib/core/object.js +19 -0
- package/lib/core/ranges.js +1 -2
- package/lib/core/relay.js +2 -2
- package/lib/core/tokens.js +34 -0
- package/lib/dsh/compaction-log.js +3 -19
- package/lib/dsh/compaction-pair.js +48 -0
- package/lib/dsh/engine.js +2 -2
- package/lib/dsh/handoff-files.js +28 -0
- package/lib/dsh/handoff-writer.js +16 -21
- package/lib/dsh/model-task.js +112 -0
- package/lib/dsh/relay.js +1 -1
- package/lib/dsh/session-view.js +5 -17
- package/lib/dsh/settings-file.js +1 -4
- package/lib/dsh/state-hub.js +95 -19
- package/lib/dsh/summarizer.js +16 -25
- package/lib/index.js +28 -6
- package/package.json +2 -2
- package/lib/actions/stage.js +0 -36
- package/lib/core/stage.js +0 -96
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@
|
|
|
4
4
|
|
|
5
5
|
(暂无新改动)
|
|
6
6
|
|
|
7
|
+
## 0.3.4(2026-10-24,判断机制修复 + 小 bug)
|
|
8
|
+
|
|
9
|
+
### 判断机制(BUG-010:适合压缩的节点几乎不触发)
|
|
10
|
+
|
|
11
|
+
- **问题与证据**:扫了 427 个真实会话(36,813 次工具调用)——0.3.1 上线后 `mark_stage` 工具调用 **0 次**,`plugin:compact-relay/compress-hint` 记录全库仅 **1 条**(0.3.0 开发期测试);而工具 schema 在当前会话日志出现 43 次,说明工具确实进了模型上下文,只是模型从不调用。
|
|
12
|
+
- **根因**:工具描述写「系统会告诉你当前容量」,但系统从未告诉模型任何容量 → 「超过最小线 / 还没到压缩线」两条条件无法判断;描述末尾「不要凑数触发」进一步抑制调用。
|
|
13
|
+
- **修复**:新增 `systemPrompt` 动态 context(`core/compress-cue.js` + `adapters/prompt.js`)——容量进入可压缩区间时主动告诉模型当前容量与区间(5% 桶取整并在显示时 clamp 到区间下限,避免「约 30%(区间 33.6%–40%)」自相矛盾;刚记过 hint 且差 <10 个百分点时抑制;异常静默跳过——`systemPrompt` 是 DSH 0.2.x 核心根服务,缺服务时插件不激活);`dsh/state-hub.js` 无论有无 SSE 订阅者都维护最新状态缓存(`latestState`)供同步读取。
|
|
14
|
+
- **工具**:`mark_stage` 改名 `mark_compress_point`,重写描述(完成明确任务/阶段且认为可安全压旧历史时调用;工具自己返回容量并判断;太低/到线拒绝且不改历史;不确定可调用);记录/拒绝文案同步,不再有「系统会告诉你」的假承诺。
|
|
15
|
+
- **清理**:删除 0.3.1 遗留死代码 `lib/core/stage.js` + `lib/actions/stage.js`(无任何引用)。
|
|
16
|
+
- **测试**:新增 `test/compress-cue.mjs`(区间边界/5% 桶/动态最小线/hint 抑制 + 状态缓存无订阅者更新/防抖/卸载清理/异常安全 + prompt 适配降级);`test/plugin-load.mjs` 的 fake host 增加 `systemPrompt` 替身并断言 context 注册、区间内文案、区间外空串、异常安全。
|
|
17
|
+
|
|
18
|
+
### 小 bug
|
|
19
|
+
|
|
20
|
+
- **BUG-005 交接单详情纯文本**:新增 `core/markdown-lite.js` 轻量 Markdown 解析(标题/段落/有序无序列表/粗体/行内代码/代码块/分隔线;零依赖、**不生成 HTML**、无 XSS 面),client `16-markdown.js` 构建时内联同一源码并在交接单详情面板渲染;512KB/白名单/只读约束不变。新增 `test/markdown-lite.mjs`。
|
|
21
|
+
- **BUG-006 交接单列表只显示最近 20 份**:列表端点返回 `total`;客户端底部显示「共 N 份(显示最近 20)」并提供「加载更多」(limit 提到 50),无更多时隐藏按钮。
|
|
22
|
+
- **BUG-004**:交接失败提示带原因保持已修状态,真机抽查待一次真实 `/handoff`(本次未动真实历史)。
|
|
23
|
+
- 测试基线:smoke 53/53、handoff 35/35、plugin-load 14/14、contract 17/17、compact-log PASS、compress-cue PASS、markdown-lite PASS。
|
|
24
|
+
|
|
25
|
+
## 0.3.3(2026-10-20,跨平台补丁版)
|
|
26
|
+
|
|
27
|
+
- **Windows 适配**:完整的测试套件(smoke 53 / handoff 35 / plugin-load 13 / contract 17 / compact-log)已在 Windows 11 + Node 24.22 实测全部通过。
|
|
28
|
+
- **测试跨平台修复**:`handoff` 的「目录不可写」用例改用「普通文件当父目录」制造 ENOTDIR(Windows 没有 `/dev/null` 语义);`contract` 的交接目录断言按 `path.resolve` 比较(Windows 上 `/tmp` 会被解析为 `C:\tmp`)。
|
|
29
|
+
- **构建脚本**:`scripts/build-client.mjs` 忽略隐藏文件与 macOS 资源分叉(`._*`),拷贝/打包后的源码在 Windows 上也能通过 `--check`。
|
|
30
|
+
- **文档**:README(中英)与 DEVELOPMENT 注明测试需要 Node 22.15+(`node:module.registerHooks`);TUTORIAL 常见问题补充「headless / QQ 桥接链路不加载指示灯与命令」;ROADMAP 明确不做桥接链路接管。
|
|
31
|
+
- 运行时功能与 0.3.2 一致。
|
|
32
|
+
|
|
7
33
|
## 0.3.2(2026-10-20,补丁版,行为无变化,仅设置页文案与链接)
|
|
8
34
|
|
|
9
35
|
- 设置页「帮助」区新增 npm 包页面外链(https://www.npmjs.com/package/dsh-compact-relay);「使用教程」「反馈 / 提 bug」两条外链指向的 GitHub 地址已确认可用。
|
package/README.en.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-compact-relay)
|
|
10
10
|
[](https://www.deepseek.com/harness/)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](CHANGELOG.md)
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
@@ -32,6 +32,8 @@ The plugin mounts beside the existing composer. Default parameters are tuned for
|
|
|
32
32
|
|
|
33
33
|
The ring is colored in segments according to the compression-assessment logic: the suggested compression point determined by the model or configured threshold, the positions of pinned messages, and the DSH native auto-compaction line (80% by default). When occupancy crosses the native auto-compaction line, the ring flashes red.
|
|
34
34
|
|
|
35
|
+
When occupancy enters the compressible band (between the minimum compression line and the compression line), the plugin proactively tells the model the current occupancy and band through a dynamic system-prompt context, so it can decide whether to record a compression node after finishing a natural milestone (since 0.3.4; before that the model could not see occupancy, so the condition was not actionable).
|
|
36
|
+
|
|
35
37
|
Hovering over the ring displays exact values: capacity used, cumulative compaction count, current suggestion, compression point, native auto-compaction line, and the number of pinned messages.
|
|
36
38
|
|
|
37
39
|
### 2. Preview-then-confirm compaction
|
|
@@ -58,7 +60,7 @@ Running `/pin` marks messages that must be preserved during compaction. Pinned m
|
|
|
58
60
|
| Blue arc | The normal capacity zone is full |
|
|
59
61
|
| Amber arc | The compression point has been passed; compaction is suitable |
|
|
60
62
|
| Red arc | The handoff threshold has been passed; switching to a new session is recommended |
|
|
61
|
-
| Ring notch | Suggested compression point (
|
|
63
|
+
| Ring notch | Suggested compression point (the system proactively prompts the model to assess inside the compressible band; shown after the model records one or the compression line is reached) |
|
|
62
64
|
| Violet tick | DSH native auto-compaction line (reads `compaction-basic.thresholdRatio`, 80% by default) |
|
|
63
65
|
| Magenta blocks | Distribution of pinned messages in the history |
|
|
64
66
|
| Full-ring red flash | The native auto-compaction line has been crossed; DSH native auto-compaction is imminent |
|
|
@@ -67,7 +69,7 @@ Running `/pin` marks messages that must be preserved during compaction. Pinned m
|
|
|
67
69
|
|
|
68
70
|
## Installation
|
|
69
71
|
|
|
70
|
-
**Requirements**: DSH Desktop or `dsh web` 0.2.x (tested on 0.2.1-rc.2), Node.js 22
|
|
72
|
+
**Requirements**: DSH Desktop or `dsh web` 0.2.x (tested on 0.2.1-rc.2), Node.js 22+ (the plugin uses the runtime bundled with DSH). Running the source test suite requires Node **22.15+** (the harness uses `node:module.registerHooks`). The suite is verified on macOS and Windows 11 (Node 24.22).
|
|
71
73
|
|
|
72
74
|
```sh
|
|
73
75
|
dsh plugin --profile desktop add dsh-compact-relay
|
|
@@ -118,11 +120,13 @@ The first time a session is opened, the ring plays a brief highlight animation w
|
|
|
118
120
|
- **Does not modify prompts or model replies**: history is replaced only when the user explicitly confirms a compaction or runs a handoff.
|
|
119
121
|
- **Summary quality depends on the underlying model**: summaries use a structured nine-section template with format validation and a single retry, but output quality remains limited by the selected model.
|
|
120
122
|
- **Local storage only**: plugin state is stored in `~/.dsh/compact-relay.json` and handoff documents in `~/.dsh/handoffs/`; no external data is synchronized.
|
|
123
|
+
- **Tied to the desktop composer**: the ring indicator, `/bc` and `/handoff` depend on the DSH composer and slash-command system. They are not loaded in headless sessions or in bridge links such as QQ/WeChat, where `/bc` is treated as an ordinary message. That is a limitation of the bridge link, not a plugin failure.
|
|
121
124
|
- **UI language limitation**: the current UI is Simplified Chinese only; an English UI is planned with no fixed schedule.
|
|
125
|
+
- **Prerequisite for the model hint**: the compressible-band hint relies on the DSH 0.2.x root service `systemPrompt` (same as plugins such as memory). It is a heuristic prompt and does not guarantee the model records a compression node every time, but the condition is now actionable instead of unobservable.
|
|
122
126
|
|
|
123
127
|
## Version and license
|
|
124
128
|
|
|
125
|
-
- **Current version**: 0.3.1
|
|
129
|
+
- **Current version**: 0.3.4 (0.3.1 was the first public release; 0.3.2 fixed settings links and contact details; 0.3.3 added Windows support; 0.3.4 fixes the compression-point judgment mechanism and adds handoff Markdown rendering plus list load-more). Tested on macOS (DSH 0.2.1-rc.2) and Windows 11 (Node 24.22), using only public DSH exports. See [CHANGELOG.md](CHANGELOG.md) for changes, [ROADMAP.md](ROADMAP.md) for plans and known limitations, and [DEVELOPMENT.md](https://github.com/cfanzx/dsh-compact-relay/blob/main/DEVELOPMENT.md) for build and release workflows.
|
|
126
130
|
- **License**: [MIT](LICENSE). Feedback and Pull Requests are welcome via [GitHub Issues](https://github.com/cfanzx/dsh-compact-relay/issues).
|
|
127
131
|
|
|
128
132
|
## Credits
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-compact-relay)
|
|
10
10
|
[](https://www.deepseek.com/harness/)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](CHANGELOG.md)
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
指示环按压缩评估逻辑分段显色,分别标识模型或阈值判定的建议压缩点、已锁定消息位置,以及 DSH 原生自动压缩线(默认 80%)。当上下文占用越过原生自动压缩线时,整环呈红色闪烁告警。
|
|
34
34
|
|
|
35
|
+
上下文进入「可压缩区间」(最小压缩线到压缩线之间)时,插件会在系统提示里主动告诉模型当前容量与区间,模型据此判断「刚完成一个自然节点」时是否记录压缩节点(0.3.4 起;此前模型看不到容量,判断条件不可执行)。
|
|
36
|
+
|
|
35
37
|
鼠标悬停可查看详细数值:已用容量、累计压缩次数、当前操作建议、压缩判断点、原生自动压缩线及已锁定消息数量。
|
|
36
38
|
|
|
37
39
|
### 2. 预览与确认压缩
|
|
@@ -58,7 +60,7 @@
|
|
|
58
60
|
| 蓝色弧 | 常规容量区间已满 |
|
|
59
61
|
| 琥珀色弧 | 已越过压缩判断点,适合执行压缩 |
|
|
60
62
|
| 红色弧 | 已越过交接阈值,建议切换新会话 |
|
|
61
|
-
| 环形缺口 |
|
|
63
|
+
| 环形缺口 | 建议压缩点(系统在可压缩区间主动提示模型判断;模型记录或到达压缩线时自动出现) |
|
|
62
64
|
| 紫色刻度 | DSH 原生自动压缩线(读取 `compaction-basic.thresholdRatio`,默认 80%) |
|
|
63
65
|
| 品红色块 | 已锁定消息在历史记录中的分布位置 |
|
|
64
66
|
| 整环红色闪烁 | 已越过原生自动压缩线,即将触发 DSH 原生自动压缩 |
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
|
|
68
70
|
## 安装
|
|
69
71
|
|
|
70
|
-
**环境要求**:DSH 桌面版或 `dsh web` 0.2.x(已于 0.2.1-rc.2 测试通过),Node.js 22
|
|
72
|
+
**环境要求**:DSH 桌面版或 `dsh web` 0.2.x(已于 0.2.1-rc.2 测试通过),Node.js 22+(插件跟随 DSH 自带运行时)。若要在源码里跑测试,需要 Node **22.15+**(测试脚手架用到 `node:module.registerHooks`)。测试已在 macOS 与 Windows 11(Node 24.22)实测全部通过。
|
|
71
73
|
|
|
72
74
|
```sh
|
|
73
75
|
dsh plugin --profile desktop add dsh-compact-relay
|
|
@@ -118,11 +120,13 @@ dsh plugin --profile desktop add github:cfanzx/dsh-compact-relay
|
|
|
118
120
|
- **不篡改提示词与模型回复**:插件仅在用户明确确认压缩或执行交接时替换对应历史记录。
|
|
119
121
|
- **摘要质量取决于底层模型**:摘要生成采用九段式结构化模板并包含格式校验与单次重试机制,但最终产出质量仍受限于所选模型的能力。
|
|
120
122
|
- **纯本地存储**:插件状态保存于 `~/.dsh/compact-relay.json`,交接文档保存于 `~/.dsh/handoffs/`,无任何外部数据同步。
|
|
123
|
+
- **依附桌面输入框**:环形指示器、`/bc`、`/handoff` 依附 DSH 的输入框与斜杠命令;headless 会话或 QQ / 微信等桥接链路不会加载它们,在这些链路里发 `/bc` 会被当成普通消息——这是桥接链路的能力边界,不是插件故障。
|
|
121
124
|
- **界面语言限制**:当前版本用户界面仅提供简体中文,英文界面已列入后续计划(排期未定)。
|
|
125
|
+
- **模型判断提示的前置**:可压缩区间的动态提示依赖 DSH 0.2.x 的根服务 `systemPrompt`(与 memory 等插件同款);它是启发式提示,不保证模型每次都会记录压缩节点,但条件从「不可判断」变成「可判断」。
|
|
122
126
|
|
|
123
127
|
## 版本状态与开源许可
|
|
124
128
|
|
|
125
|
-
- **当前版本**:0.3.1
|
|
129
|
+
- **当前版本**:0.3.4(0.3.1 为首个公开版本;0.3.2 修正设置页链接与联系方式;0.3.3 完成 Windows 适配;0.3.4 修复「适合压缩的节点几乎不触发」的判断机制,并补交接单 Markdown 渲染与列表加载更多)。已在 macOS(DSH 0.2.1-rc.2)与 Windows 11(Node 24.22)完成测试,仅调用 DSH 公开导出接口。变更记录详见 [CHANGELOG.md](CHANGELOG.md),后续规划与已知限制详见 [ROADMAP.md](ROADMAP.md),构建与发布流程详见 [DEVELOPMENT.md](https://github.com/cfanzx/dsh-compact-relay/blob/main/DEVELOPMENT.md)。
|
|
126
130
|
- **开源许可**:采用 [MIT](LICENSE) 许可证,欢迎通过 [GitHub Issues](https://github.com/cfanzx/dsh-compact-relay/issues) 与 Pull Requests 提交反馈。
|
|
127
131
|
|
|
128
132
|
## 参考与致谢
|
package/ROADMAP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 压缩接力 · 目标与路线图
|
|
2
2
|
|
|
3
|
-
> 版本基准:0.3.
|
|
3
|
+
> 版本基准:0.3.4(2026-10-24)|状态:0.3 已发布(npm + GitHub),0.3.4 发布中
|
|
4
4
|
> 维护方式:每次发布前更新"当前状态"与勾选进度;方向变化必须同时改本文件和 memory 项目页。
|
|
5
5
|
|
|
6
6
|
## 一、最终目标(North Star)
|
|
@@ -72,6 +72,27 @@
|
|
|
72
72
|
|
|
73
73
|
验收:多天项目不用回忆"上次做到哪",直接继续。
|
|
74
74
|
|
|
75
|
+
### 评审建议的落位(2026-10-20 小潇拍板)
|
|
76
|
+
|
|
77
|
+
> 来源:2026-10-20 首条外部评审(Gemini)+ 小潇讨论。拍板结论:先收尾发布闭环(市场 PR / 陌生人安装验证 / soak),再进 0.4 首批。
|
|
78
|
+
|
|
79
|
+
**0.4 首批(已确认,四项)**
|
|
80
|
+
1. **压缩预览里的「建议锁定」提示**:预览时高亮「包含关键决策 / 报错堆栈 / 明确要求别改」的消息,提示用户先 `/pin` 再压;只提示,不自动锁(不替用户做主)。
|
|
81
|
+
2. **设置页一行「累计压缩 X 次 / 腾出 Y tokens」**:不做独立成本面板(见 §四),只做一行正反馈数字。
|
|
82
|
+
3. **交接预览可编辑**:只允许改「下一步计划 / 约束条件」两段;程序生成的段落不可改(防编造),改完再确认切换。
|
|
83
|
+
4. **接力链元数据补全**:A5 记录已存在,补全 parent/child、第几次、时间线字段,为 0.5 的接力链视图铺路(视图本身仍留 0.5)。
|
|
84
|
+
|
|
85
|
+
**低优先(0.4 观察,有数据再动)**
|
|
86
|
+
- **阶段边界「只读提示」**:识别「子任务收尾」的对话边界,在缺口旁只读标注「可能处于阶段收尾」,暂不改变压缩范围;误判风险大,先只提示。
|
|
87
|
+
- **场景化交接模板**:等 3+ 真实用户提同一诉求再做。
|
|
88
|
+
- **简化模式(降低认知负荷)**:可选开关,暂不排期。
|
|
89
|
+
|
|
90
|
+
**研究项(先探边界,不做承诺)**
|
|
91
|
+
- **DSH 公开消息钩子**:确认宿主是否提供官方钩子可供桥接链路做文本级提醒;没有就维持 §四「不接管 headless/桥接链路」。dcr 目前只用 `ctx.on('session/event')` 观察事件,不修改模型回复。
|
|
92
|
+
|
|
93
|
+
**维持观察**
|
|
94
|
+
- **i18n(英文界面)**:README 已有英文版;界面英文等 0.5 再评估。
|
|
95
|
+
|
|
75
96
|
### 1.0 上下文生命周期自动化(远期)
|
|
76
97
|
|
|
77
98
|
- 自动化等级:建议 → 一键确认 → 在 Step 边界自动执行(可配置)。
|
|
@@ -82,6 +103,7 @@
|
|
|
82
103
|
验收:用户不再主动想"上下文会不会满"。
|
|
83
104
|
|
|
84
105
|
## 四、明确切掉 / 不做
|
|
106
|
+
- **不接管 headless / 桥接链路**:指示灯与 `/bc`、`/handoff` 依附 DSH 输入框和命令系统;headless 会话、QQ / 微信桥接里不加载(那里发 `/bc` 会被当成普通消息)。只保证插件加载不报错、不额外烧模型调用。
|
|
85
107
|
|
|
86
108
|
| 方向 | 原因 |
|
|
87
109
|
|---|---|
|
|
@@ -119,7 +141,12 @@
|
|
|
119
141
|
## 七、当前状态 / 下一步
|
|
120
142
|
|
|
121
143
|
- ✅ 0.3.1 发布包已就绪:版本号 / CHANGELOG / README 安装段 / .gitignore / npm `files` 收窄 / 文件权限归一;测试基线 smoke 50、handoff 35、plugin-load 13、contract 17、compact-log PASS。
|
|
144
|
+
- ✅ 0.3.1 已发布:npm `dsh-compact-relay@0.3.1` + GitHub tag/Release;0.3.2 补设置页链接与联系方式;0.3.3 完成 Windows 适配(Win11 + Node 24.22 全测试通过)。
|
|
145
|
+
- ✅ 0.3.4 判断机制修复(BUG-010:`mark_compress_point` + systemPrompt 可压缩区间提示 + 状态缓存;427 会话数据佐证旧工具 0 调用)+ BUG-005/BUG-006 小 bug;npm latest 待本次发布。
|
|
122
146
|
- ✅ 真实压缩 e2e:`test/e2e/real-compact.mjs`(真实模型 + 真实压缩事务 + `maxSummaries=1` 合并旧摘要分支)6 项断言通过。
|
|
123
147
|
- ✅ P3-01~14(13 修/1 确认无需修)+ BUG-004 + P2-T1 已收口。
|
|
124
|
-
- ⏳
|
|
125
|
-
- ⏳
|
|
148
|
+
- ⏳ 市场收录 PR:仓库满 1 天(2026-10-21 19:09 后)提,yml 与说明已备。
|
|
149
|
+
- ⏳ 陌生人安装路径验证(npm 安装 → 重启 → 设置/指示灯)—— 发布后第一次外部用户安装时一并确认。
|
|
150
|
+
- ⏳ 真实长会话 soak(至少 3 条真实任务,含压缩→交接→继续)—— 建议下一批功能开发时顺带跑(dogfooding)。
|
|
151
|
+
- ⏳ 与其他上下文插件共存测试(dsh-auto-handoff / hypercompact 等)—— 等市场收录后有用户反馈再做。
|
|
152
|
+
- ⏳ 真机复核残留:交接成功文案(0.3.1 起文案已改,待一次真交接确认)。
|
package/TUTORIAL.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| 警示红(原生线) | 80% 处的 DSH 原生自动压缩线(紫色径向刻度);越过 80% 后整环红色闪烁 | 再往前 DSH 会自己压历史,先压缩或交接 |
|
|
18
18
|
| 品红(锁定色) | 这里锁了一条重要消息(按它在对话里的位置显示,可多个) | 知道「我锁的东西在哪」 |
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
**断点**(环上的小缺口)=「我判断这里适合压缩」的位置:上下文进入可压缩区间(最小压缩线到压缩线之间)时,系统会主动在提示里告诉模型当前容量,模型判断「刚完成一个自然节点」时**可能**会记录一个压缩节点;到了压缩线也会自动出现。压缩后环变短,断点自然消失。
|
|
21
21
|
|
|
22
22
|
把鼠标放到指示灯上,会显示具体数字:`会话容量已用 xx.x%(xxxK / 1.00M),已压缩 n 次|建议压缩 / 建议交接 / 正常|压缩判断点 x%|原生自动压缩线 80%|已锁定 n 条`。
|
|
23
23
|
|
|
@@ -58,8 +58,9 @@
|
|
|
58
58
|
- **压缩到一半取消了**:历史不会变、锁会释放;已发出的模型请求可能仍计费。
|
|
59
59
|
- **换模型后阈值变了**:阈值按窗口比例算,小窗口模型会自动变小,这是预期。
|
|
60
60
|
- **设置保存了但没变**:设置栏保存即时生效;改代码或 profile 配置才需要重启 DSH。
|
|
61
|
+
- **在 headless / QQ 桥上发 `/bc` 没反应**:指示灯和 `/bc`、`/handoff` 依附桌面输入框与斜杠命令,headless 会话和 QQ / 微信桥接链路不会加载它们,`/bc` 会被当成普通消息。插件本身不报错、也不额外烧模型调用;要在电脑上开 DSH 桌面版才能用。
|
|
61
62
|
|
|
62
63
|
## 5. 更多
|
|
63
64
|
|
|
64
65
|
- 安装、完整说明见仓库 `README.md`;设计文档见 `ARCHITECTURE.md`。
|
|
65
|
-
- 遇到 bug / 想提改进:到仓库 Issues
|
|
66
|
+
- 遇到 bug / 想提改进:到仓库 Issues 提一条(设置页「帮助」区的「反馈 / 提 bug」入口会直接打开);也可以发邮件到 cfanzx@136.com。
|
|
@@ -19,13 +19,13 @@ import { actionError } from '../core/errors.js';
|
|
|
19
19
|
import { livePinnedSeqs } from '../dsh/session-view.js';
|
|
20
20
|
import { compactionPolicy } from '../core/policy.js';
|
|
21
21
|
import { SUMMARY_INSTRUCTION } from '../core/prompts.js';
|
|
22
|
+
import { sumShadowedTokens, tokenOf } from '../core/tokens.js';
|
|
22
23
|
import { selectCompactableRange } from '../dsh/select-range.js';
|
|
23
|
-
import { replaceRangeWithSummary } from '../dsh/compact-transaction.js';
|
|
24
|
-
import { runIdle } from '../dsh/idle.js';
|
|
25
24
|
import { measurePressure } from '../dsh/pressure.js';
|
|
26
25
|
import { countSummaries } from '../dsh/session-view.js';
|
|
27
|
-
import {
|
|
26
|
+
import { runCompaction } from './compact-run.js';
|
|
28
27
|
import { withLock } from './lock.js';
|
|
28
|
+
import { beginInflight, cancelErrorOf, combinedSignal, finishInflight } from './support.js';
|
|
29
29
|
|
|
30
30
|
/** 范围小于这个 token 数时,摘要大概率比原文大:预览阶段就提示、前端据此禁用确认(0.3.1)。 */
|
|
31
31
|
const MIN_MEANINGFUL_TOKENS = 800;
|
|
@@ -55,11 +55,6 @@ function planIdOf(range) {
|
|
|
55
55
|
.slice(0, 16);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function rememberPreviewPlan(sessionId, plan) {
|
|
59
|
-
if (typeof sessionId !== 'string' || sessionId.length === 0) return;
|
|
60
|
-
previewPlans.set(sessionId, { ...plan, expiresAt: Date.now() + PREVIEW_TTL_MS });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
/**
|
|
64
59
|
* 取出并消费一次预览计划;过期/不存在返回 null。
|
|
65
60
|
* @param {string|undefined} sessionId
|
|
@@ -86,13 +81,6 @@ export function clearPreviewPlans() {
|
|
|
86
81
|
previewPlans.clear();
|
|
87
82
|
}
|
|
88
83
|
|
|
89
|
-
function shadowedTokensOf(nodes, shadowedSeqs) {
|
|
90
|
-
const wanted = new Set(shadowedSeqs);
|
|
91
|
-
return nodes
|
|
92
|
-
.filter((node) => wanted.has(node.seq))
|
|
93
|
-
.reduce((sum, node) => sum + (node.heuristicTokens ?? node.tokens ?? 0), 0);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
84
|
/**
|
|
97
85
|
* 算一遍「这次能压什么」—— 预览与正式执行共用,保证两边口径一致。
|
|
98
86
|
* @param {{ctx:object, cfg:object, engine:object}} deps
|
|
@@ -112,12 +100,17 @@ async function planCompact(deps, agent) {
|
|
|
112
100
|
nodes,
|
|
113
101
|
engine: deps.engine,
|
|
114
102
|
cfg: deps.cfg,
|
|
115
|
-
tokenOf
|
|
103
|
+
tokenOf,
|
|
116
104
|
});
|
|
117
105
|
|
|
118
106
|
return { measured, nodes, summaries, allowMergeOldSummaries, range, pinned, planId: range === null ? null : planIdOf(range) };
|
|
119
107
|
}
|
|
120
108
|
|
|
109
|
+
function rememberPreviewPlan(sessionId, plan) {
|
|
110
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) return;
|
|
111
|
+
previewPlans.set(sessionId, { ...plan, expiresAt: Date.now() + PREVIEW_TTL_MS });
|
|
112
|
+
}
|
|
113
|
+
|
|
121
114
|
/**
|
|
122
115
|
* 没有可压范围时给用户的解释——被钉住挡住时要指出来,否则用户不知道去 /unpin。
|
|
123
116
|
* @param {number[]|undefined} pinned
|
|
@@ -156,38 +149,18 @@ export async function betterCompactLocked(deps, { agent, signal, commandId, expe
|
|
|
156
149
|
};
|
|
157
150
|
}
|
|
158
151
|
|
|
159
|
-
const freshRange = { ...range, shadowedTokenCount:
|
|
160
|
-
const flush =
|
|
161
|
-
typeof deps.ctx.sessions?.flush === 'function' ? () => deps.ctx.sessions.flush(session) : undefined;
|
|
162
|
-
const estimateMessage =
|
|
163
|
-
typeof deps.ctx?.tokenMeter?.estimateMessage === 'function'
|
|
164
|
-
? (message) => deps.ctx.tokenMeter.estimateMessage(message)
|
|
165
|
-
: undefined;
|
|
152
|
+
const freshRange = { ...range, shadowedTokenCount: sumShadowedTokens(nodes, range.shadowedSeqs) };
|
|
166
153
|
|
|
167
|
-
const outcome = await
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
summarize({
|
|
178
|
-
ctx: deps.ctx,
|
|
179
|
-
engine: deps.engine,
|
|
180
|
-
agent,
|
|
181
|
-
shadowedSeqs,
|
|
182
|
-
endSeq: freshRange.endSeq,
|
|
183
|
-
instruction: SUMMARY_INSTRUCTION,
|
|
184
|
-
maxTokens: deps.cfg.summary.maxTokens,
|
|
185
|
-
inputTokens: plan.measured.tokens,
|
|
186
|
-
timeoutMs: deps.cfg.timeouts?.llmMs,
|
|
187
|
-
signal: innerSignal,
|
|
188
|
-
}),
|
|
189
|
-
flush,
|
|
190
|
-
});
|
|
154
|
+
const outcome = await runCompaction(deps, {
|
|
155
|
+
agent,
|
|
156
|
+
session,
|
|
157
|
+
range: freshRange,
|
|
158
|
+
instruction: SUMMARY_INSTRUCTION,
|
|
159
|
+
signal,
|
|
160
|
+
commandId,
|
|
161
|
+
inputTokens: plan.measured.tokens,
|
|
162
|
+
// 正式精压继承配置超时(/bctest 不传、保持 llm-call 的 180s 默认)。
|
|
163
|
+
timeoutMs: deps.cfg.timeouts?.llmMs,
|
|
191
164
|
});
|
|
192
165
|
|
|
193
166
|
const note = allowMergeOldSummaries
|
|
@@ -227,7 +200,7 @@ export async function previewBetterCompact(deps, { agent } = {}) {
|
|
|
227
200
|
};
|
|
228
201
|
}
|
|
229
202
|
|
|
230
|
-
const shadowedTokenCount =
|
|
203
|
+
const shadowedTokenCount = sumShadowedTokens(nodes, range.shadowedSeqs);
|
|
231
204
|
rememberPreviewPlan(agent.id, {
|
|
232
205
|
planId,
|
|
233
206
|
startSeq: range.startSeq,
|
|
@@ -283,12 +256,6 @@ export function cancelBetterCompact(deps, { agent } = {}) {
|
|
|
283
256
|
: { ok: false, cancelled: false, kind: null, message: '当前没有正在进行的压缩/交接任务。' };
|
|
284
257
|
}
|
|
285
258
|
|
|
286
|
-
function combinedSignal(a, b) {
|
|
287
|
-
if (a === undefined) return b;
|
|
288
|
-
if (b === undefined) return a;
|
|
289
|
-
return AbortSignal.any([a, b]);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
259
|
/**
|
|
293
260
|
* 压缩动作入口:带每会话单飞锁 + 可取消登记。
|
|
294
261
|
* @param {{ctx:object, cfg:object, lock:object, engine:object, inflight?:object}} deps
|
|
@@ -297,30 +264,15 @@ function combinedSignal(a, b) {
|
|
|
297
264
|
export async function betterCompact(deps, input) {
|
|
298
265
|
if (input?.agent === undefined || input?.agent === null) throw actionError('nosession', '没有活动会话');
|
|
299
266
|
const agent = input.agent;
|
|
300
|
-
const controller =
|
|
301
|
-
deps.inflight !== undefined && typeof deps.inflight.begin === 'function'
|
|
302
|
-
? deps.inflight.begin(agent.id, 'compact')
|
|
303
|
-
: null;
|
|
267
|
+
const controller = beginInflight(deps, agent.id, 'compact');
|
|
304
268
|
const signal = combinedSignal(controller?.signal, input.signal);
|
|
305
269
|
|
|
306
270
|
try {
|
|
307
271
|
return await withLock(deps.lock, agent.id, () => betterCompactLocked(deps, { ...input, signal }));
|
|
308
272
|
} catch (error) {
|
|
309
273
|
// 取消 → 统一成 cancelled 文案(宿主 HTTP 层据此回「历史没改」)。
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
signal !== undefined &&
|
|
313
|
-
signal.aborted &&
|
|
314
|
-
error?.code !== 'cancelled' &&
|
|
315
|
-
error?.code !== 'commit' &&
|
|
316
|
-
error?.code !== 'persistence'
|
|
317
|
-
) {
|
|
318
|
-
throw actionError('cancelled', '已取消,会话记录没有改动。', error);
|
|
319
|
-
}
|
|
320
|
-
throw error;
|
|
274
|
+
throw cancelErrorOf(error, signal, '已取消,会话记录没有改动。');
|
|
321
275
|
} finally {
|
|
322
|
-
|
|
323
|
-
deps.inflight.finish(agent.id, controller);
|
|
324
|
-
}
|
|
276
|
+
finishInflight(deps, agent.id, controller);
|
|
325
277
|
}
|
|
326
278
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 压缩执行流程 —— 正式精压(actions/better-compact.js)与实验动作 /bctest
|
|
3
|
+
* (actions/test-compact.js)共用的那一段「idle 准入 → 替换事务 → 调摘要」。
|
|
4
|
+
*
|
|
5
|
+
* 两个动作的差异只在**怎么选段、用什么摘要指令、要不要六段校验、超时从哪来**;
|
|
6
|
+
* 真正改历史的执行路径必须完全同一条(否则 /bctest 就不是「小范围真事务」了):
|
|
7
|
+
* 1. 从 deps.ctx 取 flush / tokenMeter.estimateMessage(宿主能力,可选);
|
|
8
|
+
* 2. runIdle(agent, ...) 里跑事务(手动压缩的 idle 准入 / 与自动压缩互斥);
|
|
9
|
+
* 3. 用户取消信号与 idle 信号合并;
|
|
10
|
+
* 4. replaceRangeWithSummary:summarize 回放前缀(cache 对齐)+ 替换历史 + 成对 start/end。
|
|
11
|
+
*
|
|
12
|
+
* 超时不在本函数内部读 cfg:由调用方传 `timeoutMs`。正式精压传 cfg.timeouts.llmMs
|
|
13
|
+
* (原有行为),/bctest 不传 → summarize 用 llm-call 的 180s 默认(同样保持原行为)。
|
|
14
|
+
*
|
|
15
|
+
* 以前这 20+ 行在 better-compact.js 与 test-compact.js 里逐字重复(含 summarize
|
|
16
|
+
* 参数的拼装),改一处漏一处就会让 /bctest 与正式压缩行为分叉。
|
|
17
|
+
*/
|
|
18
|
+
import { replaceRangeWithSummary } from '../dsh/compact-transaction.js';
|
|
19
|
+
import { runIdle } from '../dsh/idle.js';
|
|
20
|
+
import { summarize } from '../dsh/summarizer.js';
|
|
21
|
+
import { combinedSignal } from './support.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 执行一次压缩替换事务。
|
|
25
|
+
* @param {{ctx:object, cfg:object, engine:object}} deps
|
|
26
|
+
* @param {object} input
|
|
27
|
+
* @param {object} input.agent
|
|
28
|
+
* @param {object} input.session
|
|
29
|
+
* @param {{startSeq:number, endSeq:number, shadowedSeqs:number[], shadowedTokenCount:number}} input.range
|
|
30
|
+
* @param {string} input.instruction 摘要指令(正式版 SUMMARY_INSTRUCTION / 实验版 TEST_INSTRUCTION)
|
|
31
|
+
* @param {string[]} [input.requiredSections] 摘要结构校验的小节;省略=默认六段,[]=不校验(/bctest)
|
|
32
|
+
* @param {AbortSignal} [input.signal]
|
|
33
|
+
* @param {string} [input.commandId]
|
|
34
|
+
* @param {number} [input.inputTokens] 调用前预算预检的输入 token 估算(可选)
|
|
35
|
+
* @param {number} [input.timeoutMs] 单次模型调用超时;**由调用方决定**(省略 = llm-call 的 180s 默认)
|
|
36
|
+
* @returns {Promise<{compactionId:string, startSeq:number, summarySeq:number, endSeq:number, shadowedSeqs:number[], shadowedTokenCount:number}>}
|
|
37
|
+
*/
|
|
38
|
+
export async function runCompaction(
|
|
39
|
+
deps,
|
|
40
|
+
{ agent, session, range, instruction, requiredSections, signal, commandId, inputTokens, timeoutMs },
|
|
41
|
+
) {
|
|
42
|
+
const flush =
|
|
43
|
+
typeof deps.ctx.sessions?.flush === 'function' ? () => deps.ctx.sessions.flush(session) : undefined;
|
|
44
|
+
const estimateMessage =
|
|
45
|
+
typeof deps.ctx?.tokenMeter?.estimateMessage === 'function'
|
|
46
|
+
? (message) => deps.ctx.tokenMeter.estimateMessage(message)
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
return runIdle(agent, (idleSignal) => {
|
|
50
|
+
const combined = combinedSignal(signal, idleSignal);
|
|
51
|
+
return replaceRangeWithSummary({
|
|
52
|
+
session,
|
|
53
|
+
range,
|
|
54
|
+
engine: deps.engine,
|
|
55
|
+
signal: combined,
|
|
56
|
+
sourceCommandId: commandId,
|
|
57
|
+
estimateMessage,
|
|
58
|
+
summarize: ({ shadowedSeqs, signal: innerSignal }) =>
|
|
59
|
+
summarize({
|
|
60
|
+
ctx: deps.ctx,
|
|
61
|
+
engine: deps.engine,
|
|
62
|
+
agent,
|
|
63
|
+
shadowedSeqs,
|
|
64
|
+
endSeq: range.endSeq,
|
|
65
|
+
instruction,
|
|
66
|
+
requiredSections,
|
|
67
|
+
maxTokens: deps.cfg.summary.maxTokens,
|
|
68
|
+
inputTokens,
|
|
69
|
+
// 超时由调用方传:正式精压传 cfg.timeouts.llmMs(原行为),/bctest 不传 → 180s 默认。
|
|
70
|
+
timeoutMs,
|
|
71
|
+
signal: innerSignal,
|
|
72
|
+
}),
|
|
73
|
+
flush,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 「这里适合压缩」判断(0.3.1)。
|
|
2
|
+
* 「这里适合压缩」判断(0.3.1;0.3.4 工具改名 mark_compress_point)。
|
|
3
3
|
*
|
|
4
|
-
* 模型工具 `
|
|
4
|
+
* 模型工具 `mark_compress_point` 在这里落一条 plugin record;环上把它画成断点1。
|
|
5
5
|
* 用户手动入口已删除(作者 2026-10-20 拍板:判断由模型/阈值自动完成)。
|
|
6
|
+
*
|
|
7
|
+
* 服务端仍复核区间:太低(压缩不划算)/已经到压缩线都拒绝,不改历史。
|
|
8
|
+
* 返回文案是给模型看的,措辞要和工具描述一致(0.3.4 起不再说「系统会告诉你」)。
|
|
6
9
|
*/
|
|
7
10
|
import { COMPRESS_HINT_RECORD_TYPE } from '../core/compress-hint.js';
|
|
8
11
|
import { compressionMinRatio } from '../core/policy.js';
|
|
@@ -17,11 +20,11 @@ import { measurePressure } from '../dsh/pressure.js';
|
|
|
17
20
|
export async function markCompressHint(deps, { agent } = {}) {
|
|
18
21
|
const session = agent?.session;
|
|
19
22
|
if (session === null || session === undefined) {
|
|
20
|
-
return { ok: false, ratio: 0, at: 0, reason: 'no-session', message: '
|
|
23
|
+
return { ok: false, ratio: 0, at: 0, reason: 'no-session', message: '没有活动会话,无法记录压缩节点' };
|
|
21
24
|
}
|
|
22
25
|
const measured = await measurePressure(deps.ctx, agent, { windowFallback: deps.cfg.contextWindowFallback }).catch(() => null);
|
|
23
26
|
if (measured === null || typeof measured.ratio !== 'number') {
|
|
24
|
-
return { ok: false, ratio: 0, at: 0, reason: 'no-pressure', message: '
|
|
27
|
+
return { ok: false, ratio: 0, at: 0, reason: 'no-pressure', message: '拿不到当前上下文容量,先别记录压缩节点' };
|
|
25
28
|
}
|
|
26
29
|
const ratio = measured.ratio;
|
|
27
30
|
const window = measured.window;
|
|
@@ -34,7 +37,9 @@ export async function markCompressHint(deps, { agent } = {}) {
|
|
|
34
37
|
ratio,
|
|
35
38
|
at: Date.now(),
|
|
36
39
|
reason: 'below-min',
|
|
37
|
-
message:
|
|
40
|
+
message:
|
|
41
|
+
`当前容量 ${percent(ratio)} 还没到最小压缩线 ${percent(minRatio)}——现在压缩旧历史不划算,` +
|
|
42
|
+
'没有记录压缩节点,继续做你的工作',
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
if (ratio >= compactRatio) {
|
|
@@ -43,7 +48,9 @@ export async function markCompressHint(deps, { agent } = {}) {
|
|
|
43
48
|
ratio,
|
|
44
49
|
at: Date.now(),
|
|
45
50
|
reason: 'already-over',
|
|
46
|
-
message:
|
|
51
|
+
message:
|
|
52
|
+
`当前容量 ${percent(ratio)} 已经到压缩线 ${percent(compactRatio)} 了——` +
|
|
53
|
+
'直接建议用户压缩旧会话即可,不用再记录压缩节点',
|
|
47
54
|
};
|
|
48
55
|
}
|
|
49
56
|
const at = Date.now();
|
|
@@ -53,6 +60,8 @@ export async function markCompressHint(deps, { agent } = {}) {
|
|
|
53
60
|
ratio,
|
|
54
61
|
at,
|
|
55
62
|
seq,
|
|
56
|
-
message:
|
|
63
|
+
message:
|
|
64
|
+
`已记录压缩节点:当前容量 ${percent(ratio)}(在最小线 ${percent(minRatio)} 和压缩线 ${percent(compactRatio)} 之间)。` +
|
|
65
|
+
'用户会在指示灯环上看到一个断点;可以继续工作,或建议用户压缩旧会话',
|
|
57
66
|
};
|
|
58
67
|
}
|
|
@@ -3,17 +3,23 @@
|
|
|
3
3
|
*
|
|
4
4
|
* HTTP 只关心「列最近 N 份 / 读一篇的 JSON」,目录白名单/防穿越/大小上限
|
|
5
5
|
* 全部留在 dsh/handoff-files.js。
|
|
6
|
+
*
|
|
7
|
+
* 0.3.4(BUG-006):列表额外返回 `total`(全量份数),客户端据此显示
|
|
8
|
+
* 「共 N 份(显示最近 20)」并用「加载更多」把 limit 提到 50。
|
|
6
9
|
*/
|
|
7
|
-
import { listHandoffFiles, readHandoffFile } from '../dsh/handoff-files.js';
|
|
10
|
+
import { countHandoffFiles, listHandoffFiles, readHandoffFile } from '../dsh/handoff-files.js';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* @param {{cfg:{handoffDir:string}}} deps
|
|
11
14
|
* @param {{limit?:number}} [options]
|
|
15
|
+
* @returns {{dir:string, items:object[], total:number}}
|
|
12
16
|
*/
|
|
13
17
|
export function listHandoffDocs(deps, { limit = 20 } = {}) {
|
|
18
|
+
const dir = deps.cfg.handoffDir;
|
|
14
19
|
return {
|
|
15
|
-
dir
|
|
16
|
-
items: listHandoffFiles(
|
|
20
|
+
dir,
|
|
21
|
+
items: listHandoffFiles(dir, { limit }),
|
|
22
|
+
total: countHandoffFiles(dir),
|
|
17
23
|
};
|
|
18
24
|
}
|
|
19
25
|
|
package/lib/actions/handoff.js
CHANGED
|
@@ -31,6 +31,7 @@ import { surfaceMessages } from '../dsh/prefix.js';
|
|
|
31
31
|
import { handoffTitlePlanOf, createHandoffSession, sourceTitleOf } from '../dsh/sessions.js';
|
|
32
32
|
import { writeRelayRecord } from '../dsh/relay.js';
|
|
33
33
|
import { withLock } from './lock.js';
|
|
34
|
+
import { beginInflight, cancelErrorOf, combinedSignal, finishInflight } from './support.js';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* 压缩阶段:能压就压,压不了就带着原因往下走。
|
|
@@ -87,16 +88,8 @@ export async function handoff(deps, { agent, signal, commandId }) {
|
|
|
87
88
|
|
|
88
89
|
return withLock(deps.lock, agent.id, async () => {
|
|
89
90
|
// P1-4:交接登记进 inflight,client 的取消按钮才能真正 abort 模型调用。
|
|
90
|
-
const controller =
|
|
91
|
-
|
|
92
|
-
? deps.inflight.begin(agent.id, 'handoff')
|
|
93
|
-
: null;
|
|
94
|
-
const combined =
|
|
95
|
-
controller?.signal === undefined
|
|
96
|
-
? signal
|
|
97
|
-
: signal === undefined
|
|
98
|
-
? controller.signal
|
|
99
|
-
: AbortSignal.any([controller.signal, signal]);
|
|
91
|
+
const controller = beginInflight(deps, agent.id, 'handoff');
|
|
92
|
+
const combined = combinedSignal(controller?.signal, signal);
|
|
100
93
|
|
|
101
94
|
try {
|
|
102
95
|
// P2-2:先做可失败的 preflight(目录可写),再动历史(压缩)。
|
|
@@ -242,19 +235,9 @@ export async function handoff(deps, { agent, signal, commandId }) {
|
|
|
242
235
|
};
|
|
243
236
|
} catch (error) {
|
|
244
237
|
// 取消统一收口;已提交/落盘状态不明的错误不许谎报「没改」。
|
|
245
|
-
|
|
246
|
-
combined?.aborted === true &&
|
|
247
|
-
error?.code !== 'cancelled' &&
|
|
248
|
-
error?.code !== 'commit' &&
|
|
249
|
-
error?.code !== 'persistence'
|
|
250
|
-
) {
|
|
251
|
-
throw actionError('cancelled', '已取消交接;已完成的压缩不会回滚。', error);
|
|
252
|
-
}
|
|
253
|
-
throw error;
|
|
238
|
+
throw cancelErrorOf(error, combined, '已取消交接;已完成的压缩不会回滚。');
|
|
254
239
|
} finally {
|
|
255
|
-
|
|
256
|
-
deps.inflight.finish(agent.id, controller);
|
|
257
|
-
}
|
|
240
|
+
finishInflight(deps, agent.id, controller);
|
|
258
241
|
}
|
|
259
242
|
});
|
|
260
243
|
}
|
package/lib/actions/settings.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { resolveConfig } from '../config.js';
|
|
14
14
|
import { paletteForBothSchemes } from '../core/colors.js';
|
|
15
|
+
import { isPlainObject } from '../core/object.js';
|
|
15
16
|
import { readNativeCompaction } from '../dsh/native-compaction.js';
|
|
16
17
|
import { filterSettingsPatch, publicConfigOf } from '../core/config-schema.js';
|
|
17
18
|
import {
|
|
@@ -24,10 +25,6 @@ import {
|
|
|
24
25
|
|
|
25
26
|
export { defaultSettingsPath } from '../dsh/settings-file.js';
|
|
26
27
|
|
|
27
|
-
function isPlainObject(value) {
|
|
28
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
28
|
/** 深合并普通对象;数组/标量直接覆盖。 */
|
|
32
29
|
function deepMerge(base, patch) {
|
|
33
30
|
if (!isPlainObject(base) || !isPlainObject(patch)) return patch;
|
package/lib/actions/state.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { paletteForBothSchemes } from '../core/colors.js';
|
|
8
8
|
import { adviceFor, ratioOf, thresholdsForWindow } from '../core/policy.js';
|
|
9
|
+
import { tokenOf } from '../core/tokens.js';
|
|
9
10
|
import { measurePressure } from '../dsh/pressure.js';
|
|
10
11
|
import { selectCompactableRange } from '../dsh/select-range.js';
|
|
11
12
|
import { compressHint as compressHintOf, countSummaries, lastCheckpointSurfaceSeq, livePinnedSeqs, liveSessionOf, pickAgent, surfaceSeqs } from '../dsh/session-view.js';
|
|
@@ -75,7 +76,7 @@ export async function readState(deps, { sessionId, agent } = {}) {
|
|
|
75
76
|
nodes: measured.nodes.filter((node) => Number.isInteger(node?.seq)),
|
|
76
77
|
engine: deps.engine,
|
|
77
78
|
cfg: deps.cfg,
|
|
78
|
-
tokenOf
|
|
79
|
+
tokenOf,
|
|
79
80
|
});
|
|
80
81
|
compactRange =
|
|
81
82
|
compactRange === null
|