dsh-compact-relay 0.4.2 → 0.5.1
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 +351 -0
- package/README.en.md +4 -3
- package/README.md +22 -5
- package/ROADMAP.md +44 -2
- package/TUTORIAL.md +29 -5
- package/lib/actions/handoff.js +72 -12
- package/lib/actions/pin.js +7 -6
- package/lib/actions/recover.js +49 -0
- package/lib/actions/relay-chain.js +17 -0
- package/lib/actions/state.js +137 -8
- package/lib/adapters/http.js +65 -5
- package/lib/client.js +1946 -218
- package/lib/core/clip.js +92 -0
- package/lib/core/errors.js +3 -1
- package/lib/core/native-line.js +27 -0
- package/lib/core/native-pin-risk.js +106 -0
- package/lib/core/pin-anchors.js +3 -1
- package/lib/core/pin-health.js +112 -0
- package/lib/core/relay.js +83 -6
- package/lib/dsh/compaction-log.js +16 -9
- package/lib/dsh/compaction-pair.js +27 -1
- package/lib/dsh/diag-counters.js +4 -1
- package/lib/dsh/diagnostics.js +3 -1
- package/lib/dsh/handoff-archive.js +225 -0
- package/lib/dsh/native-compaction.js +60 -4
- package/lib/dsh/relay-chain.js +238 -0
- package/lib/dsh/relay.js +147 -5
- package/lib/dsh/session-view.js +16 -10
- package/lib/dsh/sessions.js +206 -15
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,356 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1(2026-10-26 · 未发布,待 Lead 验收)
|
|
4
|
+
|
|
5
|
+
> 分支 `fix/0.5.1`(从 `main` = 0.5.0 拉出,只带 0.5.x 需要的改动;0.6 的 timeline/marks/milestone
|
|
6
|
+
> 不进来)。**四批功能合并定稿 + 1 条设计修正**:批 1 核心 bug(DCR-01/02/03/04 + D01 + D03)、
|
|
7
|
+
> 批 2 交互(pin 失效标注 / 原生压缩保护 / 置顶保护 / 归档开关)、批 3 功能补全(完整交接链 /
|
|
8
|
+
> 父→子入口)、批 4 完整链在祖先未加载时只显示 1 层(DCR-05)、**设计定稿:环只表达容量
|
|
9
|
+
> (pin 位置标记归 0.6 时间线)**。
|
|
10
|
+
> 测试 **257 → 340 PASS**,bundle 重建且 `--check` 通过。
|
|
11
|
+
>
|
|
12
|
+
> **契约变化(全部 additive,老客户端忽略新字段即可)**:`readState` 增 `chain`(入站投影)/
|
|
13
|
+
> `next`(出站投影)/ `nextArchived` / `parentArchived` / `capabilities.unarchive` /
|
|
14
|
+
> `sessionPinned` / `pins.stale` / `pins.atRisk`;`handoff` 增 `archive` / `confirmUnpin` 两个
|
|
15
|
+
> 入参(默认值保持 0.5.0 行为);`relay` 旧字段一个字节不改。
|
|
16
|
+
>
|
|
17
|
+
> **降级说明**:宿主缺 `workspaceRegistry`(归档/置顶/`unarchive` 任一)时对应能力一律降级为
|
|
18
|
+
> 「不知道 / 不归档 / 隐藏入口」并只 warn,不阻断交接;老插件(0.5.0)没有 `next` 字段时父→子
|
|
19
|
+
> 入口不显示;原生压缩预判依赖宿主 `retainRatio` 默认值,文案统一写「预计」,实际损失由
|
|
20
|
+
> `pins.stale` 兜底。
|
|
21
|
+
|
|
22
|
+
### 批 1:核心 bug 修复(DCR-01 / DCR-02 / DCR-03 / DCR-04 / D01 / D03;测试 257 → 278)
|
|
23
|
+
|
|
24
|
+
#### DCR-01(P1)祖先会话再次交接后接力条消失
|
|
25
|
+
|
|
26
|
+
- `dsh/relay.js` 新增 `relayInboundRecordOf` / `relayOutboundRecordOf`(按 `childId` 与 `session.id` 的关系分开找);`relayRecordOf` 语义**不变**(仍是最近一条记录,`handoffTitlePlanOf` 的链号递增依赖它)。
|
|
27
|
+
- `actions/state.js`:`chain` 改为**本会话入站记录**(`childId === session.id`)的只读投影;旧字段 `relay` 一个字节不改;`parentArchived` 用入站 `parentId` 查归档集合(出站记录里的父会话是本会话自己,会查错);新增 `next`(最近一条出站记录的投影,只加不改,供后续父→子入口与「子会话向父会话补交接单 path」)。
|
|
28
|
+
- 客户端 `client/35-relay-bar.js` 的入站判定(`chain.childId === sessionId`)保持不变;`loadParentPath` 改用父会话状态里的 `next`(老插件没有 `next` → 回落 `chain`)。
|
|
29
|
+
- 回归:`test/relay-chain.mjs`(中段会话:入站在前、出站在后)、`test/state-passive.mjs`(只有出站记录的会话 + 入站/出站读取器单测)。
|
|
30
|
+
|
|
31
|
+
#### DCR-02(P1)0.5.0 之前的老链永远没有接力条
|
|
32
|
+
|
|
33
|
+
- `dsh/relay.js` 新增 `legacyRelayChainOf({session, title})`:没有入站记录时,若 `header.parentSession` 存在、`header.origin !== 'subagent'`、`header.isSeeded !== true`(宿主语义:`isSeeded` = fork 继承前缀,不是交接),且标题能解析出链号(`splitHandoffTitle(...).handedOff > 0`),合成只读 chain(`ordinal` / `baseTitle` / `scheme` / `childId=self` / `parentId=header.parentSession`,多一个 `synthesized:true`)。**不写日志、不改任何记录**;不带 `path`(老链的交接单路径只写在父会话记录里,客户端 `loadParentPath` 只读回查)。
|
|
34
|
+
- 回归:`test/relay-chain.mjs`(老链合成 / 「历史 N」标题 / 四种不合成:subagent、fork、无 `parentSession`、干净标题 / 有 record 时 record 优先)。
|
|
35
|
+
|
|
36
|
+
#### DCR-03(P2)归档状态不刷新 / `parentArchived=null` 死入口
|
|
37
|
+
|
|
38
|
+
- 客户端 `client/35-relay-bar.js`:
|
|
39
|
+
- 打开接力条菜单时**静默刷新一次 state**(`refreshKey` 触发;只有真的换会话才清链,刷新失败不重排队重试);
|
|
40
|
+
- `parentArchived === null`(宿主没给状态)时,「回到原会话」也先 `unarchive` 再 `open`(unarchive 幂等);
|
|
41
|
+
- 恢复失败会**换通道重试一轮**(宿主 `ctx.uiWorkspace.unarchiveSession` ⇄ 插件 HTTP `recover`;只有一条通道时同通道重试),仍失败才提示;
|
|
42
|
+
- 客户端与服务端都没有恢复能力时**隐藏「恢复旧会话」**。
|
|
43
|
+
- `actions/state.js` 新增 `capabilities.unarchive`(宿主有没有 `workspaceRegistry.unarchiveSession`)。
|
|
44
|
+
- 回归:`test/relay-bar.mjs`(打开菜单刷 state / 未知状态先恢复 / 双通道重试 / 同通道重试 / 无能力隐藏入口 / `next` 补 path)、`test/state-passive.mjs` 与 `test/relay-chain.mjs`(capabilities)。
|
|
45
|
+
|
|
46
|
+
#### DCR-04(P2)归档提示文案指向不存在的视图
|
|
47
|
+
|
|
48
|
+
- `dsh/handoff-archive.js`:`「全部会话」` → 宿主真实视图标签 `「全部对话(显示已归档)」` + 真实动作 `「取消归档」`(实测 `dsh-client-ui-workspace` 的 `viewOptions` 三选一与归档行动作)。
|
|
49
|
+
- 回归:`test/archive.mjs`(新文案存在、不再出现「全部会话」)。
|
|
50
|
+
|
|
51
|
+
#### D01(正确性)压缩「已提交」判定两份不一致
|
|
52
|
+
|
|
53
|
+
- `dsh/compaction-pair.js` 新增唯一判定 `isCommittedCompaction({compactionId, end, replace})`:**没有 `compactionId` 的旧日志也要求 replace**;有 id 时还要求同 id 的 `compaction/end` 存在且没有 `error`。
|
|
54
|
+
- `dsh/session-view.js` 的 `countSummaries` 与 `dsh/compaction-log.js` 的 `collectCompactions().committed` 都改为调用它(原来两份实现,在「无 id 且无 replace」的旧日志上给出 `1` / `false` 两个答案)。
|
|
55
|
+
- **行为变化**:没有 id、也没有 replace 的 summary 不再算「已压缩」(`readState.summaries` 与 advice 阈值会少算这一条)。`test/smoke.mjs` 两处旧期望随之更新。
|
|
56
|
+
- **未随本分支做**:0.6 的 `timeline.summaryCount` 改正向累计 —— 0.5.1 分支没有 `lib/dsh/timeline.js`,随 0.6 分支做。
|
|
57
|
+
- 回归:`test/compact-log.mjs`(countSummaries 与 committed 同口径交叉校验)。
|
|
58
|
+
|
|
59
|
+
#### D03(重复)文本截断 6 处 → 2 个共享函数
|
|
60
|
+
|
|
61
|
+
- 新增 `lib/core/clip.js`:`clipChars`(按码点,emoji 不切半)与 `clipCodeUnits`(按 UTF-16 码元,0.4/0.5 既有行为逐字节兼容);省略号预算显式命名(`'inside'` 默认 = 算进上限 / `'append'` = 多 1 字符 / `false` = 不补)。
|
|
62
|
+
- 0.5.1 迁移 6 处:`core/pin-anchors.js`(60 字摘要)、`dsh/compaction-log.js`(200 字预览,`'append'` 保留「多 1 字符」既有输出)、`dsh/diagnostics.js`、`dsh/diag-counters.js`、`core/errors.js`(140 字原因)、`actions/handoff.js`(120 字警示)。
|
|
63
|
+
- **随 0.6 做**:`core/handoff-facts.js:oneLineNote`、`core/milestone.js:clipText`(0.6 工作树新增的两份逐行相同实现)→ 换 `clipChars`。`core/relay.js:truncateTitleUtf8` 按评审结论**不并进来**(UTF-8 字节预算 = 宿主 `session-title` 契约克隆,0.5.2 S3 再迁 `dsh/title-budget.js`)。
|
|
64
|
+
- 回归:新增 `test/clip.mjs`(码点/码元/append/边界 + 三个迁移点逐字节),已加入 `npm test`。
|
|
65
|
+
|
|
66
|
+
### 批 2:pin 失效标注 + 原生压缩保护 + 归档交互(测试 278 → 311)
|
|
67
|
+
|
|
68
|
+
#### 1. pin 失效标注(C 方案,DCR 批 2-3)
|
|
69
|
+
|
|
70
|
+
- `core/pin-health.js`(新增):`pinHealthOf({pinnedSeqs, surfaceSeqs, log})` 把 pin 快照拆成
|
|
71
|
+
`live`(仍在 surface)与 `stale`(被压缩/历史变化带走),并只读日志归因:
|
|
72
|
+
`compaction/start.turn` 非空 = **native**(原生自动压缩);`turn:null` = **other**(本插件 /bc、
|
|
73
|
+
宿主 /compact);找不到覆盖该 seq 的 replace = **unknown**(撤销/分支)。`staleCauseTextOf`
|
|
74
|
+
统一文案口径。
|
|
75
|
+
- `actions/state.js`:`pins` 增加 additive 字段
|
|
76
|
+
`stale:{count,seqs,native,other,unknown}`;`pins.count/seqs/marks` 仍是 live 集合(0.5.0
|
|
77
|
+
语义不变,老客户端照用)。失效 pin **不画到环上**(不假装还在),但也不消失。
|
|
78
|
+
- 客户端 `client/30-indicator.js`:指示灯悬停卡显示「已失效 N 条(随原生压缩移除)」;菜单的
|
|
79
|
+
锁定项显示「已锁 N 条 · 失效 M」/「解除失效锁定(M 条)」;`data-cr-pins-stale` 取证属性。
|
|
80
|
+
- `actions/pin.js` 的 `togglePin` 改为按**快照**判定:全部 pin 已失效时菜单显示「解除失效锁定」,
|
|
81
|
+
点下去必须清空记录,不能因为 live=0 反而新锁一条(P3-03 的旧结论随菜单文案一起更新)。
|
|
82
|
+
- 回归:`test/pin-health.mjs`(live/stale/三种原因/混合原因 + state 下发)、`test/relay-bar.mjs`
|
|
83
|
+
(指示灯悬停卡文案)。
|
|
84
|
+
|
|
85
|
+
#### 2. 原生压缩吃掉 pin 的提前保护(事前,非事后)
|
|
86
|
+
|
|
87
|
+
- 宿主可用信号:`compaction-basic` 的 `thresholdRatio` / `headroomTokens` / `retainRatio` /
|
|
88
|
+
`retainTokens` 可读;真实触发线(0.4 P2-3)与当前压力每帧都有;选段算法在宿主源码里是
|
|
89
|
+
纯函数(保留尾部 `retainTokens` + 工具配对平衡回退)。
|
|
90
|
+
- `dsh/native-compaction.js`:`parseCompactionOverride` / `parseModelPolicies` /
|
|
91
|
+
`resolveNativePolicy` 增加 `retainRatio` / `retainTokens`(只认 config 直属子键;路由级可覆盖)。
|
|
92
|
+
- `core/native-pin-risk.js`(新增):`nativePinnedRiskOf` 按 compaction-basic 的
|
|
93
|
+
`selectCompactableRange` 同口径复刻选段,返回下一次原生压缩会覆盖的 pinned seq。
|
|
94
|
+
- `actions/state.js`:只在「原生自动压缩开 + 当前压力 ≥ 真实触发线 + 节点定价与 surface 对齐」
|
|
95
|
+
时下发 `pins.atRisk:{count,seqs}`(否则 null,不制造噪音)。
|
|
96
|
+
- 客户端:悬停卡一行「⚠ 原生压缩预计覆盖 N 条锁定消息」+ 菜单交接项提示 + 同一风险签名只弹
|
|
97
|
+
一次 toast,建议「先交接(/handoff)或解锁」。`data-cr-pins-risk` 取证属性。
|
|
98
|
+
- **为什么是事前**:触发线、保留预算、当前压力都是可读的,选段是纯函数;无需等压缩事件。
|
|
99
|
+
**限制**:宿主没有「即将压缩」事件,真正执行在 step boundary,且默认 `retainRatio` 随宿主
|
|
100
|
+
版本(0.2.1-rc.3 = 0.16;用户显式配置优先),所以文案写「预计」。真发生后 `pins.stale`
|
|
101
|
+
仍会兜底标出实际损失。
|
|
102
|
+
- 回归:`test/pin-health.mjs`(宿主选段同口径:保留尾部/配对平衡回退/system head/未到线不报
|
|
103
|
+
+ retainRatio/retainTokens 解析)、`test/relay-bar.mjs`(预警文案与主动提示)。
|
|
104
|
+
|
|
105
|
+
#### 3. 置顶保护(归档会取消置顶)
|
|
106
|
+
|
|
107
|
+
- 宿主实证(`@deepseek-ai/dsh-workspace/lib/index.js`):`WorkspaceRegistry.pinnedSessionIds`
|
|
108
|
+
可读;`archiveSession` 在**同一次 durable write** 里把该 id 从置顶集合移除(置顶与归档互斥)。
|
|
109
|
+
- `dsh/handoff-archive.js`:新增 `pinnedSessionIdsOf` / `isSessionPinned`(读不到 → null,不猜
|
|
110
|
+
false);`handoffArchiveNoteOf` 增加 `skipped` / `pinned` 文案。
|
|
111
|
+
- `actions/handoff.js`:`handoff(..., {archive = true, confirmUnpin = false})`。旧会话在侧栏置顶
|
|
112
|
+
且未 `confirmUnpin:true` 时**不归档**(`status:'pinned'`),旧会话与置顶都保留;旧客户端
|
|
113
|
+
(不传 confirmUnpin)自动降级为不归档,不会静默取消置顶。
|
|
114
|
+
- 客户端交接预览:`sessionPinned === true` 时弹「旧会话已在侧栏置顶:归档会取消置顶」,提供
|
|
115
|
+
「交接并归档(取消置顶)」与「只交接,保留置顶」两个出口;取消 = 关闭预览、什么都不做。
|
|
116
|
+
- `actions/state.js` 新增 `sessionPinned`(true/false/null)。
|
|
117
|
+
- 回归:`test/archive.mjs`(置顶未确认不归档 / 确认才归档 / archive:false 优先 / 读不到置顶
|
|
118
|
+
集合 → null / 文案)、`test/relay-bar.mjs`(两个出口的 HTTP body)。
|
|
119
|
+
|
|
120
|
+
#### 4. 归档开关 + 首次引导
|
|
121
|
+
|
|
122
|
+
- `handoff` 新增可选 `archive`(默认 `true`,0.5.0 行为不变);HTTP
|
|
123
|
+
`/api/compact-relay-action {action:'handoff', archive:false}` = 只交接、不归档旧会话(返回
|
|
124
|
+
`archive.status='skipped'` + 文案);`archive` / `confirmUnpin` 类型错 → 400(动历史前拒绝)。
|
|
125
|
+
- 交接预览新增勾选框「交接后自动归档旧会话(默认开)」;取消勾选后不弹置顶确认、确认按钮变
|
|
126
|
+
「确认交接(不归档)」。
|
|
127
|
+
- 首次引导(onboarding)文案明确写:「交接后会默认自动归档旧会话(侧栏收起,可取消勾选
|
|
128
|
+
『交接后归档』;旧会话在侧栏置顶时会先问你,因为归档会取消置顶)」。
|
|
129
|
+
- 回归:`test/archive.mjs`(archive:false / 默认 true / HTTP 透传与 400)、`test/relay-bar.mjs`
|
|
130
|
+
(勾选状态与请求体)、`test/archive.mjs` 产物守卫(onboarding 文案)。
|
|
131
|
+
|
|
132
|
+
#### 批 2 的宿主限制 / 待真机验证
|
|
133
|
+
|
|
134
|
+
- 原生压缩预判是「同口径复刻 + 估算」,依赖宿主 0.2.1-rc.3 的默认 `retainRatio=0.16`;宿主升级
|
|
135
|
+
改默认值时预判会偏(用户显式配置 retainRatio/retainTokens 不受影响)。文案已写「预计」,
|
|
136
|
+
实际损失由 `pins.stale` 兜底。
|
|
137
|
+
- 置顶状态读的是 workspace registry 的全局置顶集合;老宿主没有该字段 → `sessionPinned=null`,
|
|
138
|
+
客户端不弹确认、服务端也不拦(`isSessionPinned` 返回 null → 按未置顶处理,保持 0.5.0 自动归档)。
|
|
139
|
+
- 客户端新增交互(归档勾选 / 置顶确认 / 失效标注 / 预警 toast)需要重启宿主加载新 bundle 后
|
|
140
|
+
真机点一遍;本轮只做了 bundle 重建 + 组件级单测(mini React,不引 jsdom)。
|
|
141
|
+
|
|
142
|
+
### 批 3:完整交接链 + 父→子入口(测试 311 → 324)
|
|
143
|
+
|
|
144
|
+
#### 1. 完整交接链列表(接力条下拉「完整交接链」)
|
|
145
|
+
|
|
146
|
+
- **数据**:当前会话 `readState.chain`(入站;0.5.0 之前的老链走 DCR-02 的 `synthesized` 兜底)→
|
|
147
|
+
逐级向 `chain.parentId` 读父会话 state 的 `chain`,直到没有 `parentId`。
|
|
148
|
+
- **频率**:按需——点「完整交接链」之前**不预加载**祖先;展开后**每层一次** state GET(串行、
|
|
149
|
+
不并发、不轮询)。链路读完后不常驻任何定时器。
|
|
150
|
+
- **上限/去重**:`MAX_CHAIN_HOPS = 8`;`seen` 按 sessionId 去重 → 自指/成环立刻停;到上限时
|
|
151
|
+
界面明说「只显示最近 8 层」(不假装到头)。
|
|
152
|
+
- **每行**:「历史 N:<原名>」(旧链沿用「交接第 N 次:<原名>」,与接力条同一把尺子)+ 行内
|
|
153
|
+
「交接单」入口 + 整行点击跳转。N/原名取「下一层(子)会话的入站 chain」——正好是那一层会话
|
|
154
|
+
改名后的标题。
|
|
155
|
+
- **行内交接单**:优先用该层记录里的 `path`;旧记录没写 path 时用该层会话 state 的出站
|
|
156
|
+
`next.path`(`childId` 与链号都对得上才采用)。只认 `handoff-*.md` basename,走已有
|
|
157
|
+
`/api/compact-relay-handoffs?file=`;没有 path 时禁用并标「暂无交接单」。
|
|
158
|
+
- **跳转**:`ctx.uiWorkspace.openSession(sessionId)`。祖先会话默认已被交接流程归档,所以与
|
|
159
|
+
「回到原会话」同口径:先 `unarchive`(宿主 API,老宿主退回 HTTP `recover`,幂等)再 open。
|
|
160
|
+
- **失败/读不到**:停在断点,已读到的行保留,显示「无法读取更早的链路」+「重试」;重试
|
|
161
|
+
**从断点续读**(已读到的层不重复拉)。「← 返回」回主菜单 —— 链读失败不阻断其他入口。
|
|
162
|
+
- **「没加载」不等于「到头」**:宿主返回的 `sessionId` 对不上(重启后祖先会话通常不在内存里)
|
|
163
|
+
按「读不到」处理——停在断点 + 重试,而不是静默少列一层;用户先打开那个会话再重试就能续上。
|
|
164
|
+
- 回归:`test/relay-bar.mjs` 新增 8 组(按需/每层一次 GET、8 跳上限、成环去重、失败重试续读、
|
|
165
|
+
未加载也不误判到头、行内交接单 basename、无 path 禁用、行点击先恢复再切)。
|
|
166
|
+
|
|
167
|
+
#### 2. 父→子入口(TD-01)
|
|
168
|
+
|
|
169
|
+
- `actions/state.js` 新增 additive 字段 `nextArchived`(`next.childId` 是否在 workspace 归档
|
|
170
|
+
集合里;与 `parentArchived` 完全对称,各查各的 id,读不到 → null)。
|
|
171
|
+
- 父会话侧(有出站记录时)**也渲染接力条**:一行「已交接给新会话」+ 菜单「去新会话」;
|
|
172
|
+
中段会话同时有「回到原会话」与「去新会话」两个方向,外加「完整交接链」。
|
|
173
|
+
- 点击 → `openSession(next.childId)`;`nextArchived` 为 true 或未知(null)且宿主有恢复能力时
|
|
174
|
+
先 unarchive 再 open(复用批 1 的双通道恢复逻辑)。
|
|
175
|
+
- 两个防御:老插件没有 `next` 字段 → 不显示「去新会话」(**不回落** `chain`:老形态的 chain
|
|
176
|
+
是最近一条记录,可能是入站,回落会出现「跳回自己」的死入口);`next.childId === 本会话`
|
|
177
|
+
的脏记录一律不认。
|
|
178
|
+
- 回归:`test/relay-bar.mjs` 新增 4 组(父会话侧渲染与跳转、脏 next 不显示、归档/未知先恢复、
|
|
179
|
+
中段会话两个方向)+ `test/state-passive.mjs`(`nextArchived`:在/不在归档集合、只看出站 id、
|
|
180
|
+
无 registry、无 next → null)。
|
|
181
|
+
|
|
182
|
+
### 批 4:完整交接链在祖先未加载时只显示 1 层(DCR-05;测试 324 → 339)
|
|
183
|
+
|
|
184
|
+
- **真机现象**:点接力条「完整交接链」只显示 1 层(当前会话),并提示「无法读取更早的链路」。
|
|
185
|
+
- **根因**:客户端逐级 GET `/api/compact-relay-state?sessionId=<parentId>`;宿主对**未加载**的
|
|
186
|
+
会话返回 `sessionId:null`(`readState` 的 `target` 为 null),客户端把它当「读不到」停在断点。
|
|
187
|
+
重启后祖先会话通常都不在内存里,所以这是常态而不是异常。
|
|
188
|
+
- **宿主能力实证**(DSH 0.2.1-rc.3 app.asar):
|
|
189
|
+
- `@deepseek-ai/dsh-session-persistence` 是根服务 `ctx.sessionPersistence`;
|
|
190
|
+
- `stat(id)` → `{header, revision, sizeBytes?}`,**不读事件日志**;
|
|
191
|
+
- `open(id, 'read')` → 只读 `SessionHandle`,`read()` 返回 `{events, eventState}`,`close()`
|
|
192
|
+
释放;**不创建 live Session、不写任何状态、不占写锁**;
|
|
193
|
+
- JSONL 后端对**已归档**会话同样可读(归档只改 Workspace registry 集合,不动持久化文件)。
|
|
194
|
+
- **方案**:优先方案成立 —— 新增服务端只读读取器 `dsh/relay-chain.js`:
|
|
195
|
+
- `readRelayChain(ctx, {sessionId})` 从当前会话开始,按「子会话入站 relay 记录」逐层向上
|
|
196
|
+
(与旧客户端同一套语义),最多 8 层、`seen` 去重防成环;
|
|
197
|
+
- 每层优先用 live 会话(内存里最新),未加载则 `stat` 读 header + `open(id,'read')` 读日志;
|
|
198
|
+
- 交接单 path 优先用该跳入站记录,缺了再从父会话日志找 `childId+ordinal` 对得上的出站记录
|
|
199
|
+
(0.3/0.4 老记录只在父会话写 path);
|
|
200
|
+
- 0.5.0 前没有 relay 记录的老链同样逐层用 `header.parentSession` + `session/title` 兜底合成;
|
|
201
|
+
- 某一层读不到时返回**已读到的行** + `unreadable:{sessionId, reason}`,不整块清空。
|
|
202
|
+
- **端点**:`actions/relay-chain.js`(薄包装)+ `GET /api/compact-relay-chain?sessionId=`;
|
|
203
|
+
与其它 GET 端点同一套宿主 `connection.requestRejection` 鉴权/同源口径。一次返回整条链,
|
|
204
|
+
彻底消除逐级 GET 和未加载问题。
|
|
205
|
+
- **客户端**:
|
|
206
|
+
- 点「完整交接链」先打整链端点,一次渲染(`test/relay-bar.mjs` 断言祖先 state GET = 0);
|
|
207
|
+
- 老插件/响应异常 → **回落**原有逐级 state 读法(兼容 + 兜底),行为与 0.5.1 批 3 一致;
|
|
208
|
+
- 兜底断点从「错误提示」改成可操作入口:宿主明确回 `sessionId:null` 时显示
|
|
209
|
+
**「上一层会话未打开」+「打开并继续」**;点击先双通道 `unarchive`(复用 DCR-03,幂等;
|
|
210
|
+
无归档记录也直接继续)→ `openSession(parentId)` → 等宿主加载(≤8 次)→ **自动 `loadChain` 续读**;
|
|
211
|
+
加载不上则提示「已打开上一层会话,再点完整交接链继续」;
|
|
212
|
+
- 整链端点 `unreadable.reason==='not-found'`(持久化里根本没有这一层)**不显示**「打开并继续」
|
|
213
|
+
(打开也救不回来),只保留重试——与 DCR-03「不造必然失败的入口」同口径。
|
|
214
|
+
- **保留**:读不到时已读行不丢、返回主菜单不受影响、8 跳上限、sessionId 去重、行内交接单
|
|
215
|
+
basename 校验、每行跳转前先 unarchive。
|
|
216
|
+
- **回归**:新增 `test/relay-chain-endpoint.mjs`(12 条:祖先全未加载仍读全 / live 优先 /
|
|
217
|
+
8 跳上限 / 成环去重 / 断点保留 / 无持久化 / 老链 header+标题兜底 / subagent+fork 不合成 /
|
|
218
|
+
HTTP 200+400+405 / 无服务 ok:false / 鉴权 401);`test/relay-bar.mjs` 新增 3 条(未加载
|
|
219
|
+
入口、点击后 unarchive+openSession+自动续读、整链端点一次读全 + 部分失败入口),共 46 条。
|
|
220
|
+
|
|
221
|
+
### 设计定稿:环只表达容量(pin 位置标记归 0.6 时间线)
|
|
222
|
+
|
|
223
|
+
- **决策(用户拍板)**:环管「未来」(容量、阈值、断点、原生线刻度);pin 的位置是「历史」。
|
|
224
|
+
在环上画锁定消息位置属于复现历史功能,而 0.6 的时间线会承接 pin 历史(`state.pins.marks`
|
|
225
|
+
与 marks/timeline 已备好)。所以 0.5.1 起**环只表达容量,不再画 pin 位置块**;
|
|
226
|
+
「已锁定 N 条 / 已失效 M 条」作为状态保留在悬停卡与菜单。
|
|
227
|
+
- **客户端** `lib/client/30-indicator.js`:删除环上的 pin 位置块渲染(`pinBlocks` 那组带
|
|
228
|
+
`data-cr-pin-mark` 的 `circle`,含品红弧段);环 SVG 只剩分段弧/断点/原生线刻度/交接线警示/
|
|
229
|
+
中心点。**`state.pins.marks` 数据读取保留**(状态归一化照旧,0.6 时间线消费),注释写明
|
|
230
|
+
「位置标记归 0.6 时间线,环不再画」。
|
|
231
|
+
- **保留不动**:悬停卡「已锁定 N 条 / 已失效 M 条」、菜单「已锁 N 条 · 失效 M」、
|
|
232
|
+
`data-cr-pins` / `data-cr-pins-stale` / `data-cr-pin-stale` 取证属性;`colors.base.pin`
|
|
233
|
+
(锁定颜色)留在设置与 colors 契约里,作为 0.6 时间线的 pin 标记颜色。
|
|
234
|
+
- **清点**:`data-cr-pin-mark` 此前只被指示灯使用(设置页教学图本来就不画 pin,演示段无引用);
|
|
235
|
+
删除后源码与 bundle 全库无残留。
|
|
236
|
+
- **回归**:`test/relay-bar.mjs` 新增「有 pin 也不画位置块」+ 悬停卡/菜单计数仍在;
|
|
237
|
+
`test/archive.mjs` 产物守卫改为「源码/bundle 不得含 `data-cr-pin-mark`,但必须保留 `marks`
|
|
238
|
+
读取与 0.6 时间线注释」。测试 **339 → 340 PASS**。
|
|
239
|
+
|
|
240
|
+
### 文档
|
|
241
|
+
|
|
242
|
+
- **ARCHITECTURE**:`readState` 契约补 `chain`(入站)/ `next`(出站)/ `nextArchived` /
|
|
243
|
+
`capabilities.unarchive` / `pins.stale` / `pins.atRisk` / `sessionPinned` / `parentArchived`;
|
|
244
|
+
§8.8 读侧补「两侧都渲染」口径;§8.10(完整交接链的逐级读法、上限/去重/断点重试、
|
|
245
|
+
父→子入口的两个防御);新增 §8.11(DCR-05:服务端只读持久化日志一次读整链、端点契约、
|
|
246
|
+
客户端回落与「打开并继续」);新增 §8.12(设计定稿:环只表达容量、`pins.marks` 数据面保留、
|
|
247
|
+
测试/产物守卫);§5 补**帧体积口径**(实测 ≈ 2.1KB/帧,0.5.1 additive ≈ +300B,
|
|
248
|
+
完整链默认 **1 次** `/api/compact-relay-chain`,老宿主回落 ≤ 8 次 GET)。
|
|
249
|
+
- **README / TUTORIAL**:补 0.5.1 用法(接力条「完整交接链」、父→子「去新会话」、pin 失效
|
|
250
|
+
标注、交接时的置顶确认、交接后归档开关);指示环图例与锁定说明去掉「品红色块」,
|
|
251
|
+
统一成「环只表达容量,pin 位置标记归 0.6 时间线;已锁定/失效条数看悬停卡与菜单」
|
|
252
|
+
(README.en 同步)。
|
|
253
|
+
- 口径与已知限制见下节。
|
|
254
|
+
|
|
255
|
+
### 已知限制与口径(0.5.1 定稿)
|
|
256
|
+
|
|
257
|
+
- **DL-02(置顶丢失)**:宿主 `archiveSession` 在**同一次 durable write** 里把该会话从置顶
|
|
258
|
+
集合移除(置顶与归档互斥),这是宿主行为、不是插件副作用。0.5.1 起这件事不再静默:交接
|
|
259
|
+
预览在会话置顶时先问一次(「交接并归档(取消置顶)」/「只交接,保留置顶」),服务端在没拿
|
|
260
|
+
到 `confirmUnpin:true` 时**拒绝自动归档**(`status:'pinned'`,旧会话与置顶都保留);旧客户端
|
|
261
|
+
(不传 confirmUnpin)在置顶会话上自动降级为不归档。**边界**:用户在侧栏手动归档置顶会话时,
|
|
262
|
+
那一步的确认归宿主管辖,本插件不介入。
|
|
263
|
+
- **DL-03(分叉未定义)**:fork(宿主 `isSeeded=true`)与 subagent 会话不在接力链语义里,
|
|
264
|
+
老链兜底(DCR-02)明确排除这两类。真正的「分叉 → 各走一条链 → 再合并」**没有定义**:链记录
|
|
265
|
+
只存单亲 `parentId`,没有分支判据,插件按「各聊各的」处理(两条分叉各自成链,互不感知)。
|
|
266
|
+
0.6 再谈(需要宿主的血缘/分支信号)。
|
|
267
|
+
- **DL-04(有 pin 时程序段变化)**:九段交接单里「当前进度」「改过的文件」两段由**程序收集**
|
|
268
|
+
(`core/handoff-facts.js`);有 pin 时「当前进度」会多出「锁定消息 N 条」+ 最多 10 条锚点
|
|
269
|
+
(seq + 角色 + ≤60 字摘要)。所以同一会话在不同 pin 状态下重新生成交接单,**程序段内容会变**,
|
|
270
|
+
不保证与上一次逐字节相同(模型段本来也不保证)。这是有意的:交接单要反映交接那一刻的事实。
|
|
271
|
+
0.5.1 批 2 起只统计**还活着**的 pin(被压缩带走的失效 pin 不进交接单,改在指示灯界面标注)。
|
|
272
|
+
- **「去新会话」只指向最近一次出站**:`readState.next` 是**最近一条**出站记录,所以同一条会话
|
|
273
|
+
交接过两次时,更早的那个下一代会从「去新会话」里消失(会话本身还在,侧栏能找到);这是
|
|
274
|
+
`next` 的语义决定的,跨会话全时间线留到 0.6 的「路线图」面板。
|
|
275
|
+
- **旧链兜底无 path**:0.5.0 之前的老链(子会话没有 relay record)由 `legacyRelayChainOf` 用
|
|
276
|
+
会话头 + 标题合成**只读**链,**不带 path**(老链的交接单路径只写在父会话记录里)。客户端缺
|
|
277
|
+
path 时会向父会话只读回查一次,仍拿不到就显示「暂无交接单」并禁用入口——不伪造、不猜路径。
|
|
278
|
+
完整交接链同理:某一层没有记录路径时,那一行的「交接单」禁用,但跳转照常可用。
|
|
279
|
+
- **整链端点依赖 `ctx.sessionPersistence`**:没有该服务的宿主(老版本/裁剪构建)回落旧的逐级
|
|
280
|
+
state 读法;此时祖先未加载会停在断点并显示「上一层会话未打开」+「打开并继续」,不再只给
|
|
281
|
+
错误提示。
|
|
282
|
+
|
|
283
|
+
### 验证
|
|
284
|
+
|
|
285
|
+
- `npm test` 全绿:**339 PASS / 0 FAIL**(历史基线:0.5.0 收尾 257 → 批 1 278 → 批 2 311 →
|
|
286
|
+
批 3 324 → 批 4 **339**;批 4 新增 15 条:12 条整链服务端/端点测试 + 3 组接力条组件测试);
|
|
287
|
+
`node scripts/build-client.mjs --check` 通过(bundle 279147 字节)。
|
|
288
|
+
- 四批合计新增回归:DCR-01/02/03/04、D01、D03(批 1)、pin 失效/原生压缩预判/置顶/归档开关
|
|
289
|
+
(批 2)、完整交接链/父→子(批 3)、整链端点/未加载「打开并继续」(批 4),以及
|
|
290
|
+
`test/clip.mjs`、`test/pin-health.mjs`、`test/relay-chain-endpoint.mjs` 三个新文件。
|
|
291
|
+
- **未 commit、未发布、未打 tag**(按范围定稿:发布前须 Lead 验收)。
|
|
292
|
+
- **待真机验证**:① 旧链会话(b3ae4bac 等 4 个)打开后有接力条;② 祖先会话再次交接后接力条
|
|
293
|
+
不消失;③ 置顶会话交接时的确认弹层;④ 完整交接链:重启后祖先**未加载**时也能一次列全(新
|
|
294
|
+
`/api/compact-relay-chain`),以及老宿主断点「打开并继续」→ 自动续读;⑤ 父会话侧「去新会话」。
|
|
295
|
+
本轮只做组件级单测(mini React)+ 服务端单测,没有真机截图。
|
|
296
|
+
|
|
297
|
+
## 0.5.0(2026-10-26)
|
|
298
|
+
|
|
299
|
+
> 0.5.0 功能批次:地基 F1–F6 + **命名反转 + 接力条 + 交接后自动归档旧会话/恢复入口**。双向跳转的「父会话 → 子会话」入口与里程碑面板尚未实现;其余已提交并发布(2026-10-26)。
|
|
300
|
+
|
|
301
|
+
### F1+F2:历史前缀纯函数 + relay record 白名单(commit `5397863`)
|
|
302
|
+
|
|
303
|
+
- **F1(`lib/core/relay.js`)**:新增 `HISTORY_PREFIX='历史'`、`historyTitleOf(n, base)`(生成「历史 N:<原名>」,空格/全角冒号/序号写法与 `relayTitleOf` 同一套规则,原名先 trim,空题回落「会话」);`splitHandoffTitle` 返回值新增 `scheme`——`handoff-n`(交接第 N 次)/ `history-n`(历史 N)/ `legacy`(旧「交接:」「接棒:」堆叠、无前缀、空/脏输入)。`baseTitle` / `handedOff` 的字段名与语义不变,旧标题解析结果逐字节不变。
|
|
304
|
+
- **F2(`lib/dsh/relay.js`)**:`normalizeRelayRecord` 白名单放行可选 `scheme` / `titleSeq` / `historyTitle`;旧记录缺失/非法一律读成 `null`(不猜、不补默认值),其余字段与旧记录读取语义不变。父会话写出的 relay record 形状不变(不写这三个字段)。
|
|
305
|
+
- **测试**:新增 `test/relay-title-scheme.mjs`(生成/解析/往返/scheme 不混用/空题与脏输入/>80 字节不截断)。
|
|
306
|
+
|
|
307
|
+
### F3:子会话自带 relay record(commit `b4878b8`)
|
|
308
|
+
|
|
309
|
+
- **`lib/dsh/sessions.js`**:`createHandoffSession` 在 `followup` 之后、`flush` 之前,向**子会话**写一条 `plugin:compact-relay/relay`:`parentId` / `childId` / `ordinal` / `baseTitle` / `title` / `scheme='handoff-n'` / `titleSeq` / `at` / `atIso`。走 plugin record 通道(`ignorable`,不进模型上下文、不算 token、压缩碰不到)。
|
|
310
|
+
- **为什么**:0.5.0 把旧会话改名「历史 N:<原名>」、新会话继承原名后,子会话标题里不再有「交接第 N 次」;链号若仍靠标题解析会算成 0,第 2 次交接会重复「第 1 次」。record 让链号自带。
|
|
311
|
+
- **父记录不动**:父会话的 relay record 仍由 `actions/handoff.js` 写 0.3/0.4 的现场形状(canonical + 旧字段 + `path`/`title`/`baseTitle`),0.5.0 起父/子记录都带 `path`(交接单路径);本次只在子会话多一条记录,标题/文档/交接流程不变。
|
|
312
|
+
- **测试**:`test/handoff.mjs` 增子会话 record 字段、`titleSeq`、两跳链式(第 1 次 → 第 2 次不重号)断言。
|
|
313
|
+
|
|
314
|
+
### F4:`readState` 增只读 `chain` 投影(commit `cc6f86d`)
|
|
315
|
+
|
|
316
|
+
- **`lib/dsh/relay.js`**:新增 `relayChainOf(record)`,把最近一条 relay record 投影成 `{ordinal, baseTitle, parentId, childId, scheme, path?}`——字段全部取自记录本身,不解析标题、不补默认序号;记录里没有的字段给 `null`,没有 `path` 就不出现这个键;不是对象返回 `null`。
|
|
317
|
+
- **`lib/actions/state.js`**:`readState` 新增 `chain`(没有记录为 `null`),旧字段 `relay` 原样保留(0.3 旧名 `parentSessionId`/`index` 仍可读)。纯 additive:老客户端忽略新字段;SSE 帧 +~110B。
|
|
318
|
+
- **测试**:`test/state-passive.mjs` 增「新记录全字段 / 0.3 旧记录缺字段 null / 无记录 chain=null / 旧字段不变」四组断言。
|
|
319
|
+
|
|
320
|
+
### 命名反转(第一个用户可见功能,commit d340dff)
|
|
321
|
+
|
|
322
|
+
- **行为**:新链交接后,旧会话改名「历史 N:<原名>」,新会话继承干净原名(不带序号);第 2 次交接为「历史 2:X / 历史 1:X / X」,列表读起来是一条对话。旧链(已存在的「交接第 N 次:」链、0.3 旧 record、旧「交接:/接棒:」堆叠)继续旧命名,旧会话标题一个字符不改(链级 cutover,不迁移、不混用)。
|
|
323
|
+
- **`lib/core/relay.js`**:新增 `MAX_TITLE_BYTES=80` 与 `truncateTitleUtf8`(与宿主 `session-title` 同口径的 UTF-8 安全截断);`historyTitleOf(index, base, maxBytes?)` 支持预算——前缀先计入、超长只缩 base,不传时保持 0.4.3 纯函数行为。
|
|
324
|
+
- **`lib/dsh/sessions.js`**:
|
|
325
|
+
- `handoffTitlePlanOf` 返回新增 `scheme`(`history-n`/`handoff-n`)与 `historyTitle`;有 record 时方案以 record 为准(旧记录 scheme=null → 旧链),无 record 时干净标题/「历史 N」→ 新链,「交接第 N 次」/旧前缀 → 旧链,读不到标题 → 保守旧链。
|
|
326
|
+
- `createHandoffSession` 接受可选 `plan`(文档/双写同一份);子会话 record 的 `scheme` 改为本次链实际方案(新链 `history-n`)。
|
|
327
|
+
- 新增 `renameHistoryOf`:子会话建好后把旧会话改名「历史 N:<原名>」,成功后 `await sessions.flush`;改名失败只 warn 不阻断;当前标题已是目标(含宿主按更小预算截断)时跳过,同一链重复执行不重复加前缀。scheme=`handoff-n` 时不读标题服务、不改名。
|
|
328
|
+
- **`lib/actions/handoff.js`**:交接编排在 `createHandoffSession` 之后调用 `renameHistoryOf`;父 record 写出新增 `scheme` + `titleSeq`(旧会话 rename 的 eventSeq),新链再写 `historyTitle`;父记录写完后再次 flush 旧会话,保证标题与 record 一起落盘。
|
|
329
|
+
- **有意行为变化**:F2/F3 时期「父记录不写 scheme/titleSeq/historyTitle」的约束随本功能更新——父/子记录现在都写 `scheme` + `titleSeq`;F5 链不变量测试已改为断言新形状。
|
|
330
|
+
- **测试**:新增 `test/handoff-naming.mjs`(第 1/2 次新链标题与序号、父/子 titleSeq 各取自己 rename、旧链 cutover 不改旧标题、0.3 旧记录不迁移、80 字节预算、幂等 guard、rename 失败降级含端到端不阻断);更新 `test/handoff.mjs` 与 `test/relay-chain.mjs` 的父记录/标题期望;`test/harness/host.mjs` 支持 `sessionTitleText` 与函数式 `renameEventSeq`。
|
|
331
|
+
|
|
332
|
+
### 交接后自动归档旧会话 + 恢复入口(commit 369a235)
|
|
333
|
+
|
|
334
|
+
- **宿主接缝(0.2.1-rc.3 app.asar 实测)**:
|
|
335
|
+
- 服务端 `@deepseek-ai/dsh-workspace/lib/index.js` 的 `WorkspaceRegistry.archiveSession(sessionId, { stopActivity })`;插件通过 `ctx.get('workspaceRegistry')` 调用(建子会话已在用同一服务)。
|
|
336
|
+
- 不带 `stopActivity` 时先跑 `workspace/session-activity` waterfall,任何 provider(运行中的 turn / subagent / job / schedule)报活都会在写盘前抛 `WorkspaceActiveSessionError`;带 `stopActivity: true` 会先写归档集合、再停旧会话的工作。**自动交接不传 stopActivity**——静默取消用户后台任务比多留一行破坏性大。
|
|
337
|
+
- `unarchiveSession(id)` 幂等且不做存在性探测;归档集合是 registry 全局状态,不改变 workspace 成员/排序,恢复后旧会话回到原位。
|
|
338
|
+
- 客户端 `ctx.uiWorkspace.unarchiveSession(id)` 与侧栏「取消归档」是同一接缝(`dsh-client-ui-workspace` 的 `UiWorkspaceService`)。
|
|
339
|
+
- **`lib/dsh/handoff-archive.js`**:自动归档只传 `sessionId`;已归档 → `already` 幂等;旧会话有活 → 等 150ms 重试一次后降级 `active`,保留旧会话可见并只 warn;无 registry / 找不到会话 / 写盘失败分别降级 `unavailable` / `unknown` / `failed`;任何分支都不抛错。`handoffArchiveNoteOf` 统一生成用户文案(失败固定含「归档失败,不影响交接」)。
|
|
340
|
+
- **`lib/actions/handoff.js`**:归档顺序在**子会话建好、旧会话改名、父 record flush**之后;成功 message 追加「旧会话已归档(可在新会话输入框上方「已接力自 …」→「恢复旧会话」找回;侧栏「全部会话」也能取消归档)」,失败追加原因且不影响 `ok/childId/path`。
|
|
341
|
+
- **恢复入口**:
|
|
342
|
+
- 服务端 `lib/actions/recover.js` + HTTP `action: 'recover'`:直接调 `workspaceRegistry.unarchiveSession`,跳过 `resolveTarget`(归档会话通常没有活 agent),幂等并回报剩余归档集合。
|
|
343
|
+
- `readState` 新增 additive 字段 `parentArchived`(true/false;没有父会话或宿主不给归档状态为 null),`chain` 仍是 relay record 的纯投影。
|
|
344
|
+
- 客户端接力条下拉新增「恢复旧会话」:已归档或宿主未给状态时显示;优先 `ctx.uiWorkspace.unarchiveSession`,老宿主回退 HTTP recover;成功后更新本地归档状态并自动切回旧会话。「回到原会话」在已归档时也先恢复,避免用户撞「归档会话不可打开」。
|
|
345
|
+
- **兼容/边界**:旧链(`handoff-n`)与 0.3/0.4 旧 record 同样归档,命名逻辑一个字符不改;宿主接缝缺失/失败一律降级为「不归档」并 warn;不改交接单正文、压缩行为、命名反转逻辑。
|
|
346
|
+
- **测试**:新增 `test/archive.mjs` 16 组(归档参数/幂等/活跃重试/失败降级/旧链/恢复动作/HTTP 路由/产物静态断言);`test/relay-bar.mjs` 新增 5 组恢复交互;`test/state-passive.mjs` 增 `parentArchived` 断言。`npm test` **257 PASS / 0 FAIL**。
|
|
347
|
+
- **未真机验证**:本环境未启动真实 DSH 会话点验「交接 → 自动归档 → 接力条恢复」全链路;宿主证据来自 app.asar 源码与同形 fake ctx。真机需确认:①交接完成后侧栏旧会话消失、新会话自动打开;②「恢复旧会话」后旧会话回原位且可打开;③旧会话有后台任务时保留可见并提示任务族。
|
|
348
|
+
|
|
349
|
+
### 验证
|
|
350
|
+
|
|
351
|
+
- `npm test` 全绿:**257 PASS / 0 FAIL**(历史基线:F1+F2 后 204 → F3 后 206 → F4 后 207 → F5 后 213 → 命名反转后 220 → 接力条后 236;本次自动归档 + 恢复入口新增 21)。
|
|
352
|
+
- 未 commit、未发布、未打 tag;0.5.0 剩余:双向跳转的父会话→子会话入口、里程碑面板。
|
|
353
|
+
|
|
3
354
|
## 0.4.2(2026-10-26 · 交接线保命补丁)
|
|
4
355
|
|
|
5
356
|
### 修复:交接提醒可能晚于 DSH 原生压缩(0.4.1 的三个洞)
|
package/README.en.md
CHANGED
|
@@ -30,7 +30,7 @@ The plugin mounts beside the existing composer. Default parameters are tuned for
|
|
|
30
30
|
|
|
31
31
|
### 1. Occupancy visualization
|
|
32
32
|
|
|
33
|
-
The ring is colored in segments according to the compression-assessment logic: the suggested compression point determined by the model or configured threshold,
|
|
33
|
+
The ring is colored in segments according to the compression-assessment logic and expresses capacity only: the suggested compression point determined by the model or configured threshold, and the DSH native auto-compaction line (the actual trigger point computed from the host token budget: ~67.8% for deepseek-flash, while the configured threshold defaults to 80%). Since 0.5.1 the ring no longer draws pin positions (position marks are reserved for the 0.6 timeline); pin status is shown as counts in the hover card and menu. When occupancy crosses the actual trigger point, the ring flashes red.
|
|
34
34
|
|
|
35
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
36
|
|
|
@@ -48,7 +48,7 @@ Running `/handoff` generates a handoff document with nine standard sections (ove
|
|
|
48
48
|
|
|
49
49
|
### 4. Message pinning
|
|
50
50
|
|
|
51
|
-
Running `/pin` marks messages that must be preserved during compaction.
|
|
51
|
+
Running `/pin` marks messages that must be preserved during compaction. Pin status and counts appear in the hover card ("N pinned") and the menu (locked row / "N locked · M stale"); since 0.5.1 the ring no longer draws pin position blocks (position marks belong to the 0.6 timeline).
|
|
52
52
|
|
|
53
53
|

|
|
54
54
|
|
|
@@ -62,9 +62,10 @@ Running `/pin` marks messages that must be preserved during compaction. Pinned m
|
|
|
62
62
|
| Red arc | The handoff threshold has been passed; switching to a new session is recommended |
|
|
63
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) |
|
|
64
64
|
| Violet tick | DSH native auto-compaction line: the actual trigger point computed as `min(window × thresholdRatio, window − reserved output − headroomTokens)` (~67.8% for deepseek-flash; the configured `thresholdRatio` defaults to 80% and is only a theoretical ceiling) |
|
|
65
|
-
| Magenta blocks | Distribution of pinned messages in the history |
|
|
66
65
|
| Full-ring red flash | The native auto-compaction line has been crossed; DSH native auto-compaction is imminent |
|
|
67
66
|
|
|
67
|
+
> Since 0.5.1 the ring expresses capacity only and no longer marks pin positions; "N pinned / M stale" is shown in the hover card and menu (position marks belong to the 0.6 timeline).
|
|
68
|
+
|
|
68
69
|

