dsh-better-editDeepSeek Harness plugin
Hash-anchored read/edit/undo_last_edit tools for DeepSeek Harness (dsh), fewer token consumption, lower cost.
- Stars
- 21
- Forks
- 4
- License
- MIT
- Last commit
- Sep 3, 2026
- Latest release
- v0.6.2
Overview
Hash-anchored read/edit/undo_last_edit tools for DeepSeek Harness (dsh), fewer token consumption, lower cost.
Original README
Cached from the project repository on Sep 3, 2026. This is source content, separate from the Agents.md review above.
dsh-better-edit
A better edit tool for DeepSeek Harness
Position-free hashes — one read, many edits, fewer tokens, more room for real work.
English · 简体中文
Quick Start • Why Hashline • Tools • Benchmark • How Anchors Work • Development
"The harness — not the model — is the bottleneck." — Can Bölük, The Harness Problem
This is the harness fix. Hashes replace line numbers — edits above don't shift anchors below. One
readserves manyedits; drift outside your range passes with a notice, true conflicts retry with fresh anchors — no fullreadneeded.
3 calls vs 6 · -55.8% tokens · 23/23 correctness. Same external-drift refactor, same file (single stochastic run; method). Payload numbers are deterministic — see Benchmark.
Why you need this
If you've watched line 47 → 74 corrupt a file after an insert — this is for you.
Before: str_replace / line numbers | After: hashline edit |
|---|---|
| Re-types old code (~5-6× billed) | Two 3-char hashes, old text never echoed |
| One insert shifts every number → silent wrong line | Content addresses — edits above don't move anchors below |
| No check against what was shown | Every line verified; [E_RANGE_STALE]/[E_RANGE_UNSERVED] reject before write, then reject-and-serve returns fresh HASH│content |
[!TIP] Shining points — honest:
- Position-free.
read 1..5→insert @0→edit 10..12still lands at10..12. Anchors arecanon(line)hashes, not positions (ADR-0013). Exterior drift is a notice, not a re-read.- Fewer round-trips. Single-session
1 read → N edits— no ritual re-reads. Multi-session exteriorA:10..12 / B:20..30also passes; only overlappingA∩B≠∅retries once viaservedRows(no fullread). Harness9/9green.- Fewer tokens. Compact payload
{path, edits:[[from,to,text]]}+ never echoingold_string; diff/echo/rejection rows count as serves. Envelope-40%pinned 12-edit corpus, session-55.8%on external-drift.- Concurrent-safe, not silent.
tombstoneper(session,path)epoch blocks re-boundS@3→@3;canon+hash+changed∩[L,R]makespos-freesingle-thread andstrictonly on true overlap. One retry vs silent wrong-line.
Not for one-line touch-ups (near parity) or new files (write). Pays off in long sessions and structural edits.
Quick Start — install to verified edit in 30s
Install (pick one)
shnpx @deepseek-ai/dsh plugin --profile web add github:Rianico/dsh-better-edit # from github npx @deepseek-ai/dsh plugin --profile web add dsh-better-edit # from npm npx @deepseek-ai/dsh plugin --profile web add /path/to/dsh-better-edit # local
No config. Next session runs with hashline tools. Verify:
shdsh --profile <name> --dump-config # shows "# == dsh-better-edit" layer
| Requirement | |
|---|---|
| Node | ^22.19.0 || >=24.0.0 |
| Profile | dsh profile (dsh plugin creates one) |
| Backends | sandboxed / remote ctx.fs |
See it work
read serves HASH│content — the hash is the address:
ve7│function hello() { szJ│ console.log("world"); kQm│}
edit by hashes — always lands where you meant:
json{ "path": "src/main.ts", "edits": [["szJ", "szJ", " console.log('hi');"]] }
Returns a diff with fresh anchors — next edit needs no read:
- szJ │ console.log("world"); + a3m │ console.log('hi'); kQm │ }
Position-free in one line: read 1..5 → insert @0 → edit 10..12 still verifies 10..12 (resist mode). Multi-session honesty: A:10..12+1 shifts B:20..30→21..31 → B passes (drift notice); B:12..13 overlapping A → E_RANGE_STALE + fresh rows, one retry.
Batch atomically — one edit, up to 32 same-file ranges:
json{ "path": "src/main.ts", "edits": [["a1b","a1b","new line 1\n"], ["c3d","c3d","new line 2"]] }
One fails → none write ([E_BATCH_ABORT]).
[!TIP] Want proof before you install? Upstream 23/23 battery runs no LLM — stale edits are rejected every run. Same algorithm.
Configuration
Tenancy and prompt guidance declare once, read at agent/session-start, no code change.
Store central by default $DSH_HOME/plugins/dsh-better-edit/runtime/<name>-<hash8>/ (ls-readable + .wsPath sidecar). DBs are disposable caches — rm -rf runtime/<name>-<hash8>/ is safe, rebuilt on next read.
yaml# $DSH_HOME/plugins/dsh-better-edit/config.yaml storeDir: central # central | workspace | /abs autoGitignore: false undo_ttl_s: 604800 # 7d, -1 forever storeMaxAgeS: 2592000 # 30d janitor storeMaxTotalBytes: 524288000 # 500 MB LRU
Env overrides yaml (DSH_BETTER_EDIT_STORE_DIR, DSH_BETTER_EDIT_AUTO_GITIGNORE).
Guidance per preset — tool:read / tool:edit / tool:undo_last_edit are plain markdown per preset at $DSH_HOME/plugins/dsh-better-edit/<preset>/<section>.md (orders 130/131/133). Delete or empty a file → default re-seeds at next boot; keep a --- fence to blank on purpose.
Why Hashline
Verified against what was served. Every resolved line checked against read/diff/rejection rows. Stale or unseen → [E_RANGE_STALE]/[E_RANGE_UNSERVED]/[E_RANGE_UNVERIFIED] + fresh HASH│content, retry needs no read. Session-scoped — sub-agent serves never validate main edits.
Content-addressed. canon(line) strips ASCII whitespace, xxh32 → 62³=238,328 anchors. Re-inserting identical text keeps its hash; prettier/eslint --fix between edits doesn't invalidate. Unique by bitset probing — }/import repeats never collide; cap 238,328 lines ([E_FILE_TOO_LARGE]).
No loop, no ritual. No-op → No changes made; same no-op ×3 → [E_NOOP_LOOP]. Diff/echo/rejection rows count as serves — read is recovery, not ritual.
Token economics
Envelope change: hoist path, edits:[[from,to,text]], never repeat old_string.
| snapshot | str_replace | edit | edit multi | OMP per-edit | OMP batch |
|---|---|---|---|---|---|
| pinned 12-edit corpus | 1,015 | 609 -40.0% | 582 -42.7% | 590 -41.9% | 480 -52.7% |
| local snapshot | 358 | 272 -24.0% | 241 -32.7% | 268 -25.1% | 180 -49.7% |
Percent vs str_replace. External row pinned corpus, cl100k_base; local npm run benchmark in upstream.
| engine | calls | tokens | saved | ok |
|---|---|---|---|---|
| OMP | 6 | 28,467 | — | ✅ |
hashline edit | 3 | 12,593 | -55.8% | ✅ |
Single stochastic run, opencode-go/gpt-5.6-luna high. Artifact.
Scope & honesty. Payload deterministic; practical run stochastic. We measure payload + round-trips, not throughput. Retries are where the gap is largest — see edge cases.
Tools
| Tool | What it does |
|---|---|
read | HASH│content with offset/limit; [Showing N-M of T] paging; >200KB lines show marker |
read_skill | Plain text, no hashes, no serves — editing after it needs a serve |
edit | {path, edits:[[from,to,text]]} path:string|null inference, "" deletes, atomic ≤32, verify-then-write |
undo_last_edit | {path} restores last edit (BOM/line endings/anchors), persisted |
write stays, but refuses an exact HASH│ echo for same session/path/line before dispatch.
Error codes
| Code | Meaning |
|---|---|
[E_BAD_SHAPE]/[E_BAD_REF] | Bad tuple shape or not bare 3-char |
[E_STALE_ANCHOR]/[E_AMBIGUOUS_ANCHOR] | No line / multi-line → read |
[E_EDIT_HASH_ECHO]/[E_WRITE_HASH_ECHO] | Copied HASH│ from same session/path/line — strip and retry |
[E_WOULD_EMPTY]/[E_NOT_FOUND]/[E_ACCESS]/[E_NOT_TEXT]/[E_FILE_TOO_LARGE] | Empty guard / missing / access / binary / >238,328 lines |
[E_BAD_OP]/[E_INVALID_PATCH]/[E_BARE_HASH_PREFIX] | Swapped range / +HASH│ patch marker (auto-corrected) |
[E_BAD_ENCODING]/[E_DECODE_FAILED] | Encoding / decode failed |
[E_NOT_OBSERVED]/[E_RANGE_STALE]/[E_RANGE_UNSERVED]/[E_RANGE_UNVERIFIED] | Served-state miss — echoed fresh HASH│content |
[E_UNDO_STALE]/[E_UNDO_UNAVAILABLE] | Undo stale / unavailable |
[E_NOOP_LOOP]/[E_BATCH_ABORT] | 3× same no-op / atomic batch fail → nothing written |
Full list in src/ — every rejection echoes fresh rows, no read needed.
Comparison
| dsh-better-edit | @oh-my-pi/hashline | str_replace | |
|---|---|---|---|
| Address | HASH│ 3-char canon | [path#tag] + line | text match |
| Whitespace-insen. | ✅ | ~ n/a | ❌ |
| Duplicate lines | ✅ unique | ~ pos | ❌ first |
| Verified vs served | ✅ every line | ~ file tag | ❌ |
| Blind edit | ✅ reject | ~ | ❌ |
| Batch atomic | ✅ | ✅ | ❌ |
| Undo | ✅ | ❌ | ❌ |
| Battery | 23/23 | 10/10 | — |
~ partial, — n/a. Same lineage — patch library vs dsh tool pair; pick by seam.
Edge cases: wrong anchor impossible (verified), disk drift → reject+serve, shift above → nothing moves, repeats → unique/ambiguous, unseen → reject, batch → atomic. See upstream benchmarks.
Battery: 23/23 tool, 10/10 library (upstream npm run eval, same algorithm).
How Anchors Work
canon(line) strips ASCII whitespace → xxHash32 → A-Za-z0-9 3-char (62³). Stable across prettier; Unicode/strings stay significant except ASCII whitespace inside strings (linter-only). stride=62²+62+1 probes bitset → unique; cap 238,328. Store hash-store.sqlite per workspace (central, honoring XDG_CONFIG_HOME); 7-day served TTL, janitor storeMaxAgeS/LRU + wal_checkpoint.
How It Replaces Built-ins
dsh resolves agent → preset → global; built-ins live on preset. Plugin via cordis.patch.yml: at agent/session-start registers read/edit on agent layer (shadows, auto-unwinds); write stays with pre-execute guard + post-execute auto-read.
Project Structure
dsh-better-edit/
├── src/hashline/ # hash + served core
├── src/tool-*.ts # read / edit / undo
├── src/served-store.ts # SQLite store
├── benchmark/corpus/ # 103-line fixture
├── test/ # 108 files, 1222 tests
├── assets/ # logo + banner
└── cordis.patch.yml
Development
shpnpm install pnpm typecheck # tsc --noEmit pnpm test # vitest run pnpm benchmark # hash probe + session envelope (reads/retries/tokens)
Benchmark
103-line file, 12 replacements (8×1 + 4×3/6/10/15), cl100k_base. hashline vs str_replace vs oh-my-pi seq/batch. Upstream is source of truth — same algorithm byte-for-byte.
| Criterion | hashline | str_replace | seq / batch |
|---|---|---|---|
old_string echoed | never | every edit | never |
| 12-edit saved | 31% | 0% | 42% / 53% |
| multi-line saved | 29–47% | 0% | 40–53% |
| 5× output cost | ~1.4× less | 1× | ~1.7×/~2.1× less |
| Verified | 100% | none | tag only |
| Scenario | hashline | str_replace |
|---|---|---|
1×8 | 309 | 324 |
3–15×4 | 393 | 691 |
| TOTAL ×12 | 702 | 1015 |
Saved 313 (31%). Reproduce: upstream npm run benchmark. See pi-better-edit/benchmark/README.md.
Scope & honesty. Benchmark is request-payload tokens (reads cancel, replacement text identical). No transcription-failure model — real gap larger; see edge cases.
Roadmap
Current 0.6.1: pos-free resist/strict + tombstone/canons/epoch, per-session (session,path) store, 1222 tests, 9/9 harness.
Next
- Keep
benchmark/run.mjsin sync withADR-0013(reads/retries/tokens per session) - Re-check wiring vs next
dsh(pinned0.1.0-rc.6) README.zh.mdparity
Contributing
See CONTRIBUTING.md. Most valuable: more served-state edge-case tests.
License
MIT — see LICENSE.
Acknowledgments
From Can Bölük's The Harness Problem. Thanks to pi-hashline-edit, pi-hashline-edit-pro, pi-better-edit, @oh-my-pi/hashline. Reading: hash-anchors.
Star History
⭐ If hashline made your agent edit better, give it a star!