dsh-side-branch 0.1.1 → 0.2.2

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/AGENTS.md CHANGED
@@ -42,6 +42,7 @@
42
42
  | `cordis.patch.yml` | bundle patch,只 insert 一行,`name` 必须是真实包名 |
43
43
  | `package.json` | `dsh.bundle.patch` 声明 patch;`dsh.client` 声明浏览器半 |
44
44
  | `scripts/smoke.mjs` | 契约自检:加载宿主半、验证只读白名单与 guard 形状、核验依赖包存在。**只在源码仓库里存在**,不随 npm 包发布(它断言的是源码形状,脱离源码树跑不了) |
45
+ | `scripts/client-check.mjs` | 客户端半的**运行时**自检:在 Node 里用极简 React + 极简 DOM 加载**真实 bundle**、渲染**真实组件**、喂**真实形状的 SSE 帧**,然后断言渲染出来的树里有没有那个答案。**同样只在源码仓库里存在**,不随 npm 包发布(它断言的是真实运行行为,脱离源码树跑不了) |
45
46
 
46
47
  ## 改动前的硬约束
47
48
 
@@ -59,12 +60,34 @@
59
60
 
60
61
  **不要删掉「跑前解锁、跑完回藏」这套循环,也不要把 `unarchiveSession` 改成硬调。** DSH `0.1.7-rc.1` 起,`dsh-api-session-controller` 的 `ArchivedSessionGate` 会拒掉**已归档会话**的任何模型步(`agent/pre-step` ⇒ reject ⇒ `dsh-agent-loop` 把轮次收成 `turn/end { reason: 'blocked' }`,**连模型请求都不会发出**)。本插件正是用归档来隐藏侧会话 ⇒ 少了 `handleStart` 里 `followup` 之前那次 `releaseArchiveGate()`,或少了 `finishJob` 里那次 `rehideAfterTurn()`,侧枝在 0.1.7 上就**每一轮全废**(面板会显示 `T.blocked`;那句「内容被安全策略拦截」与内容策略毫无关系,别被它带偏)。而 `unarchiveSession` 是 **`0.1.6-alpha.2`** 才有的方法:**必须走 `typeof` 特性探测**,硬调会让 `0.1.5-rc.2` 抛错,直接毁掉向后兼容。`scripts/smoke.mjs` 的第 ⑥ 节守着这套形状。
61
62
 
63
+ **不要删掉 Re-Fork,也不要在它上面"省事"。** 每一轮追问都要先看 `shouldReFork`:**主会话结算了新回合**,或者**模型/推理等级变了**,就新建一段(种子 = 主会话最新已结算前缀)并把上一段侧枝自己的对话重放进去。四条不能动:
64
+ 1. **`shouldReFork` 必须同时判这两条。** 只判"主会话长了"的话,用户**换了模型但主会话没动**时那一轮会继续用旧模型 —— 静默忽略用户的选择。
65
+ 2. **顺序:先重放成功,再释放老会话。** 反了就没有退路(老会话没了、新的没历史)。重放失败要**丢弃刚建的新会话**、老会话原样保留、本轮回 `side-branch/refork-failed`。⛔ 绝不静默降级成"没有历史的新会话"。
66
+ 3. **重放只传 `event.data` 原样,且只重放 5 种 surface 事件。** 手搓 `assistant/message` 必被 `dsh-session` 拒(它要求真实 `source.provider/model` 与 `stream` 数组);`turn/start`/`tool/call` 之类是纯日志事件,`append` 它们会抛。⛔ **也绝不要"顺手补全" `event.sourceEventSeqs`** —— `tool/result` 在事件层带着 `[旧会话的 callSeq]`,抄过来会撞 `sourceEventSeqs must reference earlier events`。
67
+ 4. **不截断侧枝历史。** 到上下文上限就**拒绝**(`estimateRequestTokens` + `side-branch/context-full`)。"拒绝是显式失败,截断是隐式失真"。
68
+
69
+ **不要把"换模型 ⇒ 带历史换段"加回来**(`seedSource`、`sameAgentOptions` 交接、`skipPrependNotice` 手动传递)。re-fork 把这条路径整个吸收了:换模型只是"该重建"的又一个理由。`createConversation` 只能以**父会话**为种子。
70
+
71
+ **别把 `parentCut` 漏在建段路径上。** `createConversation` 建段时就要记 `parentCut`(= 那一刻父会话已结算前缀的长度),`sleepConversation`/`resumeConversation` 也要带着它走。漏了的话 `completedTurnPrefixLength(...) > undefined` 恒为真 ⇒ **每一轮都白 re-fork 一次**。
72
+
73
+ **多段回答(`job.segments`)别改回"两个字符串槽位"。** 一个 turn 的**每个 step** 都会发一个 `start` 帧:`turn`/`step` 变了才是"新的一段",同 step 换 `attemptId` 才是**重试**(只有这时才丢该 step 的段落)。把每个 `start` 都当重试清空,就是"中间那段思考和正文消失"那个 BUG。另外 `replace` **只替换当前段**(整段替换会把前面几段一起覆盖),`snapshot`/终态必须带 `segments`(否则刷新后多段又没了)。
74
+
75
+ **别把 `lib/client.js` 里那些"两边共用"的常量/纯函数塞进任何一个函数内部。** `clampString` / `SEGMENT_KINDS` / `MAX_SEGMENTS` / `MAX_SEGMENT_CHARS` / `MAX_SEGMENTS_TOTAL_CHARS` / `normalizeSegment` / `normalizeSegments` / `trimSegments` 这几个**必须留在模块作用域** —— `createPanelStore()` 与 `SideBranchBody` **两边都用**(store 里做持久化形状校验、组件里做流式写入)。放进其中一边,另一边就会 `ReferenceError`,而那个错**只会出现在浏览器 console 里、被事件派发吞掉**(现象:面板静默不动、答案不显示)。`scripts/client-check.mjs` 就是为了抓这类错误。
76
+
77
+ **不要删掉实例租约(`side-branch-instances/`),也不要绕过它做孤儿清理。** 清理判据是"磁盘上的 `side-*` 不在本进程账本里",而账本**在启动时必然是空的** ⇒ **两个共用同一个 `DSH_HOME` 的实例同时跑**时,后启动的那个会把先启动的那个**正在用的**侧枝目录当孤儿删掉。所以清理前必须先立自己的租约、再看有没有别的**活着的**外来租约 —— 有就**整个跳过**。
78
+
79
+ **`GET /side-branch/inherited` 的返回绝不能进面板持久化状态。** 一份主会话前缀可能几十万字符,而面板状态走 `sessionStorage`(5–10 MB 配额)⇒ 塞进 `rounds`/`normalizeRound` 会立刻爆配额、坏掉"刷新不丢"。它只放组件内存态、按需拉取。
80
+
62
81
  **SSE 响应是整条手写的**(`Connection: close`)。换成框架自带的写法会坏,原因写在 `lib/index.js` 的 `handleStream` 注释里。
