dsh-compact-relay 0.5.1 → 0.5.3

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 CHANGED
@@ -1,6 +1,133 @@
1
1
  # Changelog
2
2
 
3
- ## 0.5.1(2026-10-26 · 未发布,待 Lead 验收)
3
+ ## 0.5.3(2026-10-29,未发布)
4
+
5
+ > 从 0.5.2 发布点 `e4bd0b2` 拉出的分支 `fix/0.5.3`。主题:**收藏(pin 退役)+ 推荐收藏**,
6
+ > 外加 0.5.2 真机验证带出的两个 P2 语义修正。测试 **375 → 385 PASS**(含 build-client --check)。
7
+
8
+ ### 收藏:把「标记」和「落档」彻底分成两步(b72fcdf / 6bec539)
9
+
10
+ - **收藏 = 纯标记**:项目级 JSONL 只记 `{op, sessionId, seq}`;不复制正文、不改变压缩选段、
11
+ 不占预算。**标记层不依赖活 agent**:归档/关闭的会话也能按 `sessionId + seq` 标记
12
+ (`action:'favorite-mark'`),收藏文件按**链根会话 id** 稳定定位,旧「标题-短 id」文件名只读兼容。
13
+ - **固定入口**:`/favorite mark [seq]`(不带 seq 标最后一条已完成回复);助手消息底部星标保留
14
+ (`action:'favorite-toggle'`,按 messageId 反查所属会话,客户端 sessionId 缺失/过期也能落对)。
15
+ - **落档只在压缩时**:摘要里追加「收藏(本段)」目录 + `~/.dsh/dcr-favorites/snapshots/<根id>.md`
16
+ 快照。压缩后再补标记时**当场写快照**(原文还在日志里才能写;读不到明确报错,不假装成功)。
17
+ - **压缩预览卡补收藏区**(0.5.3 收口):确认卡显示「本段 N 条 · 已收藏 M 条 · 建议补 K 条」,
18
+ 列出本段已收藏条目(`[用户]/[助手]` + 标题)与「建议先收藏」候选(复用原 pin 候选识别:
19
+ 关键决策/报错堆栈/明确要求别改),每条带「收藏」按钮(`favorite-mark` 按 seq、用户发言也可标);
20
+ 只提示+手动点,绝不自动收藏。preview 响应新增 additive `favoritePreview` 字段。
21
+ - **明确局限**:标了不压缩 = 只留一个序号;归档会话能标,但 0.5.3 不读会话档案拿原文(归 0.6)。
22
+
23
+ ### 推荐收藏:打分 → 点亮建议(5ad9b3c / 30709a9)
24
+
25
+ - `dsh/favorite-scorer.js`:flash 档、关思考、temperature 0,只回 JSON 分数;失败静默降级(不亮)。
26
+ - `dsh/favorite-watch.js`:监听 `assistant/message`,去抖 1.2s **只判每轮最后一条**;必亮词/屏蔽词
27
+ 先判(屏蔽 > 必亮 > 分数);已收藏的不再推荐;**绝不自动收藏**。
28
+ - `readState.favoriteSuggest`(≤3 条:seq/messageId/分数/原因)→ 星标金色光晕(只点亮)。
29
+ - 设置项:推荐收藏开关、三档阈值(宽松 60 / 标准 75 / 严格 85,默认标准)、必亮词、屏蔽词、
30
+ 「试算」(真实打分、只读,不改任何状态)。
31
+ - 红线测试:不点星时收藏文件不新增记录。
32
+
33
+ ### P2 语义修正(0.5.2 真机验证发现)
34
+
35
+ - **P2-1 幂等顺序**:`force-archive` 的 already 判断移到 `confirmStop` 之前 —— 已归档的会话不带
36
+ 二次确认也回 200 `already`(以前直接 400),`stopActivity` 明确回报 `done/skipped/failed`。
37
+ - **P2-2 错误码分档**:`actionError` 带 `statusCode`;未知会话 404、宿主缺归档接缝 503、
38
+ 内部失败 500(以前一律 400)。HTTP 只做映射,不重猜。
39
+ - **P3-a 响应瘦身**:`force-archive` / `recover` 不再回全量 `archivedSessionIds`(142 条 ≈ 7KB,
40
+ 客户端不读),改回 `archivedCount` + `newlyArchived` / `newlyRecovered`。
41
+ - **already 防漏杀加固**:已归档 + 用户显式确认时补跑一次 `stopActivity`(幂等),并在 note 里写明
42
+ 「本次已补停 / 本次未执行 stopActivity」,不假装停干净。
43
+
44
+ ## 0.5.2(2026-10-28)
45
+
46
+ > 从 0.5.1 发布点 `e3c5ab3` 拉出的补丁批次(分支 `fix/0.5.2`),npm / GitHub 已发布。
47
+ > 按 `dcr-架构梳理与重构方案-2026-10-26.md` 的 0.5.x 窗口推进:**S1 契约快照 → S2 record 构造器 →
48
+ > 批 C 强制归档入口 + 文档口径修复**。测试 **347 → 375 PASS**,bundle 重建且 `--check` 通过。
49
+ >
50
+ > **契约变化(全部 additive)**:`readState` 增 `parentPinned`(旧会话是否在侧栏置顶)与
51
+ > `capabilities.forceArchive`(宿主有没有 `archiveSession`);`/api/compact-relay-action` 增
52
+ > `action:'force-archive'`(必须显式 `confirmStop:true`,旧会话置顶时还需 `confirmUnpin:true`)。
53
+ > 自动归档的行为一个字节不改:**默认不强制、不 stopActivity**。
54
+
55
+ ### 缓存对齐:侧调用沿用主请求的推理档位(2026-10-27 P1 取证修复)
56
+
57
+ - **根因(已实验定位)**:摘要/交接单调用的前缀回放逐字节正确(LCP 15 条 vs 主请求 14/14,tools
58
+ 与 toolHistory 一致),唯一致命差异是 `reasoningEffort`——旧实现固定传 `'off'`,主循环是 `'max'`;
59
+ 宿主把它翻成 wire 的 `thinking: enabled|disabled`,缓存前缀在**第一条 assistant 消息**的渲染处断开。
60
+ 隔离 A/B(同一组 prompt、真实模型):`off` 命中 **74.0%**(复跑 73.8–74.4),改成主循环同值
61
+ **96.9–98.2%**;生产 prompt 20–50 万 token,于是同样的断点表现为 **2–5%** 命中(≈系统提示词 11K)。
62
+ - **修法**:`prepareModelCall` 改为优先沿用 `session.requestHeader().config.reasoningEffort`
63
+ (仅当 provider/model 与本次目标一致;否则退回旧的「模型支持 off 就 off」兜底)。
64
+ 摘要与交接单共用这条前置,所以两者都吃到缓存;`cacheAlignedControls` 的注释同步纠正。
65
+ - **配套**:`summary.maxTokens` 与 `handoff.maxTokens` 默认 8,192 → **16,384**,兜住重新开启的
66
+ 思考输出,避免 2026-10-19「思考吃光上限 → 截断」旧 bug 回归;截断重试逻辑不变。
67
+ - **测试**:新增「P1 cache 对齐」1 条(header 有档位 → 摘要/交接单同档);更新旧断言(侧调用不再
68
+ 是 off、输出上限 16384);非推理模型用例补真实 header 形状(无 reasoningEffort)后仍走兜底。
69
+ - **生产验证(2026-10-27 16:2x)**:隔离 A/B 两次 96.9%;真机重启后临时会话压缩 38 条/41,149 token,
70
+ 摘要 prompt 114,032(未命中 **368**),命中 **99.7%**(修复前生产 2–5%)。素材在
71
+ `dcr-ctx-audit/forensics/findings.md §7`。
72
+
73
+ ### S1 契约快照(生产行为零变化)
74
+
75
+ - 新增 `test/contract-golden.mjs`:锁 `readState` 帧(6 场景的键集合 + 结构/类型 + 链投影取值)、
76
+ 父/子 relay record(**键顺序** + null/省略语义 + 新链/改名失败/老链三条真实流程)、标题格式,
77
+ 另有负向自检(改键 / 改值 / 改类型 / 换键序都必须被抓到)。
78
+ - 口径修正:record 的**键顺序**是 JSONL 字节契约(顺序敏感,`Object.keys` 深比较);只有 `readState`
79
+ 帧才用「排序后的键集合」。`dcr-架构梳理与重构方案-2026-10-26.md` §S1 已按实际实现同步。
80
+
81
+ ### S2 `core/relay-record.js`:record 形状的唯一构造器(字节级不变)
82
+
83
+ - 新增 `lib/core/relay-record.js`:`relayRecordOfForm({role,...})` + `parentRelayRecordOf` /
84
+ `childRelayRecordOf`。父/子 record 的键顺序、0.3 旧字段双写(`parentSessionId`/`index`)、
85
+ `planId` 只父、`historyTitle` 条件写、`path` 条件写、`titleSeq` 恒写 null 全部收口到一处;
86
+ `actions/handoff.js` 与 `dsh/sessions.js` 只传语义参数。
87
+ - 文档口径:§S2 表与附录 C 的 `atIso` 改为实际口径 —— 父/子**两处都是** `localIso(new Date(at))`
88
+ (原表写成「子 `new Date(at)`、父 `at`」,是评审时的已知漂移)。
89
+
90
+ ### 批 C:强制归档旧会话(0.5.x 计划 P4 / L 项)
91
+
92
+ - **入口**:接力条菜单「强制归档旧会话(会停止后台任务)」——旧会话还没归档时出现;
93
+ 宿主没有 `archiveSession`(`capabilities.forceArchive=false`)时隐藏,不做必然失败的入口。
94
+ - **二次确认**:面板明确写「会停止旧会话上正在跑的任务(turn / 子代理 / 后台任务 / 定时任务),
95
+ 中断后无法恢复」;旧会话在侧栏置顶时补一行「归档会同时取消置顶(宿主行为)」并带
96
+ `confirmUnpin:true`(与交接前的置顶确认同口径)。
97
+ - **服务端**:新增 `lib/actions/force-archive.js`,是生产代码里**唯一**调用
98
+ `workspaceRegistry.archiveSession(id, { stopActivity:true })` 的地方(静态守卫锁死);
99
+ 没有 `confirmStop:true` 一律拒绝;已归档幂等(不再停任务);置顶未确认返回 `pinned`(不静默
100
+ unpin);失败抛可读错误(HTTP 400 带具体原因),不静默、不假装成功。
101
+ - **告知文案**:归档成功 / 此前已归档 / 置顶未归档三套全部沿用 `handoffArchiveNoteOf`
102
+ (含「归档会取消置顶」与「恢复旧会话」入口)。
103
+ - **默认仍不强制**:交接的自动归档照旧不带 `stopActivity`,旧会话有活跃任务时保留可见
104
+ (单测断言 `options.stopActivity === undefined`)。
105
+ - **测试**:新增 `test/force-archive.mjs`(18 条:`stopActivity` 透传 / 缺确认拒绝 / 幂等 /
106
+ 置顶确认 / 失败可读 / HTTP 不要求活 agent / `readState` 能力位 / 默认不强制 / 静态守卫);
107
+ `test/relay-bar.mjs` 增 8 条组件级用例(入口 / 二次确认 / 透传 / 失败 / 置顶 / 服务端回报
108
+ pinned 后重发 / 能力缺失隐藏 / 已归档不再显示)。合计 **347 → 375 PASS**。
109
+
110
+ ### UI 修复:交接预览草稿框被 flex 压成 0 高
111
+
112
+ - **症状**:真机截图确认,交接预览弹窗里九段草稿的只读预览框塌成 14px 一条缝
113
+ (上下边框各 1px + padding 6px×2,内容高度 0),九段草稿完全看不见,只能看到第一行文字被切掉的
114
+ 上半截。
115
+ - **根因**:弹窗外层是 `display:flex; flex-direction:column; maxHeight:min(72vh, 520px); overflow-y:auto`,
116
+ 子项默认 `flex-shrink:1`;草稿预览框是 `overflow:auto` 的滚动容器,自动最小尺寸为 0,
117
+ 浏览器于是把它压到 0 高来满足 maxHeight,弹窗自身反而不出现滚动条。内容一变长(就绪警告 +
118
+ 两个可编辑 textarea)就必然触发。
119
+ - **修法**:内容包裹层加 `flexShrink:0` —— 保持自然高度,超出 maxHeight 时由弹窗自己滚动,
120
+ 不再压扁子项;草稿预览框同样加 `flexShrink:0`(防御性,任何 flex 上下文都不许压它)。
121
+ 两处各带一个测试标记(`data-cr-handoff-body` / `data-cr-handoff-draft`),
122
+ 产物 `lib/client.js` 由 `node scripts/build-client.mjs` 重建,`--check` 通过。
123
+ - **状态**:已随 0.5.2 发布。
124
+
125
+ ### 文档
126
+
127
+ - `README.md` / `TUTORIAL.md` 补 0.5.2 用法(强制归档入口、二次确认、置顶提示、默认不强制)。
128
+ - `dcr-架构梳理与重构方案-2026-10-26.md`:§S1 的 record 顺序口径、§S2 + 附录 C 的 `atIso` 口径修正。
129
+
130
+ ## 0.5.1(2026-10-26 · 已发布)
4
131
 
