DSH Plugins Marketplace

DSH Plugins

Plugins

/

Tools & Capabilities

/

dsh-rollback

T

dsh-rollback

Manifest valid

Observational file-mutation rollback for the fs tools: write/edit pre-images are checkpointed as git blobs or snapshots and restored through a model-facing rollback_files tool or a /rollback command.

hasBundlePatch

dsh-rollback

npm version License: MIT

English | 中文

File-mutation rollback plugin for DeepSeek Harness: it checkpoints the pre-image a write/edit mutation reports in its result, forward-snapshots the workspace around black-box bash/run_code calls so the files they change are diffable, retains every pre-image in the workspace git object database or a snapshot store, and exposes restore through a model-facing rollback_files tool and a /rollback human command. It registers no service and changes no loop code — capture rides the documented tools/* extension points (the tools/result observation event and the tools/execute around hook), and restore writes files directly (never through the fs policy seam or the sandbox, because undoing a mutation must not be gated by the policy the mutation passed).

Install

The package is an installable bundle (declares dsh.bundle), so it plugs into a profile without touching the harness. All you need is a dsh CLI; the runtime peer packages (@deepseek-ai/dsh-tools, @deepseek-ai/cordis, ...) resolve from the dsh installation itself, so nothing else is installed.

Prerequisites

  • A dsh CLI on the machine (dsh plugin shells out to pnpm, so pnpm must be on PATH).
  • A profile to install into — demo below is initialized on first use; use any name.

Install from npm (recommended)

dsh plugin --profile demo add dsh-rollback

Other sources work the same way:

# straight from git (sources are built by the prepare script; pin a commit)
dsh plugin --profile demo add github:you/dsh-rollback#<sha>

# or from a local tarball
dsh plugin --profile demo add ./dsh-rollback-<version>.tgz

Verify the install

  1. The profile manifest under $DSH_HOME/profiles/demo/ ($DSH_HOME defaults to ~/.dsh) now lists dsh-rollback in dependencies and in dsh.profile.bundles — the reconciler adds it automatically because the package declares dsh.bundle. The equivalent manual patch layer row:
- id: rollback
  name: dsh-rollback
  config:
    mode: auto          # auto | git | snapshot
    storeDir: ''        # '' = <harness home>/rollback
    maxRecords: 200
    gitPath: git
  1. Start a session. Any of these proves the plugin is live:
    • the model sees a rollback_files tool in its tool list,
    • typing /rollback answers rollback: nothing to restore (instead of an unknown-command error) before any mutation happened.

Usage

30-second quickstart

  1. Open a session whose working directory is a git repository.
  2. write a file notes.md with the content hello.
  3. write it again with goodbye — the first content is silently checkpointed.
  4. Tell the model "restore the file you just overwrote" (it calls rollback_files), or type /rollback yourself.
  5. Read notes.md: it says hello again.

For humans — /rollback [count]

Type it in the chat input (base web and headless profiles mount the command registry the command needs):

  • /rollback — undo the single most recent captured mutation in this session's working directory,
  • /rollback 3 — undo the three most recent ones.

The output lists every restored file and its action:

rollback: restored 2 file mutation(s):
  restored  /ws/src/lib/parse.ts
  deleted   /ws/src/lib/generated.ts

A manual rollback also tells the model. A command's lifecycle (command/run/command/done) is log-only and never enters model context, so the plugin queues a model-facing notice for the next pre-step (Agent.inject), naming every path it rewrote or deleted and stating that the model's in-context copies are stale. Without it, a human rollback would rewrite files behind the model's back while its transcript kept describing the pre-rollback world — and it would keep working against paths that no longer exist. Set notifyModel: false to keep manual rollbacks silent.

For models — rollback_files

One model-facing tool, rollback_files {count} (no prompt section). It is meant for the model to undo its own write/edit mistakes instead of asking the user. Restores are scoped to the calling session's working directory and reported as a per-file summary — restored file contents are not echoed.

What is captured

Two capture paths feed the same store: successful write/edit tool results that carry a before pre-image, and file changes made by root bash/run_code calls (forward-snapshotted in git repositories, then diffed — see Behavior). Mutations through str_replace_editor, raw subprocesses, or a bash call outside a git repository carry no recoverable pre-image and are not captured. A session can only restore records at or under its own working directory.

Demo: before and after

One write overwrite, undone. Same file, four states:

StepActionnotes.md
1original statehello
2model writes a broken edit — pre-image capturedgoodbye
3model calls rollback_files {"count": 1}(transparent)
4restored, byte-for-bytehello

The full transcript of that turn:

# 1. original
$ cat /ws/notes.md
hello

# 2. the model overwrites it; tools/result carries the pre-image "hello",
#    and the capture listener checkpoints it into the git object database
> tool/call   write {"path": "/ws/notes.md", "content": "goodbye"}
> tool/result {"path": "/ws/notes.md", "before": "hello", ...}

# 3. the model realizes the mistake and undoes it
> tool/call   rollback_files {"count": 1}
> tool/result "rollback: restored 1 file mutation(s):
               restored  /ws/notes.md"

# 4. back to the pre-mutation content
$ cat /ws/notes.md
hello

Under the hood: git hash-object -w wrote the pre-image into git's object database (zero index/branch/working-tree pollution), and one line was appended to the durable manifest.jsonl — the same undo works after a restart.

How it works

flowchart TD
    A["write/edit 工具结果"] --> B["tools/result 观察事件"]
    B --> C{结果带 before 改前映像?}
    C -- 否 --> X[忽略]
    C -- 是 --> S[CheckpointStore 捕获]
    A2["bash / run_code 调用"] --> B2["tools/execute 前向快照 + diff"]
    B2 --> C2{工作区是 git 仓库?}
    C2 -- 否 --> X2[跳过并告警]
    C2 -- 是 --> S
    S --> G{工作区是 git 仓库?}
    G -- 是 --> BLOB["git hash-object -w 存 blob"]
    G -- 否 --> P["写 storeDir/snapshots/ 快照"]
    S --> MF["追加 manifest.jsonl"]
    U["模型调 rollback_files / 用户 /rollback"] --> RS["restore 按 session.cwd 作用域"]
    RS --> RR["git cat-file / 快照 / 删除文件"]

The diagram above is a full worked example with a before/after transcript — see Demo.

Plugin (namespace: rollback)

A function/namespace plugin (name / inject / Config / apply), not a service. It is a loop-hygiene guard in the same family as dsh-tool-call-timeout-policy: it layers a safety net over the documented tools/* extension points instead of touching the agent loop.

Config

KeyTypeDefaultMeaning
mode'auto' | 'git' | 'snapshot''auto'git checkpoints every pre-image as a git blob (requires a repository; a non-repository path fails loud and captures nothing); snapshot always copies pre-images under storeDir/snapshots/; auto picks git per file when the workspace is a repository and snapshots otherwise.
storeDirstring''Root holding the durable manifest.jsonl and snapshots/. Empty resolves to rollback under the Harness home.
maxRecordsnumber200Upper bound on in-memory records per store; the oldest are dropped beyond it (the durable manifest keeps everything).
gitPathstring'git'Git executable name or absolute path.
mutationToolsstring[]['bash', 'run_code']Tool names that get a forward snapshot — black-box mutation calls without a before pre-image. Only root dispatches are snapshotted (nested Code Mode sub-calls are covered by their run_code parent) and only inside a git repository.
notifyModelbooleantrueInject a model-facing notice after a HUMAN /rollback, so the model learns that files changed on disk outside its control. The model's own rollback_files call needs no notice (its call and result are already in the transcript).

Behavior

Capture. A tools/result listener converts a successful write/edit outcome into a checkpoint: the outcome's before field is the pre-mutation content (null records a file that did not exist). blob pre-images are written with git hash-object -w --stdin inside the file's repository (discovered by walking up to .git, cached per directory) — zero index/branch/working-tree pollution, content-addressed and deduplicated by git itself. Every record is appended as one JSONL line to manifest.jsonl; a store replay on plugin load restores the in-memory list, so restores survive restarts. Only absolute local display paths are captured; relative or remote display paths (non-local filesystem backends) are ignored.

Forward snapshot (bash / run_code). A tools/execute around hook snapshots the workspace before a root bash (or run_code) call runs. Git enumerates the candidate set in one invocation (git ls-files -co --exclude-standard): tracked files plus untracked-but-not-ignored ones, so the ignore rules decide what the model owns instead of a hand-rolled walk, and build outputs and vendored trees cost nothing. .git and node_modules are never snapshotted (and therefore never restored), and the plugin's own store directory is excluded. Every candidate is then retained as a raw git blob in one batched invocation (git hash-object -w --no-filters --stdin-paths): --no-filters keeps the pre-image byte-exact (without it git applies CRLF/attribute conversion, and a restore would rewrite line endings instead of reproducing the file), and the batch replaces one process per file with one process per snapshot. A mtime + size fast path skips files unchanged since the last snapshot. When the call settles the candidate set is re-enumerated and diffed, and every changed file is checkpointed — modified or deleted files keep the pre-mutation blob, created files an absent record. The snapshot is scoped to the session working directory and requires a git repository; otherwise it is skipped with a warning. Nested Code Mode sub-calls are covered by their run_code parent, so a mutation is never checkpointed twice.

Restore. restore(count, under) re-materializes the count most-recent records whose path lies at or under under (the calling agent's session working directory): blob via git cat-file blob <hash>, snapshot from storeDir/snapshots/<ref>, and absent by deleting the file. Writes are atomic (temp file + rename) and create parent directories. Restored records are removed from the in-memory list; the manifest stays append-only, so a restart replays the same records and a later restore re-applies the identical pre-image (idempotent, no double-undo).

Exposure.

  • rollback_files tool — model-facing restore, parameter count (integer, default 1). Registered on ctx.tools; not concurrency-safe. It refuses when the calling execution has no session working directory.
  • /rollback [count] command — the same restore for the receiving agent's session; the command child activates only when a command registry is composed (base web and headless profiles mount dsh-commands). After a non-empty restore it injects one model-facing notice (see Model Experience); an agent without an inbox, or a rejected injection, leaves the command's own result untouched.

Why git, and why direct spawn

Git blobs are the same mechanism ccAgent uses: hash-object -w writes the pre-image without touching index, refs, or the working tree; cat-file restores bytes verbatim; unreferenced blobs are reclaimed by git's own gc. Git is spawned directly via node:child_process (never through ctx.shell or ctx.subprocess): a restore is a deliberate system-level undo, so it must not be confined by the sandbox or shell policy it is undoing.

Model Experience

Model-facing restore tool

What the model sees

This plugin adds one model-facing tool, rollback_files (integer parameter count, string output), and no prompt section. It changes no other tool's schema or system prompt.

The /rollback command line itself never reaches the model — a command's lifecycle is log-only — but a completed human rollback does: the plugin injects one plugin-sourced notice message naming every path it restored or deleted, plus the standing instruction that the model's copies of those paths are stale and must be re-read. Injected context is queued for the next pre-step without waking the driver, so an idle session sees it attached to whatever request comes next.

Token effect

Zero tokens on normal operation. A rollback_files call adds its small tool/result pair, and a human /rollback adds one short notice (a header line, one line per path, one instruction line; file contents are never echoed). Capture itself is invisible to the model.

KV Cache effect

Append-only; the added tool schema, results, and notices follow the reusable request prefix and do not invalidate existing KV-cache entries.

Known Limitations and Deferred Work

  • str_replace_editor and raw subprocesses are not checkpointed — bash/run_code file changes are captured by forward snapshot, but only inside a git repository (a non-repository workspace skips the snapshot with a warning), and a bash call is snapshotted at the session working directory only, so files it changes elsewhere are not captured. A plan-scoped batch backup (capture every file a plan touches before execution) is the corresponding generalization, deferred.
  • Byte-exact for the forward snapshot; text-only for write/edit — a bash/run_code pre-image is retained as raw bytes (--no-filters), so binary files and CRLF files round-trip exactly. The write/edit path still carries the tool's own before string, which is text by the fs tools' contract.
  • Ignored paths are never captured — the forward snapshot's candidate set comes from git's ignore rules, so a path that is both untracked and excluded (dist/, build/, coverage/, ...) is not checkpointed by a bash/run_code call and cannot be rolled back. A file git already tracks is always captured, even when .gitignore matches it.
  • A snapshot of a large repository pays an O(files) first pass — the forward snapshot stores one git blob per candidate file, so the first snapshot of a repository in a session is dominated by git's loose-object writes. Batching removed the one-process-per-file cost (a fixed number of git invocations per snapshot), but the bytes are still read and stored once per file; later snapshots in the same session reuse the mtime + size cache. How expensive that first pass is varies with the filesystem and with antivirus scanning of .git/objects.
  • One path the batched hash cannot address disables the snapshot — paths reach git newline-delimited (--stdin-paths), so a filename containing a newline (legal on POSIX; NTFS rejects it) makes the batch fail, and a failed batch aborts the snapshot instead of skipping that file: for as long as such a path exists in the workspace, no bash/run_code mutation in that repository is captured, and every call reports a warning.
  • Restores are workspace-scoped — records outside the calling agent's session working directory are never restored by that caller; there is no cross-directory or global restore entry point.
  • A restore overwrites without a staleness check — the pre-image is written unconditionally. If the file changed again after the mutation was captured (a human edit, another agent, a later command), restoring silently discards those newer contents: there is no conflict detection, and the replaced state is not backed up.
  • Sessions sharing a working directory can undo each other — scope is the directory, not the session. A record carries no session identity, so /rollback or rollback_files in one session also selects mutations made by any other session with the same working directory (a subagent, a second tab).
  • The manual-rollback notice is advisory — it informs the model, it does not constrain it. What actually stops a model from writing over a rollback is the harness itself: the restore replaces the file (new inode/mtime/ctime), so fs-observation-policy's version guard rejects the model's next write/edit of that path as stale and forces a re-read. Profiles without that policy get the notice only. notifyModel: false removes the notice too.
  • Append-only manifest, no pruning — restored records remain in manifest.jsonl and re-appear on replay (idempotent re-restore, never a double-undo), but a long-lived harness home grows without compaction. Replay also trims the in-memory list to maxRecords, so after a restart only the newest maxRecords records are restorable even though the file keeps every one of them.
  • Concurrent stores collide on seq — the sequence number is per-process (replayed to the manifest's tail, then incremented). Two harness processes sharing one store directory write duplicate sequence numbers, which breaks the strictly-increasing invariant the companion plugin asserts and blurs which records a bounded restore drops.
  • Capture durability is best-effort — the manifest append is neither fsynced nor awaited (capture rides the observation event fire-and-forget), so a crash between a mutation and the flush can lose the newest record(s), reported only as a warning.
  • Git gc can prune long-lived blobs — default gc reclaims unreferenced objects after a retention window; checkpoints older than that window may fail to restore. A keep-alive ref namespace is deferred.
  • No automatic restore on failure — restoreOnFailure is deliberately not offered in v1; auto-restore would need to attribute failure to specific mutations first.

Development

pnpm install     # peer packages resolve from npm releases
pnpm run build   # tsdown -> lib/ (ESM + d.mts), self-contained
pnpm test        # vitest, 23 store-level tests + 7 notice tests

License

MIT

Comments

Loading…

Similar plugins

dsh-undo

by ruaibeite

Turn-level file undo for DeepSeek Harness: every write and edit is snapshotted before it lands, and the model can call the undo tool to roll back all files a conversation turn changed.

Tools & CapabilitiesManifest valid

★ 0

MIT

JavaScript

Oct 23, 2026

dsh plugin --profile web add @ruaibeite/dsh-undo

by zoyluoblue

dsh plugin: snapshot your workspace every turn and restore the files to any earlier point. A rewind/undo for DeepSeek Harness coding sessions — your own git repo is never touched.

Manifest valid

★ 0

MIT

TypeScript

Aug 19, 2026

dsh plugin --profile web add @zoytown/dsh-rewind

by caishengshuo

Every file, one version before it breaks. A DSH plugin: snapshots a file the instant before an agent rewrites it, with a per-session GUI timeline where any point can be restored in one click.

Manifest valid

★ 0

MIT

JavaScript

Oct 24, 2026

dsh plugin --profile web add dsh-beforewrite

by ruaibeite

Records the version of the edit target as an observation when the agent inspected the file with bash instead of the read tool, so the filesystem observation policy stops rejecting edits with FS_NOT_OB

Development & InfrastructureManifest valid

★ 1

MIT

JavaScript

Oct 13, 2026

dsh plugin --profile web add @ruaibeite/dsh-auto-observe

by caihua8669

Document review workbench: select text in a Markdown file, leave comments saved as MRSF sidecar files next to it, and let the agent read them with doc_review_* tools and propose edits you accept or re

Development & InfrastructureTerminal & ClientsManifest valid

★ 1

MIT

JavaScript

Oct 17, 2026

dsh plugin --profile web add @caihua/dsh-doc-review

by roushanyyzz

让 AI 在对话中每修改一个文件,你就立刻看到它改了什么。 DSH(DeepSeek Harness)插件:拦截 write / edit 工具调用,在对话流的工具卡片位置直接渲染 git 风格文件差异——支持上下对比与左右对比,逐行高亮

Manifest valid

★ 0

MIT

TypeScript

Aug 20, 2026

dsh plugin --profile web add dsh-diff-vis