63
82
 
64
83
  **客户端一切输入都不可信。** 问题长度、模型标识、`conversationId` 归属都要在宿主侧校验。
65
84
 
66
85
  **只读的四条护栏都别动**:`ALLOWED_TOOLS` 常量、`setup(agentCtx)` 里的 `tools.guard`(拿不到就 `throw`,fail-loud)、`ROUTE_METHODS`(405 + `Allow` 的方法契约)、以及宿主侧对 `conversationId` 归属的校验。
67
86
 
87
+ **不要给一轮加"总时长"超时。** 侧枝除了工具放行名单之外不应与主会话有别的区别:主会话不限制一轮跑多久(长思考、长工具、多步),侧枝也不能。兜底只能是 `STALL_TIMEOUT_MS` 的**停滞**看门狗 —— 它的计时器由**每一个事件**重置(建 job、流式帧、会话事件三处),所以"跑得久"不会被它掐断,只有"这么久连一个事件都没有"才会。0.2.1 及以前是 90 秒的**总时长**硬超时,长回合被无条件掐断、面板显示「请求超时(90 秒)」。`scripts/smoke.mjs` 的第 ⑩ 节守着这条。
88
+
89
+ **划选入口只能出现在主会话正文区。** 监听挂在 `document` 上("在会话里划字就能用"),所以必须靠 DOM 归属判定:`insideMainConversation()` 要求祖先里有 `[data-conversation-content]`(旧版退回 `[data-conversation-scroll]`),并排除右栏的 `[data-sidebar-right-panel]` / `[data-sidebar-right-session]` / `[data-sidebar-chat]` 与 `[data-conversation-region="composer"]`。**子代理的嵌入会话也带正文标记**(右栏 sidebar chat),只判正文标记会把子会话当成主会话。找不到正文标记时**不弹**(fail-closed)。漏掉这条的后果不是界面瑕疵:用户会把右栏(文件预览 / 设置 / 别人插件的面板)里的一段原文引用进侧枝的 prompt。`scripts/client-check.mjs` 用假 DOM 驱动**真实的 `selectionchange` 路径**断言这一点(七种位置)。
90
+
68
91
  **`peerDependencies` 里的 `@deepseek-ai/cordis` 是声明性的**:插件代码**不 import 它**(宿主半的运行期服务全部经 `ctx` 软获取,客户端半只 `require` 平台内置模块)。它是按生态惯例声明的,用来表明本插件挂在哪套契约上——**别当成漏删的依赖删掉**,也别为了"用上它"去加 import。
69
92
 
70
93
  ## 为什么这样做(改动前先读,别照着直觉改回去)
@@ -75,7 +98,9 @@
75
98
 
76
99
  **为什么不用官方的可续子会话。** `ctx.subagents.startContinuable` 表面上正好合适(官方提供的、可继续对话的子会话),但它结算时会把子会话的最终回答当成一条**父会话用户消息**投回父会话,并唤醒父模型跑一整轮(`dsh-subagent` 的 `watchSettlement` / `notifySettlement`,官方 README 说明这是设计行为且没有开关)。对多数场景合理,但对本插件是致命的:直接违反"答案不进主会话"这个前提,还要为此多跑一轮模型。
77
100
 
78
- **为什么自建侧会话、以及为什么种子取"已完成回合前缀"。** 现在的做法是 `ctx.agents.create()` 建一个**普通侧会话**(`meta.parentSession` 只当血缘,**不设** `origin:'subagent'` —— 不需要子代理的生命周期,也不想让主会话里出现子代理卡片),出生时用公开的 `session.snapshotEvents()` 读出父会话事件、自己切到最后一个 `turn/end` 当种子,再在 `setup` 里 `agentPresets.composeFrom(agentCtx, parent.ctx)` 继承父会话的 preset/工具/persona。这样提示词前缀与父会话逐字一致,**继续命中原有的前缀缓存**;多轮追问就是同一个会话上继续 `followup`。代价是继承的是**开段那一刻的快照**:开段之后父会话的新回合不进这条分支(否则每次追问都要重算种子,前缀跟着变,缓存白拿),用户要带上新内容得「清空」另起一段。
101
+ **为什么自建侧会话、以及为什么种子取"已完成回合前缀"。** 现在的做法是 `ctx.agents.create()` 建一个**普通侧会话**(`meta.parentSession` 只当血缘,**不设** `origin:'subagent'` —— 不需要子代理的生命周期,也不想让主会话里出现子代理卡片),出生时用公开的 `session.snapshotEvents()` 读出父会话事件、自己切到最后一个 `turn/end` 当种子,再在 `setup` 里 `agentPresets.composeFrom(agentCtx, parent.ctx)` 继承父会话的 preset/工具/persona。这样提示词前缀与父会话逐字一致,**继续命中原有的前缀缓存**。
102
+
103
+ **为什么每轮都"重建"(re-fork),而不是让一段会话一直活下去。** 一段会话活着,它的种子就冻在建段那一刻 —— 主会话之后聊了什么,这条分支**永远看不到**,用户只能「清空」另起一段(0.1.x 就是这样)。反过来,每次追问都新建一段、重新取主会话**最新**的已结算前缀当种子、再把侧枝自己历来的问答重放进去,就同时拿到两件事:**主会话的更新**和**侧枝自己的连续**。缓存上也不亏:主会话最新的那段内容**刚被主会话自己请求过**,很可能还在供应商的热缓存里,re-fork 的请求前缀正好和它对得上(走缓存读);而"前缀冻结 + 往后追加"的做法里,同样的内容是被侧枝**第一次**以那种形态发出去(走缓存写)。**只在有变化时才重建**(主会话没长、模型也没换 ⇒ 沿用当前段):种子会逐字节相同,重建只会多一条平台删不掉的会话记录与一次深拷贝。
79
104
 
80
105
  **为什么侧会话要归档、又为什么要"跑前解锁、跑完回藏"。** 侧会话是一条**真实会话**,不处理它就会在左侧边栏多出一行、和用户自己的对话混在一起。官方把会话从分组界面藏起来的唯一手段就是**归档**(官方 README 自己把归档集描述成 "sessions hidden from every grouping surface"),所以本插件建段即归档。问题是 `0.1.7-rc.1` 给"已归档"加了第二层语义:**已归档 ⇒ 不许运行**(`ArchivedSessionGate` 在 `agent/pre-step` 上拒绝)。于是"建段即归档"这个写法在新版本上会**自己把自己锁死**(每一轮都在 `followup` 之前被拦,连模型请求都不发)。而**不能**改成"干脆不归档"就完事——那会让侧会话重新出现在会话列表里,那是产品体验、不是实现细节。现在的取舍是三步:**空闲时归档(列表干净)→ 每轮投递前取消归档(不被闸)→ 该轮结算后再归档(列表重新干净)**。代价如实登记:回答进行中它可能短暂出现在侧栏,以及每轮多两三次持久写盘。另一条路是官方 `origin: 'subagent'`(侧栏原生隐藏子代理来源的行),但它会连带子代理的生命周期语义,与"不设 `origin:'subagent'`"那条前提冲突,所以没走。
