"I'm 99% sure that grep won't find your commit because you rejected 'oauth-library' and grepping for 'auth' rejection. Given that LLM will make up category name, it will just get worse unless there is deterministic enforcement."
let me be honest about where that quote comes from, because it's not an
endorsement. it's one commenter's adversarial objection on the Contextual
Commits thread — a thread that is, overall, skeptical of structured capture,
including the kind selvedge does. i'm borrowing an objection, not a blurb.
but it's worth borrowing, because even the critics of structured capture
agree on what the failure mode is: nondeterministic labels. if a model
invents the category name at capture time, the search misses at read time,
and grep never saves you. hold that thought — the whole release is downstream
of it.
the failure that actually costs money
here's a transcript i keep showing people
(the full version is checked into the repo).
months ago, an agent session added a users.auth_token column — a
long-lived per-user token so the mobile app could stay signed in across
restarts. two days later the team reverted it. the revert reasoning, captured
at the time: tokens in the DB meant revocation required a write, so they
moved to short-lived JWTs verified statelessly. decision made, price paid,
session over.
months later, a new session, zero shared context. the user asks: mobile
users get logged out every time they reopen the app — can we keep them
signed in? the agent's first instinct is the obvious one. add a persistent
auth token column to users.
it's a plausible plan. it's delivered confidently. and it walks straight
back into a decision the team already paid to learn — the incident, the
revert, the migration, all of it, about to be purchased a second time.
that's the failure this product is aimed at. 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
killed. the knowledge existed. somebody wrote it down, even. it just lived
in a chat session that ended.
in the demo, the story goes the other way: the agent calls prior_attempts
on users.auth_token before writing the migration, gets back the reverted
verdict with the reason it was rejected, and pivots — refresh token in the
OS keychain, stateless /auth/refresh endpoint, no rejected column
reintroduced. then it logs its own decision so the next session inherits
that too.
what makes that answer possible is the shape of the store: append-only
testimony. the reasoning is what the agent itself wrote, at change time, in
the same context that produced the change — and a rejection is a permanent,
queryable record. both halves matter. the post-hoc explainer tools ask a
second LLM to look at your diff later; a model that never saw the original
prompt produces a paraphrase, and re-running it can produce different
category names — which is exactly 0x457's objection. and OpenLore, which
shares selvedge's zero-LLM determinism, purges rejected decisions from its
queryable store after sync — an annotation survives in the synced spec
markdown, but the queryable record doesn't. none of the line-attribution
tools can answer the question the agent actually had: has this been tried
before, and how did it turn out? the rejected path staying queryable is the
wedge.
the part i had wrong: storage isn't delivery
v0.3.7 shipped prior_attempts, the asking primitive. it works — when it's
called. that "when it's called" turns out to be doing an enormous amount of
work, and this year two independent papers put a number on it:
"Delivery, Not Storage" (arXiv
2607.20972) and PROJECTMEM (arXiv
2606.12329) both measured pull-model memory tools going unused — zero
voluntary memory operations across 114 turns against a pre-seeded store,
while deterministic injection landed every time.
zero. across 114 turns. with relevant memory sitting right there.
a memory the agent has to remember to consult is a memory that mostly
doesn't get consulted. selvedge already had the gate half of the answer (the
PreToolUse hook that fires when an agent touches a watched entity). what was
missing was delivery when there's nothing to veto. so v0.3.10's headline
theme is that the memory now comes to the agent:
SessionStart digest. a hook injects a compact, relevance-gated digest as
the session begins: decisions due for revisit, entities whose standing
verdict is reverted, recent changesets. the march revert is in front of
the june agent before it plans anything — no tool call required, no
remembering to ask. it's quiet when there's nothing to say, size-capped via
digest_max_bytes, read-only, fail-open, and templated like everything else
in the core.
PreCompact. context compaction is where a session's reasoning goes to
die, so selvedge now fires right before it and names the watched entities
that were edited this session with no log_change recorded — subtracting
what's already in the store. this one is advisory only, deliberately: the
hook API allows blocking, and selvedge doesn't use it. blocking a tool call
inconveniences an agent; blocking compaction wedges the whole session.
there's a test asserting we never do it.
selvedge export --format markdown. a deterministic, stably-anchored
digest of the store, grouped by entity with reverted decisions first,
designed to be committed next to .selvedge/ so captured intent is
reviewable in an ordinary diff. regenerating with no new events produces a
zero-line diff. zero LLM calls, and — notably — no new MCP tool.
that last point generalizes: the entire delivery half shipped without
touching the tool surface. still 8 MCP tools, schema tax unchanged at
3705/3800 tokens. delivery didn't cost the agent a single extra token of
tool schema.
the config half
the second theme: the store gets its dials. .selvedge/config.toml is now
first-class, with the full key set — retention_days_events (default:
never), retention_days_tool_calls, backup_keep_last, diff_bytes,
reasoning_bytes, db_size_warn_mb, stale_days, digest_max_bytes,
redaction_patterns — and one canonical precedence chain: CLI flag > env
var > project config > global ~/.selvedge/config.toml > default
(SELVEDGE_DB is the one exception and always wins for database
resolution). selvedge doctor prints the effective value and which
precedence step produced it, per setting, so "why is this 30?" has a
one-command answer.
two entries deserve a paragraph each:
selvedge prune --include-events is the first code path that can delete
captured reasoning, so it's double-gated: an interactive confirmation AND
SELVEDGE_DESTRUCTIVE=1 in the environment, plus an audit line in
.selvedge/prune.log. neither gate alone is enough — a --yes in a cron
entry defeats a prompt, and a line in a shell profile defeats an env var.
you need both, on purpose.
secret-shape warnings at log_change: a conservative built-in set —
vendor-prefixed keys, PEM headers, bearer tokens, SECRET= assignments,
credentialed connection strings — extendable via redaction_patterns. it
warns, never rejects; losing the reasoning to an overzealous filter is the
exact loss this tool exists to prevent. and since a write-time check can't
see the past, doctor grew a scan of what's already stored. (event-size
bounds follow the same philosophy: oversized diffs and reasoning truncate
loudly, with a marker, a warning, and a count in selvedge stats.)
and quickly, from the fixed column: the PreToolUse hook's allow path is ~40%
faster — p50 33.6 ms → 20.1 ms per gated call against a 14 ms interpreter
floor, measured n=60 interleaved; the hook's own logic was always 0.58 ms
and the rest was import cost. SELVEDGE_HOOK_DISABLE=1 now actually
short-circuits (it was being checked after the imports had already run).
log_change no longer silently discards revisit_after, constraint, or
stale_when on the rename and supersede branches. the docker image no
longer ships my own database (yes, really). the CLI and the MCP server now
return identical structures through one shared presenter layer. and a
mutation pass found ~12 deliberate guards that were executed but never
asserted — mutation score on the sampled set went 64% → 100%. the suite is
826 → 984 tests, coverage 88.3% → 89.0%.
if pip install selvedge broke for you in late july
two separate things happened, and both are closed:
-
mcp2.0.0 (released 2026-07-28) removedmcp.server.fastmcp, and selvedge declaredmcp>=1.0.0with no upper bound — so every fresh install from 2026-07-28 to 2026-08-01 resolved 2.0.0 and died at import. v0.3.9.3 pinnedmcp<2.0.0. - the official MCP Registry's latest-resolution is semver-shaped and
couldn't rank four-segment PEP 440 releases above
0.3.9— so?version=latestkept serving 0.3.9, which is precisely the release that dies at import under mcp 2.0.0. three-segment0.3.10is the durable correction. as of today the registry serves 0.3.10 as latest — verified.
so if you tried selvedge in late july and it fell over at import: that was a
real window, and it's closed. pip install -U selvedge gets you 0.3.10.
try it
selvedge is decision provenance for AI-coded codebases: the why, and what
was already tried and rejected.
pip install selvedge
cd your-project
selvedge setup # detects claude code / cursor / copilot
then work a session, end it, and start another. the second session opens
with a digest of what the first one decided — including anything that got
reverted, before the new agent proposes it again.
- site: selvedge.sh
- repo: github.com/masondelan/selvedge
- pypi: pypi.org/project/selvedge
- changelog: v0.3.10 release notes
- the demo transcript: docs/demos/prior-attempts.md
it's open source, MIT, local-first, zero LLM calls in the core. the thing i
most want field reports on is the SessionStart digest's relevance gating —
if it ever surfaces something useless at the top of your session, that's an
issue i want to hear about.
Top comments (10)
The storage-versus-delivery distinction is the part I’d test most aggressively. A useful regression fixture could seed a reverted decision, start a fresh session with the SessionStart hook enabled, and compare three cases: deterministic injection, voluntary retrieval, and no memory delivery. Track whether the agent avoids the rejected path, digest bytes added, false-positive digests, and time-to-first-tool-call. That turns “the memory was available” into a measurable delivery contract.
I’d also add a race test around the digest boundary: update or supersede a decision while a new session is starting, then assert the digest is internally consistent and names the effective policy version. For PreCompact, the advisory-only choice makes sense, but the warning should be idempotent across repeated compactions and clearly distinguish “edited entity with no log” from “log exists but was truncated.” Those invariants seem more valuable than another retrieval tool because they protect the handoff between sessions without relying on the model to remember the protocol.
this is basically the eval harness i want and don't have yet. the tests today cover delivery mechanics (digest is deterministic, respects the byte cap, stays quiet on an empty store, never blocks) but the claim that injection actually changes what the agent does still rests on the papers, not on a fixture in this repo. seeding a reverted decision and scripting a task that tempts the agent back down the dead path is the right shape, and you get digest bytes and time to first tool call for free once that exists. on PreCompact, the reminder is rebuilt from store state on every fire, so a second compaction emits the same text and goes quiet once things get logged. you're right that no test asserts that across repeated compactions though. adding both to the v0.3.11 list.
The root problem is that team decisions live in review threads and commit messages the agent never sees. The agent optimizes for the code it can read, not the decisions it cannot. Deterministic labels help at capture time, but the deeper issue is that agent context and team context are the same codebase with different visibility.
The expensive failure is not that the agent rewrites a rejected library. It is that the rejection was captured for humans and invisible to the tool. Structured capture fixes the surface. What fixes the depth is making the agent consult the decision history before it writes code, the same way a human would skim the PR backlog before opening a new file.
no argument with the diagnosis, that line is basically the premise of the tool. where i'd push back is on making the agent consult history before writing code. that's the part the data says doesn't happen. zero voluntary memory operations across 114 turns with a seeded store sitting right there. agents don't skim the PR backlog unprompted, and neither do most humans. so selvedge inverts it: instead of asking the agent to go look, it puts the relevant slice in front of the agent deterministically, at session start and again at edit time. on commit messages specifically,
selvedge import --from-gitalready mines revert and deletion commits into the store. review threads are the unmined half. a PR review importer is a real gap and this comment moved it up the list.The 'zero voluntary memory operations across 114 turns' number matches what I keep seeing with Claude Code's own memory tiers: anything the model has to choose to read (topic files, on-demand lookups) effectively doesn't exist, while whatever loads deterministically at session start gets used. I'd been framing that as a docs problem — your 'delivery, not storage' framing is cleaner. Does the injection side stay bounded as the store grows, or is there a relevance-ranking step in between?
bounded by construction rather than by ranking, at least today. the digest caps at five entries per section across three sections, then a hard
digest_max_bytescut on line boundaries, and it's gated on decision state (due for revisit, standing reverted verdicts, recent changesets) rather than dumping the store. so store growth doesn't grow the digest. what makes that tenable is that the digest isn't the only delivery path. the PreToolUse gate carries entity scoped history at edit time, so session start only has to hold the standing wedges. your question really bites at which five once there are fifty candidates. right now that's most overdue first plus recency, not task relevance, since at SessionStart there's no task signal to rank against yet. a deterministic ranking step over cwd and recent git activity seems plausible. ranking against the opening prompt is where it gets interesting and i haven't committed to a design.The append-only rejected verdict solves the re-implementation failure but opens a mirror-image one that I do not think the thread has touched yet: rejections go stale. The auth-token column was killed because revocation required a DB write under that architecture. If the constraint later disappears, say a managed token store with native TTL and instant revocation shows up in the stack, the correct answer flips, and a permanent queryable "reverted, do not reintroduce" verdict now steers every future agent away from a newly valid design. The digest's "decisions due for revisit" line suggests you have thought about this; what determines due? If it is time-based, it will nag about settled decisions while missing the one whose invalidating condition just landed. The version I want is capture-time invalidation conditions: the rejection records not just the reason but the predicate that made it true, "valid while token revocation requires a synchronous write", so a later changeset touching that predicate flips the verdict to "re-examine" instead of "reverted". Your changelog line about log_change no longer discarding stale_when suggests the slot for that predicate already exists; what I cannot tell from the post is whether the digest's due-for-revisit gate actually evaluates it, or only counts days. The reason a decision was made is also the condition under which it should die.
agreed, and "the reason a decision was made is also the condition under which it should die" is the design intent behind
stale_when, not a nice to have. to answer the direct question: it evaluates the condition, it doesn't just count days. two rules.revisit_afteris date based, but pure age never surfaces, a due decision only shows up with an active use signal (the entity got queried since, or its changeset kept moving).stale_whenis condition based: the text gets keyword matched against later change events (reasoning, diff, entity path, last 500 events, at least 2 meaningful tokens overlapping) and a hit surfaces as review suggested. surfacing only, nothing un-retires automatically, you supersede if the condition really triggered. the honest limit is exactly your managed token store example: the match only fires if the invalidating change lands in the store with overlapping words. a platform release note that never touches the repo is invisible to it.expires_whengets a real evaluator in v0.3.11, and prompting for astale_whenat capture time when a revert lacks one is an idea i'm taking from this comment.@masondelan The pushback lands. History consultation is expensive and the model rarely uses it well. But treating every generation as conditionally independent is how you get an agent that repeats the same mistake in the same session. The middle ground: consult only the diff of the last rejected output, not the full history.
@masondelan Making the agent consult history before writing code is not about slowing it down. It is about recognising that the model has no memory of the previous turn. The history is the only shared state. Skip it and you are coding into a vacuum, which is exactly where the expensive failure comes from.