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 +24 -24
- package/README.en.md +9 -3
- package/README.md +9 -3
- package/cordis.patch.yml +7 -7
- package/docs/ARCHITECTURE.md +13 -9
- package/docs/DEVELOPMENT.md +16 -4
- package/docs/TREE_DATA_MODEL.md +96 -96
- package/lib/index.js +30 -20
- package/lib/session-record.js +17 -2
- package/lib/tree-logic.js +343 -343
- package/package.json +70 -66
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
|
[](https://www.npmjs.com/package/dsh-plugin-message-edit)
|
|
6
6
|
[](https://github.com/SpookySandwich/dsh-plugin-message-edit/actions/workflows/ci.yml)
|
|
7
7
|
[](LICENSE)
|
|
8
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
9
9
|
[](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
|
|
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
|
[](https://www.npmjs.com/package/dsh-plugin-message-edit)
|
|
6
6
|
[](https://github.com/SpookySandwich/dsh-plugin-message-edit/actions/workflows/ci.yml)
|
|
7
7
|
[](LICENSE)
|
|
8
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
9
9
|
[](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
|
|
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
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
40
|
-
3.
|
|
41
|
-
|
|
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**:
|
|
60
|
-
2. **Durable Marker**: The
|
|
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,
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -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.
|
|
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
|
|
package/docs/TREE_DATA_MODEL.md
CHANGED
|
@@ -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 `
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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'
|
|
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;
|
package/lib/session-record.js
CHANGED
|
@@ -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
|
+
}
|