81
106
 
@@ -83,7 +108,18 @@
83
108
 
84
109
  **为什么设置不走官方设置服务。** `settings.register(ns, schema)` 要的是 **schemastery 的 `z<T>` 本身**(`dsh-settings` 的 `resolve()` 会直接调用它,schema 必须可调用),而 `@deepseek-ai/schemastery` 是平台内置包,从插件工作区里**静态 import / 动态 import / `createRequire` 三种方式都解析不到**(一律 `ERR_MODULE_NOT_FOUND`);手工造一个"兼容 schema"也不行(节点对象靠属性工作、不可调用)。⇒ 设置改为客户端 `localStorage` 持久化、每次请求把值带给宿主,宿主只做校验与夹紧。代价是官方设置页里看不到这个命名空间,插件自带齿轮面板。
85
110
 
86
- **为什么到达上下文上限是"拒绝"而不是"丢最早的轮次"。** 后者实现更简单、用户也不会被打断,但会让模型**静默失忆**:它不记得被丢掉的内容,却仍然表现得像记得,用户很难察觉。所以做法是每轮读子会话 `assistant/message` 的 usage(`input + cacheRead`)与 `request/context` 的 `contextWindow`,超过窗口的 80% 就拒绝发送,并把实际用量与上限显示在面板上。
111
+ **为什么到达上下文上限是"拒绝"而不是"丢最早的轮次"。** 后者实现更简单、用户也不会被打断,但会让模型**静默失忆**:它不记得被丢掉的内容,却仍然表现得像记得,用户很难察觉。所以做法是**到上限就拒绝发送**,把 `used` / `limit` / `window` 一起回给面板(`contextFull`),**绝不截断历史**。
112
+
113
+ ⚠️ **判据在 0.2.0 从"读 usage"改成了"估算"。** re-fork 之后新会话**还没有跑过任何一轮** ⇒ 老判据 `conv.lastContextTokens`(子会话 `assistant/message` 的 `input + cacheRead`)必然是 `undefined`,用不了。现在的口径(`estimateRequestTokens`)是:
114
+
115
+ ```text
116
+ used ≈ 父会话前缀的提示词 tokens(实测:父会话日志里最后一条 assistant/message 的 input + cacheRead + cacheWrite)
117
+ + (侧枝历史字符数 + 本轮问题字符数) × CONTEXT_ESTIMATE_SAFETY
118
+ ```
119
+
120
+ 超过 `contextWindow × CONTEXT_LIMIT_RATIO`(0.8)就拒绝。拿不到 `contextWindow` 时上限取 `CONTEXT_LIMIT_FALLBACK_TOKENS`,拿不到父会话 usage 时父前缀那半取 `CONTEXT_ESTIMATE_PARENT_FALLBACK`。⚠️ **不从父 Agent 上读 `lastContextTokens`**:**父 Agent 上没有这个字段**(那是本插件自己按会话事件维护的)。
121
+
122
+ ⚠️ **如实登记:`used` 是估算值。** 父前缀那一半是实测,侧枝历史与问题那一半是"字符数 × 安全系数" ⇒ 面板上显示的这个数字**不是**实测用量,它和该轮结束后用量行里的真实数字对不上是正常的。宁可早拒也不误发。
87
123
 
88
124
  **为什么放行名单不可配置。** 只读承诺的支点越少越好。如果放行面由"宿主常量 × 设置里的名字集合 × 客户端提交上来的子集"三者求交而成,三处必须始终一致,任何一处写错都可能让放行面变宽。现在只有一处:`ALLOWED_TOOLS` 与 `TOOL_CHOICES` 两个同源常量——模型看到的分支引导词也从同一个常量取,所以"告诉模型能用什么"与"执行层实际放行什么"不可能分叉。代价是用户无法自行收窄权限,想收窄只能改代码。
89
125
 
@@ -95,28 +131,40 @@
95
131
 
96
132
  | 方法与路径 | 请求体 | 响应 |
97
133
  | --- | --- | --- |
98
- | `POST /side-branch/start` | `{ sessionId, question, selection?, conversationId?, locale?, settings? }` | `{ jobId, conversationId, handoff? }`;失败时 `{ error, code }`,可能另带 `conversationGone: true` 或 `contextFull: { used, limit, window }` |
99
- | `GET /side-branch/stream?job=<jobId>` | — | SSE:`snapshot` / `delta` / `reasoning` / `reset` / `replace` / `tool` / `done` / `error` / `stopped` |
134
+ | `POST /side-branch/start` | `{ sessionId, question, selection?, conversationId?, locale?, settings? }` | `{ jobId, conversationId, handoff?, synced?, notice? }`;失败时 `{ error, code }`,可能另带 `conversationGone: true`、`contextFull: { used, limit, window }` |
135
+ | `GET /side-branch/stream?job=<jobId>` | — | SSE:`snapshot` / **`segment`** / `delta` / `reasoning` / `reset` / `replace` / `tool` / `done` / `error` / `stopped` |
100
136
  | `POST /side-branch/stop` | `{ job }` | `{ status }`,只停这一轮,侧会话保留 |
101
137
  | `POST /side-branch/close` | `{ conversationId }` | `{ status }`,释放这一段侧会话 |
138
+ | `GET /side-branch/inherited?conversationId=&locale=` | — | **只读**:这一段**实际继承到**的主会话回合(`{ turns, chars, truncated, parentHasNewer, synced }`;最后一轮全文、更早每轮一行摘要) |
102
139
  | `GET /side-branch/settings` | — | `{ settings: { quickEntry } }` |
103
140
  | `POST /side-branch/settings` | `{ settings: { quickEntry } }` | `{ settings: { quickEntry } }` |
104
141
 
142
+ `/start` 响应里三个可选字段的含义(客户端按它们决定行为):
143
+
144
+ - `handoff: true` = 这一次**重建了**这一段(re-fork)。历史是**接得上**的 ⇒ 客户端**不要**画"以下是新的一段"分隔线。
145
+ - `synced: { turns, chars }` = 这一次继承了多少主会话内容(面板那一行「已继承主会话 N 轮 · 约 X 字」)。
146
+ - `notice` = **注入给模型的分支引导词全文**(只有真的拼了的那一轮才回,即每段第一轮)。这是本插件注入的**唯一**文本;系统提示词与工具声明是 DSH 自己组装的,不属于本插件、不在这里给。
147
+
148
+ SSE 的载荷变化(0.2.0):新增 `segment`(进入下一个 step ⇒ 开新段);`snapshot`/`done`/`stopped`/`error` 都带 `segments`(有序段落 `{kind, text, turn, step}`);`reset`/`delta`/`reasoning`/`replace`/`tool` 都带 `turn`/`step`(客户端靠它精确地把 `reset` 限定在**同一个 step** 上)。
149
+
105
150
  设置只有一项:`quickEntry`(主会话快速入口按钮的开关)。**不接受**客户端提交工具名单——放行名单是宿主侧的常量,插件不提供"自选可用工具"的功能。若哪天要给这个功能开口子,先读 `lib/index.js` 头部「只读必须在执行层保证」那一段。
