dsh-plugin-message-edit 1.0.1 → 1.1.0

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/LICENSE CHANGED
@@ -1,24 +1,24 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 SpookySandwich
4
-
5
- Portions of the host-side branching logic are derived from dsh-message-edit,
6
- Copyright (c) Moeblack, also released under the MIT License.
7
-
8
- Permission is hereby granted, free of charge, to any person obtaining a copy
9
- of this software and associated documentation files (the "Software"), to deal
10
- in the Software without restriction, including without limitation the rights
11
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
- copies of the Software, and to permit persons to whom the Software is
13
- furnished to do so, subject to the following conditions:
14
-
15
- The above copyright notice and this permission notice shall be included in all
16
- copies or substantial portions of the Software.
17
-
18
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpookySandwich
4
+
5
+ Portions of the host-side branching logic are derived from dsh-message-edit,
6
+ Copyright (c) Moeblack, also released under the MIT License.
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/README.en.md CHANGED
@@ -5,7 +5,7 @@ English | [简体中文](README.md)
5
5
  [![npm](https://img.shields.io/npm/v/dsh-plugin-message-edit?color=cb3837&logo=npm)](https://www.npmjs.com/package/dsh-plugin-message-edit)
6
6
  [![CI](https://github.com/SpookySandwich/dsh-plugin-message-edit/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/SpookySandwich/dsh-plugin-message-edit/actions/workflows/ci.yml)
7
7
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
8
- [![dsh](https://img.shields.io/badge/dsh-0.1.2--rc.1-4b8dff)](https://github.com/deepseek-ai/deepseek-harness)
8
+ [![dsh](https://img.shields.io/badge/dsh-0.1.5--rc.2-4b8dff)](https://github.com/deepseek-ai/deepseek-harness)
9
9
  [![stars](https://img.shields.io/github/stars/SpookySandwich/dsh-plugin-message-edit?style=flat&label=stars)](https://github.com/SpookySandwich/dsh-plugin-message-edit/stargazers)
10
10
 
11
11
  Edit a message you already sent and the conversation **rewinds and branches** from that point, the way ChatGPT, Claude and DeepSeek all do it. The old version is not overwritten — a `‹ 2/4 ›` counter appears under the bubble, and a Versions tab draws the whole tree.
@@ -70,9 +70,15 @@ For technical details and developer guides, see:
70
70
  - [Tree Data Model & Algorithms](docs/TREE_DATA_MODEL.md): Turn-level message tree projection, sibling fan-out, ghost recovery, and active path calculation.
71
71
  - [Development & Testing Guide](docs/DEVELOPMENT.md): Build pipeline, automated test suite, and local installation instructions.
72
72
 
73
- ## Compatibility
73
+ ## Compatibility
74
+
75
+ Version `1.1.0`: Fix seeded edit/retry creation, clear inherited pending input before publication, identify version markers by session ownership, preserve reasoning effort, and read persisted branches through disposable session observations.
76
+
77
+ The declared host range is `>=0.1.5-rc.2 <0.1.6-0`; the official `0.1.5-rc.2` runtime was verified. DSH `0.1.6` alpha is not claimed compatible. Keep the previous plugin release on older DSH. [Validation record](.github/reviews/dsh-0.1.5.md).
78
+
79
+ Download the archive from the [GitHub Release](https://github.com/SpookySandwich/dsh-plugin-message-edit/releases/tag/v1.1.0), then run `dsh plugin --profile desktop add ./dsh-plugin-message-edit-1.1.0.tgz`.
74
80
 
75
- Version `1.0.1` has been verified in an isolated DSH `0.1.2-rc.1` Web environment:
81
+ Version `1.1.0` has been verified in an isolated DSH `0.1.5-rc.2` Web environment:
76
82
  plugin loading, images, edit/retry, nested branches, and restored sessions. Model
77
83
  responses use a local test adapter; remote model services were not exercised.
78
84
 
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/v/dsh-plugin-message-edit?color=cb3837&logo=npm)](https://www.npmjs.com/package/dsh-plugin-message-edit)
6
6
  [![CI](https://github.com/SpookySandwich/dsh-plugin-message-edit/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/SpookySandwich/dsh-plugin-message-edit/actions/workflows/ci.yml)
7
7
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
8
- [![dsh](https://img.shields.io/badge/dsh-0.1.2--rc.1-4b8dff)](https://github.com/deepseek-ai/deepseek-harness)
8
+ [![dsh](https://img.shields.io/badge/dsh-0.1.5--rc.2-4b8dff)](https://github.com/deepseek-ai/deepseek-harness)
9
9
  [![stars](https://img.shields.io/github/stars/SpookySandwich/dsh-plugin-message-edit?style=flat&label=stars)](https://github.com/SpookySandwich/dsh-plugin-message-edit/stargazers)
10
10
 
11
11
  编辑一条已经发出的消息,对话会从那一刻 **真正回溯并分叉**——和 ChatGPT、Claude、DeepSeek 的做法一致。旧版本不会被覆盖:气泡下方出现 `‹ 2/4 ›` 计数,「版本」标签页则画出整棵树。
@@ -70,9 +70,15 @@ DSH 的会话是仅追加的事件日志,本身不支持会话内分支,因
70
70
  - [树数据模型与算法 (Tree Data Model)](docs/TREE_DATA_MODEL.md):轮次级消息树构建、同级展开、删除会话(Ghost)桥接与高亮路径计算。
71
71
  - [开发与测试指南 (Development)](docs/DEVELOPMENT.md):构建流程、单元测试与本地安装说明。
72
72
 
73
- ## 兼容性
73
+ ## 兼容性
74
+
75
+ 版本 `1.1.0`:修复编辑/重试的 seed 边界,发布分支前清空继承的待发送输入,按会话身份识别版本标记,保留推理强度,并通过会话观察接口恢复持久分支。
76
+
77
+ 声明兼容范围为 `>=0.1.5-rc.2 <0.1.6-0`;已验证官方 `0.1.5-rc.2`,不声明兼容 `0.1.6` alpha。旧版 DSH 请保留上一插件版本。[验证记录](.github/reviews/dsh-0.1.5.md)。
78
+
79
+ 可从 [GitHub Release](https://github.com/SpookySandwich/dsh-plugin-message-edit/releases/tag/v1.1.0) 下载发布包,然后执行 `dsh plugin --profile desktop add ./dsh-plugin-message-edit-1.1.0.tgz`。
74
80
 
75
- `1.0.1` 已在 DSH `0.1.2-rc.1` 的隔离 Web 环境验证:插件加载、图片显示、编辑与重试、深层分支,以及持久会话的恢复读取。模型回复使用本地测试实现,未调用远程模型服务。
81
+ `1.1.0` 已在 DSH `0.1.5-rc.2` 的隔离 Web 环境验证:插件加载、图片显示、编辑与重试、深层分支,以及持久会话的恢复读取。模型回复使用本地测试实现,未调用远程模型服务。
76
82
 
77
83
  兼容层保留了旧版 `events` / `seedLength` 接口支持,并由自动测试覆盖。DSH 仍在快速迭代,尚未验证的后续版本不在此保证范围内。更新插件后请重启 DSH。
78
84
 
package/cordis.patch.yml CHANGED
@@ -1,7 +1,7 @@
1
- # The loader entry id stays `message-tree` on purpose: Moeblack's
2
- # dsh-message-edit uses the id `message-edit`, and two entries sharing an id
3
- # collide when both plugins are installed. `name` is the package to import,
4
- # so it follows the package rename.
5
- - insert:
6
- - id: message-tree
7
- name: dsh-plugin-message-edit
1
+ # The loader entry id stays `message-tree` on purpose: Moeblack's
2
+ # dsh-message-edit uses the id `message-edit`, and two entries sharing an id
3
+ # collide when both plugins are installed. `name` is the package to import,
4
+ # so it follows the package rename.
5
+ - insert:
6
+ - id: message-tree
7
+ name: dsh-plugin-message-edit
@@ -30,15 +30,18 @@ Because DSH session event logs are append-only without native in-session branchi
30
30
  - Normalizes DSH sessions through `lib/session-record.js`: current live sessions
31
31
  expose `snapshotEvents()`, query snapshots carry their header in `session`,
32
32
  and older records expose `events` / `header`. Invalid logs fail explicitly.
33
- - Current DSH branches use `meta.isSeeded` plus `inheritedEventCount`; older
34
- hosts use `meta.seedLength`. The normalized inherited cut prevents a nested
35
- branch from mistaking an inherited version marker for its own marker.
33
+ - DSH 0.1.5 branches use `meta.isSeeded` plus `inheritedEventCount`, which must
34
+ equal the complete constructor seed length. New markers carry their owning
35
+ `sessionId`; old markers without that field use the legacy inherited cut.
36
+ Cold logs use `observeSession(..., { projectionMode: 'none' })` and release
37
+ the observation lease; this restores seeded sessions through the correct API.
36
38
  - Registers the `/message-tree` HTTP route on `ctx.webServer`.
37
39
  - Owns branch creation transactions (`POST /message-tree`):
38
40
  1. Truncates parent events up to the target turn.
39
- 2. Seeds a new DSH session with the prefix events.
40
- 3. Appends a durable `message-tree/version` marker with `ignorable: true`.
41
- 4. Submits the edited prompt into the new session.
41
+ 2. Adds an ignorable `message-tree/version` marker to the constructor seed.
42
+ 3. Creates the agent and clears both inherited inbox queues in its setup,
43
+ before publication can schedule the rewound original input.
44
+ 4. Flushes the branch and submits the edited prompt exactly once.
42
45
  - Owns graph queries (`GET /message-tree?sessionId=...`):
43
46
  - Traverses the session family DAG.
44
47
  - Recovers deleted/ghost ancestors from surviving descendants' event logs.
@@ -56,13 +59,14 @@ Because DSH session event logs are append-only without native in-session branchi
56
59
 
57
60
  DSH sessions are immutable append-only logs. When branching:
58
61
 
59
- 1. **Seed Inheritance**: A new session is initialized whose log begins with an exact clone of the parent's event log up to the start of the edited turn (`seedLength`).
60
- 2. **Durable Marker**: The host appends a custom event:
62
+ 1. **Seed Inheritance**: Copy the parent prefix before the edited turn, then add the plugin marker. The full constructor seed is inherited on DSH 0.1.5; the kernel adds its own `session/end-seed` afterwards.
63
+ 2. **Durable Marker**: The seed carries a custom event with explicit child ownership:
61
64
  ```json
62
65
  {
63
66
  "type": "message-tree/version",
64
67
  "data": {
65
- "schemaVersion": 1,
68
+ "schemaVersion": 1,
69
+ "sessionId": "child-session-id",
66
70
  "effect": {
67
71
  "operation": "edit",
68
72
  "targetTurn": 1,
@@ -92,8 +92,8 @@ To add new tests, edit [`test/tree.test.mjs`](file:///D:/dsh-plugin-message-edit
92
92
 
93
93
  ### Optional real DSH acceptance
94
94
 
95
- `test/fixtures/dsh-acceptance.mjs` is an offline model adapter and live/cold
96
- session fixture for an installed official DSH `0.1.2-rc.1` runtime. Mount it
95
+ `test/fixtures/dsh-acceptance.mjs` is an offline model adapter and live/cold
96
+ session fixture for an installed official DSH `0.1.5-rc.2` runtime. Mount it
97
97
  only in a new temporary home whose name contains `message-edit-dsh-qa-`.
98
98
  Set `DSH_HOME` to that home and `DSH_QA_MODULES` to the official runtime's
99
99
  `node_modules` directory. Use a separate Web profile with the base/Web bundles,
@@ -110,8 +110,20 @@ The verifier exercises real HTTP edit/retry operations, local model execution,
110
110
  image retention, unchanged source messages, and nested branch markers. Restart
111
111
  the same isolated server and repeat to exercise persisted branches. The fixture
112
112
  adds `/qa/state` and `/qa/followup` endpoints solely for this disposable test.
113
- Browser acceptance additionally checks the settings entry, version switcher,
114
- thumbnails, and native original-image viewer. No remote API key is needed.
113
+ Browser acceptance additionally checks the settings entry, version switcher,
114
+ thumbnails, and native original-image viewer. No remote API key is needed.
115
+
116
+ For an automated fresh-boot and restart run, set `DSH_QA_MODULES` to the
117
+ official runtime's `node_modules` directory and run:
118
+
119
+ ```bash
120
+ node scripts/run-dsh-acceptance.mjs
121
+ ```
122
+
123
+ The runner owns and removes a unique temporary home, boots the official CLI
124
+ on an OS-selected port, runs the verifier, restarts the host, and repeats.
125
+ The restart pass explicitly resumes and retries an already seeded branch.
126
+ CI runs this on Linux and Windows in addition to the regression suite.
115
127
 
116
128
  ---
117
129
 
@@ -1,96 +1,96 @@
1
- # Tree Data Model & Algorithms
2
-
3
- This document details how conversation versions and turns are represented, branched, and visualized in `dsh-plugin-message-edit`.
4
-
5
- ---
6
-
7
- ## 1. Dual-Level Representation
8
-
9
- There are two distinct levels of data representation in the system:
10
-
11
- 1. **Storage Level (Session DAG)**:
12
- - DSH enforces session-level isolation. Each branch is a distinct DSH session record with `parentSession` and `seedLength`.
13
- - The host maintains durable `message-tree/version` markers detailing which turn was edited/retried and what changed.
14
-
15
- 2. **Presentation Level (Turn-Level Branching Tree)**:
16
- - A user thinks of conversation branching at the **message/turn** level, not the session container level.
17
- - `buildTurnTree` projects the session versions into individual turn nodes.
18
-
19
- ```
20
- Session DAG (Storage):
21
- Session A (Original) ──[edit turn 1]──> Session B (Fork)
22
-
23
- Turn Tree (Visualization):
24
- [Root Conversation]
25
- / \
26
- [A: Turn 1 (1/2)] [B: Turn 1 (2/2) - Edited]
27
- | |
28
- [A: Turn 2] [B: Turn 2]
29
- ```
30
-
31
- ---
32
-
33
- ## 2. Core Algorithms
34
-
35
- ### 2.1 Turn Tree Construction (`buildTurnTree`)
36
- *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L173), [`plugin.client.js`](file:///D:/dsh-plugin-message-edit/plugin.client.js#L415)*
37
-
38
- Transforms `versions` into an array of turn nodes:
39
- 1. **Root Conversation Node (`${rootSessionId}#root`)**: Represents the origin anchor of the conversation.
40
- 2. **Root Session Turns**:
41
- - Turn 1 hangs off `${rootSessionId}#root`.
42
- - Turn $k$ ($k > 1$) hangs off `${rootSessionId}#t${k-1}`.
43
- 3. **Forked Session Turns**:
44
- - For a session branched at `targetTurn = T`:
45
- - If $T = 1$: Turn 1 hangs off `${rootSessionId}#root` (sibling of the original Turn 1).
46
- - If $T > 1$: Turn $T$ hangs off `${parentSessionId}#t${T-1}` (sibling of parent's Turn $T$).
47
- - Subsequent turns $T+1, T+2, \dots$ hang off the previous turn in the same session (`${sessionId}#t${k-1}`).
48
- 4. **Safety Fallback**: Any node whose computed `parentId` does not exist in the graph is automatically attached to `${rootSessionId}#root`, preventing disconnected subtrees.
49
-
50
- ### 2.2 Sibling Fan-Out (`attachParentId`)
51
- *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L9)*
52
-
53
- When a user edits Turn 1 repeatedly (e.g. Turn 1 $\rightarrow$ Edit 1 $\rightarrow$ Edit 2 while viewing Edit 1):
54
- - Without fan-out, edits form a chain: $A \rightarrow B \rightarrow C$.
55
- - `attachParentId` traverses up versions of the same turn and stops at the first session that is *not* an edit of that turn ($A$).
56
- - Result: Both Edit 1 and Edit 2 hang off $A$ as sibling branches.
57
-
58
- ### 2.3 Ghost Ancestor Recovery (`ancestorChainFromLog` & `collectFamily`)
59
- *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L110-L171)*
60
-
61
- If an intermediate session in a family is deleted by the user in DSH:
62
- - The deleted session's own event log is gone.
63
- - However, its descendant sessions inherited its prefix log (including the `message-tree/version` marker describing the deleted parent).
64
- - `ancestorChainFromLog` inspects the surviving descendant's seed events to reconstruct deleted ancestors as **ghost nodes** (`deleted: true`).
65
- - `collectFamily` ensures the family graph remains fully connected even when intermediate nodes are deleted.
66
-
67
- ### 2.4 Active Path Calculation
68
- *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L318-L339)*
69
-
70
- To highlight only the active branch path without highlighting superseded sibling branches:
71
- 1. Locate the latest turn node in `currentSessionId`.
72
- 2. Walk upwards following `parentId` pointers until reaching `${rootSessionId}#root`.
73
- 3. Mark only nodes on this walk with `onCurrentPath = true`.
74
-
75
- ### 2.5 Bubble Version Ring (`ringFor`)
76
- *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L30-L68)*
77
-
78
- Calculates the `‹ n/m ›` counter under a message at `turn` while viewing `sessionId`:
79
- - Walks parent links to find the common fork point for that turn.
80
- - Filters out deleted/ghost sessions (renumbering over surviving versions).
81
- - Returns `{ alternatives, index }`. If fewer than 2 alternatives exist, returns `null` (counter is hidden).
82
-
83
- ---
84
-
85
- ## 3. Graph Layout & Springs
86
-
87
- *Location: [`plugin.client.js`](file:///D:/dsh-plugin-message-edit/plugin.client.js#L567-L612)*
88
-
89
- - **Tidy Tree Layout (`layoutTurnTree`)**:
90
- - Leaf nodes take successive horizontal slots (`cursor * SLOT_X`, where `SLOT_X = 206px`).
91
- - Parent nodes center horizontally over their children (`(min_x + max_x) / 2`).
92
- - Depths scale vertically (`depth * SLOT_Y`, where `SLOT_Y = 132px`).
93
- - **Spring Physics (`springs.current`)**:
94
- - Cards smoothly animate to their target coordinates using critically-damped spring equations ($k = 190, c = 24$).
95
- - New cards spawn at their parent's coordinates and spring outward.
96
- - Edges are rendered as cubic SVG bezier curves connecting parent card bottoms to child card tops.
1
+ # Tree Data Model & Algorithms
2
+
3
+ This document details how conversation versions and turns are represented, branched, and visualized in `dsh-plugin-message-edit`.
4
+
5
+ ---
6
+
7
+ ## 1. Dual-Level Representation
8
+
9
+ There are two distinct levels of data representation in the system:
10
+
11
+ 1. **Storage Level (Session DAG)**:
12
+ - DSH enforces session-level isolation. Each branch is a distinct DSH session record with `parentSession`, `isSeeded`, and a separate `inheritedEventCount`.
13
+ - The host maintains durable `message-tree/version` markers detailing which turn was edited/retried and what changed.
14
+
15
+ 2. **Presentation Level (Turn-Level Branching Tree)**:
16
+ - A user thinks of conversation branching at the **message/turn** level, not the session container level.
17
+ - `buildTurnTree` projects the session versions into individual turn nodes.
18
+
19
+ ```
20
+ Session DAG (Storage):
21
+ Session A (Original) ──[edit turn 1]──> Session B (Fork)
22
+
23
+ Turn Tree (Visualization):
24
+ [Root Conversation]
25
+ / \
26
+ [A: Turn 1 (1/2)] [B: Turn 1 (2/2) - Edited]
27
+ | |
28
+ [A: Turn 2] [B: Turn 2]
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 2. Core Algorithms
34
+
35
+ ### 2.1 Turn Tree Construction (`buildTurnTree`)
36
+ *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L173), [`plugin.client.js`](file:///D:/dsh-plugin-message-edit/plugin.client.js#L415)*
37
+
38
+ Transforms `versions` into an array of turn nodes:
39
+ 1. **Root Conversation Node (`${rootSessionId}#root`)**: Represents the origin anchor of the conversation.
40
+ 2. **Root Session Turns**:
41
+ - Turn 1 hangs off `${rootSessionId}#root`.
42
+ - Turn $k$ ($k > 1$) hangs off `${rootSessionId}#t${k-1}`.
43
+ 3. **Forked Session Turns**:
44
+ - For a session branched at `targetTurn = T`:
45
+ - If $T = 1$: Turn 1 hangs off `${rootSessionId}#root` (sibling of the original Turn 1).
46
+ - If $T > 1$: Turn $T$ hangs off `${parentSessionId}#t${T-1}` (sibling of parent's Turn $T$).
47
+ - Subsequent turns $T+1, T+2, \dots$ hang off the previous turn in the same session (`${sessionId}#t${k-1}`).
48
+ 4. **Safety Fallback**: Any node whose computed `parentId` does not exist in the graph is automatically attached to `${rootSessionId}#root`, preventing disconnected subtrees.
49
+
50
+ ### 2.2 Sibling Fan-Out (`attachParentId`)
51
+ *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L9)*
52
+
53
+ When a user edits Turn 1 repeatedly (e.g. Turn 1 $\rightarrow$ Edit 1 $\rightarrow$ Edit 2 while viewing Edit 1):
54
+ - Without fan-out, edits form a chain: $A \rightarrow B \rightarrow C$.
55
+ - `attachParentId` traverses up versions of the same turn and stops at the first session that is *not* an edit of that turn ($A$).
56
+ - Result: Both Edit 1 and Edit 2 hang off $A$ as sibling branches.
57
+
58
+ ### 2.3 Ghost Ancestor Recovery (`ancestorChainFromLog` & `collectFamily`)
59
+ *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L110-L171)*
60
+
61
+ If an intermediate session in a family is deleted by the user in DSH:
62
+ - The deleted session's own event log is gone.
63
+ - However, its descendant sessions inherited its prefix log (including the `message-tree/version` marker describing the deleted parent).
64
+ - `ancestorChainFromLog` inspects the surviving descendant's seed events to reconstruct deleted ancestors as **ghost nodes** (`deleted: true`).
65
+ - `collectFamily` ensures the family graph remains fully connected even when intermediate nodes are deleted.
66
+
67
+ ### 2.4 Active Path Calculation
68
+ *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L318-L339)*
69
+
70
+ To highlight only the active branch path without highlighting superseded sibling branches:
71
+ 1. Locate the latest turn node in `currentSessionId`.
72
+ 2. Walk upwards following `parentId` pointers until reaching `${rootSessionId}#root`.
73
+ 3. Mark only nodes on this walk with `onCurrentPath = true`.
74
+
75
+ ### 2.5 Bubble Version Ring (`ringFor`)
76
+ *Location: [`lib/tree-logic.js`](file:///D:/dsh-plugin-message-edit/lib/tree-logic.js#L30-L68)*
77
+
78
+ Calculates the `‹ n/m ›` counter under a message at `turn` while viewing `sessionId`:
79
+ - Walks parent links to find the common fork point for that turn.
80
+ - Filters out deleted/ghost sessions (renumbering over surviving versions).
81
+ - Returns `{ alternatives, index }`. If fewer than 2 alternatives exist, returns `null` (counter is hidden).
82
+
83
+ ---
84
+
85
+ ## 3. Graph Layout & Springs
86
+
87
+ *Location: [`plugin.client.js`](file:///D:/dsh-plugin-message-edit/plugin.client.js#L567-L612)*
88
+
89
+ - **Tidy Tree Layout (`layoutTurnTree`)**:
90
+ - Leaf nodes take successive horizontal slots (`cursor * SLOT_X`, where `SLOT_X = 206px`).
91
+ - Parent nodes center horizontally over their children (`(min_x + max_x) / 2`).
92
+ - Depths scale vertically (`depth * SLOT_Y`, where `SLOT_Y = 132px`).
93
+ - **Spring Physics (`springs.current`)**:
94
+ - Cards smoothly animate to their target coordinates using critically-damped spring equations ($k = 190, c = 24$).
95
+ - New cards spawn at their parent's coordinates and spring outward.
96
+ - Edges are rendered as cubic SVG bezier curves connecting parent card bottoms to child card tops.
package/lib/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // turn retries only, always truncating downstream.
13
13
 
14
14
  import { attachParentId, ancestorChainFromLog, collectFamily } from './tree-logic.js';
15
- import { sessionEventCount, sessionRecord, branchSeedOptions } from './session-record.js';
15
+ import { sessionEventCount, sessionRecord, branchSeedOptions, readSessionRecord } from './session-record.js';
16
16
 
17
17
  // The package is named dsh-plugin-message-edit, but the route, the cordis id
18
18
  // and the durable event type below deliberately keep the `message-tree`
@@ -148,15 +148,17 @@ function agentOptions(events, fallback) {
148
148
  if (provider === undefined || provider.length === 0 || model === undefined || model.length === 0) {
149
149
  throw new Error('无法从会话历史解析模型路由。');
150
150
  }
151
- const maxTokens = config?.maxTokens ?? fallback?.maxTokens;
152
- return { provider, model, ...maxTokens === undefined ? {} : { maxTokens } };
151
+ const maxTokens = config?.maxTokens ?? fallback?.maxTokens;
152
+ const reasoningEffort = config?.reasoningEffort ?? fallback?.reasoningEffort;
153
+ return { provider, model, ...maxTokens === undefined ? {} : { maxTokens },
154
+ ...reasoningEffort === undefined ? {} : { reasoningEffort } };
153
155
  }
154
156
 
155
157
  async function withSourceAgent(ctx, sessionId, operation) {
156
158
  let handle;
157
159
  let agent = ctx.agents.get(sessionId);
158
160
  if (agent === undefined) {
159
- const snapshot = sessionRecord(await ctx.sessionQuery.readSession(sessionId));
161
+ const snapshot = await readSessionRecord(ctx, sessionId);
160
162
  handle = await ctx.agents.resume({
161
163
  resumeSessionId: sessionId,
162
164
  agentOptions: agentOptions(snapshot.events),
@@ -179,19 +181,21 @@ function inheritedSeed(source, boundary) {
179
181
  return source.events.slice(0, boundary + 1);
180
182
  }
181
183
 
182
- function versionSeed(source, plan) {
184
+ function versionSeed(source, plan, childId) {
183
185
  const events = inheritedSeed(source, plan.boundary);
184
186
  const inheritedLength = events.length;
185
187
  events.push({
186
188
  type: 'message-tree/version',
187
189
  seq: events.length,
188
190
  time: Date.now(),
189
- data: plan.version,
191
+ // Ownership is explicit: modern DSH requires the entire constructor seed
192
+ // to be inherited, so log position alone cannot identify our own marker.
193
+ data: { ...plan.version, sessionId: childId },
190
194
  // Plugin event types live outside the harness vocabulary; without this
191
195
  // marker the read path refuses to interpret the whole session log.
192
196
  ignorable: true,
193
197
  });
194
- return { events, inheritedLength };
198
+ return { events, inheritedLength: typeof source.header.isSeeded === 'boolean' ? events.length : inheritedLength };
195
199
  }
196
200
 
197
201
  function sessionPreset(session) {
@@ -202,9 +206,8 @@ function sessionPreset(session) {
202
206
  return session.header.agentPreset;
203
207
  }
204
208
 
205
- async function loadSessionRecord(ctx, sessionId) {
206
- const live = ctx.sessions.get(sessionId);
207
- return sessionRecord(live ?? await ctx.sessionQuery.readSession(sessionId));
209
+ async function loadSessionRecord(ctx, sessionId) {
210
+ return readSessionRecord(ctx, sessionId);
208
211
  }
209
212
 
210
213
  function sessionRecordId(session) {
@@ -246,16 +249,21 @@ async function resolveAttachSession(ctx, sourceSession, targetTurn) {
246
249
  }
247
250
 
248
251
  async function createVersionAgent(ctx, source, childId, plan, options) {
249
- const seed = versionSeed(source, plan);
252
+ const seed = versionSeed(source, plan, childId);
250
253
  const presets = ctx.get('agentPresets');
251
254
  const presetId = sessionPreset(source);
252
255
  let agentPreset;
253
- let setup;
254
- if (presets !== undefined && presetId !== undefined) {
255
- const resolved = (await presets.resolve(presetId)).id;
256
- agentPreset = resolved;
257
- setup = async (agentCtx) => { await presets.mount(agentCtx, resolved); };
258
- }
256
+ if (presets !== undefined && presetId !== undefined) {
257
+ const resolved = (await presets.resolve(presetId)).id;
258
+ agentPreset = resolved;
259
+ }
260
+ const setup = async (agentCtx, agent) => {
261
+ // Rewinding before turn/start also rewinds before the durable inbox claim.
262
+ // Clear BOTH inherited queues before publication can start the agent. The
263
+ // original input and any queued follow-ups must not run in the new branch.
264
+ agent?.inbox?.clear();
265
+ if (agentPreset !== undefined) await presets.mount(agentCtx, agentPreset);
266
+ };
259
267
  const seedOptions = branchSeedOptions(source, seed.inheritedLength);
260
268
  const child = await ctx.agents.create({
261
269
  sessionId: childId,
@@ -289,7 +297,7 @@ async function createVersionAgent(ctx, source, childId, plan, options) {
289
297
  ...agentPreset === undefined ? {} : { agentPreset },
290
298
  },
291
299
  agentOptions: options,
292
- ...setup === undefined ? {} : { setup },
300
+ setup,
293
301
  });
294
302
  try {
295
303
  await ctx.sessions.flush(child.agent.session);
@@ -572,8 +580,10 @@ async function runOperation(ctx, operation) {
572
580
 
573
581
  /* ------------------------------------------------------- tree projection -- */
574
582
 
575
- function ownVersionEvent({ header, events, inheritedEventCount: inherited }) {
576
- const ownEvents = events.filter((event) => event.type === 'message-tree/version' && event.seq >= inherited);
583
+ function ownVersionEvent({ header, events, inheritedEventCount: inherited }) {
584
+ const ownEvents = events.filter((event) => event.type === 'message-tree/version'
585
+ && (event.data.sessionId === header.id
586
+ || (event.data.sessionId === undefined && event.seq >= inherited)));
577
587
  if (ownEvents.length === 0) return undefined;
578
588
  const event = ownEvents[0];
579
589
  const version = event.data;
@@ -30,8 +30,23 @@ export function sessionRecord(session) {
30
30
  }
31
31
 
32
32
  /** DSH moved the inherited cut from header.seedLength to a creation option. */
33
- export function branchSeedOptions(source, inheritedEventCount) {
33
+ export function branchSeedOptions(source, inheritedEventCount) {
34
34
  return typeof source.header.isSeeded === 'boolean'
35
35
  ? { inheritedEventCount, meta: { isSeeded: true } }
36
36
  : { meta: { seedLength: inheritedEventCount } };
37
- }
37
+ }
38
+
39
+ /** Read through the host's restored-session observation API, releasing its lease. */
40
+ export async function readSessionRecord(ctx, sessionId) {
41
+ const live = ctx.sessions.get(sessionId);
42
+ if (live !== undefined) return sessionRecord(live);
43
+ if (typeof ctx.sessionQuery.observeSession === 'function') {
44
+ const observation = await ctx.sessionQuery.observeSession(sessionId, { projectionMode: 'none' });
45
+ try {
46
+ return sessionRecord(observation);
47
+ } finally {
48
+ observation[Symbol.dispose]();
49
+ }
50
+ }
51
+ return sessionRecord(await ctx.sessionQuery.readSession(sessionId));
52
+ }