Taler97 / dsh-rollback

Listed

File-mutation rollback plugin for DeepSeek Harness

masterOther View source

Installation

npx -y @deepseek-ai/dsh plugin --profile web add github:Taler97/dsh-rollback

This installation command is an unverified starting point generated from the GitHub repository address.

README

Maintainer-authored documentation snapshot.

View on GitHub ↗
Commit 3deb80dSynced Aug 18, 2026

dsh-rollback

npm version License: MIT

English | 中文

File-mutation rollback plugin for DeepSeek Harness: it observes every successful tool result for the pre-image a write/edit mutation reports, checkpoints that pre-image into 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 tools/result observation event, 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.
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-0.1.0.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

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

Only successful write/edit tool results that carry a before pre-image. Mutations through bash, str_replace_editor, or raw subprocesses carry no pre-image and are not captured (see Known Limitations). 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
    M[模型调用 write/edit] --> R["tools/result 观察事件"]
    R --> C{结果带 before 改前映像?}
    C -- 否 --> X[忽略]
    C -- 是 --> S[CheckpointStore 捕获]
    S --> G{工作区是 git 仓库?}
    G -- 是 --> B["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.

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.

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).

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 is a human command plane entry; it never reaches the model.

Token effect

Zero tokens on normal operation. A rollback_files call adds its small tool/result pair; the restored file content is not echoed (only a summary line). Capture itself is invisible to the model.

KV Cache effect

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

Known Limitations and Deferred Work

  • Only write/edit mutations are checkpointed — mutations made through bash, str_replace_editor, or raw subprocesses carry no before pre-image in their result and are not captured. A plan-scoped batch backup (capture every file a plan touches before execution) is the corresponding generalization, deferred.
  • UTF-8 text only — pre-images travel as strings; binary content is outside scope (matching the fs tools' text-only contract).
  • 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.
  • 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.
  • 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 failurerestoreOnFailure 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, 9 store-level tests

License

MIT

Project files and signals

Shown items are public repository signals detected in the directory snapshot.

TestsDetected

Repository information

Language
TypeScript
License
MIT
Last updated
Aug 17, 2026, 2:05 AM

Install deliberately

Review source code, permissions, lifecycle hooks, dependencies and network access. Test untrusted plugins in an isolated environment.