106
151
 
107
152
  失败的响应带机器可读的 `code`,一律 `side-branch/` 前缀加 kebab-case,例如 `side-branch/question-too-long`、`side-branch/context-full`。客户端按 `code` 分支,不按文案分支。路径存在但方法不对时回 **405 + `Allow`**(支持的方法表在 `lib/index.js` 的 `ROUTE_METHODS`),不是 404。
108
153
 
109
154
  ## 验证改动
110
155
 
111
- > ⚠️ 下面这条命令**只在源码仓库里有效**:`scripts/smoke.mjs` 不随 npm 包发布
112
- > (它读 `lib/` 与 `package.json` 做源码级断言,从 `node_modules` 里跑没有意义)。
156
+ > ⚠️ 下面这两条命令**只在源码仓库里有效**:`scripts/smoke.mjs` 与 `scripts/client-check.mjs`
157
+ > 都不随 npm 包发布(前者读 `lib/` 与 `package.json` 做源码级断言、后者真的把客户端 bundle 跑起来,
158
+ > 两者脱离源码树都没有意义)。
113
159
  > 如果你是照 npm 装好的插件在做二次检查,请到仓库里跑。
114
160
 
115
161
  ```powershell
116
- node scripts/smoke.mjs
162
+ node scripts/smoke.mjs # ① 源码级契约自检
163
+ node scripts/client-check.mjs # ② 客户端运行时自检
164
+ # 两条一起跑:npm run check | 只跑客户端那条:npm run client-check
117
165
  ```
118
166
 
119
- 不需要 DSH 运行时,直接加载宿主半并用假 ctx 调 `apply()`。它守的六处:
167
+ **① `scripts/smoke.mjs`(源码级)** 不需要 DSH 运行时,直接加载宿主半并用假 ctx 调 `apply()`。当前共 **153 条断言**,分十节:
120
168
 
121
169
  1. 模块能加载、导出名与 `package.json` 一致、路由前缀是 `/side-branch`、卸载 disposer 可调用;
122
170
  2. **只读承诺**:`ALLOWED_TOOLS` 与 `TOOL_CHOICES` 仍是那六个名字且互相一致,九个危险工具名都不在其中;客户端没有工具开关的残留、宿主不接受客户端提交的工具名单;分支引导词的名单直接来自常量;guard 的判据仍是按名单放行、注册点仍在 `setup(agentCtx)` 里、拿不到 guard 仍 fail-loud。
@@ -124,21 +172,63 @@ node scripts/smoke.mjs
124
172
  3. 客户端 bundle 存在、inject 仍是那五个服务名、不含调试导出;
125
173
  4. `package.json` 与 `cordis.patch.yml` 的包名一致;
126
174
  5. `dsh.client.inject` 里每个包在本机 DSH 安装目录里真实存在(用 `DSH_INSTALL_DIR` 指定路径,找不到就跳过这一项);