|
|
69
70
|
|
|
70
71
|
## The three lines (deepseek-flash, 1M window defaults)
|
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
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
### 1. 状态可视化监控
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
指示环按压缩评估逻辑分段显色,只表达容量:模型或阈值判定的建议压缩点,以及 DSH 原生自动压缩线(按宿主实际 token 预算算出的触发点:deepseek-flash 约 67.8%,配置阈值默认 80%)。0.5.1 起环上不再画锁定消息的位置(位置标记归 0.6 时间线),锁定状态以悬停卡/菜单的条数显示。当上下文占用越过原生自动压缩线时,整环呈红色闪烁告警。交接提醒线晚于当前模型的实际触发线(−5pp)时,环上交接刻度旁会出现红色小警示,悬停卡与 `/handoff` 菜单也会同步标出「交接线已晚」。
|
|
34
34
|
|
|
35
35
|
上下文进入「可压缩区间」(最小压缩线到压缩线之间)时,插件会在系统提示里主动告诉模型当前容量与区间,模型据此判断「刚完成一个自然节点」时是否记录压缩节点(0.3.4 起;此前模型看不到容量,判断条件不可执行)。
|
|
36
36
|
|
|
@@ -48,10 +48,22 @@
|
|
|
48
48
|
|
|
49
49
|
### 4. 关键消息锁定
|
|
50
50
|
|
|
51
|
-
执行 `/pin`
|
|
51
|
+
执行 `/pin` 命令可标记压缩时必须保留的消息。锁定状态与条数显示在悬停卡(「已锁定 N 条」)和菜单(锁定行 /「已锁 N 条 · 失效 M」)里;0.5.1 起环上不再画 pin 位置块(位置标记归 0.6 时间线)。
|
|
52
52
|
|
|
53
53
|

