# Your agent's memory needs the word 'no' — and a way to prove nobody edited it

> Source: <https://dev.to/masondelan/your-agents-memory-needs-the-word-no-and-a-way-to-prove-nobody-edited-it-2kg8>
> Published: 2026-08-31 03:08:21+00:00

the expensive failure in AI-assisted coding isn't a bad line of code — linters and tests catch those. it's an agent confidently re-implementing something your team already tried and killed. that's been the selvedge pitch from the start: an append-only store where the agent logs its own reasoning at change time, so "we tried that and it broke revocation" survives the session that learned it.

v0.3.10 solved delivery — the memory now arrives at session start instead of waiting to be asked ("Delivery, Not Storage" [arXiv:2607.20972] measured why that matters: zero voluntary memory operations across 114 turns against a pre-seeded store, while deterministic injection landed every time).

v0.3.11 is about the two halves of the sentence "*rejected paths, retained*." first: making the rejection itself first-class. second: making *retained* something you can check instead of something you take my word for.

until now, selvedge could tell you a path was abandoned mostly by reading tea leaves: a `remove`

event near an `add`

, a proximity window, a heuristic confidence score. useful, but it's inference — and inference is exactly what this tool exists to replace with testimony.

three changes make abandonment a stated fact:

** change_type="reject".** the counterpart to

`revert`

, for paths never taken. `revert`

says "we tried this and rolled it back." `reject`

says "we considered this and decided against it" — no code was ever written, and the decision is still worth a permanent record, because the next session's agent will consider it too. no new MCP tool; it's a value on the existing `log_change`

.** prior_attempts grows an exact tier.** an attempt closed by an explicit

`reject`

or `revert`

event now reports `confidence: "exact"`

— the outcome is stated in the log, not inferred from proximity. the old heuristic drops to tiebreaker for attempts without a stated outcome. (if you filter on `confidence`

, accept `exact`

alongside `proximity_high`

— rows move.)** expires_when finally does something.** a rejection stored without the condition that would invalidate it is data that silently rots. the column shipped dormant in v0.3.8; v0.3.11 ships the evaluator, with a deliberately closed grammar — four shapes, validated at write time, everything else rejected rather than stored:

```
library:django>=5.0      # revisit when a named dependency reaches a version
entity:users.email:changes   # revisit when that entity next changes
date:2027-01-15          # revisit on a date
manual:security-review   # never auto-fires; a label for humans
```

`selvedge stale`

evaluates these locally — installed package metadata, the event log itself, and the clock. no network, no LLM, and no cleverness: a `library:`

condition whose package isn't locally observable comes back as `manual_review`

, not a guess, and a pre-release never counts as its final release. a decision that knows when it should die is a decision you can still trust in a year.

capture-time nudges close the loop: log a `reject`

or `revert`

with neither `stale_when`

nor `expires_when`

and the validator suggests recording the invalidating condition. it warns, never rejects — same posture as the secret-shape warnings.

here's the uncomfortable question under the whole product: selvedge's differentiator is that rejected decisions *stay on the record*. append-only. permanent. queryable. and until this release, that was a promise about our own conduct — nothing in the store could show you a row was the row the agent wrote.

v0.3.11 adds a tamper-evident hash chain. every logged event gets a SHA-256 chain record in a sidecar table, written in the same transaction, each record binding the previous one and its own sequence number. `selvedge verify`

gains two checks: `chain_intact`

recomputes the whole thing and fails hard — naming the exact sequence number — when a chained row was edited, deleted, or reordered out-of-band. `chain_coverage`

warns (never fails) about rows that predate the chain: *unchained, not invalid*.

the design problem worth a paragraph, because it generalizes past selvedge: **some fields are late-bound.** the commit SHA does not exist when the event is logged — the event describes the work that *produces* the commit, and a post-commit hook stamps it in afterward. a naive "hash the whole row" chain calls your own git hook an attacker. selvedge's cut: `git_commit`

sits outside the protected core, declared out loud in the attestation manifest — tolerable because a commit hash is independently checkable against git, which is itself a hash chain maintained by other software. everything else — reasoning, diff, entity path, change type, the supersede links — is chained. legitimate mutations don't break anything: `migrate-paths`

appends an `amend`

record, and the destructive-gated prune appends a `tombstone`

accounting for what it deleted. a gated prune verifies clean; a silent `sqlite3 "UPDATE events SET reasoning=..."`

does not.

and the honest scope, which is in the module docstring and belongs in this post too: **this detects casual and accidental modification, and produces an independently verifiable export. it is not proof against a motivated local attacker.** there's no key. someone who controls the file can recompute every digest. what the chain buys you is that nobody edits the record *casually* — no script, no well-meant cleanup, no buggy future code path mutates a decision without the next `selvedge verify`

naming the exact row.

we ate the dogfood on day one: selvedge's own store crossed its chain genesis with this release, and the first chained rows are the decisions behind v0.3.11 — including a real `change_type="reject"`

for the retroactive `chain seal`

command we decided *not* to ship, with the reasoning attached.

`selvedge supersede`

`-d/--diff`

, `--revisit-after`

, and `--expires-when`

(#31) — parity with `selvedge log`

, through the same size-bound and secret-shape checks.numbers: tests 984 → 1114, coverage 89.4%. the chain checks are mutation-verified — delete the threading checks and the suite goes red. the resident MCP tool-surface cost moved this release (4445 tokens core, 2.2% of a 200k window) because the rejection example and the `expires_when`

grammar now live in the tool docstrings agents actually see; that's the adoption bet, called out in the changelog.

`pip install -U selvedge`

. genesis is automatic on your first write; there's no migration and no new config keys. one expected surprise: `selvedge verify`

will *warn* about your pre-existing rows (`chain_coverage`

) — they're unchained, not invalid, and the chain deliberately makes no claim about rows that predate it. CI stays green by default.

```
pip install selvedge
cd your-project
selvedge setup        # detects claude code / cursor / copilot
```

open source, MIT, local-first, zero LLM calls in the core. the thing i most want field reports on this time: log a real rejection with an `expires_when`

, wait for it to fire, and tell me whether the revisit that surfaced was worth your attention.