127
- 6. **归档闸对策**:解锁必须排在 `followup` 之前、必须经 `typeof` 特性探测、失败不得抛;回藏必须挂在 `finishJob`、要有界退避、同样不得抛;建段仍然先归档。这一节守的就是"同一份源码同时兼容 0.1.5 与 0.1.7"这条线。
175
+ 6. **归档闸对策**:解锁必须排在 `followup` 之前、必须经 `typeof` 特性探测、失败不得抛;回藏必须挂在 `finishJob`、要有界退避、同样不得抛;建段仍然先归档。这一节守的就是"同一份源码同时兼容 0.1.5 与 0.1.7"这条线;
176
+ 7. **Re-Fork 架构(0.2.0)**:`shouldReFork` 必须**同时**判"主会话多了已结算回合"与"模型/推理等级变了"两条;`parentCut` 必须在**建段时**就记(睡觉/唤醒也要带着它走,漏了就是每轮白 re-fork);重放白名单**正好**是 5 种 surface 事件(`system/message`、`developer/message`、`user/message`、`assistant/message`、`tool/result`)且不含纯日志事件;重放**绝不复制 `sourceEventSeqs`**;重放**不截断**;**先重放成功、再释放老会话**(失败时丢弃刚建的新会话);每轮 re-fork 都回 `handoff: true`;旧的 `seedSource` 换段路径已彻底删除。这一节还守着上限改成估算的三处:`estimateRequestTokens` / `lastPromptTokensOf` 存在,且**不再**用 `conv.lastContextTokens` 那条 re-fork 之后必然为 `undefined` 的旧判据;
177
+ 8. **多段回答(0.2.0)**:`job.segments` 有序段落;`start` 帧按 `turn`/`step` 分流(同 step 换 `attemptId` 才算重试 ⇒ 只丢该 step,step 变了 ⇒ 发 `segment` 开新段);`replace` **只**替换当前段;`snapshot` 与终态都带 `segments`;`syncSegmentShortcuts` 让 `text`/`reasoning` 成为段落数组的**纯函数**;旧的 `attempts` 字段已拆成 `steps`/`retries`;客户端 `normalizeSegments` 对**老数据**(只有 `answer`/`reasoning`)向后兼容;另守「引用随轮入档」:发送时把收窄后的引用原文记进该轮(`reference`),渲染成问题上方的可折叠引用块,并**自动清空**引用槽;`normalizeRound` 保留它 ⇒ 刷新后引用块还在。
178
+ 9. **启动清理与只读路由(0.2.0)**:`/inherited` 登记进 `ROUTE_METHODS`(不登记的话错方法会掉进 404,破坏 405 + `Allow` 契约);`handleInherited` 按"最后一轮全文 + 更早每轮一行摘要"截断;继承内容**不进** `normalizeRound`(只放组件内存态);孤儿判据要求目录名**正好**是 `side-<uuid>`;**实例租约**(`side-branch-instances/*.json`,心跳 + PID 判活);发现别的活着的实例就**整个跳过**;删除留**清单日志**;清理严格限定在 `sessions` 根目录下、只认 `side-` 前缀(不碰 `session-*` 主会话)。
179
+ 10. **与主会话对齐(0.2.2)**:`ANSWER_TIMEOUT_MS`(90 秒总时长硬超时)**已删除**,只剩 `STALL_TIMEOUT_MS` 停滞看门狗(≥ 10 分钟、**任一事件**都重挂:建 job / 流式帧 / 会话事件三处);`turn/end` 的 `error` 原因取具体 `message` 交给面板(不再只显示「意外终止:error」);划选入口的作用域判据 `insideMainConversation` 存在且被 `selectionText` 调用,判 `[data-conversation-content]` / `[data-conversation-scroll]`、排除 `[data-sidebar-right-panel]` / `[data-sidebar-right-session]` / `[data-sidebar-chat]` 与 composer 座位。
180
+
181
+ **① 不验证**的东西:回答质量、真实 guard 拦截(第 2 节那几条是**源码级正则断言**,不是真驱动一次拦截)、SSE 行为与界面 —— 客户端那半由下面的 ② 补到"渲染树"为止;真实 DOM 布局 / CSS / 真实滚动位置 / 真实网络 / 真实模型它同样够不到,那几处只能靠仓库**外**的一次性 headless 端到端 harness(见下)与人工。
182
+
183
+ ### ② 客户端运行时自检(`scripts/client-check.mjs`)
184
+
185
+ ```powershell
186
+ node scripts/client-check.mjs # 只跑这一条
187
+ node scripts/client-check.mjs <别的 client.js 路径> # 反向验证脚本本身(见下)
188
+ ```
189
+
190
+ **为什么会有它。** 人工看面板时连撞了几个 bug,而当时**三处自动化全都查不出来**:`node --check` 只看语法;`scripts/smoke.mjs` 是**源码级正则断言**;端到端 harness 打的是 **HTTP 路由、根本不加载客户端**。最严重的一个是:`const MAX_SEGMENTS = 200` 等常量写在 `createPanelStore()` **内部**,却在 `SideBranchBody` **组件**里用 ⇒ 第一个 SSE `delta` 帧一到就抛 `ReferenceError: MAX_SEGMENTS is not defined`,被浏览器的事件派发**吞掉**(只在 console 里)⇒ 每个增量都丢、**答案永远不显示**,用户看到的是"问了之后面板像卡住"。这个脚本就是为补这个窟窿写的,并且**反向验证过**:拿一份故意改坏的副本跑,它确实 FAIL 并打印 `FAIL SSE 帧 snapshot 的处理函数抛错:MAX_SEGMENTS is not defined`。
191
+
192
+ **它怎么做到的。** 不装浏览器,而是在 Node 里把 `lib/client.js` **真的跑起来**:bundle 的形态是 `window.__ModuleLoader__.load({ factory })`,它只从 `require` 取三样东西 —— `react`、`react-dom`、`@deepseek-ai/dsh-client-ui-primitives`(后两样**故意不提供**,bundle 自带降级分支,正好连降级路径一起测了);用到的 React API 只有 **8 个**(`createElement` / `useState` / `useEffect` / `useLayoutEffect` / `useReducer` / `useRef` / `Fragment` / `Component`),所以一个极简 React + 极简 DOM 就够。面板的能力**全部来自席位的 `inject` face**(`inject: () => ({ startSideBranch, … })`),脚本照官方那样把它展开进 props —— 顺便也就验了 face 里该有的键。再用真实形状的 `fetch` / `EventSource` 桩驱动:往输入框打字(走真实 `onChange`)、点发送(走真实 `onClick`)、喂真实形状的 SSE 帧。
193
+
194
+ **它验了什么。** bundle 能装载、导出 `apply()`、正文组件被注册并渲染成功;客户端把词典注册进了 locale 服务(断言因此盯的是**真实文案**,不是"返回 key"那种弱断言);点发送确实发**一次** `POST /side-branch/start` 且带上问题原文,并打开指向该 job 的 SSE 流;喂进去的每一帧都有监听函数;**多段正文都渲染出来且顺序正确、工具行夹在两段正文之间**("只剩最后一段"那个 bug 的回归判据);注入的分支引导词全文可见;**自动滚底落在真正可滚动的那一层**上(`.dsh-side-branch-scroll`),而不可滚动的轮次列表上没有挂 ref;**划选入口的作用域**:假 DOM 里造出祖先链(主会话正文 / 右栏文件预览 / 设置弹层 / 右栏里的子代理嵌入会话 / composer 座位 / 本插件面板),走真实的 `selectionchange` → 120ms 防抖 → `update()` 路径,断言浮动按钮该弹的弹、该藏的一个都不弹。
128
195
 
129
- 它**不验证**回答质量、真实 guard 拦截、SSE 行为与界面。端到端验证需要:
196
+ 当前共 **42 项**,PASS/FAIL 直接看 stdout(全过时末行是 `全部通过(42 项)`,有失败则以非零退出码结束)。
197
+
198
+ ⚠️ **它不覆盖什么(如实登记)**:真实 DOM 布局、CSS、真实滚动位置、真实浏览器事件、真实网络与真实模型。它是**"数据 → 渲染树"这一段的测试,不是浏览器测试** —— 那几处仍然只能人工在浏览器里看。
199
+
200
+ **可选参数用于反向验证脚本本身**:`node scripts/client-check.mjs <别的 client.js 路径>` 让它去跑另一份 bundle(例如一份故意改坏的副本),那份**必须 FAIL**。**一个不会失败的测试等于没有测试。**
201
+
202
+ **手工**端到端验证需要:
130
203
 
131
204
  1. 一个已装好本插件的 profile;
132
205
  2. 一个主会话,至少完成一个回合;
133
206
  3. 在页面里划选文字、打开面板、提问;
134
207
  4. 检查启动日志出现 `[side-branch]` 前缀的行。
135
208
 
