Back to home

Taler97

dsh-rollback

File-mutation rollback plugin for DeepSeek Harness

Stars
0
Language
TypeScript
Created
Aug 15, 2026
Updated
Aug 15, 2026

Introduction

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.

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