dsh-compact-relay 0.5.2 → 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,5 +1,46 @@
1
1
  # Changelog
2
2
 
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
+
3
44
  ## 0.5.2(2026-10-28)
4
45
 
5
46
  > 从 0.5.1 发布点 `e3c5ab3` 拉出的补丁批次(分支 `fix/0.5.2`),npm / GitHub 已发布。
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
 
@@ -65,6 +68,23 @@
65
68
 
66
69
  > 老链(0.5.0 之前生成的会话)没有交接单路径记录,界面显示「暂无交接单」而不是猜一个——它会先向父会话只读回查一次。
67
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
+
68
88
  ## 指示环图例说明
69
89
 
70
90
  | 指示环视觉元素 | 状态含义 |
@@ -114,7 +134,8 @@ dsh plugin --profile desktop add github:cfanzx/dsh-compact-relay
114
134
  - **压缩提示**:配置最小压缩线、压缩提醒线与交接提醒线,控制告警触发时机。
115
135
  - **压缩行为**:配置摘要保留份数上限、近期原文保留 Token 数及交接文档存储目录。
116
136
  - **DSH 原生自动压缩**:读取宿主阈值并换算实际触发线,支持开启或关闭 DSH 原生自动压缩(修改 profile 配置前自动生成备份,重启后生效)。
117
- - **颜色与外观**:配置五项基准色,系统将根据浅色/深色主题自动派生适配色阶。
137
+ - **推荐收藏**:推荐开关、三档阈值(宽松 60 / 标准 75 / 严格 85)、必亮词、屏蔽词,以及「试算」(粘一段原文,按当前设置跑一次真实打分,只读)。
138
+ - **颜色与外观**:配置五项基准色(含收藏星标点亮色),系统将根据浅色/深色主题自动派生适配色阶。
118
139
  - **帮助**:提供指示环图例、使用说明、问题反馈入口、「再看一次初次引导」按钮,以及「诊断信息」(只在本机生成、可预览/复制,见下节)。
119
140
  - **关于与危险操作**:查看版本号、配置文件路径,支持恢复出厂默认设置。
120
141
 
@@ -152,7 +173,11 @@ dsh plugin --profile desktop add github:cfanzx/dsh-compact-relay
152
173
  | `/bc` | 预览压缩:显示目标范围、消息条数及预计释放容量 |
153
174
  | `/bc confirm` | 执行已预览的压缩操作 |
154
175
  | `/handoff` | 生成九段式交接文档并创建、切换至新会话 |
155
- | `/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 退役为收藏) |
156
181
  | `/bctest` | 小范围空跑测试(仅供开发调试) |
157
182
 
158
183
  ## 功能边界与限制
@@ -171,7 +196,7 @@ dsh plugin --profile desktop add github:cfanzx/dsh-compact-relay
171
196
 
172
197
  ## 版本状态与开源许可
173
198
 
174
- - **当前版本**:**0.5.2**(已发布)。版本线: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%)。已在 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)。
175
200
  - **开源许可**:采用 [MIT](LICENSE) 许可证,欢迎通过 [GitHub Issues](https://github.com/cfanzx/dsh-compact-relay/issues) 与 Pull Requests 提交反馈。
176
201
 
177
202
  ## 参考与致谢
@@ -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
+ }