akslcw
dsh-negative-ledger
No description
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 14, 2026
- Updated
- Aug 14, 2026
Introduction
dsh-negative-ledger
A negative-knowledge ledger for coding agents. It records only disproven paths — failed commands, missing files, rejected approaches, unavailable APIs — together with the evidence behind each conclusion and the conditions under which a retry becomes legitimate. When the evidence changes, the conclusion is invalidated automatically.
What it is not
- Not memory: no positive knowledge, no semantic recall.
- Not a cache: it stores conclusions, not tool results.
- Not a bug regression tracker: it covers any tool call and file read, not just fix attempts.
The core loop
- A tool call fails (non-zero exit,
FS_NOT_FOUND, …) → a negative fact is recorded with outcome witnesses (exit code, error code) and precondition witnesses (file state from DSH'sfs/observed). - The next identical attempt matches the fact's fingerprint (normalized command + cwd, or file path).
- While every precondition witness is unchanged, the attempt is warned (
warnmode) or denied (blockmode). - Any precondition change marks the fact stale — the reminder is withdrawn and the retry is allowed. A successful retry marks it resolved.
The differentiation: a DSH-native, evidence-bound persistent negative-memory gate — failure conclusions activate and revoke themselves with the environmental evidence, and stay transactionally consistent across concurrent agents.
Quick start
# engine only
node src/cli.ts --dir .ledger stats
# the three reproducible demos (S1 command dedup, S2 missing-file dedup,
# S3 evidence-change invalidation) with acceptance checks and a savings report
node demos/run-demos.ts
# real-mount smoke (run inside a deepseek-harness checkout with built libs):
# boots the actual agent spine + fs provider + read/write tools, drives a
# scripted model, and asserts the warning reaches the model's next request
node smoke/real-mount.ts
Requires Node ^22.19.0 || >=24.0.0 (aligned with the official DSH engines range).
CLI
node src/cli.ts [--dir <path>] [--backend sqlite|jsonl] <list | show <id> | stale | stats>
The backend flag wins; otherwise the directory is auto-detected (ledger.db → sqlite, ledger.jsonl → jsonl); with neither present the primary sqlite backend is used.
| Command | Output |
|---|---|
list | Every fact: status, kind, id, claim |
show <id> | One fact as pretty JSON |
stale | Facts invalidated by evidence change |
stats | Honest interception counters (duplicate failures observed, warnings emitted, calls denied) |
Engine API
Two store backends sit behind one LedgerStore seam: the default transactional SQLite store (SqliteLedgerStore: WAL, revision-based optimistic concurrency, operation receipts, retry leases, JSONL import) and the legacy single-process JSONL store (JsonlLedgerStore).
getFact(scope, kind, fingerprint)/queryFacts(filter)— current facts with revision and active-lease summaries.commitAttemptDecision(request)— the only decision entry:deny/observe-warn/verify-retry(allow and stale-allow both compete for a lease); revision conflicts re-read and re-decide.recordFact(input, meta)— records a disproven path; repeats append versions on the same id; idempotent by operation receipt and(fact, toolCallId, operation_kind).transitionFacts(batch, meta)— batched, all-or-nothing state transitions (one FS observation can invalidate many facts).settleLease(settlement)— the lease holder's retry outcome: succeeded → resolved, failed → new evidence version, released → fact untouched.summarize(scope?)— three honest counters:duplicateFailuresObserved,warningsEmitted,callsDenied. No token estimates — trajectory replay/A-B diffing owns that number.
DSH integration
- id: negative-ledger
name: dsh-negative-ledger
config:
backend: sqlite # sqlite (default, transactional) | jsonl (legacy single-process)
mode: warn # off | warn | block (default warn)
dir: .ledger # ledger directory (default .ledger)
commandRetryAfterMs: 300000 # TTL on auto-recorded command facts
commandTools: [bash, pwsh] # recorded as command_failed
readTools: [read] # recorded as file_missing
-
The store connection and the background invalidation queue are owned by the plugin fiber: disposal drains the queue and closes the store (HMR-safe).
-
warn(default): attachesadditionalContextsontools/post-execute; never blocks, never rewrites tool results. -
block: denies attools/pre-executebefore dispatch. Denied calls still flow through post-execute and are recognized by the plugin's own denial prefix, so one attempt is never double-counted. -
Auto-recorded command facts carry a short
afterTTL (commandRetryAfterMs, default 5 minutes):blockmode releases them automatically instead of locking a command forever on transient failures.never/manualare reserved for facts an explicit, trusted author recorded. -
off: disables recording and interception entirely. -
fs/observedevents (presentwith version, orabsent) map one-to-one ontofile-stateprecondition witnesses; the emitting execution is correlated so a model-supplied path (scoped by the session cwd) and the backend's resolveddisplayPathwitness the same fact; every observation change drives invalidation, so file hashing is never needed. -
Successful tool results resolve the fact through settlement or a lease-free transition — the reminder is withdrawn after a working retry.
-
The ledger is shared across agents (subagents do not repeat the parent's failures); counters are transactional columns (sqlite) or append-only hit lines (jsonl).
Security posture:
- Claims never embed raw command text; model-facing previews are control-character-sanitized and length-capped. Raw commands stay in the ledger FILE (they are the fingerprint) — the file is written 0600 inside a 0700 directory.
- The ledger renders facts as quoted data, never as instructions.
- Single-writer JSONL applies to the legacy backend only; the sqlite backend is multi-process (WAL).
Boundary with repeat-tool-reminder: that guard nudges on byte-identical consecutive repeats within one session; the ledger is persistent, evidence-bound, and auto-invalidating across sessions.
Known limitations and deferred work
- Single-writer JSONL (legacy):
backend: jsonlkeeps the v0 single-process store for migrations and debugging; concurrent multi-process writers are unsupported there. The defaultbackend: sqliteis the transactional WAL store with unique indexes, idempotent operation receipts, and crash recovery. - Command fingerprints use the calling agent's session cwd; a sandbox-policy workspace-root override is not visible to the plugin. Raw command text is preserved (no whitespace collapsing) so semantically different shell programs never collide, but equivalent re-spellings do.
- A non-zero exit does not always disprove a path (e.g.
grepexits 1 for "no match"); the short TTL and the warn-default posture bound the damage, but per-tool recording policy is deferred. - v0 matches exact fingerprints only; no semantic similarity.
approach_rejectedandapi_unavailablekinds exist in the model but are not wired to tools yet.- Token savings are deliberately not estimated here; the trajectory lab (#4) owns A/B and replay diffs.
- Future seams: failed_attempts in subagent contract results (#1), active/stale projection into task checkpoints (#3), repeat-failure rates in trajectory regression (#4), fail-closed promotion of high-risk paths (#2).