@anionex/dsh-turn-rewind 0.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 +28 -0
- package/README.md +182 -0
- package/README.zh.md +180 -0
- package/SECURITY.md +53 -0
- package/cordis.patch.yml +4 -0
- package/docs/FORMAT.md +101 -0
- package/docs/IMPLEMENTATION_PLAN.md +56 -0
- package/docs/assets/turn-rewind-action.png +0 -0
- package/docs/assets/turn-rewind-dialog.png +0 -0
- package/lib/client.js +473 -0
- package/lib/client.js.map +1 -0
- package/lib/engine.js +616 -0
- package/lib/errors.js +20 -0
- package/lib/git.js +155 -0
- package/lib/index.js +71 -0
- package/lib/path-utils.js +262 -0
- package/lib/rewind-host.js +421 -0
- package/lib/snapshot.js +183 -0
- package/lib/store.js +391 -0
- package/lib/types/client/index.d.ts +81 -0
- package/lib/types/engine.d.ts +90 -0
- package/lib/types/errors.d.ts +15 -0
- package/lib/types/git.d.ts +14 -0
- package/lib/types/index.d.ts +43 -0
- package/lib/types/path-utils.d.ts +34 -0
- package/lib/types/rewind-host.d.ts +137 -0
- package/lib/types/snapshot.d.ts +35 -0
- package/lib/types/store.d.ts +39 -0
- package/lib/types/types.d.ts +177 -0
- package/lib/types/validate.d.ts +11 -0
- package/lib/types.js +2 -0
- package/lib/validate.js +242 -0
- package/package.json +91 -0
- package/src/client/index.tsx +648 -0
- package/src/client/platform.d.ts +27 -0
- package/src/engine.ts +770 -0
- package/src/errors.ts +23 -0
- package/src/git.ts +178 -0
- package/src/index.ts +100 -0
- package/src/path-utils.ts +255 -0
- package/src/rewind-host.ts +584 -0
- package/src/snapshot.ts +229 -0
- package/src/store.ts +383 -0
- package/src/types.ts +195 -0
- package/src/validate.ts +245 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, dsh-external
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# DSH Turn Rewind
|
|
2
|
+
|
|
3
|
+
[](https://x.com/anion_ex)
|
|
4
|
+
|
|
5
|
+
[中文说明](README.zh.md)
|
|
6
|
+
|
|
7
|
+
Message-anchored project-file recovery for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness), with an option to restart from the restored request.
|
|
8
|
+
|
|
9
|
+
**Turn Rewind** is the user-facing feature, repository, and Profile Bundle name. **Change Ledger** is the durable restore engine underneath it: the `ctx.changeLedger` service, on-disk format, and storage path keep that name because they describe the reusable snapshot and recovery layer rather than the Web action alone.
|
|
10
|
+
|
|
11
|
+
Change Ledger gives a DSH session an explicit safety boundary around workspace mutations:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
create restore point
|
|
15
|
+
↓
|
|
16
|
+
agent / user / external tools modify the worktree
|
|
17
|
+
↓
|
|
18
|
+
preview exact path-level drift
|
|
19
|
+
↓
|
|
20
|
+
review a full or selective restore plan
|
|
21
|
+
↓
|
|
22
|
+
press the final restore button in the rewind dialog
|
|
23
|
+
↓
|
|
24
|
+
create rescue point → restore → verify
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It never commits, stashes, resets, switches branches, edits the Git index, or decides automatically that a change should be reverted.
|
|
28
|
+
|
|
29
|
+
## Preview
|
|
30
|
+
|
|
31
|
+
Rewind appears as an icon-only third action under each user message, after its timestamp and native Copy action:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
Opening it shows the affected files and offers two choices: restore the files and restart from before that message, or restore only the files:
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
## Why it has a Change Ledger engine
|
|
40
|
+
|
|
41
|
+
A diff button can show current changes, but it does not own a durable restore lifecycle. Change Ledger owns:
|
|
42
|
+
|
|
43
|
+
- content-addressed restore-point manifests;
|
|
44
|
+
- Git worktree, HEAD, branch, and in-progress-operation fences;
|
|
45
|
+
- stale-plan detection between review and mutation;
|
|
46
|
+
- exact two-step confirmation plus DSH human approval;
|
|
47
|
+
- automatic pre-restore rescue points;
|
|
48
|
+
- post-restore hash verification;
|
|
49
|
+
- rollback after a failed restore;
|
|
50
|
+
- startup reconciliation of interrupted restore journals;
|
|
51
|
+
- a public `ctx.changeLedger` service that other plugins can consume.
|
|
52
|
+
|
|
53
|
+
The durable format is documented in [docs/FORMAT.md](docs/FORMAT.md). The security and failure model is documented in [SECURITY.md](SECURITY.md).
|
|
54
|
+
|
|
55
|
+
## Safety contract
|
|
56
|
+
|
|
57
|
+
- **Explicit only:** nothing is restored automatically — every restore starts from the user pressing the final button in the Web dialog, or from an explicit call through the service API.
|
|
58
|
+
- **Read before write:** the dialog preview generates an expiring, session-bound plan from the current tree and changes no files.
|
|
59
|
+
- **Human gate:** the dialog's reviewed impact plus the final restore button is the human decision; direct mutation requests without a live session-bound plan pair fail closed.
|
|
60
|
+
- **Rescue before mutation:** every restore captures the current eligible tree as a durable rescue point before changing a path.
|
|
61
|
+
- **No silent omission:** unsupported submodules, sparse checkouts, oversized files, aggregate limits, and unsupported file types fail point creation.
|
|
62
|
+
- **No path escape:** every durable path is canonical and workspace-relative; restore refuses symlink parents and non-empty directory replacement.
|
|
63
|
+
- **No stale overwrite:** selected paths and the reviewed HEAD/branch/operation fence are checked again at apply time. Any relevant post-review change invalidates the plan.
|
|
64
|
+
- **No Git control-plane mutation:** the index, branch, HEAD, stash, and commits remain untouched.
|
|
65
|
+
|
|
66
|
+
## Scope
|
|
67
|
+
|
|
68
|
+
Version `0.1` intentionally supports normal Git worktrees only:
|
|
69
|
+
|
|
70
|
+
- tracked files, including currently missing tracked paths;
|
|
71
|
+
- untracked files not excluded by `.gitignore` or other standard Git excludes;
|
|
72
|
+
- regular files, binary or text;
|
|
73
|
+
- symbolic links;
|
|
74
|
+
- executable and other portable permission bits.
|
|
75
|
+
|
|
76
|
+
The following are rejected or deliberately outside the snapshot:
|
|
77
|
+
|
|
78
|
+
- sparse checkouts;
|
|
79
|
+
- submodule gitlinks (create a restore point inside each submodule instead);
|
|
80
|
+
- ignored files;
|
|
81
|
+
- special files, sockets, devices, and named pipes;
|
|
82
|
+
- extended attributes, ACLs, ownership, timestamps, and hard-link topology;
|
|
83
|
+
- the Git index and repository metadata;
|
|
84
|
+
- non-Git directories.
|
|
85
|
+
|
|
86
|
+
If an ignored or otherwise unmanaged file occupies a path that restoration would replace, the restore fails rather than deleting it.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
Build the checked-out plugin, then add it to each DSH profile that should expose the service:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pnpm install --frozen-lockfile
|
|
94
|
+
pnpm run check
|
|
95
|
+
|
|
96
|
+
dsh plugin --profile web add @anionex/dsh-turn-rewind
|
|
97
|
+
dsh plugin --profile headless add @anionex/dsh-turn-rewind
|
|
98
|
+
|
|
99
|
+
dsh --profile web --dump-config | grep turn-rewind
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Restart a running profile after changing its bundle list.
|
|
103
|
+
|
|
104
|
+
The package is a DSH Profile Bundle. `package.json` declares `dsh.bundle.patch`, and `cordis.patch.yml` mounts `@anionex/dsh-turn-rewind` without a DSH core patch.
|
|
105
|
+
|
|
106
|
+
When the profile also provides the DSH Agent service, the plugin captures a hidden checkpoint in the first `agent/pre-step` waterfall before the Agent processes the opening user message. Capture failures are reported but do not reject the user's turn; the corresponding message simply has no usable rewind point. In Web profiles, the same-origin `/turn-rewind` endpoint resolves the selected `user/message` sequence, exposes a paged file preview, mints a short-lived session-bound restore plan, and delegates child creation to DSH's official Host create/fork lifecycle. It never restores files automatically.
|
|
107
|
+
|
|
108
|
+
## User flow
|
|
109
|
+
|
|
110
|
+
In the Web profile, each direct user message gains a compact, icon-only **Rewind** action after its timestamp and native Copy control. The tooltip reads “Return to before sending this message.” Opening Rewind checks the saved file state, shows a concise preview with a “view all files” action, and offers two modes:
|
|
111
|
+
|
|
112
|
+
| Mode | Code | Conversation |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| **Restore files and restart** (default) | Restores the project files after automatically backing up their current state. | Creates and opens a Session ending before the selected message, then puts that message's text back in the composer. |
|
|
115
|
+
| **Restore files only** | Restores the project files after automatically backing up their current state. | Leaves the current Session open and unchanged. |
|
|
116
|
+
|
|
117
|
+
The dialog itself is the confirmation: there is no duplicate checkbox. It describes each file as restoring an earlier version, finding a deleted file, removing a later-added file, or restoring permissions/type. If the project files already match the state before the selected message, Turn Rewind performs no action and directs the user to the native **Branch** button for conversation-only branching.
|
|
118
|
+
|
|
119
|
+
Before mutation, Turn Rewind rechecks the selected files and repository state, then creates an automatic backup. Changes made after preview invalidate the operation. Any running Agent using the same worktree, including the source Session, blocks restoration; idle Sessions do not block. A reviewed HEAD or branch difference does not block restoration: commits, refs, branch, and index remain unchanged, so restored content may appear as ordinary uncommitted changes against the current HEAD. An in-progress Git operation still blocks. If child creation fails after “restore and restart,” Change Ledger automatically restores the pre-operation files from the backup.
|
|
120
|
+
|
|
121
|
+
DSH Session logs are append-only, so “restart” creates a new Session instead of truncating the original. For the first message, the Host creates a blank Session in the same working directory; for later messages, it forks at the previous completed `turn/end`. A child may reuse an ancestor's prompt checkpoint only while both the selected `user/message` and its exact `turn/start` remain inside every durable `seedLength` fence. Direct child checkpoints take priority and sibling checkpoints never mix. **Branch** creates only a conversation branch and keeps project files unchanged; **Turn Rewind** always restores project files, optionally followed by a new conversation with the selected prompt restored to the composer. The original Session is always retained.
|
|
122
|
+
|
|
123
|
+
## Configuration
|
|
124
|
+
|
|
125
|
+
Override configuration in the profile patch layer:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
- id: turn-rewind
|
|
129
|
+
config:
|
|
130
|
+
storageDir: ~/.dsh/change-ledger/v1
|
|
131
|
+
maxRestorePoints: 50
|
|
132
|
+
maxTurnCheckpointsPerSession: 30
|
|
133
|
+
maxFiles: 20000
|
|
134
|
+
maxFileBytes: 16777216
|
|
135
|
+
maxSnapshotBytes: 536870912
|
|
136
|
+
planTtlMs: 900000
|
|
137
|
+
staleLockMs: 30000
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
All size and user-point retention limits fail loudly. Automatic turn checkpoints have a separate per-session retention window and prune only their own oldest checkpoints; user and rescue restore points are never silently pruned. When omitted, `storageDir` resolves to `$DSH_HOME/change-ledger/v1` and falls back to `~/.dsh/change-ledger/v1`; it must not overlap the managed worktree.
|
|
141
|
+
|
|
142
|
+
## Recovery
|
|
143
|
+
|
|
144
|
+
Before writing any path, a restore creates a rescue point and a durable operation journal. If DSH stops with a non-terminal journal, the next plugin startup marks it `interrupted` unless another live DSH process still owns that workspace lock.
|
|
145
|
+
|
|
146
|
+
Recovery uses the public `ctx.changeLedger` service API: `listRecovery` finds the operation's `rescuePointId`, `inspect` reviews that rescue point, then `planRestore`/`applyRestore` handle the affected paths. Rescue points remain ordinary, inspectable restore points until explicitly deleted.
|
|
147
|
+
|
|
148
|
+
## Public service
|
|
149
|
+
|
|
150
|
+
Other Cordis plugins can inject `changeLedger` and call the same lifecycle through the structured service API:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
export const inject = ['changeLedger']
|
|
154
|
+
|
|
155
|
+
export async function apply(ctx: Context) {
|
|
156
|
+
const point = await ctx.changeLedger.create({
|
|
157
|
+
cwd: '/absolute/git/worktree',
|
|
158
|
+
sessionId: 'session-id',
|
|
159
|
+
label: 'before refactor',
|
|
160
|
+
})
|
|
161
|
+
// point.id is a durable restore-point id.
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The complete exported types are available from `@anionex/dsh-turn-rewind/format`; the engine is available from `@anionex/dsh-turn-rewind/core` for non-Cordis tests and trusted integrations.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
pnpm install --frozen-lockfile
|
|
171
|
+
pnpm run check
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The test suite creates real temporary Git repositories and covers full/selective restore, stale plans, ignored-path collision refusal, HEAD drift, rescue rollback, crash reconciliation, active-lock preservation, durable-state integrity, symlinks, size limits, sparse checkouts, submodules, deletion, and blob garbage collection.
|
|
175
|
+
|
|
176
|
+
## About
|
|
177
|
+
|
|
178
|
+
DSH Turn Rewind is maintained by [anionex](https://anionex.me/). If you would like to follow my future work, [follow me on X](https://x.com/anion_ex) or [GitHub](https://github.com/Anionex).
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
BSD-3-Clause. See [LICENSE](LICENSE).
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# DSH Turn Rewind
|
|
2
|
+
|
|
3
|
+
[](https://x.com/anion_ex)
|
|
4
|
+
|
|
5
|
+
[English](README.md)
|
|
6
|
+
|
|
7
|
+
为 DeepSeek Harness 提供 Turn 级项目文件恢复,并可选择从恢复后的这一轮继续新对话。
|
|
8
|
+
|
|
9
|
+
**Turn Rewind** 是用户看到的功能名、仓库名和 Profile Bundle 名。**Change Ledger** 是底层持久恢复引擎;`ctx.changeLedger` 服务、磁盘格式和存储路径继续保留这个名字,因为它们描述的是可复用的快照与恢复层,而不只是 Web 上的回退按钮。
|
|
10
|
+
|
|
11
|
+
它给 DSH Session 增加一条明确的安全边界:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
创建恢复点
|
|
15
|
+
↓
|
|
16
|
+
Agent / 用户 / 外部程序修改工作树
|
|
17
|
+
↓
|
|
18
|
+
预览逐路径变化
|
|
19
|
+
↓
|
|
20
|
+
审阅全部或部分恢复计划
|
|
21
|
+
↓
|
|
22
|
+
在回退弹窗中按下最终恢复按钮
|
|
23
|
+
↓
|
|
24
|
+
先建救援点 → 恢复 → 哈希验证
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
插件**不会**自动 commit、stash、reset、切分支、修改 Git index,也不会替用户判断某项改动“应该回滚”。
|
|
28
|
+
|
|
29
|
+
## 效果预览
|
|
30
|
+
|
|
31
|
+
每条用户消息下方依次显示时间、DSH 原生“复制”和第三个纯图标“回退”:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
打开后会先展示受影响文件,可选择“恢复文件并从这里继续”或“只恢复文件”:
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
## 为什么底层需要 Change Ledger
|
|
40
|
+
|
|
41
|
+
普通 Git 面板可以展示当前 diff,但不拥有完整、持久的恢复生命周期。Change Ledger 独立负责:
|
|
42
|
+
|
|
43
|
+
- 内容寻址的恢复点 manifest;
|
|
44
|
+
- Git worktree、HEAD、分支和进行中 Git 操作的状态围栏;
|
|
45
|
+
- 从审阅到执行之间的 stale plan 检测;
|
|
46
|
+
- 短期确认码与 DSH 人工批准双门槛;
|
|
47
|
+
- 每次恢复前自动建立救援点;
|
|
48
|
+
- 恢复后的内容哈希验证;
|
|
49
|
+
- 恢复失败后的自动回滚;
|
|
50
|
+
- DSH 重启时对未完成操作日志进行对账;
|
|
51
|
+
- 可供其他插件依赖的 `ctx.changeLedger` 公共服务。
|
|
52
|
+
|
|
53
|
+
持久格式见 [docs/FORMAT.md](docs/FORMAT.md),安全与故障模型见 [SECURITY.md](SECURITY.md)。
|
|
54
|
+
|
|
55
|
+
## 安全契约
|
|
56
|
+
|
|
57
|
+
- **只做显式操作:**任何恢复都不会自动发生——每一次恢复都从用户在 Web 弹窗中按下最终按钮开始,或来自对服务 API 的显式调用。
|
|
58
|
+
- **先读后写:**弹窗预览基于当前工作树生成短期、会话绑定的计划,不修改任何文件。
|
|
59
|
+
- **人工门禁:**弹窗中审阅受影响文件并按下最终恢复按钮就是人的决定;没有实时会话绑定计划对的直接修改请求一律 fail closed。
|
|
60
|
+
- **先救援再修改:**恢复任何文件前,先持久化当前 eligible tree 的救援点。
|
|
61
|
+
- **不静默漏文件:**遇到 submodule、sparse checkout、超限文件、总量超限或特殊文件类型时,创建恢复点直接失败。
|
|
62
|
+
- **不允许路径逃逸:**所有持久路径必须是规范的工作树相对路径;恢复拒绝穿过 symlink 父目录,也拒绝覆盖非空目录。
|
|
63
|
+
- **不覆盖审阅后的新变化:**执行时重新检查所选路径,以及审阅过的 HEAD、分支和 Git 操作状态;任何相关变化都会使计划失效。
|
|
64
|
+
- **不碰 Git 控制面:**index、分支、HEAD、stash 和 commit 均保持原样。
|
|
65
|
+
|
|
66
|
+
## 支持范围
|
|
67
|
+
|
|
68
|
+
`0.1` 只支持普通 Git worktree:
|
|
69
|
+
|
|
70
|
+
- tracked 文件,包括恢复点创建时已经缺失的 tracked 路径;
|
|
71
|
+
- 未被 `.gitignore` 或 Git 标准 excludes 忽略的 untracked 文件;
|
|
72
|
+
- 文本和二进制普通文件;
|
|
73
|
+
- 符号链接;
|
|
74
|
+
- 可执行位等可移植权限位。
|
|
75
|
+
|
|
76
|
+
下列对象会被拒绝或明确排除:
|
|
77
|
+
|
|
78
|
+
- sparse checkout;
|
|
79
|
+
- submodule gitlink(应分别进入每个 submodule 建恢复点);
|
|
80
|
+
- ignored 文件;
|
|
81
|
+
- socket、设备、FIFO 等特殊文件;
|
|
82
|
+
- 扩展属性、ACL、所有者、时间戳和 hard-link 拓扑;
|
|
83
|
+
- Git index 和仓库元数据;
|
|
84
|
+
- 非 Git 目录。
|
|
85
|
+
|
|
86
|
+
如果 ignored 或其他未受管理的文件占据了待恢复路径,插件会拒绝恢复,不会递归删除它。
|
|
87
|
+
|
|
88
|
+
## 安装
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
pnpm install --frozen-lockfile
|
|
92
|
+
pnpm run check
|
|
93
|
+
|
|
94
|
+
dsh plugin --profile web add @anionex/dsh-turn-rewind
|
|
95
|
+
dsh plugin --profile headless add @anionex/dsh-turn-rewind
|
|
96
|
+
|
|
97
|
+
dsh --profile web --dump-config | grep turn-rewind
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
修改 Profile Bundle 后需要重启对应 DSH 进程。
|
|
101
|
+
|
|
102
|
+
本仓库是标准 DSH Profile Bundle:`package.json` 声明 `dsh.bundle.patch`,`cordis.patch.yml` 直接挂载 `@anionex/dsh-turn-rewind`,不修改 DSH 主仓库。
|
|
103
|
+
|
|
104
|
+
当 Profile 同时提供 DSH Agent 服务时,插件会在每轮第一次 `agent/pre-step`、Agent 开始处理用户消息之前保存隐藏文件状态。保存失败只会让该消息无法回退,不会拦住用户这一轮。Web Profile 还会提供同源 `/turn-rewind` 接口:根据所选 `user/message` 序号找到对应状态,分页返回文件预览,生成短期、会话绑定的恢复授权,并把新对话创建交给 DSH 官方 Host create/fork 生命周期。插件绝不会自动恢复文件。
|
|
105
|
+
|
|
106
|
+
## 使用流程
|
|
107
|
+
|
|
108
|
+
在 Web Profile 中,每条用户消息下方都会在时间和原生“复制”之后显示一个紧凑、无文字的**回退**图标,提示是“恢复到发送这条消息之前”。图标使用明确的向后/撤销箭头,而不是“重试”圆形箭头。打开后会检查保存的文件状态,先显示简洁预览,需要时可“查看全部文件”,并提供两种模式:
|
|
109
|
+
|
|
110
|
+
| 模式 | 代码 | 对话 |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| **恢复文件并从这里继续**(默认) | 自动备份当前状态后恢复项目文件。 | 创建一个从这里开始的新会话(当前对话会保留)。 |
|
|
113
|
+
| **只恢复文件** | 自动备份当前状态后恢复项目文件。 | 当前 Session 保持原位且内容不变。 |
|
|
114
|
+
|
|
115
|
+
弹窗和最终主按钮就是确认流程,不再要求重复勾选。文件会按实际结果显示为“恢复之前的版本”“找回文件”“移除后来新增的文件”“恢复文件权限”或“恢复之前的文件类型”。如果项目文件已经是发送所选消息之前的状态,Turn Rewind 不会退化为“分支新对话”,而是提示无需恢复并引导用户使用原生的**分支新对话**按钮。
|
|
116
|
+
|
|
117
|
+
真正修改前,Turn Rewind 会再次检查所选文件和项目版本状态,并先创建自动备份。预览后出现的新变化会让本次恢复失效。同一工作树只要还有正在运行的 Agent(包括当前来源 Session),恢复就会被阻止;仅处于空闲状态的 Session 不会阻止恢复。已经审阅过的 HEAD 或分支变化不会阻止恢复:commit、分支、HEAD 和 index 都保持不变,恢复后的内容可能表现为当前 HEAD 上的未提交修改。Git 正在合并、变基等操作时仍会阻止。如果“恢复文件并从这里继续”在创建新对话时失败,Change Ledger 会自动从备份恢复操作前的文件。
|
|
118
|
+
|
|
119
|
+
DSH Session 日志只追加不改写,因此“恢复文件并从这里继续”会创建新 Session,而不是截断原对话。选中第一条消息时,Host 会在同一目录创建空 Session;选中后续消息时,会在上一轮 `turn/end` 处分叉。只有所选 `user/message` 和对应的精确 `turn/start` 都低于每一层持久 `seedLength` 时,子 Session 才能复用祖先保存的状态;子 Session 自己的状态优先,兄弟分支绝不混用。**分支新对话**只创建对话分支且保持项目文件不变;**Turn Rewind**一定恢复项目文件,再由用户决定是否创建新对话并把原消息文字填入新对话的输入框。原 Session 始终保留。
|
|
120
|
+
|
|
121
|
+
## 配置
|
|
122
|
+
|
|
123
|
+
在 Profile 的 patch 层覆盖:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
- id: turn-rewind
|
|
127
|
+
config:
|
|
128
|
+
storageDir: ~/.dsh/change-ledger/v1
|
|
129
|
+
maxRestorePoints: 50
|
|
130
|
+
maxTurnCheckpointsPerSession: 30
|
|
131
|
+
maxFiles: 20000
|
|
132
|
+
maxFileBytes: 16777216
|
|
133
|
+
maxSnapshotBytes: 536870912
|
|
134
|
+
planTtlMs: 900000
|
|
135
|
+
staleLockMs: 30000
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
所有容量与用户恢复点数量限制都采用 fail loud。自动 Turn 检查点使用独立的每会话保留窗口,并且只清理自己最旧的检查点;用户和救援恢复点永远不会被静默删除。未配置时,`storageDir` 使用 `$DSH_HOME/change-ledger/v1`,未设置 `DSH_HOME` 时回退到 `~/.dsh/change-ledger/v1`;它不得与被管理 worktree 重叠。
|
|
139
|
+
|
|
140
|
+
## 故障恢复
|
|
141
|
+
|
|
142
|
+
任何路径写入前,插件都会先创建救援点和持久 operation journal。如果 DSH 在非终态操作期间退出,下次启动会把该操作标记为 `interrupted`;如果另一个仍存活的 DSH 进程持有工作树锁,则不会误判其操作。
|
|
143
|
+
|
|
144
|
+
恢复通过公开的 `ctx.changeLedger` 服务 API 进行:用 `listRecovery` 找到操作的 `rescuePointId`,用 `inspect` 审阅该救援点,再对相关路径走 `planRestore`/`applyRestore`。救援点在被显式删除前始终是普通、可检查的恢复点。
|
|
145
|
+
|
|
146
|
+
## 公共服务
|
|
147
|
+
|
|
148
|
+
其他 Cordis 插件可以注入 `changeLedger`,直接调用结构化 API:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
export const inject = ['changeLedger']
|
|
152
|
+
|
|
153
|
+
export async function apply(ctx: Context) {
|
|
154
|
+
const point = await ctx.changeLedger.create({
|
|
155
|
+
cwd: '/absolute/git/worktree',
|
|
156
|
+
sessionId: 'session-id',
|
|
157
|
+
label: 'before refactor',
|
|
158
|
+
})
|
|
159
|
+
// point.id 是持久恢复点 ID。
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
完整格式类型从 `@anionex/dsh-turn-rewind/format` 导出;可信集成和测试可以从 `@anionex/dsh-turn-rewind/core` 使用独立 Engine。
|
|
164
|
+
|
|
165
|
+
## 开发
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
pnpm install --frozen-lockfile
|
|
169
|
+
pnpm run check
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
测试会创建真实的临时 Git 仓库,覆盖全部/部分恢复、stale plan、ignored 路径冲突拒绝、HEAD 变化、救援回滚、崩溃对账、活动锁保护、持久状态完整性、symlink、容量限制、sparse checkout、submodule、删除和 blob GC。
|
|
173
|
+
|
|
174
|
+
## 关于
|
|
175
|
+
|
|
176
|
+
DSH Turn Rewind 由 [anionex](https://anionex.me/) 维护。如果你想了解我后续的更多工作,欢迎在 [X](https://x.com/anion_ex) 或 [GitHub](https://github.com/Anionex) 关注我。
|
|
177
|
+
|
|
178
|
+
## 许可证
|
|
179
|
+
|
|
180
|
+
BSD-3-Clause,见 [LICENSE](LICENSE)。
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Security model
|
|
2
|
+
|
|
3
|
+
## Trusted boundary
|
|
4
|
+
|
|
5
|
+
The plugin runs in the DSH host process and therefore has the host user's filesystem authority. It treats model arguments, durable state, current worktree contents, and concurrent external changes as untrusted inputs.
|
|
6
|
+
|
|
7
|
+
## Mutation gates
|
|
8
|
+
|
|
9
|
+
Workspace restoration requires all of the following:
|
|
10
|
+
|
|
11
|
+
1. an existing durable restore point;
|
|
12
|
+
2. a fresh in-memory plan generated from the current tree;
|
|
13
|
+
3. an exact short-lived confirmation value;
|
|
14
|
+
4. the same DSH session when the plan was session-bound;
|
|
15
|
+
5. an explicit human decision: the Web rewind dialog's reviewed impact plus final restore button for a direct browser action, or an equivalent human approval path implemented by a trusted service-API consumer;
|
|
16
|
+
6. still-fresh selected-path hashes and the exact reviewed HEAD/branch/operation fence under the workspace lock;
|
|
17
|
+
7. a durable rescue point and operation journal.
|
|
18
|
+
|
|
19
|
+
The Web rewind adapter obtains the same session-bound plan and exact confirmation from a same-origin preview request, then sends both only after the user reviews the affected files and presses the final restore button. Direct mutation requests without that live plan pair fail closed. Conversation-only branching is deliberately absent and remains the responsibility of DSH's native Branch action.
|
|
20
|
+
|
|
21
|
+
Restore-and-restart verifies that the durable checkpoint still names the exact `turn/start` before the selected `user/message`. A forked Session may resolve that checkpoint through its parent lineage only while both event sequences remain below every durable `seedLength` and every ancestor contains the same message and turn boundary. A direct checkpoint wins over an inherited checkpoint, sibling checkpoints are isolated, and malformed or missing lineage fails closed. The first message restarts through Host `session.create`; later messages fork at the previous completed `turn/end`. The Host owns seed creation, model-target inheritance, persistence, and Workspace attachment. The original append-only Session remains intact.
|
|
22
|
+
|
|
23
|
+
Restore-and-restart restores files first, then creates the conversation child. If child creation fails, the adapter immediately applies the first restore's rescue point to compensate the file change. If compensation also fails, the ordinary durable restore journals and rescue points remain available for manual recovery rather than hiding a partial outcome. Any running Agent using the same canonical worktree, including the source Session, blocks Web restoration before mutation and is rechecked at apply time; idle Agents do not create a concurrent writer risk. A reviewed HEAD or branch difference is allowed because restoration never moves refs, HEAD, the branch, or the index; a changed or newly active Git operation remains blocked.
|
|
24
|
+
|
|
25
|
+
An absent or expired confirmation fails closed at apply time: the engine applies a restore only when the confirmation exactly matches a fresh session-bound plan.
|
|
26
|
+
|
|
27
|
+
## Filesystem containment
|
|
28
|
+
|
|
29
|
+
- State storage and the managed worktree may not overlap.
|
|
30
|
+
- Manifest paths are validated before joining them to the canonical worktree root.
|
|
31
|
+
- Existing parent components are checked with `lstat`; symlink parents are rejected.
|
|
32
|
+
- A regular file, symlink, or special file omitted by Git's current eligible-path inventory is never overwritten as though it were absent.
|
|
33
|
+
- Added paths are removed individually. Recursive deletion is never used for worktree content.
|
|
34
|
+
- A directory is removed only when empty. Unmanaged contents therefore block restoration.
|
|
35
|
+
- Blobs are SHA-256 verified before use and size-checked against their manifest entry.
|
|
36
|
+
|
|
37
|
+
## Concurrency and crashes
|
|
38
|
+
|
|
39
|
+
Each canonical worktree has an exclusive owner-only lock file with PID, timestamp, and nonce. A second live DSH process does not reconcile or take over an active restore. Locks whose owner no longer exists become reclaimable after `staleLockMs`.
|
|
40
|
+
|
|
41
|
+
Restore journals are written before worktree mutation. A failed restore attempts rollback from the rescue point without honoring a newly aborted caller signal, because leaving a partial mutation is less safe than completing rollback. If rollback also fails, the journal becomes `recovery-required` and preserves both diagnostics and the rescue point.
|
|
42
|
+
|
|
43
|
+
## Explicit non-goals
|
|
44
|
+
|
|
45
|
+
- This plugin does not sandbox other processes or prevent them from changing files concurrently.
|
|
46
|
+
- It does not restore Git index entries, refs, commits, stash state, ignored files, submodules, or repository operation metadata.
|
|
47
|
+
- It does not preserve extended attributes, ACLs, ownership, timestamps, or hard-link identity.
|
|
48
|
+
- It does not provide confidentiality or tamper resistance against the same operating-system user. State files are owner-only by default, but the host user remains trusted.
|
|
49
|
+
- It automatically captures bounded, hidden checkpoints before the first step of newly observed DSH turns when the rewind adapter is active; it never automatically applies one. User and rescue restore points remain explicit.
|
|
50
|
+
|
|
51
|
+
## Reporting
|
|
52
|
+
|
|
53
|
+
Report a vulnerability through the private repository's GitHub security channel or to a repository maintainer. Include the plugin commit, platform, Git version, DSH snapshot, reproduction repository shape, and whether the failure happened before or after workspace mutation.
|
package/cordis.patch.yml
ADDED
package/docs/FORMAT.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Change Ledger durable format v1
|
|
2
|
+
|
|
3
|
+
The state root defaults to `$DSH_HOME/change-ledger/v1`, falling back to `~/.dsh/change-ledger/v1`. It must not overlap a managed worktree.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
v1/
|
|
7
|
+
└── workspaces/
|
|
8
|
+
└── <sha256(canonical-worktree-path)>/
|
|
9
|
+
├── lock.json
|
|
10
|
+
├── manifests/
|
|
11
|
+
│ └── rp_<time>_<random>.json
|
|
12
|
+
├── operations/
|
|
13
|
+
│ └── op_<time>_<random>.json
|
|
14
|
+
└── blobs/
|
|
15
|
+
└── <first-two-hex>/
|
|
16
|
+
└── <sha256>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Every JSON write uses a sibling temporary file followed by rename. Blob publication writes and fsyncs a sibling temporary file, then creates the final content-addressed name through an atomic hard link. Existing blobs are read back and hash-verified.
|
|
20
|
+
|
|
21
|
+
## Restore-point manifest
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
interface RestorePointManifestV1 {
|
|
25
|
+
version: 1
|
|
26
|
+
id: `rp_${string}`
|
|
27
|
+
kind: 'user' | 'rescue' | 'turn'
|
|
28
|
+
workspace: string
|
|
29
|
+
repository: {
|
|
30
|
+
root: string
|
|
31
|
+
commonDir: string
|
|
32
|
+
head?: string
|
|
33
|
+
branch?: string
|
|
34
|
+
operation?: string
|
|
35
|
+
stagedPaths: string[]
|
|
36
|
+
}
|
|
37
|
+
sessionId?: string
|
|
38
|
+
label?: string
|
|
39
|
+
parentRestorePoint?: string
|
|
40
|
+
turn?: number
|
|
41
|
+
turnStartSeq?: number
|
|
42
|
+
turnEndSeq?: number
|
|
43
|
+
createdAt: number
|
|
44
|
+
treeHash: string
|
|
45
|
+
fileCount: number
|
|
46
|
+
totalBytes: number
|
|
47
|
+
entries: Record<string, FileEntry | SymlinkEntry>
|
|
48
|
+
restoreCount: number
|
|
49
|
+
lastRestoredAt?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface FileEntry {
|
|
53
|
+
kind: 'file'
|
|
54
|
+
blob: string
|
|
55
|
+
size: number
|
|
56
|
+
mode: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SymlinkEntry {
|
|
60
|
+
kind: 'symlink'
|
|
61
|
+
target: string
|
|
62
|
+
mode: number
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`turn` manifests are automatic, hidden checkpoints used by the Web rewind surface. New checkpoints require `sessionId`, `turn`, and the inclusive `turnStartSeq` observed before the Agent enters its first step. Version-1 checkpoints written by the earlier assistant-tail implementation may instead carry `turnEndSeq`; readers preserve them for format compatibility, but prompt-anchored Web rewind never binds them to a user message. Exactly one turn boundary is required, and turn metadata is rejected on `user` and `rescue` manifests. Malformed or partially anchored turn manifests fail closed.
|
|
67
|
+
|
|
68
|
+
Entry keys are canonical Git-style relative paths using `/`. Absolute paths, empty segments, `.` segments, `..` segments, and NUL bytes are rejected before filesystem resolution.
|
|
69
|
+
|
|
70
|
+
`treeHash` is SHA-256 over every sorted path and its complete type/content/mode signature. Readers recompute it from validated entries and reject mismatches. Regular-file blobs are SHA-256 addressed and verified on every restore read.
|
|
71
|
+
|
|
72
|
+
## Restore operation
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
interface RestoreOperationV1 {
|
|
76
|
+
version: 1
|
|
77
|
+
id: `op_${string}`
|
|
78
|
+
workspace: string
|
|
79
|
+
restorePointId: string
|
|
80
|
+
rescuePointId: string
|
|
81
|
+
sessionId?: string
|
|
82
|
+
paths: string[]
|
|
83
|
+
startedAt: number
|
|
84
|
+
finishedAt?: number
|
|
85
|
+
state:
|
|
86
|
+
| 'running'
|
|
87
|
+
| 'rollback-running'
|
|
88
|
+
| 'completed'
|
|
89
|
+
| 'rolled-back'
|
|
90
|
+
| 'interrupted'
|
|
91
|
+
| 'recovery-required'
|
|
92
|
+
error?: string
|
|
93
|
+
rollbackError?: string
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`running` and `rollback-running` are non-terminal. Startup changes them to `interrupted` only when the workspace has no live lock owner. Terminal journals remain as local audit evidence; restore-point deletion never removes an incomplete journal's referenced point.
|
|
98
|
+
|
|
99
|
+
## Compatibility
|
|
100
|
+
|
|
101
|
+
Readers reject unknown `version` values and malformed durable data. There is no best-effort fallback, path normalization, or legacy coercion. A future incompatible format must use a new state-root version and an explicit migration tool.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Turn Rewind 对齐计划
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
让 Web 端 Rewind 以用户消息为锚点:用户点击消息下方的回退图标,就能查看并恢复“发送这条消息之前”的项目文件,并可选择在新对话里重新开始。产品语义对齐 Claude Code,同时保留 Change Ledger 对完整工作树、自动备份、失败回滚和持久恢复的支持。
|
|
6
|
+
|
|
7
|
+
## 已实现内容
|
|
8
|
+
|
|
9
|
+
### 1. 用户消息锚点
|
|
10
|
+
|
|
11
|
+
- 回退图标从 Assistant 回复操作栏移到用户消息下方,排在时间和“复制”之后,无文字显示。
|
|
12
|
+
- 提示和弹窗标题统一为“回到发送这条消息之前”。
|
|
13
|
+
- 客户端按 `user/message` 的 `seq` 请求预览,不再用 Assistant Turn 尾部作为入口。
|
|
14
|
+
- 新对话创建后,会把所选用户消息的文字放回输入框。
|
|
15
|
+
|
|
16
|
+
### 2. 消息发送前保存文件
|
|
17
|
+
|
|
18
|
+
- 在每轮第一次 `agent/pre-step` 中、调用 `next()` 之前保存文件状态。
|
|
19
|
+
- 保存点精确绑定 `turn/start`,保证语义是 Agent 尚未处理这条用户消息。
|
|
20
|
+
- 同一工作树的保存操作串行执行。
|
|
21
|
+
- 保存失败会记录错误,但不会阻断用户 Turn;失败的消息不会伪造可用状态,也不会在 Turn 结束后重试并错误标记当前文件。
|
|
22
|
+
|
|
23
|
+
### 3. 恢复与重新开始
|
|
24
|
+
|
|
25
|
+
- **恢复文件并重新开始:**先恢复文件,再创建新对话,并把原消息文字放回输入框。
|
|
26
|
+
- **只恢复文件:**只改项目文件,当前对话保持不变。
|
|
27
|
+
- 第一条消息之前没有完整 Turn,使用 Host `session.create` 在同一目录创建空 Session。
|
|
28
|
+
- 后续消息使用 Host `session.fork`,边界是所选消息之前的上一条 `turn/end`。
|
|
29
|
+
- 原 Session 始终保留;只分支对话、文件不变的需求继续由原生“分支新对话”负责。
|
|
30
|
+
|
|
31
|
+
### 4. Git 状态处理
|
|
32
|
+
|
|
33
|
+
- HEAD 前进或当前分支变化不再直接阻止恢复;预览说明影响后,以 `allowHeadChange` 生成恢复计划。
|
|
34
|
+
- 恢复不会撤销 commit、切换分支、移动 HEAD 或修改 index;恢复结果可以表现为当前 HEAD 上的未提交修改。
|
|
35
|
+
- 合并、变基、cherry-pick 等 Git 操作状态变化继续阻止恢复。
|
|
36
|
+
- 预览后 HEAD、分支、Git 操作或所选文件再次变化,现有计划仍会失效。
|
|
37
|
+
|
|
38
|
+
### 5. 并发、分叉与失败处理
|
|
39
|
+
|
|
40
|
+
- 同一工作树只要存在正在运行的 Agent,包括来源 Session 自己,就阻止 Web 恢复;idle Session 不阻止。
|
|
41
|
+
- 子 Session 先使用自己的保存点,再沿祖先查找;兄弟分支互不混用。
|
|
42
|
+
- 祖先复用要求所选 `user/message` 和对应 `turn/start` 在每一层都低于 `seedLength`,并且边界完全一致。
|
|
43
|
+
- 父 Session 缺失、分叉循环或边界不一致时安全失败。
|
|
44
|
+
- 恢复前创建自动备份;创建新对话失败时,立即用备份补偿恢复前的文件状态。
|
|
45
|
+
|
|
46
|
+
### 6. 持久格式兼容
|
|
47
|
+
|
|
48
|
+
- 新 Turn 保存点写入 `turnStartSeq`。
|
|
49
|
+
- 旧版 `turnEndSeq` manifest 仍可读取,避免破坏格式版本 1,但不会被绑定到新的用户消息入口。
|
|
50
|
+
- `turn` manifest 必须且只能携带一种 Turn 边界。
|
|
51
|
+
|
|
52
|
+
## 最终功能边界
|
|
53
|
+
|
|
54
|
+
- **分支新对话(Branch):**从现有对话创建分支,项目文件保持不变。
|
|
55
|
+
- **Turn Rewind:**恢复到发送所选用户消息之前的项目文件,并可选择创建新对话重新开始。
|
|
56
|
+
- **Change Ledger:**提供持久保存、差异预览、短期恢复计划、自动备份、原子恢复、失败回滚和重启对账,不在普通用户主流程中暴露底层术语。
|
|
Binary file
|
|
Binary file
|