|
|
54
54
|
|
|
55
|
+
### 5. 接力条:交接链与双向跳转
|
|
56
|
+
|
|
57
|
+
交接之后,输入卡上方会出现一条「已接力自 历史 N:<原名>」的接力条;点开它就是一个会话导航菜单:
|
|
58
|
+
|
|
59
|
+
- **完整交接链**:点「完整交接链」才**按需**读取(不展开不发请求),一次列出整条链「历史 N:<原名>」(默认服务端只读持久化日志,祖先会话没打开也能读;老宿主回落逐级查询)。每行都能打开那一跳的交接单、点整行跳到那个会话(已归档的会先自动恢复)。最多回看 8 层,链路异常成环会自动停;某一层实在读不到时保留已读行 +「无法读取更早的链路 / 上一层会话未打开」,老宿主还会给「打开并继续」(打开那一层后自动续读),不影响菜单其他入口。
|
|
60
|
+
- **父 → 子**:把接力棒交出去之后,父会话侧的接力条变成「已交接给新会话」,「去新会话」一键切回这次交接出来的新会话(新会话若被归档过,也会先恢复再切)。
|
|
61
|
+
- **pin 失效标注**:被压缩(尤其 DSH 原生自动压缩)带走的锁定消息不再凭空消失——悬停卡与菜单标「已失效 N 条(随原生压缩移除)」;原生压缩**即将**覆盖锁定消息时会提前提示「先交接(/handoff)或解锁」。
|
|
62
|
+
- **置顶确认**:交接后归档旧会话会取消它的侧栏置顶(宿主行为)。旧会话置顶时,交接预览先问一次:点「交接并归档(取消置顶)」才归档,点「只交接,保留置顶」则旧会话和置顶都留着。
|
|
63
|
+
- **归档开关**:交接预览里可以勾掉「交接后自动归档旧会话」(默认开);首次引导里也写了这个选项。
|
|
64
|
+
|
|
65
|
+
> 老链(0.5.0 之前生成的会话)没有交接单路径记录,界面显示「暂无交接单」而不是猜一个——它会先向父会话只读回查一次。
|
|
66
|
+
|
|
55
67
|
## 指示环图例说明
|
|
56
68
|
|
|
57
69
|
| 指示环视觉元素 | 状态含义 |
|
|
@@ -62,9 +74,10 @@
|
|
|
62
74
|
| 红色弧 | 已越过交接阈值,建议切换新会话 |
|
|
63
75
|
| 环形缺口 | 建议压缩点(系统在可压缩区间主动提示模型判断;模型记录或到达压缩线时自动出现) |
|
|
64
76
|
| 紫色刻度 | DSH 原生自动压缩线:按 `min(窗口 × thresholdRatio, 窗口 − 输出预留 − headroomTokens)` 算出的**实际触发点**(deepseek-flash 约 67.8%;`thresholdRatio` 配置默认 80%,只是理论上限) |
|
|
65
|
-
| 品红色块 | 已锁定消息在历史记录中的分布位置 |
|
|
66
77
|
| 整环红色闪烁 | 已越过原生自动压缩线,即将触发 DSH 原生自动压缩 |
|
|
67
78
|
|
|
79
|
+
> 0.5.1 起环只表达容量,不再标注 pin 位置;「已锁定 N 条 / 已失效 M 条」在悬停卡与菜单查看(位置标记归 0.6 时间线)。
|
|
80
|
+
|
|
68
81
|