136
- 仓库内没有端到端测试套件。改动 guard、提示词前缀、模型可见文本(`PROMPT_TEXT`)、归档闸那几处或 SSE 时,第 2 步到第 4 步必须人工跑一遍。
209
+ 仓库内**有两个自检(源码级 + 客户端运行时),但没有端到端套件**。改动 guard、提示词前缀、模型可见文本(`PROMPT_TEXT`)、归档闸那几处、Re-Fork、多段回答或 SSE 时,第 2 步到第 4 步必须人工跑一遍;改动**客户端半**(`lib/client.js`)时同样必须人工跑一遍 —— `scripts/client-check.mjs` 只把"数据 → 渲染树"那一段自动化了,界面本身还是得自己看。
210
+
211
+ ### 仓库外的一次性 headless 端到端 harness
212
+
213
+ 仓库里**没有**端到端套件(它是测试资产:不随 npm 包发布,也不该留在工作区里)。当改动落在单测够不到的地方 —— guard 的真实拦截、提示词前缀、归档闸、Re-Fork、多段回答、SSE —— 就自己搭一个**一次性** harness。做法:在**同一个 DSH 进程**里同时扮演两个角色 ——
214
+
215
+ - **客户端**:先按官方信任栅栏换成浏览器会话 cookie,再打插件**真实的** `POST /side-branch/start`,并读 `GET /side-branch/stream` 的 SSE;
216
+ - **主会话**:用 `ctx.agents.create` 建一条**真的**父会话(`agentPresets.resolve` + `mount` 挂上 `standard` preset,所以**有工具**)并跑真回合。
217
+
218
+ 它能覆盖 `scripts/smoke.mjs` 够不到的那几处:侧枝这一轮**真的走了工具**(`tool/call` + `tool/result` 都在);主会话推进后追问 ⇒ re-fork(`conversationId` 变了、`handoff: true`、继承轮数递增);re-fork 后模型同时答得出**三个来源**的暗号(只在侧枝历史里的 / 只在最新主会话前缀里的 / 只在**被重放的工具结果**里的);主会话没变化 ⇒ **不**重建;只换推理等级 ⇒ **也**重建;`GET /side-branch/inherited` 返回 200 且最后一轮有全文、错方法回 **405 + `Allow: GET`**;侧枝自己的暗号**没有回流**主会话;跑完的侧枝**回到归档集合**;伪造 `conversationId` 回 `conversation-gone`;以及"被重放的 `tool/result` 与新段里那条 `tool/call` 的 `toolCallId` 对得上"(孤儿工具结果是这条链路最阴的坑)。
219
+
220
+ ⚠️ 只在**发布前**做一次性验证:跑完别把 harness 提交进仓库,也别把它的日志或结论写进 `docs/`。
137
221
 
138
222
  ## 提交与推送
139
223
 
140
224
  - **提交信息(commit message)一律用英文。** 这是公开仓库、历史面向英文读者:标题用 `type: 祈使句`(如 `fix: un-archive the side session before each turn`),正文讲"为什么",**不要混中文**。
141
225
  - 建议拆成两个提交:`lib/` + `scripts/` 的行为改动一个,纯文档 / 版本号一个。
142
- - **推之前**先跑 `node scripts/smoke.mjs`(上一节),并在真宿主里人工跑一遍受影响的链路。
143
- - 版本号与 `docs/CHANGELOG.md` 同步更新;发布时打 `v<version>` 标签(已有 `v0.1.0`)。
226
+ - **推之前**先跑 `node scripts/smoke.mjs` 与 `node scripts/client-check.mjs`(上一节;或 `npm run check` 两条一起跑),并在真宿主里人工跑一遍受影响的链路。
227
+ - 版本号与 `docs/CHANGELOG.md` 同步更新;发布时打 `v<version>` 标签(已有 `v0.1.0`、`v0.1.1`、`v0.2.0`;本次发布打 `v0.2.2`)。
228
+ - **发布走 GitHub Actions(npm Trusted Publishing / OIDC)+ 暂存发布**:推 `v<version>` 标签触发 `.github/workflows/release.yml`,流水线校验 tag 与 `package.json` 一致、跑两条自检,再 `npm stage publish` 把 tarball 放进暂存队列;维护者在 npmjs.com 的 **Staged versions** 点 **Approve**(安全密钥确认)之后才真正上线并更新 `latest`。
229
+ - 信任配置是 **stage-only**(只勾 `npm stage publish`,**不勾** `npm publish`):该配置下 `npm publish` 会被 npm 直接拒掉,流水线也只能暂存 ⇒ 即使有人推了恶意 tag,没有人工批准也上不了线。
230
+ - 首次配置:npmjs.com → 该包 → **Settings → Trusted Publishing** → 添加 GitHub Actions,Repository = `wucl12/dsh-side-branch`,Workflow filename = `release.yml`,Allowed actions 只勾 `npm stage publish`。
231
+ - 审批**必须在网页上做**:本机 2FA 是安全密钥,而 `npm stage approve` 命令行要的是 TOTP 验证码,用不了。
232
+ - 流水线需要 npm ≥ 11.15.0 才有 `npm stage`,所以 workflow 里显式 `npm install --global npm@^11.15.0`,不押运行器自带哪个版本。
233
+ - 本地 `npm publish` 只当应急手段:npm 现在要求 2FA 或带 bypass 的 granular token,而后者正在被下线。
144
234
  - 远端:`origin` = `github.com/wucl12/dsh-side-branch`,主分支 `main`。
