dsh-side-branch 0.2.0 → 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 +15 -4
- package/README.md +92 -59
- package/docs/AGENTS.en.md +15 -4
- package/docs/CHANGELOG.md +42 -0
- package/docs/README.zh-CN.md +90 -57
- package/docs/TECHNICAL.md +4 -4
- package/docs/TECHNICAL.zh-CN.md +4 -4
- package/lib/client.js +42 -6
- package/lib/index.js +62 -17
- package/package.json +7 -3
package/AGENTS.md
CHANGED
|
@@ -84,6 +84,10 @@
|
|
|
84
84
|
|
|
85
85
|
**只读的四条护栏都别动**:`ALLOWED_TOOLS` 常量、`setup(agentCtx)` 里的 `tools.guard`(拿不到就 `throw`,fail-loud)、`ROUTE_METHODS`(405 + `Allow` 的方法契约)、以及宿主侧对 `conversationId` 归属的校验。
|
|
86
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
|
+
|
|
87
91
|
**`peerDependencies` 里的 `@deepseek-ai/cordis` 是声明性的**:插件代码**不 import 它**(宿主半的运行期服务全部经 `ctx` 软获取,客户端半只 `require` 平台内置模块)。它是按生态惯例声明的,用来表明本插件挂在哪套契约上——**别当成漏删的依赖删掉**,也别为了"用上它"去加 import。
|
|
88
92
|
|
|
89
93
|
## 为什么这样做(改动前先读,别照着直觉改回去)
|
|
@@ -160,7 +164,7 @@ node scripts/client-check.mjs # ② 客户端运行时自检
|
|
|
160
164
|
# 两条一起跑:npm run check | 只跑客户端那条:npm run client-check
|
|
161
165
|
```
|
|
162
166
|
|
|
163
|
-
**① `scripts/smoke.mjs`(源码级)** 不需要 DSH 运行时,直接加载宿主半并用假 ctx 调 `apply()`。当前共 **
|
|
167
|
+
**① `scripts/smoke.mjs`(源码级)** 不需要 DSH 运行时,直接加载宿主半并用假 ctx 调 `apply()`。当前共 **153 条断言**,分十节:
|
|
164
168
|
|
|
165
169
|
1. 模块能加载、导出名与 `package.json` 一致、路由前缀是 `/side-branch`、卸载 disposer 可调用;
|
|
166
170
|
2. **只读承诺**:`ALLOWED_TOOLS` 与 `TOOL_CHOICES` 仍是那六个名字且互相一致,九个危险工具名都不在其中;客户端没有工具开关的残留、宿主不接受客户端提交的工具名单;分支引导词的名单直接来自常量;guard 的判据仍是按名单放行、注册点仍在 `setup(agentCtx)` 里、拿不到 guard 仍 fail-loud。
|
|
@@ -172,6 +176,7 @@ node scripts/client-check.mjs # ② 客户端运行时自检
|
|
|
172
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` 的旧判据;
|
|
173
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` 保留它 ⇒ 刷新后引用块还在。
|
|
174
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 座位。
|
|
175
180
|
|
|
176
181
|
**① 不验证**的东西:回答质量、真实 guard 拦截(第 2 节那几条是**源码级正则断言**,不是真驱动一次拦截)、SSE 行为与界面 —— 客户端那半由下面的 ② 补到"渲染树"为止;真实 DOM 布局 / CSS / 真实滚动位置 / 真实网络 / 真实模型它同样够不到,那几处只能靠仓库**外**的一次性 headless 端到端 harness(见下)与人工。
|
|
177
182
|
|
|
@@ -186,9 +191,9 @@ node scripts/client-check.mjs <别的 client.js 路径> # 反向验证脚本
|
|
|
186
191
|
|
|
187
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 帧。
|
|
188
193
|
|
|
189
|
-
**它验了什么。** bundle 能装载、导出 `apply()`、正文组件被注册并渲染成功;客户端把词典注册进了 locale 服务(断言因此盯的是**真实文案**,不是"返回 key"那种弱断言);点发送确实发**一次** `POST /side-branch/start` 且带上问题原文,并打开指向该 job 的 SSE 流;喂进去的每一帧都有监听函数;**多段正文都渲染出来且顺序正确、工具行夹在两段正文之间**("只剩最后一段"那个 bug 的回归判据);注入的分支引导词全文可见;**自动滚底落在真正可滚动的那一层**上(`.dsh-side-branch-scroll`),而不可滚动的轮次列表上没有挂 ref
|
|
194
|
+
**它验了什么。** bundle 能装载、导出 `apply()`、正文组件被注册并渲染成功;客户端把词典注册进了 locale 服务(断言因此盯的是**真实文案**,不是"返回 key"那种弱断言);点发送确实发**一次** `POST /side-branch/start` 且带上问题原文,并打开指向该 job 的 SSE 流;喂进去的每一帧都有监听函数;**多段正文都渲染出来且顺序正确、工具行夹在两段正文之间**("只剩最后一段"那个 bug 的回归判据);注入的分支引导词全文可见;**自动滚底落在真正可滚动的那一层**上(`.dsh-side-branch-scroll`),而不可滚动的轮次列表上没有挂 ref;**划选入口的作用域**:假 DOM 里造出祖先链(主会话正文 / 右栏文件预览 / 设置弹层 / 右栏里的子代理嵌入会话 / composer 座位 / 本插件面板),走真实的 `selectionchange` → 120ms 防抖 → `update()` 路径,断言浮动按钮该弹的弹、该藏的一个都不弹。
|
|
190
195
|
|
|
191
|
-
当前共 **
|
|
196
|
+
当前共 **42 项**,PASS/FAIL 直接看 stdout(全过时末行是 `全部通过(42 项)`,有失败则以非零退出码结束)。
|
|
192
197
|
|
|
193
198
|
⚠️ **它不覆盖什么(如实登记)**:真实 DOM 布局、CSS、真实滚动位置、真实浏览器事件、真实网络与真实模型。它是**"数据 → 渲染树"这一段的测试,不是浏览器测试** —— 那几处仍然只能人工在浏览器里看。
|
|
194
199
|
|
|
@@ -219,5 +224,11 @@ node scripts/client-check.mjs <别的 client.js 路径> # 反向验证脚本
|
|
|
219
224
|
- **提交信息(commit message)一律用英文。** 这是公开仓库、历史面向英文读者:标题用 `type: 祈使句`(如 `fix: un-archive the side session before each turn`),正文讲"为什么",**不要混中文**。
|
|
220
225
|
- 建议拆成两个提交:`lib/` + `scripts/` 的行为改动一个,纯文档 / 版本号一个。
|
|
221
226
|
- **推之前**先跑 `node scripts/smoke.mjs` 与 `node scripts/client-check.mjs`(上一节;或 `npm run check` 两条一起跑),并在真宿主里人工跑一遍受影响的链路。
|
|
222
|
-
- 版本号与 `docs/CHANGELOG.md` 同步更新;发布时打 `v<version>` 标签(已有 `v0.1.0`、`v0.1.1
|
|
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,而后者正在被下线。
|
|
223
234
|
- 远端:`origin` = `github.com/wucl12/dsh-side-branch`,主分支 `main`。
|
package/README.md
CHANGED
|
@@ -1,92 +1,125 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Side Ask · DSH Side Branch
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-side-branch)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-side-branch)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
2
7
|
|
|
3
8
|
English | [简体中文](./docs/README.zh-CN.md)
|
|
4
9
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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="
|
|
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
|
-
##
|
|
31
|
+
## What makes it different
|
|
27
32
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
* **Follows the Main Session, Every Turn:** Every follow-up rebuilds the branch from the main session's **latest settled** content — a fresh seed plus a replay of the branch's own earlier questions and answers — so the branch always sees the main session as it is now **without losing anything it was asked before**. Reusing the main session's model keeps hitting 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
|
-
* **Inspectable by Design:** Expand "Inherited from the main session" to see, turn by turn, what the branch actually received — the question, its reasoning, and the answer. The branch notice injected into the prompt is shown verbatim as well, so nothing about what the model sees is a black box.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
## 📦 Installation & Setup
|
|
52
|
-
|
|
53
|
-
After installation, you **must restart DSH** for the client plugin to load properly.
|
|
54
|
-
|
|
55
|
-
**Install via npm (Recommended):**
|
|
56
43
|
```bash
|
|
44
|
+
# npm
|
|
57
45
|
dsh plugin --profile web add dsh-side-branch
|
|
58
|
-
```
|
|
59
46
|
|
|
60
|
-
|
|
61
|
-
```bash
|
|
47
|
+
# or straight from GitHub
|
|
62
48
|
dsh plugin --profile web add github:wucl12/dsh-side-branch
|
|
63
49
|
```
|
|
64
50
|
|
|
65
|
-
|
|
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) | — |
|
|
66
106
|
|
|
67
|
-
|
|
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.
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
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).
|
|
71
|
-
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
|
|
72
110
|
|
|
73
|
-
|
|
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.
|
|
74
112
|
|
|
75
|
-
|
|
76
|
-
* **What the Branch Can See:** Every follow-up rebuilds it from the main session's **latest settled turns**, so turns the main session has finished do follow along automatically. A turn the main session **has not finished yet** is not visible to the branch (the cut is the last `turn/end`) — but **manual selection quoting is not affected by that**: the limitation is on "seeing it automatically", not on "being able to quote it".
|
|
77
|
-
* **Rebuilds Leave Session Records:** Each rebuild creates a new side-session record, and the platform has **no API to delete a session** ⇒ records would pile up. They are reclaimed by an **orphan sweep at plugin startup**, which logs item by item what it deleted. While **two instances share the same `DSH_HOME`** and run at the same time, that sweep **skips entirely** — a second DSH never has its live branches deleted.
|
|
78
|
-
* **The Context Ceiling Is a Refusal:** On reaching it the plugin **refuses** the turn and tells you to "Clear"; the oldest turns are never silently dropped. `contextFull.used` is an **estimate** (a rebuilt session has no usage yet), so it will not match the real usage once a turn settles.
|
|
79
|
-
* **Cache Hits Are Measured, Not Guaranteed:** The high prefix-cache hit rate is a measurement, not a design promise — the platform may replace the system node in place on the side session's first step, invalidating the cache **from token 0 on**. And **hitting the cache does not mean those tokens stop occupying the context window**.
|
|
80
|
-
* **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.
|
|
81
114
|
|
|
82
|
-
##
|
|
115
|
+
## Documentation
|
|
83
116
|
|
|
84
|
-
* [简体中文](./docs/README.zh-CN.md):
|
|
117
|
+
* [简体中文](./docs/README.zh-CN.md): the same overview in Chinese.
|
|
85
118
|
* [`docs/TECHNICAL.md`](docs/TECHNICAL.md): full technical reference — prefix-cache mechanics, the read-only enforcement pipeline, PTC behaviour, and every known limitation.
|
|
86
|
-
* [`AGENTS.md`](AGENTS.md):
|
|
87
|
-
* [`SECURITY.md`](SECURITY.md):
|
|
88
|
-
* [`docs/CHANGELOG.md`](docs/CHANGELOG.md):
|
|
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.
|
|
89
122
|
|
|
90
|
-
##
|
|
123
|
+
## License
|
|
91
124
|
|
|
92
125
|
[MIT](LICENSE)
|
package/docs/AGENTS.en.md
CHANGED
|
@@ -85,6 +85,10 @@ Failure criteria and what they mean:
|
|
|
85
85
|
|
|
86
86
|
**Do not touch any of the four read-only guardrails**: the `ALLOWED_TOOLS` constant, the `tools.guard` in `setup(agentCtx)` (`throw` if it cannot be obtained — fail-loud), `ROUTE_METHODS` (the 405 + `Allow` method contract), and the host-side validation of `conversationId` ownership.
|
|
87
87
|
|
|
88
|
+
**Do not give a turn a wall-clock timeout.** Apart from the tool allow-list, a branch should not behave differently from the main session: the main session does not cap how long a turn runs (long thinking, long tools, several steps), so neither may a branch. The only backstop is the `STALL_TIMEOUT_MS` **stall** watchdog — its timer is re-armed by **every** event (job creation, stream frames, session events), so running long never trips it; only "no event at all for this long" does. Up to 0.2.1 this was a 90-second **wall-clock** cap, which cut long turns off and showed "Request timed out (90s)". Section 10 of `scripts/smoke.mjs` guards this.
|
|
89
|
+
|
|
90
|
+
**The quote entry may only appear inside the main session's body.** Its listener sits on `document` (so selecting anywhere in a conversation works), which means scope has to come from the DOM: `insideMainConversation()` requires an ancestor with `[data-conversation-content]` (falling back to `[data-conversation-scroll]` for older hosts) and excludes the right sidebar's `[data-sidebar-right-panel]` / `[data-sidebar-right-session]` / `[data-sidebar-chat]` and the `[data-conversation-region="composer"]` seat. **An embedded sub-agent conversation carries the same surface marker** (the right-sidebar chat), so testing the marker alone would treat a child session as the main one. When no surface marker is present the entry does **not** appear (fail-closed). Getting this wrong is not cosmetic: the user ends up quoting text from a file preview, the settings dialog or another plugin's panel into the branch's prompt. `scripts/client-check.mjs` drives the **real `selectionchange` path** against a fake DOM and asserts this for seven locations.
|
|
91
|
+
|
|
88
92
|
**The `@deepseek-ai/cordis` in `peerDependencies` is declarative**: the plugin code **does not import it** (the host half obtains all of its runtime services softly via `ctx`, and the client half only `require`s platform built-in modules). It is declared by ecosystem convention, to state which contract this plugin hangs off — **do not delete it as a dependency someone forgot to remove**, and do not add an import just "to use it".
|
|
89
93
|
|
|
90
94
|
## Why it is done this way (read before changing anything; do not revert to intuition)
|
|
@@ -162,7 +166,7 @@ node scripts/client-check.mjs # ② client runtime self-check
|
|
|
162
166
|
# both at once: npm run check | only the client one: npm run client-check
|
|
163
167
|
```
|
|
164
168
|
|
|
165
|
-
**① `scripts/smoke.mjs` (source-level)** needs no DSH runtime; it loads the host half directly and calls `apply()` with a fake ctx. There are currently **
|
|
169
|
+
**① `scripts/smoke.mjs` (source-level)** needs no DSH runtime; it loads the host half directly and calls `apply()` with a fake ctx. There are currently **153 assertions**, in ten sections:
|
|
166
170
|
|
|
167
171
|
1. the module loads, the export name matches `package.json`, the route prefix is `/side-branch`, and the unload disposer is callable;
|
|
168
172
|
2. **the read-only promise**: `ALLOWED_TOOLS` and `TOOL_CHOICES` are still those six names and consistent with each other, and none of the nine dangerous tool names is among them; the client has no leftover tool switches and the host does not accept a tool list submitted by the client; the branch preamble's list comes directly from the constants; the guard's criterion is still to allow by list, its registration point is still inside `setup(agentCtx)`, and failing to obtain the guard is still fail-loud.
|
|
@@ -174,6 +178,7 @@ node scripts/client-check.mjs # ② client runtime self-check
|
|
|
174
178
|
7. **the Re-Fork architecture (0.2.0)**: `shouldReFork` must test **both** "the main session settled a new turn" and "the model/reasoning effort changed"; `parentCut` must be recorded **when the segment is created** (and carried through sleep/wake, or every turn re-forks for nothing); the replay allow-list is **exactly** the 5 surface event types (`system/message`, `developer/message`, `user/message`, `assistant/message`, `tool/result`) and contains no pure log events; the replay **never copies `sourceEventSeqs`**; the replay **never truncates**; **replay succeeds first, then the old session is released** (on failure the just-created segment is discarded); every re-fork answers `handoff: true`; the old `seedSource` handover path is gone for good. This section also guards the three spots of the estimate switch: `estimateRequestTokens` / `lastPromptTokensOf` exist, and the old `conv.lastContextTokens` criterion (necessarily `undefined` after a re-fork) is **no longer** used;
|
|
175
179
|
8. **multi-segment answers (0.2.0)**: the ordered `job.segments`; the `start` frame being split by `turn`/`step` (a changed `attemptId` within the same step means a retry ⇒ only that step is dropped; a changed step ⇒ `segment` opens a new one); `replace` rewriting **only** the current segment; `snapshot` and the terminal events carrying `segments`; `syncSegmentShortcuts` making `text`/`reasoning` a **pure function** of the segment array; the old `attempts` field split into `steps`/`retries`; the client's `normalizeSegments` staying backward-compatible with **old data** (which only had `answer`/`reasoning`); They also guard "the quote is recorded with the round": sending stores the clipped reference on that round (`reference`), renders it as a collapsible quote block above the question, and **clears the composer's quote slot**; `normalizeRound` keeps it, so the block survives a refresh.
|
|
176
180
|
9. **startup sweep and the read-only route (0.2.0)**: `/inherited` registered in `ROUTE_METHODS` (without it a wrong method falls into 404 and breaks the 405 + `Allow` contract); `handleInherited` truncating to "the last turn in full + one line per earlier turn"; the inherited content **not** entering `normalizeRound` (component memory only); the orphan criterion requiring the directory name to be **exactly** `side-<uuid>`; the **instance lease** (`side-branch-instances/*.json`, heartbeat + PID liveness); a live foreign instance making it **skip the entire sweep**; deletions leaving a **manifest log**; the sweep strictly confined under the `sessions` root and recognising only the `side-` prefix (never the `session-*` main sessions).
|
|
181
|
+
10. **parity with the main session (0.2.2)**: `ANSWER_TIMEOUT_MS` (the 90-second wall-clock cap) is **gone**, leaving only the `STALL_TIMEOUT_MS` stall watchdog (>= 10 minutes, re-armed by **every** event: job creation, stream frames, session events); `turn/end`'s `error` reason now carries the concrete `message` to the panel (no more "ended abnormally: error"); the selection entry's scope predicate `insideMainConversation` exists and is called by `selectionText`, testing `[data-conversation-content]` / `[data-conversation-scroll]` and excluding `[data-sidebar-right-panel]` / `[data-sidebar-right-session]` / `[data-sidebar-chat]` and the composer seat.
|
|
177
182
|
|
|
178
183
|
**① does not verify**: answer quality, real guard interception (those items in section 2 are **source-level regex assertions**, not a driven interception), SSE behaviour or the interface — the client half is covered by ② below, up to the rendered tree; real DOM layout, CSS, real scroll positions, the real network and a real model are equally out of its reach, and those spots depend on the one-off headless end-to-end harness **outside the repository** (see below) plus manual checking.
|
|
179
184
|
|
|
@@ -188,9 +193,9 @@ node scripts/client-check.mjs <another client.js> # reverse-verify the script
|
|
|
188
193
|
|
|
189
194
|
**How it does it.** No browser is installed; instead it **really runs** `lib/client.js` inside Node: the bundle's shape is `window.__ModuleLoader__.load({ factory })`, and it takes only three things from `require` — `react`, `react-dom` and `@deepseek-ai/dsh-client-ui-primitives` (the last two are **deliberately not provided**, and the bundle carries its own degraded branches, which conveniently exercises those branches too); only **8** React APIs are used (`createElement` / `useState` / `useEffect` / `useLayoutEffect` / `useReducer` / `useRef` / `Fragment` / `Component`), so a minimal React + minimal DOM is enough. The panel's capabilities **all come from the slot's `inject` face** (`inject: () => ({ startSideBranch, … })`), which the script spreads into props exactly as the official host does — which also verifies that the keys the face should have are there. Then real-shaped `fetch` / `EventSource` stubs drive it: type into the input through the real `onChange`, press send through the real `onClick`, and feed in SSE frames of the real shape.
|
|
190
195
|
|
|
191
|
-
**What it verifies.** The bundle loads, exports `apply()`, and the body component is registered and renders successfully; the client registers its dictionaries with the locale service (so the assertions can watch **real copy** rather than the weak "returns the key" kind); pressing send really issues **one** `POST /side-branch/start` carrying the question verbatim and opens an SSE stream for that job; every frame fed in has a listener; **all multi-segment answer text is rendered, in the right order, with the tool line between the two answer segments** (the regression criterion for the "only the last segment is left" bug); the injected branch preamble is visible in full; and **auto-scroll lands on the layer that actually scrolls** (`.dsh-side-branch-scroll`), while the non-scrolling rounds list has no ref attached.
|
|
196
|
+
**What it verifies.** The bundle loads, exports `apply()`, and the body component is registered and renders successfully; the client registers its dictionaries with the locale service (so the assertions can watch **real copy** rather than the weak "returns the key" kind); pressing send really issues **one** `POST /side-branch/start` carrying the question verbatim and opens an SSE stream for that job; every frame fed in has a listener; **all multi-segment answer text is rendered, in the right order, with the tool line between the two answer segments** (the regression criterion for the "only the last segment is left" bug); the injected branch preamble is visible in full; and **auto-scroll lands on the layer that actually scrolls** (`.dsh-side-branch-scroll`), while the non-scrolling rounds list has no ref attached. It also covers **the quote entry's scope**: a fake DOM builds the ancestor chains (main-session body, right-sidebar file preview, settings overlay, an embedded sub-agent conversation in the right sidebar, the composer seat, this plugin's own panel) and drives the real `selectionchange` → 120 ms debounce → `update()` path, asserting that the button appears exactly where it should and nowhere else.
|
|
192
197
|
|
|
193
|
-
There are currently **
|
|
198
|
+
There are currently **42 items**; PASS/FAIL is read straight from stdout (when everything passes the last line is the script's own `全部通过(42 项)`, and any failure exits with a non-zero code).
|
|
194
199
|
|
|
195
200
|
⚠️ **What it does not cover (recorded honestly)**: real DOM layout, CSS, real scroll positions, real browser events, the real network and a real model. It is a test of the **"data → rendered tree"** stretch, **not a browser test** — those spots still have to be eyeballed in a browser.
|
|
196
201
|
|
|
@@ -221,5 +226,11 @@ It covers exactly what the unit check (`scripts/smoke.mjs`) cannot reach: the br
|
|
|
221
226
|
- **Commit messages are always in English.** This is a public repository and its history is read in English: use `type: imperative subject` (e.g. `fix: un-archive the side session before each turn`) and explain "why" in the body. Do not mix Chinese into it.
|
|
222
227
|
- Prefer two commits: one for the `lib/` + `scripts/` behaviour change, one for documentation / version bumps.
|
|
223
228
|
- **Before pushing**, run `node scripts/smoke.mjs` and `node scripts/client-check.mjs` (previous section; or `npm run check` to run both at once) and run the affected path manually in a real host.
|
|
224
|
-
- Keep the version in `package.json` and `docs/CHANGELOG.md` in sync; tag releases as `v<version>` (`v0.1.0` and `v0.
|
|
229
|
+
- Keep the version in `package.json` and `docs/CHANGELOG.md` in sync; tag releases as `v<version>` (`v0.1.0`, `v0.1.1` and `v0.2.0` exist; this release tags `v0.2.2`).
|
|
230
|
+
- **Releases go through GitHub Actions (npm Trusted Publishing / OIDC) with staged publishing.** Pushing a `v<version>` tag triggers `.github/workflows/release.yml`, which checks the tag against `package.json`, runs both self-checks, and then `npm stage publish`es the tarball into the stage queue. A maintainer approves it under **Staged versions** on npmjs.com (security key) before the version goes live and `latest` moves.
|
|
231
|
+
- The trusted publisher is **stage-only** (only `npm stage publish` is allowed, `npm publish` is not): npm rejects a direct publish from that workflow, so even a malicious tag push can only leave a stage entry that still needs a human approval.
|
|
232
|
+
- One-time setup: npmjs.com → the package → **Settings → Trusted Publishing** → add GitHub Actions with Repository = `wucl12/dsh-side-branch`, Workflow filename = `release.yml`, and only `npm stage publish` under Allowed actions.
|
|
233
|
+
- Approval **has to happen on the website**: the local 2FA method is a security key, while `npm stage approve` on the CLI wants a TOTP code.
|
|
234
|
+
- `npm stage` needs npm >= 11.15.0, so the workflow installs `npm@^11.15.0` explicitly instead of trusting whatever the runner ships.
|
|
235
|
+
- A local `npm publish` is an emergency path only: npm now requires 2FA or a granular token with bypass, and the latter is being retired.
|
|
225
236
|
- Remote: `origin` = `github.com/wucl12/dsh-side-branch`, main branch `main`.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](#中文版本记录) · English · [README](../README.md)
|
|
4
4
|
|
|
5
|
+
## 0.2.2
|
|
6
|
+
|
|
7
|
+
Two bugs fixed. Both were cases of a branch behaving differently from the main session for no good reason.
|
|
8
|
+
|
|
9
|
+
- **A turn no longer has a wall-clock timeout.** Every turn used to be killed 90 seconds after it started ("Request timed out (90s)"), even while the model was still streaming, still thinking, or inside a long tool run — while the same question in the main session simply kept waiting. The hard cap is gone. What remains is a **stall watchdog**: it only fires after 30 minutes with **no event at all**, and every stream frame and every session event (tool calls, usage, messages) resets it. So a long turn is never cut off, while a genuinely wedged job cannot stay "running" forever (which would leave that branch permanently busy and leak a concurrency slot). A dead model stream is still caught earlier by the platform's own `streamIdleTimeoutMs` (5 minutes per request, reset on every chunk).
|
|
10
|
+
- **The "quote and ask" entry now appears only in the main session's body.** The entry listens on `document` so it works anywhere in a conversation, and it used to be filtered by a blacklist of inputs — which meant that selecting text in the right-hand sidebar (an open markdown file, the settings dialog, another plugin's panel) also raised it. Clicking it then quoted **text that is not part of the main session** into the branch's prompt. The entry now requires the selection to sit inside the conversation surface (`[data-conversation-content]`, falling back to `[data-conversation-scroll]`) and excludes the right sidebar — including the embedded sub-agent conversation it can host, which carries the same surface marker — and the composer seat. No conversation surface on screen means no entry (fail-closed) rather than a fallback to the old guesswork.
|
|
11
|
+
- **A failed turn now says why.** `turn/end` with `reason: { kind: 'error', error }` used to be flattened into "The ask ended abnormally: error", dropping the real cause (provider error, platform stream timeout, model offline). The message/code is now shown to the user.
|
|
12
|
+
- **0.2.1's documentation work ships here.** 0.2.1 was never published on its own; the README rewrite, the metadata changes and the `engines.dsh` declaration are all in this release.
|
|
13
|
+
- **Verified on DSH `0.2.1-rc.3`** in an isolated playground instance: the plugin loads, the `/side-branch` routes register and answer (200, and 405 + `Allow` for a wrong method), and the client bundle the browser receives carries the scoped entry. The long-turn path was not re-run against a real model in this round — it is covered by the new source-level assertions instead.
|
|
14
|
+
|
|
15
|
+
## 0.2.1
|
|
16
|
+
|
|
17
|
+
Documentation and metadata only — no behaviour change, no code change.
|
|
18
|
+
|
|
19
|
+
- **README rebuilt around what a first-time reader needs.** The first screen leads with the three situations the plugin is for, the install command and the restart warning follow immediately, and the guarantees are stated as outcomes rather than as mechanisms.
|
|
20
|
+
- **The comparison table moved to the end**, and one row is left as a deliberate loss: `dsh-sidebar-qa` supports nested follow-ups, this plugin does not.
|
|
21
|
+
- **Dropped an unsupported claim.** The README and `TECHNICAL.md` used to say that comparable plugins usually implement read-only through the prompt layer (`toolFilter` / `tools.restrict`). That was never verified, and it is not true of the closest alternative — the text now explains the design decision without asserting what anyone else does.
|
|
22
|
+
- **Documented how the plugin was built.** The codebase was written by DSH itself; the human work was direction, plan review and hands-on verification. See *How this was built* in the README.
|
|
23
|
+
- **Bilingual `package.json` description, English first, plus an `engines.dsh: >=0.1.5-rc.2` declaration.** dsh-market reads the latter for host-aware discovery and evaluates it with `includePrerelease`, so it is a declaration and not a gate.
|
|
24
|
+
- **Verified on DSH `0.2.0-rc.2`** — the plugin loads, the `/side-branch` routes register, and the panel was exercised by hand. The `0.2.0` boot-time peer gate only inspects `@deepseek-ai/dsh*` peer ranges, which this plugin declares none of.
|
|
25
|
+
|
|
5
26
|
## 0.2.0
|
|
6
27
|
|
|
7
28
|
Behaviour change: the branch now **follows the main session** instead of being frozen at the moment it was opened.
|
|
@@ -49,6 +70,27 @@ First public release.
|
|
|
49
70
|
|
|
50
71
|
## 中文版本记录
|
|
51
72
|
|
|
73
|
+
## 0.2.2
|
|
74
|
+
|
|
75
|
+
修两个 BUG。两个都是"侧枝在没有理由的地方表现得和主会话不一样"。
|
|
76
|
+
|
|
77
|
+
- **一轮不再有总时长超时。** 以前每一轮从建 job 起算 90 秒就被无条件掐断(面板显示「请求超时(90 秒)」),哪怕模型还在流式输出、还在思考、或正卡在一次长工具里;而同样的提问在主会话里只是继续等。硬上限已删除,只留一个**停滞看门狗**:**整整 30 分钟一个事件都没有**才触发,而任何一个流式帧、任何一个会话事件(工具调用、用量、消息)都会重新计时。所以长回合永远不会被它掐断,而真卡死的 job 也不会永驻(永驻会让这一段会话永久 busy,还占着并发名额)。模型流真断掉的情况仍然由平台自己的 `streamIdleTimeoutMs`(每请求 5 分钟、收到 chunk 就重置)先收口。
|
|
78
|
+
- **「引用并提问」只在主会话正文区出现。** 入口的监听挂在 `document` 上(这样在会话里划字都能用),过滤原本只有一层输入框黑名单——于是右栏(打开的 md、设置弹层、其它插件的面板)里划字也会弹,点下去就把**不属于主会话的原文**引进了侧枝的 prompt。现在要求选区落在会话正文里(`[data-conversation-content]`,旧版退回 `[data-conversation-scroll]`),并排除右栏(含右栏里的子代理嵌入会话——它自己也带同样的正文标记)与 composer 座位。屏幕上没有会话正文时**不弹**(fail-closed),不再退回旧的黑名单猜测。
|
|
79
|
+
- **失败的一轮现在会说原因。** `turn/end` 的 `reason: { kind: 'error', error }` 以前被压成「提问意外终止:error」,真正的原因(供应商报错、平台流空闲超时、模型下线)全丢了;现在把 message/code 显示给用户。
|
|
80
|
+
- **0.2.1 的文档改动随这一版发布。** 0.2.1 没有单独发布;README 重写、元数据改动与 `engines.dsh` 声明都在这一版里。
|
|
81
|
+
- **已在 DSH `0.2.1-rc.3` 上验证**(隔离试验场):插件加载成功、`/side-branch` 路由注册并能应答(200;方法不对回 405 + `Allow`),浏览器收到的 client bundle 带上了限定作用域的引用入口。这一轮**没有**再用真实模型跑长回合,那部分由新增的源码级断言守着。
|
|
82
|
+
|
|
83
|
+
## 0.2.1
|
|
84
|
+
|
|
85
|
+
仅文档与元数据,无行为改动、无代码改动。
|
|
86
|
+
|
|
87
|
+
- **README 按「第一次来的人需要什么」重写。** 首屏先讲这个插件适用于哪三种情况,紧接着就是安装命令与重启提示;各项保证改成讲结果,而不是讲机制。
|
|
88
|
+
- **对比表挪到文末**,并保留一行主动认输:`dsh-sidebar-qa` 支持嵌套追问,本插件不支持。
|
|
89
|
+
- **删掉一条没有依据的说法。** README 与 `TECHNICAL.md` 原先写「同类插件普遍在提示词层实现只读(`toolFilter` / `tools.restrict`)」。这从未核实过,而且对最接近的替代品并不成立——现在只讲我们自己的设计取舍,不再断言别人怎么做。
|
|
90
|
+
- **补上「这个插件是怎么写出来的」**:整个代码库由 DSH 自己完成,人负责的是定方向、审计划、亲手验证。见 README 的《它是怎么被写出来的》。
|
|
91
|
+
- **`package.json` 的 description 改为中英双语、英文在前**,并声明 `engines.dsh: >=0.1.5-rc.2`。dsh-market 会读取后者做宿主兼容性发现,并用 `includePrerelease` 求值,所以这是声明而不是闸门。
|
|
92
|
+
- **已在 DSH `0.2.0-rc.2` 上验证**——插件加载、`/side-branch` 路由注册、面板人工实跑通过。`0.2.0` 启动期的 peer 闸门只检查 `@deepseek-ai/dsh*` 的 peer 区间,本插件一个都没声明。
|
|
93
|
+
|
|
52
94
|
## 0.2.0
|
|
53
95
|
|
|
54
96
|
行为变更:侧枝不再冻结在开段那一刻,而是**跟着主会话走**。
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -1,92 +1,125 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 临时会话 Side Ask · DSH 侧枝分支
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-side-branch)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-side-branch)
|
|
5
|
+
[](../LICENSE)
|
|
6
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
2
7
|
|
|
3
8
|
[English](../README.md) | 简体中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
**在主会话里划选一段文字,在右栏开一条只读分支追问。答案流式显示在侧栏,不进入主会话的模型上下文。**
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
有些问题你只想顺口一问:agent 正在重构,你想知道「这个函数别处是怎么用的」,但不想让主会话记住这次提问;主会话上下文已经很长,你不想为一句追问让它再膨胀一轮;或者主会话跑的是贵模型,你想换个便宜模型在边上试错。
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* **已验证 DSH 版本:** `0.1.5-rc.2` 与 `0.1.7-rc.2`
|
|
14
|
+
- 基于当前对话追问,主会话上下文一个 token 都不增加。
|
|
15
|
+
- 分支可以单独指定模型和推理等级,不用跟随主会话。
|
|
16
|
+
- 长任务在主线跑着,你在侧栏问别的,互不打断。
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
```bash
|
|
19
|
+
dsh plugin --profile web add dsh-side-branch
|
|
20
|
+
# 装完必须重启 DSH——客户端插件只在启动时装载,刷新浏览器没用
|
|
21
|
+
```
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
`--profile` 用你实际在跑的那个(`web`、`desktop`……)。需要 Node.js ≥ 20,且 `PATH` 上有 `pnpm`。
|
|
24
|
+
|
|
25
|
+
> **这个插件的代码是 agent 自己写的。** 本仓库的每一行代码都由 DSH 在真实会话里完成;作者负责的是工作流:定方向、审计划、亲手验证行为。详见 [它是怎么被写出来的](#它是怎么被写出来的)。
|
|
21
26
|
|
|
22
27
|
<p align="center">
|
|
23
|
-
<img src="../assets/panel-answering.png" alt="
|
|
28
|
+
<img src="../assets/panel-answering.png" alt="分支在右侧栏流式作答" width="700">
|
|
24
29
|
</p>
|
|
25
30
|
|
|
26
|
-
##
|
|
31
|
+
## 它有什么不一样
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
**答案不回流。** 宿主端从不把分支的输出写进父会话。你在分支里问过什么,就留在分支里——主会话的 agent 不会莫名其妙地「知道」你在旁边聊了什么。
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
2. **支持同一段内多轮追问**(不是一次性气泡)。
|
|
32
|
-
3. **逐词流式输出**(包含推理过程)。
|
|
33
|
-
4. **轻量稳定**(只使用官方公开 API,无臃肿依赖)。
|
|
34
|
-
5. **执行层只读,且绝不破坏前缀缓存**。
|
|
35
|
+
**它碰不到你的文件。** 只读是在**调用工具的那一刻**拦下的,不是靠把工具从提示词里藏起来:`ctx.tools.guard()` 只放行六个无副作用的工具(`read`、`glob`、`grep`、`web_search`、`web_fetch`、`lsp`),其余一律拒绝。因为工具仍留在提示词里,分支的提示词前缀与主会话逐字一致,能继续命中父会话攒下的前缀缓存——实测 97%–98%,这也是「顺口问一句几乎不额外花钱」的原因。为什么这么做、代价是什么,见 [`TECHNICAL.zh-CN.md`](./TECHNICAL.zh-CN.md)。
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
**它跟着主会话走。** 每次追问都用主会话最新落定的内容重建分支,再把这条分支自己之前的问答放回去:主会话刚聊完的那一轮它已经看得到,你三轮前问过什么它也还记得。
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
本项目坚决选择将“只读”拦截放在**执行层**,分支的提示词前缀与主会话**逐字一致**,从而能够继续享受极高的前缀缓存命中率(实测可达 97%–98%)。
|
|
39
|
+
**注入内容可以逐字核对。** 面板里展开「已继承主会话」,就能逐轮看到分支实际收到了什么,连注入给模型的引导词全文都在。
|
|
40
40
|
|
|
41
|
-
##
|
|
41
|
+
## 安装
|
|
42
42
|
|
|
43
|
-
* **真分支,零污染:** 宿主端绝不向父会话写入任何内容。临时会话的问答不会出现在主会话的模型上下文中。
|
|
44
|
-
* **每轮都跟着主会话走:** 每一次追问都用主会话**最新的已结算内容**重建这一段——重新取种子,再把这条分支自己历来的问答重放进去。于是它看到的永远是主会话此刻的样子,**同时一条都不丢自己之前问过什么**。沿用主会话模型时,继续命中原有前缀缓存,无需重算。
|
|
45
|
-
* **执行层只读隔离:** 安全策略不依赖提示词。由底层 `ctx.tools.guard` 强制接管,仅白名单放行六个无副作用的查询类工具(`read`, `glob`, `grep`, `web_search`, `web_fetch`, `lsp`),其余指令(如写文件、执行脚本)在执行层被直接拦截。
|
|
46
|
-
* **极致的流式体验:** 正文与推理过程(可折叠的“思考”行)逐词流式输出。即使关闭浏览器标签,后台回答也不会中断,重新打开即可续接。
|
|
47
|
-
* **会话状态持久化:** 面板状态按「会话 + 面板格」本地持久化。闲置自动休眠,下次提问静默唤醒。
|
|
48
|
-
* **可核查:** 展开「已继承主会话」即可逐轮看到这条分支究竟收到了什么——问、它的思考、以及答案;注入给模型的分支引导词同样逐字展示,模型看到的输入没有黑箱。
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
## 📦 安装与启用
|
|
52
|
-
|
|
53
|
-
安装后必须**重启 DSH** 以使客户端插件加载生效。
|
|
54
|
-
|
|
55
|
-
**通过 npm 安装 (推荐):**
|
|
56
43
|
```bash
|
|
44
|
+
# npm
|
|
57
45
|
dsh plugin --profile web add dsh-side-branch
|
|
58
|
-
```
|
|
59
46
|
|
|
60
|
-
|
|
61
|
-
```bash
|
|
47
|
+
# 或直接从 GitHub 装
|
|
62
48
|
dsh plugin --profile web add github:wucl12/dsh-side-branch
|
|
63
49
|
```
|
|
64
50
|
|
|
65
|
-
|
|
51
|
+
`--profile` 用你实际在跑的那个(`web`、`desktop`……)。装完**必须重启 DSH**——客户端插件只在启动时装载,刷新浏览器不够。
|
|
52
|
+
|
|
53
|
+
前置:Node.js ≥ 20,且 `PATH` 上有 `pnpm`(`dsh plugin` 会转发给 pnpm)。
|
|
54
|
+
|
|
55
|
+
也可以在市场里装——[dsh-market](https://github.com/dsh-market/dsh-market) 与 [awesome-dsh-plugin 目录](https://awesome-dsh-plugin.com) 都已收录。
|
|
56
|
+
|
|
57
|
+
> 🤖 **给 AI Agent 的提示:** 如果你是负责安装此插件或阅读源码的 Agent,请先读本仓库的 [`AGENTS.md`](../AGENTS.md)——硬性约束、架构细节与安装指引都在那里。
|
|
58
|
+
|
|
59
|
+
## 如何使用
|
|
60
|
+
|
|
61
|
+
1. **划选提问:** 在主会话里选中文字,点击弹出的「引用并提问」,分支就在右侧面板打开,原文作为引用一并发送;这段引用会**随该轮入档**(问题上方一个可折叠的引用块),输入框上方的引用槽**自动清空**,下一轮从干净的输入框开始。
|
|
62
|
+
2. **快速入口:** 主会话输入框右侧有一个 `↗` 按钮,点击直接呼出面板(可在设置中关闭)。
|
|
63
|
+
3. **独立设置:** 面板里可以给当前分支单独指定模型与推理等级,不用跟随主会话。
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="../assets/selection-entry.png" alt="在主会话里划选文本,打开分支" width="700">
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<img src="../assets/sidebar-guide.png" alt="右侧栏引导页里的「临时会话」入口" width="700">
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
## 兼容性
|
|
74
|
+
|
|
75
|
+
| DSH | 状态 |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `0.1.5-rc.2` | ✅ 已验证 |
|
|
78
|
+
| `0.1.7-rc.2` | ✅ 已验证(归档闸) |
|
|
79
|
+
| `0.2.0-rc.2` | ✅ 已验证——加载、`/side-branch` 路由注册、面板人工实跑 |
|
|
80
|
+
| `0.2.1-rc.3` | ✅ 已验证——加载、`/side-branch` 路由注册且能应答(200 / 405 + `Allow`),浏览器收到的 client bundle 带上了限定作用域的引用入口 |
|
|
81
|
+
|
|
82
|
+
版本相关的接缝(归档闸、`unarchiveSession`)走的是运行期特性探测而非版本号比较,所以一个已发布版本同时服务这四条线。细节见 [`TECHNICAL.zh-CN.md`](./TECHNICAL.zh-CN.md)。
|
|
83
|
+
|
|
84
|
+
`0.2.2` 的引用入口依赖 DSH 给会话正文加的那组标记(`[data-conversation-content]`,旧宿主退回 `[data-conversation-scroll]`)。宿主若两者都没有渲染,入口就不出现(而不是退回猜测);插件其余部分不受影响。
|
|
85
|
+
|
|
86
|
+
## 安全与限制说明
|
|
87
|
+
|
|
88
|
+
* **关于「只读」边界:** 这里的只读是指**不产生系统副作用**(不写文件、不执行命令)。但 `web_fetch` / `web_search` 仍有网络访问,`read` 可以读取进程权限内的任何文件。放行名单硬编码在代码里,不能通过设置放宽。
|
|
89
|
+
* **分支能看到什么:** 它用主会话**最新的已结算回合**重建,所以主会话**答完结算**的新回合会自动跟上;主会话**还没答完**的那一轮看不到(切点是最后一个 `turn/end`)——但**手动划选引用不受影响**:受限的是「自动看到」,不是「能不能引用」。
|
|
90
|
+
* **重建会留下会话记录:** 每次重建都新增一条侧会话记录,而平台**没有删除会话的 API**,记录会堆积。它们由插件启动时的孤儿清理回收,删了什么会逐条记日志。同一时刻有两个共用同一个 `DSH_HOME` 的实例在跑时,清理会整个跳过——第二个 DSH 绝不会删掉它正在用的分支。
|
|
91
|
+
* **到上下文上限是「拒绝」:** 到达上限时插件拒绝发送并提示清空,绝不静默丢掉最早的轮次。它报出的 `used` 是**估算值**,与该轮结束后的真实用量对不上是正常的。
|
|
92
|
+
* **缓存命中率是实测值,不是设计保证:** 97%–98% 是量出来的,不是承诺——平台可能在分支第一步**原地替换 system 节点**,那样从第 0 个 token 起全废。并且命中缓存 ≠ 不占上下文窗口,命中的 token 一样占窗口。
|
|
93
|
+
* **PTC 模式:** 主会话若用 `ptc` preset(纯代码执行工具面),分支会退化为纯文本问答,因为 `run_code` 永远被拒。
|
|
94
|
+
|
|
95
|
+
## 选型对比
|
|
96
|
+
|
|
97
|
+
这个生态里的侧边问答插件,差别主要在**答案最后落在哪**,以及**侧会话被允许做什么**。
|
|
98
|
+
|
|
99
|
+
| | **临时会话**(`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
|
+
| 答案最后落在哪 | 独立的一条分支;宿主端不回写 | 一条独立 DSH 会话 | **当前会话**——划选文本作为用户消息发进去 |
|
|
102
|
+
| 侧会话的工具权限 | 六个无副作用工具;调用写操作时被拒 | 其 README 未说明 | 同一条会话,因此是完整工具集 |
|
|
103
|
+
| 是否跟到主会话最新已结算回合 | 每轮追问都强制重建 | 可选——全量继承 / 摘要压缩 / 机械裁切 | 是——本来就是同一条会话 |
|
|
104
|
+
| 嵌套追问 / 追问记录树 | 无 | 有 | 无 |
|
|
105
|
+
| 运行时依赖 | 无 | 无(1.0.0 起) | — |
|
|
66
106
|
|
|
67
|
-
|
|
107
|
+
> 另外两列取自它们当时公开的 README,之后可能已变化。表里有一行是我们主动认输的:`dsh-sidebar-qa` 支持嵌套追问,本插件不支持。这张表是帮你选型,不是给人排名。
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
2. **快速入口:** 主会话底部输入框右侧有一个 `↗` 快速入口按钮,点击可直接呼出临时会话面板(可在设置中关闭)。
|
|
71
|
-
3. **独立设置:** 在临时会话面板中,支持为当前分支单独指定模型与推理等级,无需跟随主会话。
|
|
109
|
+
## 它是怎么被写出来的
|
|
72
110
|
|
|
73
|
-
|
|
111
|
+
这个插件是一次「吃自己的狗粮」的结果:整个代码库——宿主半、客户端半、`scripts/` 下的两套自检——都由 DSH 自己写,照着 [`AGENTS.md`](../AGENTS.md) 里的契约干活。没有一行代码是手写的。
|
|
74
112
|
|
|
75
|
-
|
|
76
|
-
* **分支能"看到"什么:** 每一次追问都会用主会话**最新的已结算回合**重建这一段,所以主会话**答完结算**的新回合会自动跟上。主会话**还没答完**的那一轮则看不到(切点是最后一个 `turn/end`)—— 但**主动划选引用不受这个限制**:受限的是"自动看到",不是"能不能引用"。
|
|
77
|
-
* **重建会留下会话记录:** 每次重建都新增一条侧会话记录,而平台**没有删除会话的 API** ⇒ 记录会堆积。它们由插件**启动时的孤儿清理**回收,删了什么会逐条记日志。同一时刻有**两个共用同一个 `DSH_HOME` 的实例**在跑时,清理会**整个跳过** —— 第二个 DSH 绝不会删掉它正在用的分支。
|
|
78
|
-
* **到上下文上限是"拒绝":** 到达上限时插件**拒绝发送**并提示「清空」,绝不静默丢掉最早的轮次。`contextFull.used` 是**估算值**(重建出来的会话还没有 usage),与该轮结束后的真实用量对不上是正常的。
|
|
79
|
-
* **缓存命中率是实测值,不是设计保证:** 高命中率是量出来的,不是承诺 —— 平台可能在子会话第一步**原地替换 system 节点**,那样**从第 0 个 token 起全废**。并且**命中缓存 ≠ 不占上下文窗口**,命中的 token 一样占窗口。
|
|
80
|
-
* **PTC 模式的特殊处理:** 若主会话使用 `ptc` preset(纯代码执行工具面),由于 `run_code` 永远被本插件拒绝,此分支将变为**纯文本问答模式**(无法调用任何工具)。
|
|
113
|
+
人的部分在流程上:决定做什么、定方向、在每个方案执行前审一遍、然后在真实会话里亲手验证结果。验证发现的问题会作为新需求回给它——归档闸的修法、多段回答只显示最后一段的 bug、以及整条 re-fork 设计,都是这样找出来的。
|
|
81
114
|
|
|
82
|
-
##
|
|
115
|
+
## 更多文档
|
|
83
116
|
|
|
84
117
|
* [English](../README.md):English documentation (the same content)。
|
|
85
118
|
* [`TECHNICAL.zh-CN.md`](./TECHNICAL.zh-CN.md):完整技术文档——前缀缓存机制、只读拦截链路、PTC 行为与全部已知限制。
|
|
86
|
-
* [`AGENTS.md`](../AGENTS.md)
|
|
119
|
+
* [`AGENTS.md`](../AGENTS.md):agent 干活时依据的架构契约与硬约束。
|
|
87
120
|
* [`SECURITY.zh-CN.md`](./SECURITY.zh-CN.md):安全承诺范围与漏洞报告渠道。
|
|
88
121
|
* [`CHANGELOG.md`](./CHANGELOG.md):版本更新历史。
|
|
89
122
|
|
|
90
|
-
##
|
|
123
|
+
## 许可证
|
|
91
124
|
|
|
92
|
-
[MIT](../LICENSE)
|
|
125
|
+
[MIT](../LICENSE)
|
package/docs/TECHNICAL.md
CHANGED
|
@@ -10,7 +10,7 @@ Typical use: ask a follow-up question, look up a term, or explore an idea *along
|
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| **Package name** (implementation) | `dsh-side-branch` |
|
|
12
12
|
| **UI name** (what you see) | **Side Ask** · 中文「临时会话」 |
|
|
13
|
-
| **Verified DSH version** | `0.1.5-rc.2
|
|
13
|
+
| **Verified DSH version** | `0.1.5-rc.2`, `0.1.7-rc.2`, `0.2.0-rc.2` and `0.2.1-rc.3` (see "Archived sessions" below) |
|
|
14
14
|
| **Prerequisites** | Node.js ≥ 20 and `pnpm` on your `PATH` |
|
|
15
15
|
| **Runtime dependencies** | none — no build step, no third-party packages |
|
|
16
16
|
| **License** | MIT |
|
|
@@ -40,7 +40,7 @@ The closest alternatives each traded something away. For example:
|
|
|
40
40
|
- [`dsh-sidenote`](https://github.com/g-yixuan/dsh-sidenote) is a full-featured side chat with one-click "reflow into the main session"; by design its conclusions are meant to be written back into the main conversation.
|
|
41
41
|
- [`@lukeknow0/dsh-side-chat`](https://github.com/Lukeknow0/dsh-side-chat) has an excellent architecture and a comparable read-only model, but its README states it is an MVP.
|
|
42
42
|
|
|
43
|
-
The
|
|
43
|
+
The obvious way to get read-only is to work at the *prompt layer* — remove the tools from the prompt (`toolFilter`) or restrict them (`tools.restrict`). Functionally that does produce read-only. But it also makes the branch's prompt prefix diverge from the main session's, **which invalidates every prefix-cache entry the parent session has accumulated**; the whole context is then recomputed on every turn.
|
|
44
44
|
|
|
45
45
|
This plugin enforces read-only in the **execution layer** instead (`ctx.tools.guard`, the public `dsh-tools` contract). The tools stay in the prompt; the model may attempt to call them, but execution is denied — so the prompt is byte-identical to the main session and the cache keeps hitting (measured at 97%–98%).
|
|
46
46
|
|
|
@@ -109,7 +109,7 @@ Install the DSH plugin dsh-side-branch into my web profile:
|
|
|
109
109
|
|
|
110
110
|
## 🚀 Usage
|
|
111
111
|
|
|
112
|
-
Select text in the main session's body: an ask entry appears above the selection. Click it, the right-hand panel opens, type your question and send. The selected text travels with the question as a quote.
|
|
112
|
+
Select text in the main session's body: an ask entry appears above the selection. It appears **only** there — not in the right-hand panels (file preview, settings, other plugins' panes) and not in an embedded sub-agent conversation hosted there, because those are not the main session's content. Click it, the right-hand panel opens, type your question and send. The selected text travels with the question as a quote.
|
|
113
113
|
|
|
114
114
|
There is a second entry point: a button on the right of the main session's composer opens the panel directly (it can be turned off in settings).
|
|
115
115
|
|
|
@@ -304,7 +304,7 @@ Measured: in a PTC parent session the PTC preamble applied from the first round,
|
|
|
304
304
|
- **The cache hit rate is measured, not guaranteed.** The platform may **replace the system node in place** on the side session's first step (node 0) — the official wording is *"A prompt change that replaces a system node in place makes the request differ from that node's first token — in full when the node is node 0"* ⇒ if that happens, **everything from token 0 on is billed at full price**. A mismatched tool set also forces a new sequence. So there is no "always hits": 97%–98% is a measurement, not a promise. And **hitting the cache does not mean the tokens stop occupying the context window**: the `inputTokens + cacheRead + cacheWrite` total counts the cached tokens just the same.
|
|
305
305
|
- With a `ptc` main session the branch **cannot use a single tool** (the only visible entry, `run_code`, is always denied). A custom preset saved from the PTC template also defeats the opening-time detection, wasting the first round.
|
|
306
306
|
- After being denied by the guard, the model often **retries the same tool**, wasting a round. This is independent of PTC mode and was observed in both.
|
|
307
|
-
- Verified on DSH `0.1.5-rc.2` (before the archive gate) and `0.1.7-rc.2` (with it). `0.1.6-alpha.2` was checked by reading its published packages only — it already has `unarchiveSession` and does **not** yet have `ArchivedSessionGate`, so the feature-detected path keeps it working — but no end-to-end run was done there.
|
|
307
|
+
- Verified on DSH `0.1.5-rc.2` (before the archive gate) and `0.1.7-rc.2` (with it). `0.1.6-alpha.2` was checked by reading its published packages only — it already has `unarchiveSession` and does **not** yet have `ArchivedSessionGate`, so the feature-detected path keeps it working — but no end-to-end run was done there. Also verified end-to-end on `0.2.0-rc.2`: the plugin loads, the `/side-branch` routes register, and the panel was exercised by hand.
|
|
308
308
|
- The MCP tool conclusion comes from reading the code path, not from a live test (`run_code` was tested live).
|
|
309
309
|
- The effect of the branch preamble on model behaviour has not been quantified. It asks the model to say "this needs the main session" when it would have to read a file to answer; whether that makes the model overly conservative needs human judgement.
|
|
310
310
|
|
package/docs/TECHNICAL.zh-CN.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| **包名**(实现) | `dsh-side-branch` |
|
|
12
12
|
| **界面显示名** | **临时会话**(英文界面 `Side Ask`) |
|
|
13
|
-
| **已验证的 DSH 版本** | `0.1.5-rc.2` 与 `0.1
|
|
13
|
+
| **已验证的 DSH 版本** | `0.1.5-rc.2`、`0.1.7-rc.2`、`0.2.0-rc.2` 与 `0.2.1-rc.3`(见下文「归档与解锁」) |
|
|
14
14
|
| **前置要求** | Node.js ≥ 20,且 `PATH` 上有 `pnpm` |
|
|
15
15
|
| **运行时依赖** | 无——零第三方依赖,无构建步骤 |
|
|
16
16
|
| **许可证** | MIT |
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
- [`dsh-sidenote`](https://github.com/g-yixuan/dsh-sidenote) 是功能完整的侧边聊天,带一键「回流主会话」;按设计它的结论就是要写回主会话的。
|
|
41
41
|
- [`@lukeknow0/dsh-side-chat`](https://github.com/Lukeknow0/dsh-side-chat) 架构优秀、只读模型与本插件相近,但其 README 自述仍是 MVP。
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
最直觉的只读做法是在**提示词层**动手:用 `toolFilter` 把工具从提示词里删掉,或用 `tools.restrict` 限制工具。功能上确实达到只读,但代价是侧会话的提示词前缀与主会话分叉,**父会话已经积累的前缀缓存全部失效**,每轮都要重算整个上下文。
|
|
44
44
|
|
|
45
45
|
本插件把只读拦在**执行层**(`ctx.tools.guard`,`dsh-tools` 的公开契约):工具仍留在提示词里,模型可以尝试调用,但执行被拒——于是提示词与主会话逐字一致,缓存照旧命中(实测 97%–98%)。
|
|
46
46
|
|
|
@@ -110,7 +110,7 @@ dsh plugin --profile web add C:\path\to\dsh-side-branch
|
|
|
110
110
|
|
|
111
111
|
## 🚀 使用
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
在主会话正文里选中文字,选区上方出现询问入口。它**只会**在这里出现——右栏(文件预览、设置、其它插件的面板)与右栏里的子代理嵌入会话里划字不会弹,因为那些不是主会话的内容。点它打开右栏面板,输入问题并发送——选中的原文会作为引用一起交给分支。
|
|
114
114
|
|
|
115
115
|
主会话输入框右侧另有一个入口按钮,点它可以直接打开面板(可在设置里关掉)。
|
|
116
116
|
|
|
@@ -306,7 +306,7 @@ read glob grep web_search web_fetch lsp
|
|
|
306
306
|
- **缓存命中率是实测值,不是设计保证。** 平台可能在子会话第一步**原地替换 system 节点**(node 0)——官方原话是 *"A prompt change that replaces a system node in place makes the request differ from that node's first token — in full when the node is node 0"* ⇒ 一旦如此,**从第 0 个 token 起全废**;工具集不一致也会强制开一条新序列。所以不会有"必然命中"这回事,97%–98% 是实测,不是承诺。并且**命中缓存 ≠ 不占上下文窗口**:`inputTokens + cacheRead + cacheWrite` 里命中的那些 token **一样占窗口**。
|
|
307
307
|
- 主会话用 `ptc` preset 时,这条分支**一个工具都用不了**(唯一可见入口 `run_code` 永远被拒)。用 PTC 模板另存出来的自定义 preset 还会让开段判定漏判,第一轮白试一次。
|
|
308
308
|
- 模型被 guard 拒绝后往往会**再试一次**同一个工具,白花一轮。这与主会话是否 PTC 无关,实测两种模式下都会发生。
|
|
309
|
-
- 已在 DSH `0.1.5-rc.2`(尚无归档闸)与 `0.1.7-rc.2`(已有归档闸)上验证。`0.1.6-alpha.2` 只核对了它已发布的包:它已有 `unarchiveSession`、尚未有 `ArchivedSessionGate
|
|
309
|
+
- 已在 DSH `0.1.5-rc.2`(尚无归档闸)与 `0.1.7-rc.2`(已有归档闸)上验证。`0.1.6-alpha.2` 只核对了它已发布的包:它已有 `unarchiveSession`、尚未有 `ArchivedSessionGate`,特性探测的分支足以让它照常工作,但**没有在那里跑过端到端**。`0.2.0-rc.2` 上则做过完整实跑:插件加载、`/side-branch` 路由注册、面板人工验证通过。
|
|
310
310
|
- MCP 工具的拦截结论来自代码路径核对,没有实测(`run_code` 已实测)。
|
|
311
311
|
- 分支引导词对模型行为的影响没有量化。它要求模型在需要读文件才能得出结论时说明「这需要回主会话做」,是否会让模型过度保守需要人工评判。
|
|
312
312
|
|
package/lib/client.js
CHANGED
|
@@ -3188,18 +3188,52 @@ window.__ModuleLoader__.load({
|
|
|
3188
3188
|
}
|
|
3189
3189
|
|
|
3190
3190
|
/**
|
|
3191
|
-
*
|
|
3191
|
+
* 选区是否落在**主会话正文区**里(「引用并提问」入口的作用域)。
|
|
3192
|
+
*
|
|
3193
|
+
* ★ 0.2.2 修的缺陷:入口的监听挂在 `document` 上(这样"在会话里划字就能用"),
|
|
3194
|
+
* 而 DSH 页面里除了主会话还有**右栏面板**(文件预览、设置、其它插件的面板、本插件面板)
|
|
3195
|
+
* 与**设置弹层**,它们都在同一棵 DOM 下 ⇒ 只靠输入框黑名单挡不住:
|
|
3196
|
+
* 用户在右栏打开的 md 里划字,也会弹出「引用并提问」,点下去就把**不属于主会话**的原文
|
|
3197
|
+
* 引进了侧枝的 prompt。
|
|
3198
|
+
*
|
|
3199
|
+
* 判据(官方 DOM 标记,已核对 0.2.1-rc.3 的 `dsh-client-ui-conversation`):
|
|
3200
|
+
* · 主会话正文 = `[data-conversation-content]`(其内是 `[data-conversation-scroll]`,
|
|
3201
|
+
* 消息列表与 composer 座位都在里面);
|
|
3202
|
+
* · composer 座位 = `[data-conversation-region="composer"]` ⇒ 排除(输入框另有黑名单);
|
|
3203
|
+
* · **右栏** = `[data-sidebar-right-panel]` ⇒ 排除。这一条不能省:
|
|
3204
|
+
* 子代理的嵌入会话(右栏里的 sidebar chat)**也带 `[data-conversation-content]`**,
|
|
3205
|
+
* 只判正文标记会把"子会话的正文"当成主会话。
|
|
3206
|
+
* · 找不到任何会话正文标记 ⇒ **不弹**(fail-closed)。宁可少一个入口,
|
|
3207
|
+
* 也不要引用来源不明的文字;旧版 DSH 上这个入口会因此不出现(已登记在 CHANGELOG)。
|
|
3208
|
+
*
|
|
3209
|
+
* @param {Element} element - 选区起点所在的元素
|
|
3210
|
+
* @returns {boolean} 是否允许弹出「引用并提问」
|
|
3211
|
+
*/
|
|
3212
|
+
function insideMainConversation(element) {
|
|
3213
|
+
// `[data-conversation-scroll]` 是兜底:0.1.7 一代只有它、没有 `-content`
|
|
3214
|
+
const surface = element.closest('[data-conversation-content], [data-conversation-scroll]')
|
|
3215
|
+
if (surface === null) return false
|
|
3216
|
+
// 右栏的三个标记都要判:panel = 右栏根;session = 同一根上的会话归属;
|
|
3217
|
+
// chat = 右栏里"带会话的标签页"(子代理的嵌入会话就在里面,它自己也有正文标记)
|
|
3218
|
+
if (element.closest('[data-sidebar-right-panel], [data-sidebar-right-session], [data-sidebar-chat]') !== null) return false
|
|
3219
|
+
if (element.closest('[data-conversation-region="composer"]') !== null) return false
|
|
3220
|
+
return true
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
/**
|
|
3224
|
+
* 划选命中范围的**黑名单闸**(黑名单 + 主会话正文白名单,见 `insideMainConversation`)。
|
|
3192
3225
|
*
|
|
3193
3226
|
* 为什么必须有:composer(Lexical 输入区)与 `[data-conversation-scroll]` 在**同一个容器**内
|
|
3194
3227
|
* (`official-ui-conversation\lib\client.js:14953-14956`),
|
|
3195
3228
|
* 所以"在输入框里划字"会被当成"划选了会话原文"引用并发给子代理 —— **这是信任级缺陷**。
|
|
3196
3229
|
*
|
|
3197
3230
|
* 契约取舍:
|
|
3198
|
-
* ·
|
|
3199
|
-
* `
|
|
3200
|
-
*
|
|
3201
|
-
*
|
|
3202
|
-
*
|
|
3231
|
+
* · **不做"消息级"白名单**。`closest('[data-chat-flow-kind="assistant-step"|"assistant"|"user"]')`
|
|
3232
|
+
* 看着更准,但那是**私有契约**(官方核心包里存在、`.d.ts` 与 README 里零命中),
|
|
3233
|
+
* 而且粒度是"行"不是"消息";用它会把入口绑死在更细的内部结构上。
|
|
3234
|
+
* · **做"正文区级"白名单**(`insideMainConversation`):`[data-conversation-content]`
|
|
3235
|
+
* / `[data-conversation-scroll]` 是会话外壳自己的标记(右栏面板、设置弹层、左侧列表里都没有),
|
|
3236
|
+
* 恰好回答"这段文字属不属于某个会话的正文"。找不到标记时**不弹**(fail-closed)。
|
|
3203
3237
|
*
|
|
3204
3238
|
* @param selection - 当前 `window.getSelection()`(可能为 null)
|
|
3205
3239
|
* @returns 合法的选区文本(已 trim);不合法时为 `''`(调用方据此隐藏按钮)
|
|
@@ -3223,6 +3257,8 @@ window.__ModuleLoader__.load({
|
|
|
3223
3257
|
// · `[data-side-branch-menu]` = 模型选择悬浮窗(挂在 `document.body` 下,不在面板子树里)。
|
|
3224
3258
|
const refused = 'input, textarea, button, [contenteditable]:not([contenteditable="false"]), [hidden], .dsh-side-branch-panel, [data-side-branch-menu]'
|
|
3225
3259
|
if (startElement.closest(refused) !== null) return ''
|
|
3260
|
+
// ★ 只允许**主会话正文区**(见 insideMainConversation;0.2.2 修的"右栏/设置里划字也弹")
|
|
3261
|
+
if (insideMainConversation(startElement) === false) return ''
|
|
3226
3262
|
|
|
3227
3263
|
const text = String(selection.toString()).trim()
|
|
3228
3264
|
return text
|
package/lib/index.js
CHANGED
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
*
|
|
58
58
|
* ══ 改动前须知 ══════════════════════════════════════════════════════════════════
|
|
59
59
|
* · SSE 响应**整条手写**(`Connection: close`)——见 `handleStream` 里那段注释,换成框架自带写法会坏。
|
|
60
|
-
* ·
|
|
60
|
+
* · 客户端断开**不中止**这一轮:让它跑到自然结束,兜底是"长时间没有任何事件"的停滞看门狗
|
|
61
|
+
* (**不是**总时长上限 —— 总时长与主会话一样不设限,见 `STALL_TIMEOUT_MS`)。
|
|
61
62
|
* · 请求体上限 64KiB,超了**先回 413 再收尾**(不能 `req.destroy()`)。
|
|
62
63
|
* · 客户端传来的一切都是**不可信输入**(问题长度、模型标识、conversationId 归属都要校验)。
|
|
63
64
|
*/
|
|
@@ -99,9 +100,21 @@ const ROUTE_METHODS = {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
/**
|
|
102
|
-
*
|
|
103
|
+
* 单轮的**停滞**看门狗(**不是**总时长上限)。
|
|
104
|
+
*
|
|
105
|
+
* ★ 0.2.2 起的语义:一轮**跑多久都不中止,与主会话一致**。这里只兜"整轮完全没有任何事件"的病
|
|
106
|
+
* (模型/网络卡死、本插件侧的事件订阅断了)—— 没有它,那一轮会永驻:面板一直转圈、
|
|
107
|
+
* 这一段会话永久 `busy`(之后每次提问都回"正在生成回答")、并发名额也漏掉。
|
|
108
|
+
*
|
|
109
|
+
* ⛔ 别再改回"总时长硬超时"。0.2.1 及以前用的是 90 秒的总时长上限,于是**长思考 / 长工具 /
|
|
110
|
+
* 多步**的一轮到了 90 秒就被无条件掐断,面板显示「请求超时(90 秒)」;而同样的提问在主会话里
|
|
111
|
+
* 只是继续等。这不是"权限差异",是缺陷 —— 侧枝除了工具放行名单之外不应与主会话有别的区别。
|
|
112
|
+
*
|
|
113
|
+
* 平台自己还有一层:`dsh-llm-deepseek` 的 `streamIdleTimeoutMs`(默认 5 分钟,收到 chunk 就重置)。
|
|
114
|
+
* 真卡住的模型请求通常先被它以 `TIMEOUT` 收口 ⇒ 这里的 30 分钟几乎不会先触发;
|
|
115
|
+
* 它兜的是"连一个会话事件都没有"的那种卡死。**每次事件都会重新计时**(见 `stallArm`)。
|
|
103
116
|
*/
|
|
104
|
-
const
|
|
117
|
+
const STALL_TIMEOUT_MS = 30 * 60 * 1000
|
|
105
118
|
/** 同时进行的侧枝会话轮次上限。 */
|
|
106
119
|
const MAX_RUNNING_JOBS = 8
|
|
107
120
|
/** 请求体上限(官方自挂路由取 64 KiB,"anything larger is hostile")。 */
|
|
@@ -422,7 +435,9 @@ const HOST_TEXT = {
|
|
|
422
435
|
inheritedFailed: '读取继承内容失败:{msg}',
|
|
423
436
|
// ⚠️ 措辞要点:这是**显式拒绝**,不是"内容被丢了"。用户按「清空」就能继续。
|
|
424
437
|
contextFull: '**对话长度已达上限**({used}/{limit}),无法继续追问。请「清空」面板后开始新的一轮。',
|
|
425
|
-
|
|
438
|
+
// ★ 停滞看门狗(不是总时长上限):只在这一轮**完全没有事件**这么久时才出现
|
|
439
|
+
stalled: '已经 {n} 分钟没有任何输出,已中止这一轮。你可以直接重新提问。',
|
|
440
|
+
turnFailed: '本轮失败:{msg}',
|
|
426
441
|
deliverFailed: '投递问题失败:{msg}',
|
|
427
442
|
blocked: '内容被安全策略拦截',
|
|
428
443
|
maxTokens: '已达到输出长度上限,回答可能被截断',
|
|
@@ -458,7 +473,8 @@ const HOST_TEXT = {
|
|
|
458
473
|
'Could not rebuild this branch from the main session\u2019s latest content: {msg}. This turn was not sent; you can keep using the current branch.',
|
|
459
474
|
inheritedFailed: 'Could not read the inherited content: {msg}',
|
|
460
475
|
contextFull: '**Context limit reached** ({used}/{limit}); follow-ups are not possible. Clear the panel to start a new round.',
|
|
461
|
-
|
|
476
|
+
stalled: 'No output for {n} minutes, so this turn was aborted. You can ask again right away.',
|
|
477
|
+
turnFailed: 'This turn failed: {msg}',
|
|
462
478
|
deliverFailed: 'Could not deliver the question: {msg}',
|
|
463
479
|
blocked: 'Blocked by security policy',
|
|
464
480
|
maxTokens: 'The output limit was reached; the answer may be truncated',
|
|
@@ -1077,7 +1093,7 @@ function handleStream(jobId, req, res) {
|
|
|
1077
1093
|
|
|
1078
1094
|
// ⚠️ **不能**"一断就往死里掐":`EventSource` 会因为瞬时抖动/重连而 close 一次。
|
|
1079
1095
|
// **客户端断开不中止这一轮** —— 让它跑到自然结束,也**不要**加"宽限期到了就 cancel"那种逻辑:
|
|
1080
|
-
// 那属于"未告知就中止"。唯一兜底 =
|
|
1096
|
+
// 那属于"未告知就中止"。唯一兜底 = `STALL_TIMEOUT_MS` 停滞看门狗(**不是**总时长上限);
|
|
1081
1097
|
// 客户端回来时用 snapshot 补齐已有正文/推理。
|
|
1082
1098
|
req.on('close', () => {
|
|
1083
1099
|
closeFn()
|
|
@@ -1471,6 +1487,8 @@ function subscribeConversation(ctx, conv) {
|
|
|
1471
1487
|
if (frame === undefined) return
|
|
1472
1488
|
const job = conv.job
|
|
1473
1489
|
if (job === undefined || job.status !== 'running') return
|
|
1490
|
+
// ★ 有输出 ⇒ 这一轮是活的:重挂停滞看门狗(总时长不设限)
|
|
1491
|
+
stallArm(job)
|
|
1474
1492
|
|
|
1475
1493
|
// ★ `start` 帧按 **turn/step 分流**(这一处就是"多段回答只剩最后一段"那个 BUG 的根因):
|
|
1476
1494
|
// · 同 turn 同 step、但 attemptId 变了 ⇒ **同一个 step 内重试** ⇒ 只丢掉该 step 的段落;
|
|
@@ -1515,6 +1533,8 @@ function subscribeConversation(ctx, conv) {
|
|
|
1515
1533
|
conv.offSession = ctx.on('session/event', (session, event) => {
|
|
1516
1534
|
if (String(session?.id) !== childId) return
|
|
1517
1535
|
const type = event?.type
|
|
1536
|
+
// ★ 任何会话事件(工具、用量、消息…)都算"还活着" ⇒ 重新计时(停滞看门狗,不是总时长上限)
|
|
1537
|
+
if (conv.job !== undefined && conv.job.status === 'running') stallArm(conv.job)
|
|
1518
1538
|
// ★ 工具调用:白名单内的工具真的会被执行 ⇒ 面板必须**看得见**(否则面板会毫无理由地停住不动)。
|
|
1519
1539
|
// 官方事件:`tool/call` = `{ turn, step, callId, name, arguments }`(`dsh-session` 的会话事件)。
|
|
1520
1540
|
// 只把**工具名**推给客户端(不推 arguments:可能很长,且含用户内容)。
|
|
@@ -1561,7 +1581,13 @@ function subscribeConversation(ctx, conv) {
|
|
|
1561
1581
|
if (type === 'turn/end') {
|
|
1562
1582
|
const job = conv.job
|
|
1563
1583
|
if (job === undefined || job.status !== 'running') return
|
|
1564
|
-
|
|
1584
|
+
// `reason` 的形状(`dsh-agent-loop`):`{ kind: 'completed' | 'aborted' | 'blocked' |
|
|
1585
|
+
// 'max-tokens' | 'error', error?: { message, code } }`。
|
|
1586
|
+
// ⚠️ 0.2.2 之前只取了 `kind`,于是模型/供应商报错时面板只显示「提问意外终止:error」——
|
|
1587
|
+
// 真正的原因(例如平台流式空闲超时的 TIMEOUT、余额不足、模型下线)全丢了。
|
|
1588
|
+
// 主会话会把这条错误原文显示出来,这里同样把 `message`/`code` 带给用户。
|
|
1589
|
+
const reasonObject = event?.data?.reason
|
|
1590
|
+
const reason = typeof reasonObject?.kind === 'string' ? reasonObject.kind : 'completed'
|
|
1565
1591
|
if (reason === 'completed') {
|
|
1566
1592
|
job.status = 'done'
|
|
1567
1593
|
} else if (reason === 'aborted') {
|
|
@@ -1572,6 +1598,10 @@ function subscribeConversation(ctx, conv) {
|
|
|
1572
1598
|
} else if (reason === 'max-tokens') {
|
|
1573
1599
|
job.status = 'error'
|
|
1574
1600
|
job.error = T.maxTokens
|
|
1601
|
+
} else if (reason === 'error') {
|
|
1602
|
+
job.status = 'error'
|
|
1603
|
+
const detail = messageOf(reasonObject?.error?.message ?? reasonObject?.error?.code ?? '')
|
|
1604
|
+
job.error = detail === '' ? fill(T.endedAbnormal, { reason }) : fill(T.turnFailed, { msg: detail })
|
|
1575
1605
|
} else {
|
|
1576
1606
|
job.status = 'error'
|
|
1577
1607
|
job.error = fill(T.endedAbnormal, { reason })
|
|
@@ -1816,6 +1846,29 @@ function scheduleConversationTtl(conv) {
|
|
|
1816
1846
|
* 只负责发取消信号;终态与回收一律交给 `finishJob`(单一收尾路径,避免两处各写一遍)。
|
|
1817
1847
|
* @param {object} job - 轮次
|
|
1818
1848
|
*/
|
|
1849
|
+
/**
|
|
1850
|
+
* 重挂**停滞**看门狗(每收到一个事件就重新计时)。
|
|
1851
|
+
*
|
|
1852
|
+
* ⚠️ 这是 `STALL_TIMEOUT_MS` 的唯一用法:**任何**流式帧 / 会话事件都调它 ⇒ 一轮的总时长不受限,
|
|
1853
|
+
* 只有"这么久连一个事件都没有"才中止。别改成建 job 时挂一次就不再动 —— 那就是老的总时长超时。
|
|
1854
|
+
* @param {object} job - 轮次
|
|
1855
|
+
*/
|
|
1856
|
+
function stallArm(job) {
|
|
1857
|
+
if (job.timer !== undefined) clearTimeout(job.timer)
|
|
1858
|
+
const timer = setTimeout(() => {
|
|
1859
|
+
if (job.status !== 'running') return
|
|
1860
|
+
const minutes = Math.round(STALL_TIMEOUT_MS / 60_000)
|
|
1861
|
+
log('job', job.id, '停滞看门狗:' + minutes + ' 分钟没有任何事件,中止这一轮')
|
|
1862
|
+
// 先置终态,避免事件回调又把它改成别的
|
|
1863
|
+
job.status = 'error'
|
|
1864
|
+
job.error = fill(hostTexts(job.locale).stalled, { n: minutes })
|
|
1865
|
+
cancelTurn(job)
|
|
1866
|
+
finishJob(job)
|
|
1867
|
+
}, STALL_TIMEOUT_MS)
|
|
1868
|
+
job.timer = timer
|
|
1869
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1819
1872
|
function cancelTurn(job) {
|
|
1820
1873
|
const conv = conversations.get(job.conversationId)
|
|
1821
1874
|
if (conv === undefined) return
|
|
@@ -2294,16 +2347,8 @@ async function handleStart(ctx, payload, res) {
|
|
|
2294
2347
|
conv.rounds += 1
|
|
2295
2348
|
log('轮次建立:', jobId, '(第', conv.rounds, '轮,当前 job 数 =', jobs.size, ')')
|
|
2296
2349
|
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
log('job', job.id, '超时(' + ANSWER_TIMEOUT_MS + 'ms),中止这一轮')
|
|
2300
|
-
// 先置终态,避免事件回调又把它改成别的
|
|
2301
|
-
job.status = 'error'
|
|
2302
|
-
job.error = fill(hostTexts(job.locale).timeout, { n: Math.round(ANSWER_TIMEOUT_MS / 1000) })
|
|
2303
|
-
cancelTurn(job)
|
|
2304
|
-
finishJob(job)
|
|
2305
|
-
}, ANSWER_TIMEOUT_MS)
|
|
2306
|
-
if (typeof job.timer.unref === 'function') job.timer.unref()
|
|
2350
|
+
// ★ 只挂**停滞**看门狗(每来一个事件就重新计时,见 `stallArm`)——**没有**总时长上限。
|
|
2351
|
+
stallArm(job)
|
|
2307
2352
|
|
|
2308
2353
|
// ---- 投递这一轮的问题(第一轮带"分支引导词",拼在问题前面)
|
|
2309
2354
|
// ⚠️ re-fork 出来的段(`skipPrependNotice`)不拼:重放进来的历史里**已经有一份**引导词,
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-side-branch",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "DSH
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "A read-only branch: quote text in the main session to ask follow-ups without polluting its model context. · DSH 只读侧枝:划选文字即可追问,答案不进入主会话的模型上下文。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
8
|
+
"deepseek",
|
|
8
9
|
"deepseek-harness",
|
|
9
10
|
"cordis",
|
|
11
|
+
"sidebar",
|
|
10
12
|
"side-chat",
|
|
11
13
|
"side-branch",
|
|
12
14
|
"read-only",
|
|
15
|
+
"context-isolation",
|
|
13
16
|
"prefix-cache"
|
|
14
17
|
],
|
|
15
18
|
"type": "module",
|
|
@@ -59,7 +62,8 @@
|
|
|
59
62
|
},
|
|
60
63
|
"license": "MIT",
|
|
61
64
|
"engines": {
|
|
62
|
-
"node": ">=20"
|
|
65
|
+
"node": ">=20",
|
|
66
|
+
"dsh": ">=0.1.5-rc.2"
|
|
63
67
|
},
|
|
64
68
|
"peerDependencies": {
|
|
65
69
|
"@deepseek-ai/cordis": "^4.0.2"
|