|
|
69
82
|
|
|
70
83
|
## 三条线(deepseek-flash 1M 默认)
|
|
@@ -150,10 +163,14 @@ dsh plugin --profile desktop add github:cfanzx/dsh-compact-relay
|
|
|
150
163
|
- **依附桌面输入框**:环形指示器、`/bc`、`/handoff` 依附 DSH 的输入框与斜杠命令;headless 会话或 QQ / 微信等桥接链路不会加载它们,在这些链路里发 `/bc` 会被当成普通消息——这是桥接链路的能力边界,不是插件故障。
|
|
151
164
|
- **界面语言限制**:当前版本用户界面仅提供简体中文,英文界面已列入后续计划(排期未定)。
|
|
152
165
|
- **模型判断提示的前置**:可压缩区间的动态提示依赖 DSH 0.2.x 的根服务 `systemPrompt`(与 memory 等插件同款);它是启发式提示,不保证模型每次都会记录压缩节点,但条件从「不可判断」变成「可判断」。
|
|
166
|
+
- **接力链只认单亲血缘**:链记录只有 `parentId`/`childId`,「分叉 → 各走一条链 → 再合并」**没有定义**(fork 出来的副本与 subagent 会话不参与接力链,按各自独立处理)。「去新会话」只指向**最近一次**交接出来的会话(同一会话交接过两次时,更早的那个下一代请从侧栏找)。0.5.0 之前的老链由会话头 + 标题兜底合成,**没有交接单路径**,界面显示「暂无交接单」(不伪造、不猜)。
|
|
167
|
+
- **归档与置顶互斥**:宿主归档会话时会同时取消置顶。插件在**自动**归档前置顶会话会先问(见上),但你在侧栏手动归档置顶会话时,那一步的确认归宿主。
|
|
168
|
+
- **交接单的程序段随现场变化**:有锁定消息时,「当前进度」程序段会列出锁定条数与锚点摘要,所以不同 pin 状态下重新生成交接单,**程序段内容会变**(这是有意的:交接单要反映交接那一刻的事实)。
|
|
169
|
+
- **完整交接链是按需读取**:展开才查(默认一次 `/api/compact-relay-chain` 由服务端只读持久化 header/日志,最多 8 层;祖先未加载也能读;不展开零额外请求)。老宿主没有该端点时回落逐级 state 查询;某层未加载会显示「上一层会话未打开」+「打开并继续」。归档会话的记录能读,但**跳转前会先恢复**(否则会被宿主拒绝)。
|
|
153
170
|
|
|
154
171
|
## 版本状态与开源许可
|
|
155
172
|
|
|
156
|
-
-
|
|
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)。
|
|
157
174
|
- **开源许可**:采用 [MIT](LICENSE) 许可证,欢迎通过 [GitHub Issues](https://github.com/cfanzx/dsh-compact-relay/issues) 与 Pull Requests 提交反馈。
|
|
158
175
|
|
|
159
176
|
## 参考与致谢
|
package/ROADMAP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 压缩接力 · 目标与路线图
|
|
2
2
|
|
|
3
|
-
> 版本基准:0.4.2(2026-10-25)|状态:0.3 已发布(npm + GitHub),0.3.6 已发布,0.4.0 已发布(npm latest + GitHub tag/Release),0.4.1 已发布;**0.4.2
|
|
3
|
+
> 版本基准:0.4.2(2026-10-25)|状态:0.3 已发布(npm + GitHub),0.3.6 已发布,0.4.0 已发布(npm latest + GitHub tag/Release),0.4.1 已发布;**0.4.2 交接线保命补丁已实现、待验收与发布**;**0.5.0 命名反转 + 接力条 + 交接后自动归档/恢复已实现(未提交、未发布);双向跳转的父→子入口与里程碑面板待实现(见 §三 0.5.0)**
|
|
4
4
|
> 维护方式:每次发布前更新"当前状态"与勾选进度;方向变化必须同时改本文件和 memory 项目页。
|
|
5
5
|
|
|
6
6
|
## 一、最终目标(North Star)
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
- ✅ **P1 换模型跟随**:状态刷新即重判;观察路由变化后首次违规弹一次 toast;算不出实际线不告警、不阻断。
|
|
69
69
|
- ✅ **P2 文档三线**:README / README.en / TUTORIAL 写清 40% / 60% / 67.8%,删掉「原生线 −1%」旧文案;CHANGELOG 0.4.2 段同步。
|
|
70
70
|
- ⏳ 待真机点验:设置页 max=62 与提示、环上警示、菜单 ⚠、换模型 toast、截图存档(本仓库自动化无 Chrome/CDP)。
|
|
71
|
-
- 明确不做(0.4.2 边界):历史前缀 / 接力条(0.5.0
|
|
71
|
+
- 明确不做(0.4.2 边界):历史前缀 / 接力条(0.5.0)、自动触发(1.0)、改宿主触发逻辑、环上 A/B/C 画法。**自动归档旧会话已从 1.0 挪入 0.5.0**(见 §三 0.5.0 第 4 条)。
|
|
72
72
|
|
|
73
73
|
1. ✅ **交接单六段 → 九段**(0.3 已提前交付):总体目标 / 约束 / 已完成 / 当前进度 / 改过的文件 / 关键决策 / 已知问题 / 未完成 / 下一步。
|
|
74
74
|
- 文件改动、关键决策由程序收集(`core/handoff-facts.js`),模型只写 7 段、编不了。
|
|
@@ -79,8 +79,49 @@
|
|
|
79
79
|
|
|
80
80
|
验收:真实长任务交接后新会话**无需补充说明**即可继续;交接单一次通过率 ≥90%。
|
|
81
81
|
|
|
82
|
+
### 0.5.0 无痕交接(命名反转 + 接力条 + 自动归档/恢复已实现;父→子跳转/里程碑面板待实现)
|
|
83
|
+
|
|
84
|
+
> 范围口径:`default-workspace/dcr-版本路线图-2026-10-26.md` + `dcr-无痕handoff-运行目标与地基层-2026-10-25.md`。0.5.0 是用户可见的第一次大更新;**地基本身不单独发版**,随 0.5.0 一起发。
|
|
85
|
+
|
|
86
|
+
**用户能看到的变化(0.5.0 范围):**
|
|
87
|
+
|
|
88
|
+
1. **命名反转**:交接后旧会话改名「历史 N:<原名>」,新会话继承原名;旧链(交接第 N 次)一个字符不动;列表读起来是一条对话。
|
|
89
|
+
2. **接力条**:新会话输入框上方一行「已接力自 历史 1 ▾」,下拉含查看交接单 / 回到原会话 / 恢复旧会话 / 路线图占位;落位 `conversation.input.dock`(输入框单行时间线;早期审查记录的 `conversation.composer.dock` 同属输入框区域,宿主两个槽都在)。
|
|
90
|
+
3. **双向跳转**:接力条回父会话(已实现,已归档时先自动恢复);父会话侧回子会话(待实现,需要父会话接力记录/入口)。
|
|
91
|
+
4. **交接后自动归档旧会话**:交接成功(子会话已建、旧会话已改名、record 已落盘)即归档旧会话;交接完成提示 + 接力条「恢复旧会话」两处告知,一键恢复(正式 unarchive API),恢复后回原位;旧会话仍有 turn/subagent/job/schedule 时**不自动停止任务**,有限等待后保留可见并提示;失败不阻断交接。
|
|
92
|
+
|
|
93
|
+
**命名反转(第一个用户可见功能,已实现、未提交):**
|
|
94
|
+
|
|
95
|
+
- 新链:旧会话「历史 N:<原名>」,新会话继承干净原名;第 2 次为「历史 2:X / 历史 1:X / X」。
|
|
96
|
+
- 旧链:交接第 N 次 / 0.3 旧 record / 旧堆叠前缀继续旧命名,旧会话标题一个字符不改(链级 cutover)。
|
|
97
|
+
- 数据面:父/子 relay record 都写 `scheme` + `titleSeq`,新链父记录带 `historyTitle`;链号只从 record 续。
|
|
98
|
+
- 80 字节:前缀先计入、超长只缩 base;`renameHistoryOf` 幂等 + flush,失败只 warn。
|
|
99
|
+
- 测试:`test/handoff-naming.mjs`(7 组)+ F5 期望更新;接力条 `test/relay-bar.mjs`;自动归档/恢复 `test/archive.mjs`。`npm test` **257 PASS / 0 FAIL**。
|
|
100
|
+
|
|
101
|
+
**地基进度(数据面 / 纯函数 / 测试):**
|
|
102
|
+
|
|
103
|
+
| # | 内容 | 状态 |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| F1 | 历史前缀纯函数:`HISTORY_PREFIX` / `historyTitleOf` / `splitHandoffTitle` 返回 `scheme`(`handoff-n` / `history-n` / `legacy`) | ✅ 已提交 `5397863` |
|
|
106
|
+
| F2 | relay record 白名单放行 `scheme` / `titleSeq` / `historyTitle`(旧记录读成 null;父会话记录写出形状不变) | ✅ 已提交 `5397863` |
|
|
107
|
+
| F3 | 建子会话时往**子会话**写 relay record(ordinal/baseTitle/scheme/titleSeq)——不做这步第 2 次交接序号会重复 | ✅ 已提交 `b4878b8` |
|
|
108
|
+
| F4 | `readState.relay` 扩成只读 `chain`(ordinal/baseTitle/parentId/childId/scheme/path),旧字段原样保留 | ✅ 已提交 `cc6f86d` |
|
|
109
|
+
| F5 | 链不变量测试 `test/relay-chain.mjs`(三跳 1→2→3、双写形状、scheme 不混、旧链逐字节、chain 投影),并入 `npm test` | ✅ 已完成(未提交) |
|
|
110
|
+
| F6 | ROADMAP / CHANGELOG / ARCHITECTURE 登记 0.5.0 地基 | ✅ 本次(未提交) |
|
|
111
|
+
| 命名反转 | 旧会话「历史 N」/新会话继承原名/旧链 cutover(`dsh/sessions.js` + `actions/handoff.js`) | ✅ 已实现(未提交) |
|
|
112
|
+
| 自动归档/恢复 | 交接成功后 `workspaceRegistry.archiveSession`(不 stopActivity)+ `recover` 动作 + 接力条「恢复旧会话」+ `readState.parentArchived` | ✅ 已实现(未提交) |
|
|
113
|
+
|
|
114
|
+
**宿主侧没有接缝(0.5.0 不追):**
|
|
115
|
+
|
|
116
|
+
- **消息流拼接不可行**:`conversation.chat.flow` 是 `single` 槽且已被宿主 ChatFlow 注册,`conversation.chat.node` 只按宿主 entryKey 渲染 —— 旧消息拼不进新会话,只能「接力条 + 跳旧会话」。
|
|
117
|
+
- **通栏接力条不可行**:宿主 header 没有通栏 band,接力条落在输入框内的 `conversation.input.dock`(单行;早期审查记录的 `conversation.composer.dock` 同区域)。
|
|
118
|
+
|
|
119
|
+
**验收:** 交接 2 次后列表为 `历史 2:X / 历史 1:X / X`;接力条「查看交接单 / 回到原会话 / 恢复旧会话 / 路线图占位」可用;旧会话自动归档且能从接力条恢复;旧链标题不变;`npm test` 全绿 + 真机截图。
|
|
120
|
+
|
|
82
121
|
### 0.5 项目记忆(1–2 月)
|
|
83
122
|
|
|
123
|
+
> 0.5.0 实际范围是上面的「无痕交接」;本节为早期规划,不随 0.5.0 交付。
|
|
124
|
+
|
|
84
125
|
1. **项目上下文包**:目标 / 关键决策 / 待办 / 改过的文件,新会话自动带上。
|
|
85
126
|
2. **接力链视图**:父子会话树/时间线,一键跳转。
|
|
86
127
|
3. **Memory 双向**:读取项目页作为上下文,写回交接单(先跑稳单向再开)。
|
|
@@ -164,6 +205,7 @@
|
|
|
164
205
|
- ✅ 0.3.6 已发布(BUG-012):原生压缩线解析只认 `config:` 直接子键(按缩进判断),修复 `modelPolicies` 嵌套 `thresholdRatio` 被误报为全局线(Windows 真实会话误显示 50%);真实触发线按模型 route 计算留 0.4。
|
|
165
206
|
- ✅ 0.4.0 判断机制与诊断包(2026-10-24 完成):P0-2 诊断包最小版已交付(commit `41699c8`,`npm test` 全绿 + 隔离 e2e 真机验证);P0-1 判断机制加强(cue 文案 v2 + 注入门控 10%–40%/≥3 轮 + 工具描述 v2 + cue/mark 计量)代码已落地、测试全绿,标准 B 已达标(3/3)并随 0.4.0 发布。
|
|
166
207
|
- ✅ 真实压缩 e2e:`test/e2e/real-compact.mjs`(真实模型 + 真实压缩事务 + `maxSummaries=1` 合并旧摘要分支)6 项断言通过。
|
|
208
|
+
- ✅ 0.5.0 功能已落地(**未发布**):F1–F6 地基 + 命名反转 + 接力条 + 交接后自动归档/恢复入口;`test/relay-chain.mjs`、`test/relay-bar.mjs`、`test/archive.mjs` 并入 `npm test`(当前 **257 PASS / 0 FAIL**)。剩余:父→子跳转入口、里程碑面板。
|
|
167
209
|
- ✅ P3-01~14(13 修/1 确认无需修)+ BUG-004 + P2-T1 已收口。
|
|
168
210
|
- ⏳ 市场收录 PR:仓库满 1 天(2026-10-21 19:09 后)提,yml 与说明已备。
|
|
169
211
|
- ⏳ 陌生人安装路径验证(npm 安装 → 重启 → 设置/指示灯)—— 发布后第一次外部用户安装时一并确认。
|