dsh-rewind-plugin 0.4.1 → 0.5.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/SECURITY.md ADDED
@@ -0,0 +1,154 @@
1
+ # Security model
2
+
3
+ This document is the security model of **dsh-rewind** — how the plugin treats
4
+ untrusted input, what it is allowed to mutate, and how it survives crashes.
5
+ It is derived from the implementation (`src/index.ts`, `src/snapshot.ts`); if
6
+ this document and the code ever disagree, the code wins and this document is
7
+ a bug.
8
+
9
+ ## Trusted boundary
10
+
11
+ The plugin runs in the DSH host process and therefore holds the host user's
12
+ filesystem authority — it reads and writes files with plain `node:fs`. The
13
+ following are treated as **untrusted inputs**:
14
+
15
+ - **Model arguments** — the `file_path` / `path` / `command` fields of write,
16
+ `edit`, and `str_replace_editor` tool calls (they name the paths the
17
+ checkpoint store records).
18
+ - **Session log contents** — events are parsed structurally; a hostile or
19
+ malformed id must never escape the store root.
20
+ - **Current worktree state** — restore planning reconciles against the live
21
+ disk, which may have been changed by anything.
22
+ - **Concurrent external modifications** — a restore never assumes the disk
23
+ still matches its records.
24
+
25
+ The DSH host (and its other plugins) is trusted; this plugin does not
26
+ re-verify the host's own authority boundaries.
27
+
28
+ ## Mutation gates
29
+
30
+ The plugin **automatically captures** before-backups of tracked mutations, but
31
+ **never automatically applies** one. A workspace restore happens only through
32
+ an explicit user-invoked `/rewind @<seq> both` (the per-message ↶ button or the
33
+ command channel), and only when all of the following hold:
34
+
35
+ 1. **A validated target**: `planRewind` accepts only a `user/message` seq that
36
+ is currently on the session surface (`parseRewindTarget` → `RewindPlan`).
37
+ 2. **A fresh plan**: the plan is derived from the current `events` + `surface`
38
+ at execution time — never cached across events.
39
+ 3. **Committed backups exist**: every planned action comes from a committed
40
+ checkpoint entry anchored at or after the target.
41
+ 4. **Live-disk reconciliation**: `planRestore` compares each entry against the
42
+ current disk and plans only actions that would actually change it — an
43
+ already-matching state is a no-op (restores are idempotent).
44
+ 5. **Exclusive execution**: a per-session in-flight guard rejects concurrent
45
+ rewinds; a running turn is force-cancelled (`keepInbox`) and quiescence is
46
+ awaited before the surface is cut.
47
+ 6. **Session binding**: the restore reads/writes only the store of the rewound
48
+ session (paths are resolved display paths the session's own tools touched).
49
+
50
+ **Concurrency scope**: the in-flight guard above only serializes concurrent
51
+ *rewinds* of one session; it does not stop other processes from modifying the
52
+ workspace. `planRestore` reconciles against the live disk at plan time, but
53
+ there is no re-validation between planning and applying — a file changed by
54
+ another process in that window is overwritten by the restore. The rescue state
55
+ is captured at apply start, so `rollbackRestore` still returns the workspace
56
+ to exactly the apply-start state, and per-file failures are reported rather
57
+ than hidden.
58
+
59
+ A failed gate fails closed: an invalid target, a missing store, an absent
60
+ backup, or a cancelled invocation aborts the rewind with an error.
61
+
62
+ **Automatic store deletion (opt-in)**: unrelated to restores, an enabled
63
+ `snapshot-auto-cleanup` background sweep deletes whole session directories of
64
+ **long-inactive** sessions (default off — the store is otherwise only mutated by
65
+ an explicit rewind apply). It is confined to the store root, uses `lstat` (so it
66
+ never follows a symlink out of the root), skips the active session, and never
67
+ touches the conversation log. When disabled (the default), no automatic deletion
68
+ runs.
69
+
70
+ ## Conversation integrity
71
+
72
+ The session log is **append-only** — the plugin never deletes or rewrites
73
+ recorded history. A rewind appends a single marker event: an **empty**
74
+ `assistant/message` whose `surfaceOp` replaces every surface node after the
75
+ target. The raw log (audit trail, search, `/export`) is untouched — only the
76
+ model-visible surface is cut, so the next request derives its context from the
77
+ target onward. The marker sits inside a ghost `step/start … step/end` frame so
78
+ the harness token-meter replay keeps accepting the log; a malformed marker
79
+ (duplicated turn/step, dangling open step) is what earlier versions produced
80
+ and is repaired offline (see `docs/compat/troubleshooting.md`).
81
+
82
+ ## Filesystem containment
83
+
84
+ - **Store root**: `~/.dsh/rewind-snapshots/` by default (`DSH_REWIND_SNAPSHOT_DIR`
85
+ overrides it). The store never overlaps the workspace; deleting it only
86
+ removes file backups and the store rebuilds from scratch.
87
+ - **Path sanitization**: session ids and call ids are scrubbed to
88
+ `[a-zA-Z0-9._-]` (`safeSessionId` / `safeFileId`) before joining the store
89
+ root; `.` and `..` bare values are replaced — hostile ids cannot traverse
90
+ out of the root.
91
+ - **Never written through links**: symlinked and hard-linked paths
92
+ (`lstat().nlink > 1`) are skipped and reported, never restored — a symlink
93
+ would redirect the write outside the checkpoint, and a hard link would
94
+ clobber every other name of the same inode (e.g. pnpm-installed files).
95
+ - **Restores name only recorded paths**: the store contains resolved display
96
+ paths of the session's own write-class tool calls (plus boundary re-checks
97
+ over that same tracked set) — a restore can never write an arbitrary path.
98
+ - **Path resolution rule**: relative paths resolve against the session
99
+ workspace cwd, mirroring the fs tools' own rule (`src/session-cwd.ts`).
100
+ - **Bounded backups**: `prune` keeps the newest 100 anchor groups per session
101
+ (`MAX_ANCHOR_GROUPS`), so backup accumulation cannot grow the store without
102
+ bound (the exact cap is pinned in `docs/format.md`). Across sessions, the
103
+ opt-in `pruneStale` sweep removes whole **long-inactive** session directories
104
+ (measured by the newest member being idle past `maxAgeDays`); it uses `lstat`
105
+ (no symlink following), skips dot-prefixed temp files, and never targets the
106
+ active session (`keepActiveId`).
107
+
108
+ ## Crash safety
109
+
110
+ - **Atomic commits**: every JSON write (checkpoint entries, restore journals)
111
+ goes to a sibling temp file and is renamed over the target. A host crash
112
+ mid-write can leave only an inert `<target>.tmp` — never a readable
113
+ half-written file — and readers never pick up temp files.
114
+ - **Journaled restores**: before mutating anything, the restore captures each
115
+ planned path's pre-restore ("rescue") state and persists an intent journal,
116
+ then marks each action done as it is applied. A crash at any point leaves
117
+ the journal on disk.
118
+ - **Disk is truth**: after a restart, `reconcileRestores` re-derives from the
119
+ real disk which paths already match the goal (restored) and which are
120
+ pending; `continueRestore` finishes the interrupted op, `rollbackRestore`
121
+ undoes it to the exact pre-restore state. A journal whose goal is already
122
+ reached auto-heals to its terminal state.
123
+ - **Fail-loud vs fail-soft**: a corrupt **journal** is reported
124
+ `recovery-required` — never silently dropped (dropping it would erase the
125
+ interrupted restore's recovery record). Corrupt **checkpoint entries** are
126
+ silently ignored (they only lose one backup, not the recovery path).
127
+ - **Journal IO never fails the restore**: if the journal cannot be written the
128
+ restore proceeds with pre-journal semantics (crash safety degrades,
129
+ behavior does not).
130
+
131
+ ## Explicit non-goals
132
+
133
+ - This plugin does **not** sandbox other processes or stop them from changing
134
+ files concurrently.
135
+ - It does **not** provide confidentiality or tamper resistance against the
136
+ same operating-system user: state files are created with the process
137
+ default permissions (no special modes are set — a standard umask applies),
138
+ and the host user remains trusted.
139
+ - It does **not** touch git (no refs, index, or worktree operations), makes
140
+ **no network requests**, and does **not** access credentials.
141
+ - It does **not** roll back whole-log state: telemetry, search, and `/export`
142
+ still see the withdrawn messages (documented behavior, not a bug).
143
+ - It does **not** restore files written by a cancelled tool call that never
144
+ committed a backup.
145
+ - **Subagent session edits are not tracked** (Claude Code alignment): a
146
+ subagent runs its own session, so the files it changes are not backed up and
147
+ cannot be restored by a rewind of the parent session.
148
+
149
+ ## Reporting
150
+
151
+ Report a vulnerability through the repository's GitHub security channel or to
152
+ a repository maintainer. Include: the plugin version/commit, the DSH
153
+ (`@deepseek-ai/*`) version, the platform, and a minimal reproduction — and
154
+ whether the failure happened before or after workspace mutation.
package/docs/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Docs
2
+
3
+ Documentation lives under `docs/`, organized by concern. Everything here is
4
+ maintainer-facing unless the purpose column says otherwise. This file is the
5
+ index/navigation entry point.
6
+
7
+ ## Index
8
+
9
+ | Path | Purpose | Audience |
10
+ | --- | --- | --- |
11
+ | `architecture.md` | Module layering, rewind/checkpoint pipelines, compatibility strategy, roadmap | maintainers |
12
+ | `format.md` | Durable on-disk format spec (checkpoint entries + restore journals) | maintainers |
13
+ | `harness-reference.md` | DeepSeek Harness interface reference + plugin source layout | maintainers |
14
+ | `contract/client-contract.md` | Rewind visibility contract for third-party DOM plugins (`.zh` mirror) | integrators |
15
+ | `compat/audit.md` | Compatibility audit: verified surfaces, recorded findings, probe matrix | maintainers |
16
+ | `compat/troubleshooting.md` | Known issues & repair steps (`.zh` mirror) | users / maintainers |
17
+ | `release/release.md` | Release workflow & DSH peer-version alignment (`.zh` mirror) | maintainers |
18
+
19
+ Repo-root docs outside `docs/`: `SECURITY.md` (security model) and
20
+ `CONTRIBUTING.md` (contribution guide).
21
+
22
+ ## Conventions
23
+
24
+ - Bilingual docs use the `.md` / `.zh.md` file split (e.g. `contract/client-contract.md`
25
+ + `contract/client-contract.zh.md`, `release/release.md` + `release/release.zh.md`).
26
+ - `compat/audit.md` is the single source of truth for compatibility conclusions;
27
+ other docs link to it instead of restating them.
28
+ - Cross-links are relative so the whole `docs/` directory stays relocatable — and it
29
+ ships intact in the npm tarball via the `docs` entry in `files` (`package.json`).
@@ -0,0 +1,128 @@
1
+ # Architecture
2
+
3
+ How dsh-rewind is built: module layering, the rewind pipeline, the checkpoint
4
+ pipeline, the compatibility strategy, and the roadmap. The module map below is
5
+ the same one in `AGENTS.md`; this document adds the wiring between the
6
+ modules. The durable on-disk format is specified separately in
7
+ `docs/format.md`, the security model in `SECURITY.md`.
8
+
9
+ ## Purpose
10
+
11
+ One thing: rewind a conversation **in place** to any earlier user message —
12
+ never forking a session or switching windows — with an optional Claude-Code-
13
+ style workspace file restore (`both` mode). No network, no git operations, no
14
+ credentials access.
15
+
16
+ ## Module layering
17
+
18
+ ```
19
+ src/
20
+ ├── index.ts host plugin: /rewind command + checkpoint pipeline
21
+ ├── rewind.ts pure planning: target parsing, surface-range plan,
22
+ │ candidate listing, marker turn/step math
23
+ ├── snapshot.ts checkpoint store: disk before-backups, journaled restore,
24
+ │ reconcile / continue / rollback, bounded prune
25
+ ├── session-cwd.ts session working-directory resolution (fs-tools rule)
26
+ └── client/ browser half: per-message ↶ button (portal bridge),
27
+ mode popover, hidden-span computation, candidate parsing,
28
+ locales, styles
29
+ ```
30
+
31
+ Two dependency rules keep the design testable:
32
+
33
+ 1. **`rewind.ts` is pure** — no I/O, no `Session` dependency; everything
34
+ derives from the event log + ordered surface, so the whole planning layer
35
+ is unit-testable without a host.
36
+ 2. **`snapshot.ts` is host-independent** — it talks to the disk through plain
37
+ `node:fs` plus injected seams (`DiskProbe`, `DeleteFile`, a test-only
38
+ `crash` hook), so the store is testable without the harness.
39
+
40
+ The client never reads the DOM to derive rewind state; it consumes the host's
41
+ machine channels (see [Compatibility strategy](#compatibility-strategy)).
42
+
43
+ ## Rewind pipeline
44
+
45
+ ```
46
+ ↶ button / /rewind @<seq> both
47
+ → handleRewind: parseRewindTarget + planRewind (target must be a
48
+ user/message currently on the surface)
49
+ → agent.cancel({ keepInbox: true }) if running; waitForAgentIdle
50
+ → dropPendingSteering (next-step inbox only; queued messages untouched)
51
+ → append ghost step frame: step/start (turn = last started turn,
52
+ step = that turn's next unused number) … marker … step/end
53
+ → marker = empty assistant/message with
54
+ surfaceOp { op: 'replace', start, end } over every surface node
55
+ after the target (+ sourceEventSeqs = shadowed seqs)
56
+ → if mode 'both': store.restoreAfter(targetSeq) + syncRestoreObservations
57
+ → result text carries machine tokens (impact=<n>, restore:/delete: lines)
58
+ → client: hides withdrawn rows (data-dsh-rewind-hidden), refills composer
59
+ with the target message's text
60
+ ```
61
+
62
+ Key invariants:
63
+
64
+ - **The log is append-only.** The marker is the *only* mutation: it cuts the
65
+ model-visible surface, never the raw history (search/export still see it).
66
+ - **The marker is empty** (`content: []`, derives to `null` in the model
67
+ context) so it renders nothing and enters no model context.
68
+ - **The ghost step frame exists for the token-meter**: replay requires every
69
+ `assistant/message` inside an open step of the same `(turn, step)`; a bare
70
+ marker appended while idle would throw on the next `measure()` and silently
71
+ break `/compact`. The turn/step numbers are chosen so they can never collide
72
+ with a future real turn (`markerTurnOf` / `markerStepOf` — see
73
+ `src/rewind.ts`).
74
+ - **Restore is reconciled against the live disk** (`planRestore`), so repeated
75
+ rewinds are idempotent and a rewind whose target state already matches is a
76
+ no-op.
77
+
78
+ ## Checkpoint pipeline (Claude Code before-backup model)
79
+
80
+ ```
81
+ tools/execute captureBefore: for write / edit / str_replace_editor
82
+ (mutating commands only), read the file's BEFORE state;
83
+ subagent edits are NOT tracked (Claude Code alignment).
84
+ tools/post-execute commitEntry: anchor = latest user/message seq; skip
85
+ failed calls; write the before-backup to the store.
86
+ session/event user/message boundary: reconcileTracked re-reads every
87
+ (user/message) tracked file and records a new before-backup for any
88
+ whose disk state changed since last seen — external
89
+ edits/deletions enter the record this way.
90
+ prune keeps the newest 100 anchor groups per session, storing
91
+ identical before-content as in-place links that are
92
+ materialized before their group is dropped, and recycles
93
+ terminal restore journals.
94
+ pruneStale cross-session auto-cleanup (default off): whole
95
+ long-inactive session dirs past the cutoff are removed;
96
+ the active session is never targeted.
97
+ ```
98
+
99
+ ## Compatibility strategy
100
+
101
+ - **Peer ranges as OR-union of DSH rc tuples** (`^0.1.0-rc.6 || ^0.1.1-rc.2`):
102
+ npm's prerelease rules require the peer range to share the host's
103
+ `[major, minor, patch]` tuple; `scripts/check-dsh-version.mjs` detects when
104
+ a new tuple requires appending. See `docs/release/release.md`.
105
+ - **Test-driven investigation**: `tests/compat-invariants.ts` /
106
+ `compat-interop` / `compat-gaps` probe harness behavior and pin findings in
107
+ `docs/compat/audit.md`; `scripts/verify-host.mjs` runs a real end-to-end
108
+ rewind + `/compact` chain (the full check suite).
109
+ - **Stable machine channels for third parties**: `dsh-rewind-plugin/client`
110
+ exports the pure hidden-span computation; withdrawn rows carry
111
+ `data-dsh-rewind-hidden`; both are semver-protected
112
+ (`docs/contract/client-contract.md`). DOM coupling is minimized to a small
113
+ set of marked rows plus a structurally-typed slot registration.
114
+
115
+ ## Roadmap
116
+
117
+ Ideas under consideration, not commitments:
118
+
119
+ - **Multi-process identity/lock**: the current in-flight guard is
120
+ per-process; a cross-process exclusive lock (like the change-ledger
121
+ competitors) would cover multiple host processes on one worktree.
122
+ - **Lazy-commit UX**: an explicit confirm-then-apply step (a pending state)
123
+ to reduce mis-touch risk on in-window rewinds, which are inherently less
124
+ reversible than forked branches.
125
+ - **Locale expansion**: client/host copy is zh/en today; the copy layer
126
+ (`src/locales.ts`, `src/client/locales.ts`) is already keyed for more.
127
+ - **Composer re-send polish**: the target text is already refilled after a
128
+ rewind; a first-class "edit and re-send" affordance is a small extension.
@@ -0,0 +1,103 @@
1
+ # File-rewind tracking boundary
2
+
3
+ > Behavioral note — this is **not** a bug. The plugin's file rewind deliberately
4
+ > tracks only the model's dedicated editing tools, and applies an additional
5
+ > boundary (message-boundary re-check) to registered files. Written for issue
6
+ > [#5](https://github.com/SiriLee/dsh-rewind/issues/5); aligned with Claude Code's
7
+ > checkpoint semantics. [中文](tracking-boundary.zh.md)
8
+
9
+ ## One-line summary
10
+
11
+ Rewind **always** restores files edited with the dedicated editing tools
12
+ (`write` / `edit` / `str_replace_editor`) because the plugin snapshots them
13
+ before the edit. Command-line (or hand) edits are covered **only when the file
14
+ was previously registered** by an editing tool in the same conversation — and,
15
+ in that case, the change itself is recorded, but rewinding to a boundary inside
16
+ an unchanged interval is only as precise as the next recorded state (see
17
+ [Rewind precision](#rewind-precision-for-a-registered-file-changed-outside-editing-tools)).
18
+
19
+ ## How file changes are classified
20
+
21
+ **Editing tools** (create/overwrite, edit lines, insert): each call tells the
22
+ plugin *which file* it will change, so the plugin snapshots the file's current
23
+ content **before** the edit and can restore it on rewind. This path is
24
+ deterministic — using them always yields a restorable rewind.
25
+
26
+ **Command-line or manual edits**: PowerShell (`pwsh`) writes on Windows, `sed -i`
27
+ on Linux, or saving a file by hand. These do **not** tell the plugin which file
28
+ is being touched, so there is nothing to snapshot beforehand.
29
+
30
+ Command/manual edits are nevertheless covered in one case: if the file was
31
+ **previously registered** by an editing tool in this conversation, the plugin
32
+ re-checks registered files at each user-message boundary and records a change it
33
+ sees, so those files restore on rewind too.
34
+
35
+ ## When a rewind is not available
36
+
37
+ These are cases where there is **no recorded pre-change state at all** (as
38
+ opposed to the precision gap in the next section, where a state exists but an
39
+ intermediate message's exact value is lost):
40
+
41
+ 1. **The model edits the file with a command on its first touch, and the file
42
+ was never registered.** No snapshot, no registration — nothing was recorded
43
+ around the change, so there is nothing to restore from. Every rewind target
44
+ reports no restorable change for this file.
45
+ 2. **The file was manually edited before the model's first editing-tool change.**
46
+ The plugin's first snapshot is taken at the editing-tool call, so rewind can
47
+ return the file only to its state *after* the manual edit, not before it.
48
+
49
+ Both stem from a deliberate **lightweight** trade-off. Supporting command edits
50
+ in every case would require scanning the whole workspace and classifying what
51
+ arbitrary commands changed — costly, error-prone, hard to maintain. Instead the
52
+ plugin registers only the editing tools that hand it an exact path, and relies
53
+ on the message-boundary re-check as its lightweight safety net. The cost is that
54
+ unregistered files' command edits are not recorded; the benefit is a simple,
55
+ reliable, maintainable plugin — the same behavior Claude Code exhibits.
56
+
57
+ ## Rewind precision for a registered file changed outside editing tools
58
+
59
+ A registered file's snapshots come only from (a) an editing tool's before-capture
60
+ and (b) the boundary's change-detection. So a message is a *recording point* for
61
+ a file only when the file's state differs from its most recent recorded state;
62
+ rewinding to message *M* restores each file to the earliest entry at/after *M*.
63
+
64
+ If the file was **unchanged** at *M* (relative to its last record), then *M* is
65
+ **not** a recording point. If the file is later changed by a command/manual
66
+ edit, the earliest entry at/after *M* is the one that recorded the **post-change**
67
+ state — so rewinding to *M* restores that post-change state, not the state the
68
+ file actually held at *M*:
69
+
70
+ ```
71
+ message 1 write sets f = "X" records before=null (file created)
72
+ message 2 boundary records f = "X" (null → X)
73
+ a command changes f "X" → "Y"
74
+ message 3 boundary records f = "Y" (X → Y)
75
+ rewind to message 2 → restores "X" (correct: f was "X" at message 2)
76
+ ```
77
+
78
+ Now insert one *unchanged* message, and the precision gap appears:
79
+
80
+ ```
81
+ message 1 write sets f = "X" records before=null (file created)
82
+ message 2 boundary records f = "X" (null → X)
83
+ message 3 reply, f still "X" NOT a recording point (unchanged)
84
+ a command changes f "X" → "Y"
85
+ message 4 boundary records f = "Y" (X → Y)
86
+ rewind to message 3 → restores "Y" (wrong: f was "X" at message 3)
87
+ ```
88
+
89
+ Message 2 is a recording point, so rewinding to it recovers the pre-change state
90
+ `X`; message 3 is not, so its exact state is lost once the command changes the
91
+ file. Only recording every registered file at every message — even when nothing
92
+ changed — would close this, at the cost of a per-message file per registered
93
+ file. The plugin avoids that to stay lightweight (see
94
+ [Position](#position)). Claude Code's checkpoints behave the same way.
95
+
96
+ ## Position
97
+
98
+ Making command edits restorable in the *unregistered-first-touch* case would
99
+ mean repeatedly backing up the entire workspace, or depending on a Git working
100
+ tree — both heavy, and at odds with the plugin's lightweight-snapshot design.
101
+ Git is already the right tool for workspace-change management. The plugin's
102
+ position is to favor **fast, conversation-scoped rewinds**, so this capability
103
+ is deliberately not adopted.
@@ -0,0 +1,55 @@
1
+ # 文件回退的追踪边界
2
+
3
+ > 行为说明——这**不是 bug**。插件的文件回退**有意只追踪模型的专用编辑工具**,并对**已登记文件**额外施加一层边界(消息边界复查)。为 issue [#5](https://github.com/SiriLee/dsh-rewind/issues/5) 编写;与 Claude Code 的 checkpoint 语义保持一致。 [English](tracking-boundary.md)
4
+
5
+ ## 一句话概括
6
+
7
+ 回退**始终能还原**用专用编辑工具(`write` / `edit` / `str_replace_editor`)编辑过的文件,因为插件会在编辑前对其快照。**命令行(或手动)编辑**仅当该文件在同一会话中**先前已被某个编辑工具登记**时才会被覆盖——此时变更本身会被记录,但回退到"未变区间中间的边界"时,只能精确到**下一处被记录的状态**(见[已登记文件被编辑工具之外的方式改动时的回退精度](#已登记文件被编辑工具之外的方式改动时的回退精度))。
8
+
9
+ ## 文件变更的分类
10
+
11
+ **编辑工具**(创建/覆盖、按行编辑、插入):每次调用都会告诉插件*将要改动哪个文件*,因此插件会在编辑前对文件的当前内容**快照**,回退时能还原。这条路径是**确定性**的——用了就一定可还原。
12
+
13
+ **命令行或手动编辑**:Windows 上的 PowerShell(`pwsh`)写入、Linux 上的 `sed -i`,或手动保存文件。它们**不告诉**插件动了哪个文件,所以**没有可预先快照的对象**。
14
+
15
+ 命令/手动编辑仍有一种情况会被覆盖:若该文件在本会话中**先前被某个编辑工具登记过**,插件会在每个用户消息边界复查已登记文件并记录它看到的变更,因此这类文件也能在回退时还原。
16
+
17
+ ## 何时无法回退
18
+
19
+ 这类是**完全没有记录到"变更前"状态**(有别于下一节的"精度缺口"——那是有记录、但某个中间消息的精确值丢失):
20
+
21
+ 1. **模型第一次触达时用命令编辑,且该文件从未被登记。** 无快照、无登记——变更前后都没有记录,无从还原;回退到任何消息对该文件都报告"无变更可恢复"。
22
+ 2. **文件在模型第一次编辑工具改动前就被手动编辑过。** 插件的首个快照是在编辑工具调用时拍的,因此回退只能把文件还原到手动编辑**之后**的状态,而非**之前**。
23
+
24
+ 两者都来自**刻意求轻**的取舍。要覆盖各种命令编辑,就得扫描整个工作区、并分类任意命令改了什么——代价高、易错、难维护。插件只登记会交出手确切路径的编辑工具,并以消息边界复查作为轻量安全网。代价是未登记文件的命令编辑不被记录;收益是简单、可靠、易维护——与 Claude Code 相同。
25
+
26
+ ## 已登记文件被编辑工具之外的方式改动时的回退精度
27
+
28
+ 已登记文件的快照只来自:(a) 编辑工具的 before 捕获,(b) 边界复查的变更检测。所以**只有"文件状态相对上次记录发生变化"的那条消息才是记录点**;回退到消息 *M* 会把各文件还原到 *M* 之后**最早**的那条记录。
29
+
30
+ 若文件在 *M* 处**未变**(相对上次记录),则 *M* **不是记录点**。若随后文件被命令/手动编辑改掉,*M* 之后最早的那条记录是**变更后**状态——于是回退到 *M* 会还原成**变更后**状态,而非 *M* 时刻文件**真实**的状态:
31
+
32
+ ```
33
+ 消息 1 write 设 f="X" 记录 before=null(文件新建)
34
+ 消息 2 边界记录 f="X" (null→X)
35
+ 命令把 f "X"→"Y"
36
+ 消息 3 边界记录 f="Y" (X→Y)
37
+ 回退到消息 2 → 还原成 "X" (正确:消息 2 时 f 是 "X")
38
+ ```
39
+
40
+ 插入一条**未变**消息,精度缺口才显现:
41
+
42
+ ```
43
+ 消息 1 write 设 f="X" 记录 before=null(文件新建)
44
+ 消息 2 边界记录 f="X" (null→X)
45
+ 消息 3 回复,f 仍="X" 不是记录点(未变)
46
+ 命令把 f "X"→"Y"
47
+ 消息 4 边界记录 f="Y" (X→Y)
48
+ 回退到消息 3 → 还原成 "Y" (错:消息 3 时 f 实际是 "X")
49
+ ```
50
+
51
+ 消息 2 是记录点,所以回退它能恢复**变更前**状态 `X`;消息 3 不是记录点,一旦文件被命令改掉,它那刻的精确状态就丢了。要关闭这个缺口,就得让每条边界都是快照——每条消息都记录每个已登记文件(哪怕没变)——代价是**每条消息×每个文件都写一个文件**。插件**刻意不做**,以保持轻量(见[定位](#定位))。Claude Code 的 checkpoint 同样如此。
52
+
53
+ ## 定位
54
+
55
+ 要把"未登记首触即命令编辑"的情形也做成可还原,意味着对整个工作区反复备份,或依赖 Git 工作树——都笨重,且与插件"轻量快照"的设计相悖。Git 才是管理工作区变更的正确工具。插件的定位是**快速、会话内回退**,因此**刻意不采纳**该能力。
@@ -0,0 +1,41 @@
1
+ # Troubleshooting
2
+
3
+ [简体中文](troubleshooting.zh.md)
4
+
5
+ ## History load failure: `…turn-tail… received an update before its start Match`
6
+
7
+ Rewinds from `≤ 0.2.4` collided with the next real turn's `turn/start`, so reopening the session showed
8
+
9
+ ```
10
+ Failed to load history: conversation Context …:turn-tail… received an update before its start Match (internal)
11
+ ```
12
+
13
+ and the history vanished. Rewinds from `0.2.5` on no longer collide; already-corrupted sessions need an offline repair (the log is append-only). The repair tool (`dsh-rewind-repair`) is no longer shipped from v0.4.0 — install a pre-v0.4.0 release to get it (fully quit dsh web / host first, then):
14
+
15
+ ```sh
16
+ npm exec --yes --package=dsh-rewind-plugin@0.3.3 -- dsh-rewind-repair
17
+ npm exec --yes --package=dsh-rewind-plugin@0.3.3 -- dsh-rewind-repair -- --dry-run # preview only
18
+ ```
19
+
20
+ It only rewrites the marker events' `data.turn` (seqs, order, and zstd frame structure intact) and backs up the original file before writing — safe to run repeatedly. From a source checkout of a pre-v0.4.0 tag: `node scripts/repair-markers.mjs` (identical flags).
21
+
22
+ ## Known compatibility boundaries
23
+
24
+ Behavioral notes — none of these is a crash — verified by the compatibility
25
+ probe suites; the audit [audit.md](audit.md) is the source of truth and lists
26
+ the probes that pin each one:
27
+
28
+ - Session stats / telemetry do **not** roll back with a rewind.
29
+ - Withdrawn messages stay **searchable and exported** (`/export` and full-text
30
+ search read the raw log).
31
+ - Session titles may **regenerate** (title derives from the current surface).
32
+ - Files written by a **cancelled tool call** (write happened, no snapshot
33
+ commit) cannot be restored by "conversation and code".
34
+
35
+ **R-OPENSTEP** (harness-side, plugin does not guard): a session log carrying an
36
+ *unclosed* `step/start` (a crash before the agent loop's `finally` closed the
37
+ step) makes later step activity break token-meter replay, so `/compact` can
38
+ fail after a rewind. Harness `0.1.1-rc.2` fixes the crash path on load
39
+ (`interruptedTurnClosers`); a plugin-side up-front rejection was tried and
40
+ **reverted** (`177ec14`, false positives on real logs). Deep analysis:
41
+ [audit.md](audit.md) → R-OPENSTEP.
@@ -12,3 +12,18 @@ npm exec --yes --package=dsh-rewind-plugin@0.3.3 -- dsh-rewind-repair -- --dry-r
12
12
  ```
13
13
 
14
14
  工具只改写标记事件的 `data.turn`(保持 seq / 顺序 / zstd 帧结构不变),改前自动备份原文件——可安全重复运行。源码方式:v0.4.0 之前的 tag 下 `node scripts/repair-markers.mjs`(参数相同)。
15
+
16
+ ## 已知兼容边界
17
+
18
+ 以下均为**预期行为而非崩溃**(探针钉住,完整依据见 [audit.md](audit.md) 事实源):
19
+
20
+ - 会话统计 / 遥测**不回退**(折叠的是完整日志);
21
+ - 被撤回的消息**仍可搜索、可导出**(`/export` 与全文搜索基于原始日志);
22
+ - 会话标题可能**重新生成**(标题由当前 surface 派生);
23
+ - 被**取消的工具调用**写入的文件(无快照提交)无法由「回退对话和代码」恢复。
24
+
25
+ **R-OPENSTEP**(harness 侧,插件不设守卫):日志中存在*未闭合* `step/start`
26
+ (agent 循环的 `finally` 闭合 step 前崩溃)时,后续 step 活动会破坏 token-meter
27
+ 重放,回退后 `/compact` 可能报错。harness `0.1.1-rc.2` 已在加载时自动闭合
28
+ (`interruptedTurnClosers`);插件曾实现前置拒绝但已回退(`177ec14`,真实日志
29
+ 误判)。深入分析:[audit.md](audit.md) → R-OPENSTEP。