5
132
  > 分支 `fix/0.5.1`(从 `main` = 0.5.0 拉出,只带 0.5.x 需要的改动;0.6 的 timeline/marks/milestone
6
133
  > 不进来)。**四批功能合并定稿 + 1 条设计修正**:批 1 核心 bug(DCR-01/02/03/04 + D01 + D03)、
package/README.en.md CHANGED
@@ -10,6 +10,9 @@
10
10
  [![DSH](https://img.shields.io/badge/DSH-0.2.x-4d6bfe)](https://www.deepseek.com/harness/)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow)](LICENSE)
12
12
  [![version](https://img.shields.io/badge/version-0.4.1-blue)](CHANGELOG.md)
13
+ [![XERJ Corpus Hub contributor](https://hub.xerj.org/contributors/cfanzx/badge.svg)](https://hub.xerj.org/contributors/cfanzx)
14
+
15
+ Indexed in the [XERJ Corpus Hub](https://hub.xerj.org/corpus/cfanzx--dsh-compact-relay) · pinned at `435601b` · MIT · with attribution
13
16
 
14
17
  </div>
15
18
 
package/README.md CHANGED
@@ -10,6 +10,9 @@
10
10
  [![DSH](https://img.shields.io/badge/DSH-0.2.x-4d6bfe)](https://www.deepseek.com/harness/)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow)](LICENSE)
12
12
  [![version](https://img.shields.io/badge/version-0.5.0-blue)](CHANGELOG.md)
13
+ [![XERJ Corpus Hub contributor](https://hub.xerj.org/contributors/cfanzx/badge.svg)](https://hub.xerj.org/contributors/cfanzx)
14
+
15
+ 收录于 [XERJ Corpus Hub](https://hub.xerj.org/corpus/cfanzx--dsh-compact-relay) · 钉住 commit `435601b` · MIT · 带署名
13
16
 
14
17
  </div>
15
18
 
@@ -61,9 +64,27 @@
61
64
  - **pin 失效标注**:被压缩(尤其 DSH 原生自动压缩)带走的锁定消息不再凭空消失——悬停卡与菜单标「已失效 N 条(随原生压缩移除)」;原生压缩**即将**覆盖锁定消息时会提前提示「先交接(/handoff)或解锁」。
62
65
  - **置顶确认**:交接后归档旧会话会取消它的侧栏置顶(宿主行为)。旧会话置顶时,交接预览先问一次:点「交接并归档(取消置顶)」才归档,点「只交接,保留置顶」则旧会话和置顶都留着。
63
66
  - **归档开关**:交接预览里可以勾掉「交接后自动归档旧会话」(默认开);首次引导里也写了这个选项。
67
+ - **强制归档(0.5.2)**:旧会话还有任务在跑时,自动归档会「保留可见、不停止任务」。确实要清理时,接力条菜单里有「强制归档旧会话(会停止后台任务)」:点开还会再确认一次,确认面板明确写「会停止旧会话上正在跑的任务(turn / 子代理 / 后台任务 / 定时任务),中断后无法恢复」;旧会话在侧栏置顶时会补「归档会同时取消置顶(宿主行为)」。**默认不会自动做这一步**,只有你在确认面板里点「强制归档(停止任务)」才会执行。
64
68
 
65
69
  > 老链(0.5.0 之前生成的会话)没有交接单路径记录,界面显示「暂无交接单」而不是猜一个——它会先向父会话只读回查一次。
66
70
 
71
+ ### 6. 收藏与推荐收藏(0.5.3)
72
+
73
+ **收藏 = 给「这条以后可能想回看原话」留个预约**,不是把正文存下来:
74
+
75
+ - **两步走**:点星(或命令)只写一行标记(项目级 JSONL,按会话链根存);**原文只在压缩时落档**——
76
+ 压缩后摘要里出现「收藏(本段)」目录,原文另存 `~/.dsh/dcr-favorites/snapshots/` 快照。
77
+ 所以「标了不压缩」= 只留一个序号,这是设计而非 bug。
78
+ - **入口**:助手消息底部星标;`/favorite mark [seq]` 可标任意已完成回复(不带 seq 标最后一条)。
79
+ **不需要会话开着**:归档/关闭的会话也能按 seq 标记(0.5.3 起标记层不依赖活 agent)。
80
+ - **推荐收藏(半自动)**:每条助手回复落地后,小模型异步打分(0-100);够档位的星标会**金色微亮**
81
+ 提示「值得收藏」。三档:宽松 60 / 标准 75 / 严格 85(默认标准),另有必亮词与屏蔽词。
82
+ **只点亮、绝不自动收藏**——确认仍要你点星或 `/favorite mark`。设置页可调档位/词表,并支持「试算」。
83
+ - **压缩预览卡补收藏**:点「压缩旧会话」后的确认卡会显示「本段 N 条 · 已收藏 M 条 · 建议补 K 条」,
84
+ 列出本段已收藏条目(含你的发言)和"建议先收藏"的关键候选,每条可一键收藏——这是压缩前最后的补救机会。
85
+ - **压缩后再补标**:若原文还在会话日志里,标记时当场写快照;读不到会明确报错,不假装成功。
86
+ (从归档会话档案里读原文归 0.6。)
87
+
67
88
  ## 指示环图例说明
68
89
 
69
90
  | 指示环视觉元素 | 状态含义 |
@@ -113,7 +134,8 @@ dsh plugin --profile desktop add github:cfanzx/dsh-compact-relay
113
134
  - **压缩提示**:配置最小压缩线、压缩提醒线与交接提醒线,控制告警触发时机。
114
135
  - **压缩行为**:配置摘要保留份数上限、近期原文保留 Token 数及交接文档存储目录。
115
136
  - **DSH 原生自动压缩**:读取宿主阈值并换算实际触发线,支持开启或关闭 DSH 原生自动压缩(修改 profile 配置前自动生成备份,重启后生效)。
116
- - **颜色与外观**:配置五项基准色,系统将根据浅色/深色主题自动派生适配色阶。
137
+ - **推荐收藏**:推荐开关、三档阈值(宽松 60 / 标准 75 / 严格 85)、必亮词、屏蔽词,以及「试算」(粘一段原文,按当前设置跑一次真实打分,只读)。
138
+ - **颜色与外观**:配置五项基准色(含收藏星标点亮色),系统将根据浅色/深色主题自动派生适配色阶。
117
139
  - **帮助**:提供指示环图例、使用说明、问题反馈入口、「再看一次初次引导」按钮,以及「诊断信息」(只在本机生成、可预览/复制,见下节)。
118
140
  - **关于与危险操作**:查看版本号、配置文件路径,支持恢复出厂默认设置。
119
141
 
@@ -151,7 +173,11 @@ dsh plugin --profile desktop add github:cfanzx/dsh-compact-relay
151
173
  | `/bc` | 预览压缩:显示目标范围、消息条数及预计释放容量 |
152
174
  | `/bc confirm` | 执行已预览的压缩操作 |
153
175
  | `/handoff` | 生成九段式交接文档并创建、切换至新会话 |
154
- | `/pin` / `/unpin` | 锁定或解锁指定消息,锁定后跳过压缩 |
176
+ | `/favorite` / `/favorite list` | 当前会话收藏目录(编号 + 角色 + 标题) |
177
+ | `/favorite mark [seq]` | 收藏一条(不带 seq 标最后一条已完成回复;不需要会话开着) |
178
+ | `/favorite <序号> [read]` | 报体积,或输出收藏原文 |
179
+ | `/favorite rm <序号>` / `/favorite suggest` | 取消收藏 / 列出推荐收藏建议 |
180
+ | `/pin` / `/unpin` | 锁定或解锁指定消息,锁定后跳过压缩(0.5.3 起 pin 退役为收藏) |
155
181
  | `/bctest` | 小范围空跑测试(仅供开发调试) |
156
182
 
157
183
  ## 功能边界与限制
@@ -170,7 +196,7 @@ dsh plugin --profile desktop add github:cfanzx/dsh-compact-relay
170
196
 
171
197
  ## 版本状态与开源许可
172
198
 
173
- - **当前版本**:**0.5.0**(已发布);**0.5.1** 收尾补丁在 `fix/0.5.1` 分支待验收(未发布)。版本线:0.3.1 首个公开版本;0.3.2 修设置页链接与联系方式;0.3.3 Windows 适配;0.3.4 修判断机制并补交接单渲染;0.3.5 修无活 agent 时压缩按钮误禁;0.3.6 修原生压缩线解析作用域(BUG-012);**0.4.0** 判断机制加强(cue 文案 v2 + 注入门控 + 计量)、诊断包、压缩预览建议锁定、pin 保留契约、轻账本累计、交接预览两段可编辑、接力链元数据、原生自动压缩真实触发线;**0.4.1** 统一原生压缩线显示口径;**0.4.2** 交接线保命补丁(硬上限 = 实际触发线 −5pp、三处告警 + 一键重算);**0.5.0** 无痕交接(命名反转「历史 N」+ 新会话继承原名、输入框上方接力条、交接后自动归档旧会话 + 恢复入口);**0.5.1** 收尾(祖先会话交接后接力条消失、0.5.0 前老链无条、归档入口死路与文案;pin 失效标注、原生压缩提前保护、置顶确认、归档开关、完整交接链、父→子入口;完整链在祖先未加载时只显示 1 层——改为服务端只读持久化日志一次读全,老宿主保留「打开并继续」兜底;设计定稿环只表达容量,pin 位置标记归 0.6 时间线)。已在 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)。
199
+ - **当前版本**:**0.5.3**(本次发布)。版本线:0.3.1 首个公开版本;0.3.2 修设置页链接与联系方式;0.3.3 Windows 适配;0.3.4 修判断机制并补交接单渲染;0.3.5 修无活 agent 时压缩按钮误禁;0.3.6 修原生压缩线解析作用域(BUG-012);**0.4.0** 判断机制加强(cue 文案 v2 + 注入门控 + 计量)、诊断包、压缩预览建议锁定、pin 保留契约、轻账本累计、交接预览两段可编辑、接力链元数据、原生自动压缩真实触发线;**0.4.1** 统一原生压缩线显示口径;**0.4.2** 交接线保命补丁(硬上限 = 实际触发线 −5pp、三处告警 + 一键重算);**0.5.0** 无痕交接(命名反转「历史 N」+ 新会话继承原名、输入框上方接力条、交接后自动归档旧会话 + 恢复入口);**0.5.1** 收尾(祖先会话交接后接力条消失、0.5.0 前老链无条、归档入口死路与文案;pin 失效标注、原生压缩提前保护、置顶确认、归档开关、完整交接链、父→子入口;完整链在祖先未加载时只显示 1 层——改为服务端只读持久化日志一次读全,老宿主保留「打开并继续」兜底;设计定稿环只表达容量,pin 位置标记归 0.6 时间线);**0.5.2** 契约快照 golden、record 形状唯一构造器、强制归档旧会话入口 + 二次确认/置顶确认、摘要缓存对齐(对话模型沿用主循环推理档位,生产命中 2–5% → 99.7%);**0.5.3** 收藏(pin 退役;标记/落档两步解耦、无活会话可标、压缩后补标写快照)、推荐收藏(模型打分 + 三档阈值 + 必亮/屏蔽词 + 设置页试算;只点亮不自动收藏)、P2 语义修正(already 顺序、错误码 404/503/500、归档响应瘦身、防漏杀加固)。已在 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)。
174
200
  - **开源许可**:采用 [MIT](LICENSE) 许可证,欢迎通过 [GitHub Issues](https://github.com/cfanzx/dsh-compact-relay/issues) 与 Pull Requests 提交反馈。
175
201
 
176
202
  ## 参考与致谢
package/TUTORIAL.md CHANGED
@@ -46,6 +46,7 @@
46
46
  | 去新会话 | 父会话侧才有:切回这次交接出来的新会话(它被归档过会先自动恢复) |
47
47
  | 回到原会话 | 切回上一个会话;已归档时先「取消归档」再切,不会撞墙 |
48
48
  | 恢复旧会话 | 旧会话被自动归档时把它恢复出来(侧栏「全部对话(显示已归档)」里也有) |
49
+ | 强制归档旧会话 | 旧会话还有任务在跑也能收起(0.5.2):点开先二次确认,面板写明「会停止旧会话上正在跑的任务(turn / 子代理 / 后台任务 / 定时任务),中断后无法恢复」;确认后才停止任务并归档。**默认不会自动做**,旧会话置顶时还会说明「归档会同时取消置顶」 |
49
50
  | 完整交接链 | 点开才读取,一次列出「历史 3 / 历史 2 / 历史 1」(祖先没打开也能读);每行可开那一跳的交接单、点整行跳过去(最多 8 层;某层未加载时给「打开并继续」) |
50
51
  | 路线图 | 跨会话时间线的占位,0.6 之后 |
51
52
 
@@ -54,6 +55,7 @@
54
55
  - 一条会话既接过别人、又交出去过(中段会话),菜单里会同时有「回到原会话」和「去新会话」。
55
56
  - 祖先会话交接后接力条**不会**消失(0.5.1 修复);0.5.0 之前生成的老链也会补上接力条。
56
57
  - 交接预览里有一个勾选框「交接后自动归档旧会话」(默认开)。**旧会话在侧栏置顶时**,归档会取消置顶,所以预览会先问你:点「交接并归档(取消置顶)」才归档,点「只交接,保留置顶」就都留着。
58
+ - 旧会话还有任务在跑时,自动归档会**保留可见、不停止任务**(不静默杀后台任务)。确实要清理时,用接力条菜单的「强制归档旧会话(会停止后台任务)」;它一定会先弹二次确认,确认面板会写清「会停止旧会话上正在跑的任务」,**只有你点了确认才会真的停任务**——别把这个入口当成默认行为。
57
59
  - 锁定消息被压缩带走后不会凭空消失:悬停卡与菜单会标「已失效 N 条(随原生压缩移除)」;如果 DSH 原生压缩**即将**覆盖锁定消息,插件会提前提示「先交接(/handoff)或解锁」。
58
60
 
59
61
  ## 4. 设置在哪
@@ -19,6 +19,9 @@ 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 { suggestPinCandidates } from '../core/pin-suggest.js';
22
+ import { roleLabelOf } from '../core/pin-anchors.js';
23
+ import { DIRECTORY_MAX, buildFavoriteTitle, favoriteSeqsOf } from '../core/favorites.js';
24
+ import { favoriteMapOf } from '../dsh/favorites-file.js';
22
25
  import { SUMMARY_INSTRUCTION } from '../core/prompts.js';
23
26
  import { sumShadowedTokens, tokenOf } from '../core/tokens.js';
24
27
  import { selectCompactableRange } from '../dsh/select-range.js';
@@ -223,14 +226,30 @@ export async function previewBetterCompact(deps, { agent } = {}) {
223
226
  }
224
227
 
225
228
  const shadowedTokenCount = sumShadowedTokens(nodes, range.shadowedSeqs);
226
- // 0.4 P1-1:对「本次将被压掉的历史」做本地启发式,指出值得先 /pin 的消息。
227
- // 只提示、绝不自动锁:这里不写任何 pin 记录,锁定动作仍由用户 /pin 触发。
229
+ // 0.4 P1-1 / 0.6:对「本次将被压掉的历史」做本地启发式,挑出值得先收藏的消息。
230
+ // 只提示、绝不自动收藏:这里不写任何收藏记录,收藏动作仍由用户点星/点卡内按钮触发。
231
+ // (字段名沿用 pinSuggest 只为不扩散契约;语义已从「建议锁定」转成「建议收藏」。)
228
232
  const pinSuggestItems = suggestPinCandidates(
229
233
  range.shadowedSeqs.map((seq) => (typeof agent.session?.eventAt === 'function' ? agent.session.eventAt(seq) : undefined)),
230
234
  );
231
235
  // 0.4 P1-2:pin 保留契约命中数——inRange = 本次候选范围里被锁定保护而跳过的条数;
232
236
  // total = 当前会话仍活着的锁定总数(范围外也可能有锁定)。
233
237
  const pinHits = { inRange: range.pinnedCut === true ? range.pinnedKept : 0, total: pinned.length };
238
+ // 0.6 收藏提醒:压缩后被压掉的消息就点不了星了,预览时先把「本段已收藏多少条」摆出来。
239
+ const favoriteSeqs = new Set(favoriteSeqsOf(favoriteMapOf(deps.ctx, agent.session), agent.session.id));
240
+ // 0.6 预览卡收藏区:本段已收藏的「条目」(不只是个数)——角色复用 pin-anchors 的
241
+ // roleLabelOf,标题复用 favorites 的 buildFavoriteTitle(与 /favorite list 同一口径)。
242
+ // 只读:这里不写任何收藏记录;列表条数按 DIRECTORY_MAX 截断(favoritedCount 仍是全量数)。
243
+ const favoriteItems = range.shadowedSeqs
244
+ .filter((seq) => favoriteSeqs.has(seq))
245
+ .map((seq) => {
246
+ const event = typeof agent.session?.eventAt === 'function' ? agent.session.eventAt(seq) : undefined;
247
+ return { seq, role: roleLabelOf(event?.type), title: buildFavoriteTitle(event) };
248
+ });
249
+ const favoriteInRange = favoriteItems.length;
250
+ // 「建议补收藏」= 同一套 pinSuggest 识别,只去掉本段里已经收藏过的 seq(语义从
251
+ // 「建议锁定」改成「建议收藏」,不新写启发式)。仍是只提示:收藏动作由用户点按钮触发。
252
+ const favoriteSuggestItems = pinSuggestItems.filter((item) => !favoriteSeqs.has(item.seq));
234
253
  rememberPreviewPlan(agent.id, {
235
254
  planId,
236
255
  startSeq: range.startSeq,
@@ -254,14 +273,29 @@ export async function previewBetterCompact(deps, { agent } = {}) {
254
273
  pinSuggest: { count: pinSuggestItems.length, items: pinSuggestItems },
255
274
  // 0.4 P1-2:pin 命中数(范围内/范围外分开说明)。
256
275
  pinHits,
276
+ // 0.6:本段收藏命中数(inRange/total)——压缩后无法再收藏,提醒用户先点。
277
+ favoriteHits: { inRange: favoriteInRange, total: range.shadowedSeqs.length },
278
+ // 0.6+ 预览卡收藏区(additive,不改上面任何既有字段):客户端只渲染,压缩事务不消费。
279
+ // total = 本段条数;favoritedCount = 本段已收藏全量条数(favorited 最多列 DIRECTORY_MAX 条);
280
+ // favorited = [{seq, role, title}];suggest = 本段未收藏、值得先收藏的候选(seq/role/reason/snippet)。
281
+ // 红线:只提示,绝不自动收藏——本函数不写收藏记录、不调动作层。
282
+ favoritePreview: {
283
+ total: range.shadowedSeqs.length,
284
+ favoritedCount: favoriteInRange,
285
+ favorited: favoriteItems.slice(0, DIRECTORY_MAX),
286
+ suggest: favoriteSuggestItems,
287
+ },
257
288
  ...(range.pinnedCut === true ? { pinnedKept: range.pinnedKept } : {}),
258
289
  },
259
290
  message:
260
291
  `压缩预估:将把 ${range.shadowedSeqs.length} 条旧会话压成 1 条摘要` +
261
292
  `(腾出约 ${fmtTokens(shadowedTokenCount)} tokens${range.pinnedCut === true ? `,已跳过 ${range.pinnedKept} 条锁定的消息` : ''})。` +
262
- (pinSuggestItems.length > 0
263
- ? `建议先 /pin 再压:这 ${pinSuggestItems.length} 条看起来是关键决策/报错堆栈/明确要求别改(只提示,不会自动锁定)。`
293
+ (favoriteSuggestItems.length > 0
294
+ ? `建议先点星收藏:这 ${favoriteSuggestItems.length} 条还没收藏,看起来是关键决策/报错堆栈/明确要求别改(只提示,不会自动收藏)。`
264
295
  : '') +
296
+ (favoriteInRange >= range.shadowedSeqs.length
297
+ ? `本段 ${range.shadowedSeqs.length} 条已全部收藏(压缩会把原文留档)。`
298
+ : `本段 ${range.shadowedSeqs.length} 条里已收藏 ${favoriteInRange} 条;压缩后被压掉的消息就点不了星了,重要的先点一下收藏。`) +
265
299
  (tooSmall
266
300
  ? `这段偏小,压了可能反而更大;建议再聊几轮或把「保留近期原文」调小后再压缩。`
267
301
  : '只估算范围和容量,摘要内容确认后才生成。') +
@@ -19,6 +19,10 @@ import { replaceRangeWithSummary } from '../dsh/compact-transaction.js';
19
19
  import { errorCodeOf } from '../dsh/diag-counters.js';
20
20
  import { runIdle } from '../dsh/idle.js';
21
21
  import { summarize } from '../dsh/summarizer.js';
22
+ import { appendFavoriteDirectory, buildFavoriteDirectoryLines, buildFavoriteTitle } from '../core/favorites.js';
23
+ import { appendSnapshotEntries, favoriteMapOf, snapshotFileOf } from '../dsh/favorites-file.js';
24
+ import { eventTextOf } from './favorite.js';
25
+ import { roleLabelOf } from '../core/pin-anchors.js';
22
26
  import { combinedSignal } from './support.js';
23
27
 
24
28
  /**
@@ -72,17 +76,17 @@ async function runCompactionProcess(
72
76
  ? (message) => deps.ctx.tokenMeter.estimateMessage(message)
73
77
  : undefined;
74
78
 
75
- return runIdle(agent, (idleSignal) => {
79
+ return runIdle(agent, async (idleSignal) => {
76
80
  const combined = combinedSignal(signal, idleSignal);
77
- return replaceRangeWithSummary({
81
+ const result = await replaceRangeWithSummary({
78
82
  session,
79
83
  range,
80
84
  engine: deps.engine,
81
85
  signal: combined,
82
86
  sourceCommandId: commandId,
83
87
  estimateMessage,
84
- summarize: ({ shadowedSeqs, signal: innerSignal }) =>
85
- summarize({
88
+ summarize: async ({ shadowedSeqs, signal: innerSignal }) => {
89
+ const result = await summarize({
86
90
  ctx: deps.ctx,
87
91
  engine: deps.engine,
88
92
  agent,
@@ -95,8 +99,53 @@ async function runCompactionProcess(
95
99
  // 超时由调用方传:正式精压传 cfg.timeouts.llmMs(原行为),/bctest 不传 → 180s 默认。
96
100
  timeoutMs,
97
101
  signal: innerSignal,
98
- }),
102
+ });
103
+ // 0.6:摘要后追「本段收藏目录」——纯文本后处理,不调模型;失败只丢目录,不阻断压缩。
104
+ try {
105
+ const wanted = new Set(shadowedSeqs);
106
+ const seqs = [...(favoriteMapOf(deps.ctx, session).get(session.id) ?? [])].sort((a, b) => a - b);
107
+ const entries = [];
108
+ for (const seq of seqs) {
109
+ if (!wanted.has(seq)) continue;
110
+ const event = typeof session.eventAt === 'function' ? session.eventAt(seq) : undefined;
111
+ entries.push({ index: entries.length + 1, role: roleLabelOf(event?.type), title: buildFavoriteTitle(event) });
112
+ }
113
+ return { ...result, summary: appendFavoriteDirectory(result.summary, buildFavoriteDirectoryLines(entries, 20)) };
114
+ } catch {
115
+ return result;
116
+ }
117
+ },
99
118
  flush,
100
119
  });
120
+ // 0.6:把本段被压掉、且被收藏的消息原文落到压缩快照。
121
+ // 失败只 warn,不影响压缩结果(与「目录注入」同一条红线)。
122
+ writeFavoriteSnapshots(deps, session, range);
123
+ return result;
101
124
  });
102
125
  }
126
+
127
+ /**
128
+ * 压缩成功后:把「本段被压掉且被收藏」的消息原文追加进快照(按 seq 去重)。
129
+ * @param {{ctx:object, log?:object}} deps
130
+ * @param {object} session
131
+ * @param {{shadowedSeqs:number[]}} range
132
+ */
133
+ function writeFavoriteSnapshots(deps, session, range) {
134
+ try {
135
+ const set = favoriteMapOf(deps.ctx, session).get(session.id);
136
+ if (set === undefined || set.size === 0) return;
137
+ const shadowed = new Set(Array.isArray(range.shadowedSeqs) ? range.shadowedSeqs : []);
138
+ const entries = [];
139
+ for (const seq of set) {
140
+ if (!shadowed.has(seq)) continue;
141
+ const event = typeof session.eventAt === 'function' ? session.eventAt(seq) : undefined;
142
+ if (event === undefined || event === null) continue;
143
+ const text = eventTextOf(event);
144
+ if (text.length === 0) continue;
145
+ entries.push({ seq, role: roleLabelOf(event.type), title: buildFavoriteTitle(event), text, at: event.time });
146
+ }
147
+ appendSnapshotEntries(snapshotFileOf(deps.ctx, session), entries);
148
+ } catch (error) {
149
+ deps.log?.warn?.(`compact-relay: 收藏快照写入失败:${error instanceof Error ? error.message : String(error)}`);
150
+ }
151
+ }