package/README.md CHANGED
@@ -1,88 +1,125 @@
1
- # DSH Side Ask (Side Branch)
1
+ # Side Ask · DSH Side Branch
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-side-branch?label=npm&color=4D6BFE)](https://www.npmjs.com/package/dsh-side-branch)
4
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-side-branch?label=downloads)](https://www.npmjs.com/package/dsh-side-branch)
5
+ [![License](https://img.shields.io/github/license/wucl12/dsh-side-branch?label=license)](LICENSE)
6
+ [![DSH](https://img.shields.io/badge/DSH-0.1.5%20%E2%86%92%200.2.1-4D6BFE)](https://github.com/deepseek-ai/deepseek-harness)
2
7
 
3
8
  English | [简体中文](./docs/README.zh-CN.md)
4
9
 
5
- Select text in the main session, and a read-only branch session will pop up in the right sidebar. It inherits the existing context of the main session, streams its answers token-by-token, and **its responses never pollute the model context of the main session**.
10
+ **Quote any passage in the main session and ask about it in a read-only branch in the right sidebar. The answer streams there — and never enters the main session's model context.**
6
11
 
7
- Core Scenario: Asking follow-up questions, looking up terms, or exploring ideas based on the current conversation without interrupting the flow of the main session.
12
+ You know the moment: the agent is mid-refactor and you just want to ask "where else is this function used?" — without the main session remembering the question, and without re-processing a long, expensive context for a side query.
8
13
 
9
- * **Package Name (Implementation):** `dsh-side-branch`
10
- * **UI Name (Display):** Side Ask
11
- * **Prerequisites:** Node.js ≥ 20, and `pnpm` installed in the local `PATH`.
12
- * **Verified DSH Version:** `0.1.5-rc.2` and `0.1.7-rc.2`
14
+ - Ask about the current conversation without adding a token to it.
15
+ - Point the branch at a cheaper model while the main session keeps the expensive one.
16
+ - Let a long task keep running, and ask something unrelated on the side.
13
17
 
14
- <p align="center">
15
- <img src="assets/sidebar-guide.png" alt="The Side Ask entry in the right sidebar guide" width="600">
16
- </p>
18
+ ```bash
19
+ dsh plugin --profile web add dsh-side-branch
20
+ # then restart DSH — client plugins load at startup; refreshing the browser is not enough
21
+ ```
17
22
 
18
- <p align="center">
19
- <img src="assets/selection-entry.png" alt="Selection Entry" width="600">
20
- </p>
23
+ `--profile` should be the profile you actually run (`web`, `desktop`, …). Requires Node.js ≥ 20 and `pnpm` on your `PATH`.
24
+
25
+ > **Written by the agent itself.** Every line of code in this repository was written by DSH in a real session. The author's part was the workflow: setting the direction, reviewing the plans, and verifying the behaviour by hand. See [How this was built](#how-this-was-built).
21
26
 
22
27
  <p align="center">
23
- <img src="assets/panel-answering.png" alt="Panel Answering" width="600">
28
+ <img src="assets/panel-answering.png" alt="The branch streams its answer in the right sidebar" width="700">
24
29
  </p>
25
30
 
26
- ## 💡 Why Build This? (Motivation)
31
+ ## What makes it different
27
32
 
28
- Before developing this project (Sep 2026), I surveyed 14 side-chat/bubble plugins in the DSH ecosystem and found that **none of them could simultaneously meet all the following hard requirements**:
33
+ **The answer never comes back.** The host never writes the branch's output into the parent session. What you ask in the branch stays in the branch — the main agent will not suddenly "know" what you discussed on the side.
29
34
 
30
- 1. **True Context Inheritance** (not just a truncated summary).
31
- 2. **Support for Multi-turn Follow-ups** within the same branch (not a one-off bubble).
32
- 3. **Token-by-Token Streaming** (including reasoning processes).
33
- 4. **Lightweight & Stable** (using only official public APIs, no bloated dependencies).
34
- 5. **Execution-Layer Read-Only, strictly preserving the prefix cache.**
35
+ **It cannot touch your files.** Read-only is enforced when a tool is *called*, not by hiding tools from the prompt. `ctx.tools.guard()` passes six side-effect-free tools (`read`, `glob`, `grep`, `web_search`, `web_fetch`, `lsp`) and refuses everything else. Because the tools stay in the prompt, the branch's prompt prefix is byte-identical to the main session's, so it keeps hitting the parent's prefix cache — measured 97%–98%, which is what makes a side question nearly free. Why it works this way, and what it costs, is in [`docs/TECHNICAL.md`](docs/TECHNICAL.md).
35
36
 
36
- The closest alternatives all had compromises: `@michengai/dsh-btw` was one-off and lacked streaming; `dsh-side-chat-plus` didn't inherit history by default; `dsh-sidenote` was feature-rich but extremely heavy (requiring a 14.7 MB dependency); `@lukeknow0/dsh-side-chat` had excellent architecture but was unmaintained.
37
+ **It follows the main session.** Every follow-up rebuilds the branch from the main session's latest settled turns and replays the branch's own earlier questions and answers into it. A turn the main session just finished is already visible; something you asked three rounds ago is still remembered.
37
38
 
38
- The biggest divergence lies in how "read-only" is implemented: similar plugins universally use `toolFilter` to forcefully remove tools from the prompt. However, this causes the branch's prompt prefix to diverge from the main session, **rendering the parent session's accumulated prefix cache completely invalid**.
39
- This project strictly chooses to enforce read-only at the **execution layer**. Thus, the branch's prompt prefix remains **byte-identical** to the main session, continuing to enjoy an extremely high prefix cache hit rate (empirically 97%–98%).
39
+ **You can inspect what it was given.** Expand *Inherited from the main session* to see, turn by turn, what the branch actually received — plus the preamble injected into its prompt, verbatim.
40
40
 
41
- ## ✨ Core Features
41
+ ## Install
42
42
 
43
- * **True Branch, Zero Pollution:** The host strictly prevents writing anything back to the parent session. The Side Ask Q&A will not appear in the main session's model context.
44
- * **Rapid Prefix Cache Hits:** Automatically reads the completed turn prefix of the main session as a seed. When using the same model, it hits the existing prefix cache, avoiding recalculation.
45
- * **Execution-Layer Read-Only Isolation:** Security policies do not rely on prompts. Forced takeover by the underlying `ctx.tools.guard` whitelists only six query tools without side effects (`read`, `glob`, `grep`, `web_search`, `web_fetch`, `lsp`). Other instructions (like writing files or executing scripts) are directly rejected at the execution layer.
46
- * **Ultimate Streaming Experience:** Text and reasoning processes (collapsible "Thinking" rows) are streamed token-by-token. Background generation won't interrupt even if the browser tab is closed.
47
- * **Persistent Session State:** Panel states are persisted locally per "Session + Panel Grid". Automatically sleeps when idle and silently wakes up on the next query.
48
-
49
-
50
- ## 📦 Installation & Setup
51
-
52
- After installation, you **must restart DSH** for the client plugin to load properly.
53
-
54
- **Install via npm (Recommended):**
55
43
  ```bash
44
+ # npm
56
45
  dsh plugin --profile web add dsh-side-branch
57
- ```
58
46
 
59
- **Install via GitHub:**
60
- ```bash
47
+ # or straight from GitHub
61
48
  dsh plugin --profile web add github:wucl12/dsh-side-branch
62
49
  ```
63
50
 
64
- > 🤖 **Prompt for AI Agents:** If you are an Agent responsible for installing this plugin or reading its source code, please prioritize reading [`AGENTS.md`](AGENTS.md) in this repository for hard constraints, architectural details, and automated installation guides.
51
+ Use the profile you actually run (`web`, `desktop`, …). **Then restart DSH** — client plugins load at startup, and refreshing the browser is not enough.
52
+
53
+ Prerequisites: Node.js ≥ 20, and `pnpm` on your `PATH` (`dsh plugin` forwards to pnpm).
54
+
55
+ Also available from a marketplace — [dsh-market](https://github.com/dsh-market/dsh-market) and the [awesome-dsh-plugin catalog](https://awesome-dsh-plugin.com) both list it.
56
+
57
+ > 🤖 **Prompt for AI agents:** if you are the agent installing this plugin or reading its source, read [`AGENTS.md`](AGENTS.md) first — it carries the hard constraints, the architecture, and the installation guide.
58
+
59
+ ## How to use
60
+
61
+ 1. **Ask via selection:** select text in the main session and click the pop-up **Quote and ask**. The branch opens in the right panel with the passage quoted. That passage is recorded with the round as a collapsible quote block, and the composer's quote slot clears itself so the next question starts clean.
62
+ 2. **Quick entry:** the `↗` button to the right of the main session's input opens the panel directly (can be disabled in settings).
63
+ 3. **Independent settings:** inside the panel you can give the current branch its own model and reasoning effort, without following the main session.
64
+
65
+ <p align="center">
66
+ <img src="assets/selection-entry.png" alt="Select text in the main session to open the branch" width="700">
67
+ </p>
68
+
69
+ <p align="center">
70
+ <img src="assets/sidebar-guide.png" alt="The Side Ask entry in the right sidebar guide" width="700">
71
+ </p>
72
+
73
+ ## Compatibility
74
+
75
+ | DSH | Status |
76
+ | --- | --- |
77
+ | `0.1.5-rc.2` | ✅ verified |
78
+ | `0.1.7-rc.2` | ✅ verified (archive gate) |
79
+ | `0.2.0-rc.2` | ✅ verified — loads, `/side-branch` routes register, panel exercised by hand |
80
+ | `0.2.1-rc.3` | ✅ verified — loads, `/side-branch` routes register and answer (200 / 405 + `Allow`), and the served client bundle carries the scoped quote entry |
81
+
82
+ The version-dependent seams (the archive gate, `unarchiveSession`) are feature-detected at runtime rather than compared by version number, which is why one published version serves all four. Details in [`docs/TECHNICAL.md`](docs/TECHNICAL.md).
83
+
84
+ The quote entry added in `0.2.2` needs the conversation-surface markers DSH renders around a session's body (`[data-conversation-content]`, or `[data-conversation-scroll]` on older hosts). On a host that renders neither, the entry stays hidden rather than guessing — everything else in the plugin is unaffected.
85
+
86
+ ## Security & limitations
87
+
88
+ * **Read-only boundaries:** "read-only" here means **no system side effects** (no writing files, no running commands). `web_fetch` / `web_search` still have network access, and `read` can read any file the process can. The allowlist is hardcoded and cannot be relaxed through settings.
89
+ * **What the branch can see:** it is rebuilt from the main session's **latest settled turns**, so a turn the main session has finished follows along automatically. A turn it **has not finished yet** is not visible (the cut is the last `turn/end`) — but **quoting it by hand is unaffected**: the limit is on seeing it automatically, not on being able to quote it.
90
+ * **Rebuilds leave session records:** each rebuild creates a new side-session record, and the platform has **no API to delete a session**, so records accumulate. They are reclaimed by an orphan sweep at plugin startup, which logs what it deleted. While two instances share the same `DSH_HOME` and run at once, that sweep skips entirely — a second DSH never has its live branches deleted.
91
+ * **The context ceiling is a refusal:** on reaching it the plugin refuses the turn and asks you to clear; the oldest turns are never silently dropped. The `used` figure it reports is an **estimate**, so it will not match the real usage once a turn settles.
92
+ * **Cache hits are measured, not guaranteed:** 97%–98% is a measurement, not a promise — the platform may replace the system node in place on the branch's first step, invalidating the cache from token 0 on. And hitting the cache does not mean those tokens stop occupying the context window.
93
+ * **PTC mode:** if the main session uses the `ptc` preset (a code-execution-only tool surface), the branch falls back to plain text Q&A, because `run_code` is always refused.
94
+
95
+ ## How it compares
96
+
97
+ Side-chat plugins here differ mainly in where the answer ends up and what the side session is allowed to do.
98
+
99
+ | | **Side Ask** (`dsh-side-branch`) | [`dsh-sidebar-qa`](https://github.com/ChenRuoT/dsh-sidebar-qa) | [`dsh-selection-toolbar`](https://github.com/suiyideali/dsh-selection-toolbar) |
100
+ | --- | --- | --- | --- |
101
+ | Where the answer ends up | A separate branch; the host never writes it back | A separate DSH session | **The current conversation** — the selection is sent as a user message |
102
+ | Tool access of the side session | Six side-effect-free tools; writes refused when called | Not stated in its README | Same session, so the full tool set |
103
+ | Sees the main session's newest settled turns | Every follow-up (forced rebuild) | Optional — full inherit / summarize / truncate | Yes — it is the same session |
104
+ | Nested follow-ups / follow-up history tree | No | Yes | No |
105
+ | Runtime dependencies | none | none (since 1.0.0) | — |
65
106
 
66
- ## 🚀 How to Use
107
+ > The other two columns are what those projects' public READMEs said at the time of writing, and may have moved on since. One row is deliberately a loss: `dsh-sidebar-qa` does nested follow-ups, this plugin does not. The table is here to help you choose, not to rank anyone.
67
108
 
68
- 1. **Ask via Selection:** Select any text in the main session's body, click the pop-up "Quote and ask", and a temporary session will open in the right panel with the original text sent as a quote.
69
- 2. **Quick Entry:** There is a quick entry button `↗` on the right side of the main session's input box. Click it to directly summon the Side Ask panel (can be disabled in settings).
70
- 3. **Independent Settings:** In the Side Ask panel, you can individually specify the model and reasoning effort for the current branch without following the main session.
109
+ ## How this was built
71
110
 
72
- ## 🛡️ Security & Limitations
111
+ This plugin is a dogfooding result: the entire codebase — host half, client half, and the self-checks under `scripts/` — was written by DSH itself, working against the contracts in [`AGENTS.md`](AGENTS.md). No line of code was written by hand.
73
112
 
74
- * **Read-Only Boundaries:** "Read-only" here means **no system side effects** (like writing files or executing commands). However, `web_fetch` / `web_search` still have network access, and `read` can read files within the process's permissions. The allowlist is hardcoded and cannot be relaxed via settings.
75
- * **Context Snapshot Timing:** The temporary branch inherits a history snapshot **at the exact moment it is opened**. New content generated in the main session afterward will not automatically sync to the current branch. Click "Clear" at the bottom left to start a new topic if you need to include newer context.
76
- * **PTC Mode Exception:** If the main session uses the `ptc` preset (pure code execution tool surface), since `run_code` is permanently rejected by this plugin, the branch will fall back to a **pure text Q&A mode** (no tools can be called).
113
+ The human part was the workflow around it: choosing what to build, setting the direction, reviewing each plan before it was carried out, and verifying the result by hand in a real session. When that verification found something wrong, it went back as a requirement — which is how the archive-gate fix, the multi-segment answer bug and the re-fork design were found.
77
114
 
78
- ## 📚 Further Documentation
115
+ ## Documentation
79
116
 
80
- * [简体中文](./docs/README.zh-CN.md): 中文说明(同一份内容)。
117
+ * [简体中文](./docs/README.zh-CN.md): the same overview in Chinese.
81
118
  * [`docs/TECHNICAL.md`](docs/TECHNICAL.md): full technical reference — prefix-cache mechanics, the read-only enforcement pipeline, PTC behaviour, and every known limitation.
82
- * [`AGENTS.md`](AGENTS.md): Architectural contracts and hard constraints strictly written for AI Agents.
83
- * [`SECURITY.md`](SECURITY.md): Security commitment scope and vulnerability reporting channels.
84
- * [`docs/CHANGELOG.md`](docs/CHANGELOG.md): Version release history.
119
+ * [`AGENTS.md`](AGENTS.md): the architecture contract and hard constraints the agent worked from.
120
+ * [`SECURITY.md`](SECURITY.md): security commitment scope and vulnerability reporting channels.
121
+ * [`docs/CHANGELOG.md`](docs/CHANGELOG.md): release history.
85
122
 
86
- ## 📄 License
123
+ ## License
87
124
 
88
125
  [MIT](LICENSE)