Repository navigation
v0.4.1
Fixed
-
A store that could not be opened was reported as a project with no memory.
buildHintFormat
wrapped the whole ofbuildHintFormatUnsafein one catch, andopenStorageis called inside it, so
a permissions error, a WAL lock or a schema mismatch produced bytes identical to an empty store: the
bareTGMEM/2header and nothing else. The hook commands mem installs end in|| true, so nothing
downstream surfaced it either, and the failure mode of the one channel this tool exists to keep
reliable was indistinguishable from its quietest success. The open now has its own catch and its own
error type, so the three cases -- unreadable store, nothing to recall, some other internal fault --
are told apart rather than collapsed. An unreadable store emits a footer clause saying so and still
exits 0, because failing open is the right behaviour and silence about it was not. The clause points
atmem doctor, which opens the same store and will fail the same way: that is deliberate, and the
wording says only that doctor shows the underlying error, which is exactly what it does. Promising
a command that fixes this would repeat the refusal loop this project fixed one release ago. -
mem scan-sessiondated every fact from the moment of the scan. The scanner read a transcript
entry's text and never itstimestamp, and the capture call omittedcapturedAtentirely -- so a
statement made in March and scanned in September was stored as having been said in September. Under
theStopandPreCompacthooks the drift is seconds and harmless, but--transcript <path>is a
first-class flag, and a rescan of an archived session mis-dated everything it produced by the whole
age of the transcript. That is not cosmetic: preference confidence decays fromcaptured_at, the
--stalecutoff is measured from it, recall breaks recency ties on it, and the review queue is
ordered by it.mem suggestandmem import --from-mdalready distinguished when a thing was said
from when mem stored it; the scanner was the one capture path that did not. It now reads the entry's
timestamp through the same validator, which already refuses a future date. Anything absent,
malformed or hostile falls back to the scan time without failing the run -- a transcript mem cannot
date is the situation that existed until now, not an error. The source row'sstored_atstill
records the scan: said-at and stored-at are genuinely different columns. -
A source excerpt could omit the sentence it was evidence for. Excerpts were truncated from the
head at 600 characters, and formem scan-sessionthe raw material is the whole user turn --
deliberately larger than the extracted sentence. A turn longer than the cap whose durable statement
came last therefore produced asourcesrow that did not contain the fact at all, andmem review
printed it under that fact as the reviewer's grounds for promoting or rejecting it. Evidence that
does not contain the claim is worse than no evidence: it invites a decision on the strength of an
unrelated fragment. The truncation window is now centred on the fact's own sentence, located through
the samenormalizeFactTextthe store uses everywhere else so collapsed whitespace cannot defeat
the search, with a marker on whichever side was cut. A raw excerpt that does not contain the fact
falls back to head truncation, and the screening order is untouched: the untruncated text is
screened first, and a screened-positive excerpt still yields no source row and still captures the
fact. -
A query that matched nothing was answered with recent facts and no mention of it. When a
non-empty query produces no lexical hit and no other rank list has anything to say, every fact ties
at zero and the caps fill with whatever is newest. The seam's own comment calls these filler. Under
the installed hooks this is mostly masked, since--deltasuppresses what was already sent, but a
plainmem recallwith a query and--hint-formatis a documented command, and it handed a host a
page of facts under a query none of them matched. The footer now says so. The condition is
deliberately not "no result matched":matchedQueryis computed from BM25 alone by design, so a
query carrying real embedding signal reads false on every row, and claiming nothing matched there
would put a false statement into the wire output. Retrieval now reports whether any rank list ranked
anything at all, and the clause is gated on that, on a non-empty query, and on there being a fact to
qualify. -
An exported supersession edge did not survive being imported.
mem exportrecorded that a fact
was superseded but not what superseded it, so after a round tripmem showprintedsuperseded_by: unknown-- a caveat about information the store had held and thrown away, given that fact ids
survive import intact. Export now carries the edge for superseded facts, and import re-establishes
it. The winner is resolved once the whole file has been inserted rather than row by row, so a file
naming a successor that appears further down its own list still links; a per-row resolve would have
silently dropped exactly that case. A named winner that resolves to nothing in the store is ignored
without writing a dangling edge and without failing the import, and the existing caveat then stands
-- now meaning a genuinely untraceable supersession rather than a limitation of the format. The
field is optional in both directions, so the export schema version is unchanged and an older reader
is unaffected. -
Nothing mem ever printed told anyone how to say a fact helped.
mem usedshipped,recall_log
carried aused_atcolumn for it, and no output on any path named the command -- so in a real store
that column is null on every row, and every consumer of the signal was reading evidence that could
not be produced.mem consolidate --stalegated on "never marked used" against a mark nobody could
make; usefulness counts broke ranking ties that were never actually broken. The recall footer now
carries a ready-to-runmem used <ids> --session-id <id>clause whenever the response is logged
under a session and actually emitted a fact -- not when either is missing, since an invocation
naming a row that was never written earns the exact "was never surfaced in session" refusal this is
meant to stop producing. Footer text is free prose in the wire grammar precisely so a clause can be
added here without bumpingTGMEM/2: an unknown header version fails open to no hints at all, so
paying a protocol bump for one footer clause would cost every consumer every fact-line. Separately,
--stableno longer suppressesrecall_logwrites. It reorders output for reproducible tests,
which is no reason for the store to forget what it showed. -
mem initnever upgraded an install that was already there. It wrote its marker, saw a mem hook
already insettings.jsonand left the entry as it found it -- so an install predating a hook flag
or a hook event kept the older command string forever, and re-runningmem initafter upgrading mem
did nothing to fix it. On this machine a July install carried onlySessionStart, without
--hook-stdin: norecall_logrow had ever been written andmem scan-sessionhad never run once,
a dead feature that read as a code defect rather than an install-age symptom.mem initnow
recognises an unstamped hook as its own by matching mem's invocation shape -- same guard wrapper,
same subcommand for that event, any flags in between -- and adopts it, rewriting the command to the
current one and stamping it so uninstall still reverses exactly what install wrote. A hand-written
entry that merely mentionsmemdoes not match the shape and is left alone. An adoption is reported
in the install output rather than performed silently, because absorbing another install's hook is
the kind of thing a user should hear about from the command that did it. -
mem consolidate --staleasked a lifetime question of evidence that rotates. The gate read
"never surfaced by recall", over arecall_logthatmem epoch --gcprunes at 30 days. A fact
surfaced steadily for a year and last read 31 days ago had no surviving row to prove it, so it
qualified as never-read and was proposed for supersession on the strength of a deletion. The query
now asks whether the fact was surfaced since the cutoff, which is a question the retained window
can actually answer, and--helpand the summary line say "unsurfaced since then" rather than
"never surfaced". Usefulness decays with the same rows by the same design, everywhere it is read;
permanence has its own mechanism,mem pin, which this query excludes by construction. -
mem import --from-mdre-filed facts the store already had. Its dedup key is source location
plus text, which correctly catches re-importing an unchanged file, and catches nothing about the
same sentence arriving by another door. A statement captured bymem scan-session, or typed with
mem remember, would be filed again as a freshpendingcandidate the moment it also appeared in a
markdown note -- so the two capture paths disagreed about what the store knew, andmem reviewgot
a question already answered. Import now also checks the store-wide text index, scope-gated by the
sameisBoundToRootrule recall and scan-session use, so an identical fact bound to an unrelated
project cannot suppress a candidate here. Such a bullet is reported asskipped (already known),
distinct fromskipped (duplicate), because "you imported this file before" and "mem learned this
elsewhere" are different things to tell someone. -
mem init --userquietly configured a project instead. For a tool whose only target is a file
in the repository --codex,copilot-cli----userwas accepted and ignored, and mem's block
landed in the working directory'sAGENTS.md. A user asking to set up their own machine got an
edit to a repository they may not own, discovered later bygit status. Those tools now exit with
"has no user-level config; omit --user" and write nothing.mem init copilot-vscode --userhad the
same shape with a real answer available: VS Code's keybindings live in the editor's own user
directory, so--usernow installs those alone and leaves the project'stasks.jsonand
AGENTS.mduntouched. -
mem import --from-json --dry-runpromised imports the real run would refuse. The plan skipped
both the--rootscope binding and secret screening, so a fact whosescopeRootfalls outside the
import root, or whose text trips the screener, was reported aswould-importand then rejected on
the real pass. Both checks are pure -- file paths and the on-disk allowlist, no database -- so the
dry run now runs them and reproduces the refusal reason verbatim. Duplicate-id detection is the one
check it still cannot reproduce, since that needs the store, and the output says so rather than
letting a clean-looking plan imply there is nothing left to find. -
Promoting an unkeyed fact produced one nothing could ever replace. Contradiction resolution keys
on subject plus value, and its detection pass filters to facts that have a subject -- so a fact
promoted without one is not merely unlikely to be superseded, it is structurally exempt from
supersession for the rest of its life.mem suggest,mem scan-sessionandmem import --from-md
all extract text and no subject, which makes this the ordinary promotion rather than an exotic one,
andpromoted <id>said nothing about it.mem review --promotenow prints the consequence and the
one command that undoes it,mem edit <id> --subject <key> --value <value>. A keyed promotion says
nothing extra. -
mem reviewnamed a resolution neither of its own commands would perform. A pair of facts that
tie on provenance and capture time is detected as contested live, in memory, while both rows are
still persistedactive.mem reviewlisted them as contested and said "resolve with
--promote/--reject";mem recallwithheld both as "contested, excluded" and said "mem review to
resolve"; and both of those commands then exited 1 with "is not pending or contested
(status=active)". Every instruction the tool gave led to a refusal, and the facts stayed
unreachable, until an unrelatedmem epoch --gchappened to persist the detection.--promoteand
--rejectnow ask the same questionmem reviewasks, the same way -- one answer to "is this
contested" instead of two. A fact that is contested by neither measure still gets the original
refusal. -
Stating a fact explicitly did not answer the suggestion that proposed it.
mem suggestfiles a
candidate aspending; stating the same sentence withmem rememberwrote a second row and left
the suggestion queued, somem reviewkept asking a question the user had already answered -- and
the obvious response,--promote, then produced two identical active facts thatmem consolidate
went on to report as a duplicate cluster. An explicit restatement now resolves the queue for that
sentence: the earliest matching suggestion is promoted in place (same id), and any further
duplicates of it are superseded through the same status machinery, with audit entries, rather than
stranded. Scope binding is respected -- an identical suggestion pending in another project is
untouched. The invariant that apendingfact never promotes on its own is unchanged: passive
signals (time, repetition, confidence) still cannot promote one,mem suggestandmem scan-sessionstill cannot, and this path requires the user to state the fact themselves. -
A UTF-8 BOM made
mem initandmem uninstallrefuse, and blame the user for it. A
settings.json(or VS Codetasks.json/keybindings.json) beginning with a byte-order mark --
what Windows editors and PowerShell's defaultOut-File/Set-Contentwrite, and what Claude Code
itself reads without complaint -- was reported as "is not valid JSON/JSONC; refusing to modify a
hand-edited config". The file was valid; the refusal named a cause that was not true, and there was
no flag or workaround, so on an affected machine mem could neither install nor cleanly remove
itself. The mark is now stripped for the validation parse only. The text mem edits and writes back
still carries it, so a file that had a BOM keeps it byte-for-byte through an install/uninstall
round trip and a file that never had one does not acquire one. A genuinely malformed config still
gets the same refusal, with or without a BOM: this narrows a false positive rather than relaxing
the guard. -
mem scan-sessionthrew away another project's knowledge and called it nothing new. Its
cross-scan duplicate check asked only "is this exact sentence already stored", across the whole
store, with no regard for which project the stored copy belongs to. State a rule in project A,
review it there, then state the same rule in project B, and B's scan silently skipped it --
reporting "no new durable statements found", which is the one thing it must not say about a
candidate it chose to drop. Worse with a rejection: a candidate rejected in A stays in the store
assuperseded, so A's decision about A suppressed the suggestion in B permanently. The check
now narrows to facts bound to the scanning root viaisBoundToRoot-- the same rule recall uses
to decide what a root may surface -- so a global-scope fact still dedupes everywhere (it applies
everywhere) while a project fact only dedupes within its own project. -
The two dedup layers disagreed about case outside ASCII. In-scan dedup keyed on
sentence.toLowerCase()(Unicode-aware) while the cross-scan store check used SQLLOWER()
(ASCII-only), so a sentence containing an uppercase non-ASCII letter -- "Émacs is the editor
here" -- collapsed to one candidate within a scan but stored a second copy once the earlier
occurrence aged out of the scan window. Both layers now key on one shared function. The store
check reads the table once per scan and matches in JS rather than in SQL, because no
LOWER()-based prefilter can be trusted not to drop the very row it is meant to find; building
that index in a single pass, rather than rescanning per candidate, keeps the Stop hook from
re-reading every fact (embedding blobs included) once per sentence -- measured at 2000 stored
facts and a 40-sentence transcript, 1.10s per-candidate versus 0.20s indexed. -
An unreadable
--transcriptexited 0 and reported success.mem scan-session --transcript /typo/path.jsonlprinted "no new durable statements found" and exited clean, which is a positive
claim about a file it never opened. An explicitly named transcript that cannot be read is now a
usage error (exit 1) naming the path and the reason. The hook path is deliberately unchanged: a
Stop/PreCompactscan that cannot find its transcript still fails open and silently, because
it is background convenience and must never interrupt a session. -
mem uninstallleft husks of its own making, and two paths read the same snapshot by different
rules. Init takes a one-time<file>.token-goat-mem.bakbefore its first write, and that snapshot
is the only evidence of what existed beforehand.writeManagedFiletreats a snapshot holding only
mem's own content as no snapshot at all, so a file mem created gets deleted rather than left empty;
preInstallHooksread the same file and counted any parseablehooksobject as the user's. Two
ordinary sequences fell through the gap. Re-runningmem init claude-codeafter an upgrade that
changed a hook command snapshotted mem's own settings and uninstalled to
{"hooks": {"SessionStart": []}}plus a stale.bak. And an install/uninstall cycle, followed by
the user deleting the files and installing again, uninstalled to{}and a zero-byteCLAUDE.md—
which Claude Code loads as a real instruction file — because the first cycle's snapshot was still
sitting there describing an era that had ended. Both now end with the file gone, which is what
README's "removes exactly whatmem initwrote" says. No user content was ever at risk in either
sequence; what was left behind was mem's own residue, reported as a clean removal. The snapshot is
also now discarded once uninstall has deleted the file or written it back with no mem markers left:
at that point the file itself is the baseline, and keeping the old snapshot is what made the second
sequence possible. -
mem uninstall --userwas documented as doing something broader than it does. The help read
"Also target the tool's user-level config", while the code targets only that level — so after
installing at both,--userremoved the user-level file and left the project's hooks firing.
mem init --useralready says "instead of" and means it; the help now matches the code on both
commands rather than inviting a user to expect one command to clear both levels. -
A
--path-scoped fact could never reach the agent. On the hook path apathfact was in scope
only when the caller passed--context-filesnaming a file at or under it — and every hook and
commandmem initinstalls callsmem recall --hint-format --root <dir>with no context files at
all. Somem remember --pathstored facts that plainmem recalllisted and the agent-facing path
structurally could not deliver, for every integration mem ships. With no context files the seam now
applies the same root-containment rulemem recallalready used; a caller that does pass context
files still gets the narrower match, because it told mem what it is looking at. The hint ceiling is
unchanged, so widening scope changes which facts compete for the same slots rather than how many
are emitted. -
Two clones of one repository never contradicted each other, so mem served both answers as
current. Recall widens aprojectfact into scope by repository identity — that is how a fact
captured in the main checkout reaches a worktree — but contradiction bucketing keyed onscope_root
alone, so facts from two checkouts of the same repository were never compared.--subject package-manager --value pnpmcaptured in one clone and--value npmin the other left both
active: recall from either root listed both as current,mem review --summaryreported
contested: 0, andmem epoch --gcresolved nothing, so no correction captured in a worktree ever
superseded the decision it corrected. That is the failure P3 exists to prevent, in its sharpest
form — not a caveated answer, but two mutually exclusive answers asserted at once with nothing
marking the disagreement.Bucket membership is now a relation rather than a derived key: two facts share a bucket when they
share a repository identity or a normalized root. Keying on the repository instead of the path
was the obvious fix and is wrong —scope_repohas never shipped, so every fact in every existing
store has it NULL, and a key that prefers it would put a fact captured before this release and its
correction captured after it in different buckets in the same directory, breaking the ordinary
single-checkout case for every upgrading store in order to fix a worktree one. Widening detection is
safe; splitting it is not.mem consolidatenow clusters through the same relation instead of its
own copy of the rule.On upgrade: the first
mem epoch --gcafter installing this will supersede facts across clones
and worktrees of one repository that were previously treated as independent. That is the correction
this fixes, but it is a change to stored status that happens without being asked for. Nothing is
deleted — a superseded fact keeps itsprior_statusand stays visible tomem show— so a
resolution you disagree with can be inspected and reversed. -
Four anchor predicates called a fact false from a check they never performed. Each is the same
P3 violation:contradictedis a positive claim that a fact has stopped being true, and reaching it
by any route other than actually evaluating the predicate silently deletes knowledge the user
stated.git-tracked <dir>contradicted every directory. The git index stores files, never directories,
so a directory target matched no entry and fell through to "not tracked" — while a sibling fact
anchored to a file inside it was affirmed in the same repository. The README documents
git-tracked <path>, with no file-only restriction. A directory is now affirmed when any tracked
path lives under it, andcontradictedstays reachable only for a directory that genuinely holds
none.valid-until 2026-12-31expired at UTC end-of-day. West of UTC the fact was withheld from
16:00 local on the very day the anchor promised it would hold; the README says "still affirmed
during the 31st" and meant it. The date is now read as the end of the machine's own local day,
and both the README and the doc comment say so rather than leaving it to be inferred. Expiring
late merely caveats a fact for a few more hours; expiring early destroys it.- A permission or I/O error read as "the file is not there". An unreadable subtree made
file-existscontradict,file-absentaffirm, andfile-newer-thancontradict for whichever
side could not be stat'ed — and an unreadable directory mid-walk letglob-existsreach
contradictedwhile skipping the one place a match could have been. OnlyENOENT/ENOTDIRmean
absence now; every other errno isunverified. - A casing-only mismatch contradicted off Windows. macOS supports both case-sensitive and
case-insensitive APFS volumes and this process cannot tell which it is looking at, so neither
affirmednorcontradictedis honest — a miss explainable purely by casing is nowunverified
on bothgit-trackedand the literal segments ofglob-exists.
-
Three commands kept asking for work no command would ever do.
mem doctor's embedding
coverage counted every fact in its denominator whilemem embedrefuses to embed asuperseded
one, so a store that had ever superseded an unembedded fact reported a permanent shortfall that no
amount of backfilling could close. Facet coverage had the same shape for a different reason: "needs
extraction" was inferred from the absence of afact_termsrow, which cannot be told apart from
"extracted, and the text is entirely stopwords" -- somem facets --backfillran, found nothing to
write, and doctor asked again forever. Afacts.terms_checked_atcolumn now records that
extraction happened rather than leaving it to be inferred from its output. Both denominators now
describe work that exists. -
scan-sessionstored the same rule twice when the restatement changed case. Within one scan,
duplicate sentences collapse on a lowercased key. Across scans the only guard is
factWithTextExists, which comparedtext = ?under SQLite's binary collation -- so once the
original turn aged past the 200-turn scan window, "Never commit generated files." came back as a
second pending candidate alongside "never commit generated files." The two dedup layers now
normalise the same way. -
The session scanner's length floor measured the trigger word it was supposed to look past. The
constant is documented as the shortest candidate kept past the trigger, and the check read the
whole sentence, so"Remember that."and"We have decided."cleared a floor that exists to reject
exactly that: a matched trigger with no claim behind it. Both became pending candidates carrying no
content, for a user to read and reject by hand. -
mem exportdropped which model wrote the vectors, so an import silently compared two vector
spaces. Thefacts.embeddingblobs were exported; theembedding_model/embedding_dimension
pair that says what they mean was not. A store restored from that export held vectors it could not
attribute, and the one guard against comparing incomparable spaces —planEmbeddingRanking, which
refuses to rank when the configured model differs from the recorded one — has nothing to compare
against when the recording is missing, so it permitted ranking under whatever model the destination
happened to configure. Cosine distance between two models' vectors is a number, not an error: recall
came back ordered, plausible, and wrong. The envelope now carriesembeddingMeta, validated on the
way in; a fresh store adopts it, a store that already has one keeps its own and strips arriving
vectors it cannot attribute rather than mixing them. Unlabelled vectors from an interrupted
mem embedor an export written before this release are treated as incomparable and named as such,
withmem embed --allas the stated way to relabel the store.mem embed --allnow clears the
recorded meta unconditionally instead of leaving the old model's name over the new model's vectors. -
The hook path decided the embedding question differently from the CLI, for the fifth time.
src/integration-seam.tskeeps its own copy of the ranking decision, and that copy did not know
about unlabelled vectors:mem recallskipped the endpoint and said why, while the same store
behindmem recall --hint-format— the path that runs on every prompt — called out to the
configured model and ranked against vectors of unknown provenance. Same store, same question, two
answers, and only the silent one is on the hot path. This is the fifth column or decision the seam's
hand-maintained copy has dropped;tests/guards/fact-columns.test.tscatches the column shape of
the drift, and this one is now pinned by a test that drives the seam directly and asserts the
endpoint is untouched. -
mem dreamreasoned from premises mem itself refuses to state. Its pool was everyactiveand
pinnedfact, with no correctness gate — no contradiction resolution, no freshness check. Because
contradiction outcomes are resolved in memory on each recall but persisted only bymem epoch --gc
andmem review --reject, two conflicting facts captured withmem rememberare both still
active, so both went to the model as premises whilemem recallcorrectly withheld the loser;
anchor-contradictedfacts went too. The code comment claimed otherwise, describing the store as it
would be after a gc rather than as it is. That is P3's failure one step removed: mem does not state
the false thing itself, it asks a model to reason from it and reports the conclusion. The gate
retrieve()already applies is now extracted asselectVerifiedFactsand shared by both, rather
than reimplemented beside it — this codebase has been bitten repeatedly by a second hand-maintained
copy drifting from the first.unverifiedis still not grounds for exclusion; onlycontradicted
is. Freshness evaluates against the working directory, which is what every other command defaults
to without--root, and the help text now says so instead of leaving it to be inferred. -
mem uninstalldeleted files that existed beforemem initand said they held only mem's own
content. Uninstall removes a file rather than leaving it empty, which is right when mem created
it — but emptiness alone cannot tell that from a file the user already had as empty or{}. A
project with a hand-made emptyCLAUDE.mdand a.claude/settings.jsoncontaining{}lost both,
against this repository's own stated guarantee that uninstall reverses only what init wrote. The
evidence was already on disk: init snapshots any pre-existing file to<file>.token-goat-mem.bak
before its first write, so a snapshot holding real outside content now blocks the delete and the
emptied file stays. One wrinkle is handled deliberately: on a shared file likeAGENTS.md, a
second tool's install can be the write that first sees "existing content" and snapshots it — but
that content is the first tool's own marker block, not user data, so a snapshot containing only
mem's markers is read as no snapshot at all. A file mem created is still removed. -
mem consolidate --applyreversed an explicit correction and filed it as a duplicate. Its
comparability key is kind + scope + scope root; subject and value were never consulted. So two facts
that share a subject and differ in value — the definition of a live contradiction — clustered as
near-duplicates as soon as their wording overlapped, and the survivor was then picked by
consolidate's own rule (pinned, then confidence, then newest) rather than contradiction's
(provenance, then newest, where a pin gets no protection, exactly asmem pin's help says). Tell
mem the database is postgres, pin it, later correct yourself to mysql, and oneconsolidate --apply
superseded mysql "as a duplicate" and put postgres back as ground truth. Contradiction resolution
now owns that pair: a candidate sharing a subject bucket with a different value is skipped by
duplicate detection, using contradiction's own bucket function rather than a second normalization
that could drift from it. Genuine duplicates — including unkeyed facts, which is what consolidate
is actually for — cluster exactly as before. -
Three commands disagreed with the commands next to them about the same fact. Contradiction
outcomes are resolved in memory on every recall but persisted by only two paths, so the rest of the
CLI read stale statuses.mem forgeton half of a contested pair left the survivor stranded at
contested:mem recallsurfaced it with no caveat whilemem showcalled it contested,mem list --status activedid not list it, andmem pinrefused it as "contested, not active" — all at once.
mem review --undoof a contested rejection restored the fact without undoing the rival's promotion
that the rejection had performed, leaving two winners where there had been one. Both now reconcile,
symmetrically withmem review --reject, which already did. -
mem shownamed a cause of supersession it could not know. Supersession edges live in the audit
log, whichmem exportdoes not carry, so after an export/import round trip a superseded fact
reportedsuperseded_by: (nothing -- retired by forget, reject, or staleness)while the fact that
actually superseded it sat in the same store. The line now says the edge is unknown and names both
possibilities instead of asserting one;mem show --json's help forsupersededByis corrected to
match, sincenullthere covers three cases and the fact's status separates only the first. -
Three anchor predicates said your fact was wrong about a check they never performed.
contradictedis not a shrug: it tells the user their fact is false and drops it from recall. Three
paths reached it without ever making the comparison.package-versionmatched the prefix of a
compound range, so a manifest declaring"foo": "1.0.0 - 2.0.0"contradicted an anchor asking for
major 2 — a range that plainly admits it — as did"1.0.0 || 2.0.0", while the equivalent
"^1 || ^2"correctly declined; the comparison is now gated on a positive allowlist for a single
simple version, so any range syntax it does not genuinely understand yieldsunverified, which is
what the README already promised.glob-existsdeliberately skips.gitandnode_moduleswhile
walking a wildcard segment, then reportedcontradictedwhen the only thing that would have matched
sat inside one — "no such file" about a file that is right there; a skipped candidate now yields
unverified, andcontradictedis reserved for a walk that completed and found nothing. And
file-newer-than,newest-ofandpackage-versionreturnedcontradictedwhen a comparison
target turned out to be a symlink, which mem refuses to follow: every path predicate now answers
unverifiedthere. The old justification was that those three have no negated counterpart to turn
the verdict into a lie — but the absence of a negated predicate name does not make a negated
verdict honest.file-newer-than a breturningcontradictedasserts "a is not newer than b",
a positive claim about a filesystem mem declined to read.git-trackedalready answered
unverifiedin the same situation, so the policy is now uniform in the direction it already
half-held. Simple versions are unaffected:^1.2.3still contradicts an anchor asking for major 2,
and prerelease and build tags still compare. -
The hook path dropped the column that had just been added to fix the anchor-root defect. The
recall seam keeps its own hand-writtenSELECToverfacts, and it has silently omitted a needed
column three separate times now:prior_status, thenscope_repo(which made every
project-scoped fact invisible from a worktree), and nowcapture_root— so the capture-root fix
below worked frommem recalland not from the hook that runs on every prompt, which is the path that matters
most. The same fact readaffirmedwhen you asked for it andunverifiedwhen mem offered it.
Each omission failed open: no error, no crash, just a different answer on one path, and each was
preceded by a comment in the drifting file asking the next person to keep the list in step.
tests/guards/fact-columns.test.tsnow asserts it instead: every column offactshas to reach
the seam'sSELECTand its row mapper,insertFact,EDITABLE_FACT_FIELDS, and the export
envelope, or be named in an allowlist with a sentence saying why it is deliberately absent. -
mem edit --undorestored an anchor and left it pointed at the wrong tree.captureRootwas
missing from the list of fields an edit snapshots, somem edit --anchorre-pointed the capture
root while undo put back only the old anchor: the original predicate returned aimed at the tree
its replacement had been written for, and the fact could never be affirmed anywhere again —
after a command whose help promises to restore the fields it touched. -
A filesystem anchor returned a decisive verdict about a directory that was not there. Against
a missing root,file-absentaffirmed (nothing is absent from a directory that does not exist)
andfile-existscontradicted, both read off nothing at all, whilegit-trackedalready said
unverified— so the predicates disagreed with each other about the same absence. Reachable by
moving a project, importing a fact from another machine, or handing the hook a stale--root.
Filesystem predicates now yieldunverifiedwhen the root they name is gone. Date-only
predicates are deliberately unaffected:valid-untilreads no path, so a missing root tells it
nothing it needed. -
A path-scoped fact's anchor was judged against whatever directory you happened to be in. An
anchor's target is validated at capture time to sit inside the--rootit was captured under, but
nothing on the fact recorded that root — a path-scoped fact'sscope_rootholds the file it is
bound to, not the tree its anchor describes — so evaluation fell back to the caller's current
directory. Asked from a parent directory, mem printed afile-absentfact as plain ground truth
while the file plainly existed, and listed agit-trackedfact under the heading inviting the user
to delete it while the file was tracked: confidently wrong in both directions on the same query.
mem showhad no root check at all and reported a decisive verdict from any directory on the
machine. This is the case a monorepo hits on every prompt, since the recall hook runs at the
repository root while the facts belong to packages inside it.Facts now record the root they were captured under, and an anchor is evaluated against that root
from the capture directory or any ancestor of it — the two places that demonstrably hold the tree
the predicate describes. Every other directory, and any fact whose capture root is unknown
(captured before this release, or imported from an envelope carrying none), now yields
unverifiedrather than a guess. That is the design's own rule: mem may decline to answer, but it
must never assert the opposite of what it knows.mem editre-points the capture root when it
writes a new anchor, so an anchor is judged against the root it was validated against rather than
the one the fact was first captured in.Global scope is deliberately unchanged: a global fact carries no location binding and its anchor is
meant to be re-checked wherever you currently are, so a preference like "uses pnpm" anchored to a
lockfile keeps answering about the project you are in rather than the one you first said it in. -
A restored store deleted the facts you used most.
mem exportcalls itself a "full-fidelity
JSON envelope" but omittedlast_surfaced_at, while faithfully preservingcaptured_at— and
those are precisely the two signalsmem consolidate --stalereads to decide a fact is dead. So
every fact came back from a backup looking old and never-recalled, and the first--stale --apply
on the destination machine superseded the ones that had been surfacing daily on the source. The
envelope now carrieslast_surfaced_atandprior_status, so a restored store reaches the same
verdict as the store it came from; verified by running the stale pass on both sides of an export.
Envelopes written before this release still import — both fields are optional, and a malformed
value is rejected rather than coerced. Withoutprior_status,mem review --undoon a restored
rejected fact also silently restored it topendinginstead of the status it actually held. -
mem edit --scope projectbound the fact to the wrong repository. The edit path wrote
scope_rootand neverscope_repo, which only the capture path had ever set. Moving a fact from
one repository to another left the first repository's identity attached, and because a fact is in
scope when either its path or its identity matches, the fact went on surfacing in the repository
you moved it away from while staying invisible in the one you moved it to. Rebinding a global fact
to a project had the mirror-image failure: no identity was recorded, so worktrees and second clones
could not see it, unlike an identical fact captured directly. -
Restating a fact from a worktree duplicated it instead of reaffirming it. Reaffirm matched on
the capture-time path and the repository identity, while recall binds on either — so the same
sentence restated from a second clone failed to match itself and inserted a second row, and every
subsequent recall listed it twice, spending the hook path's line budget twice on one fact. Reaffirm
now uses the same binding recall does. Contradiction bucketing is untouched: it keys on
scope_rootalone by an explicit, separately documented decision. -
mem edit --textleft the old text's embedding attached, andmem embedcould not repair it.
Editing a fact's text re-extracted its search terms in the same transaction but kept the vector
built from the text that no longer existed, so with embeddings configured, recall fused a dense
rank computed from deleted words. The backfill selects on a null embedding and therefore reported
nothing to do; onlymem embed --all, documented as the model-migration path, would have fixed it.
Changing the text now clears the vector, so the ordinary backfill picks it up. -
mem consolidate --stale --applydeleted facts on the strength of a false claim. Only the
hook path ever recorded that a fact had been surfaced, so a fact recalled twenty times by plain
mem recallstill hadlast_surfaced_atunset and norecall_logrow — and--stalereads
exactly those two signals to decide a fact is dead. It reported "never surfaced by recall, never
marked used" about a fact recall had just returned, and--applysuperseded it; the next recall
said "no matching facts". Both recall shapes now stamplast_surfaced_at, including
--hint-format --stable, which previously suppressed the stamp along with the session log.
Reproduced end to end against the shipping bundle before the fix and after. The stamp is
best-effort: a write failure warns and never fails the recall. -
mem scan-sessionreported "no new durable statements found" over a broken store. Its
per-candidatecatchswallowed every error, not just the two that mean "this candidate is not
worth storing" (validation and secret screening). ASqliteError, a full disk, or a read-only
store produced a clean exit 0 and a message saying the session held nothing worth keeping — when
in truth nothing could be kept at all. The two screening rejections are still swallowed by design;
everything else now surfaces and exits non-zero. -
mem scan-session --scope pathsilently stored facts bound to a directory.pathscope binds
a fact to a single file, and a transcript scan has no file to bind to, so the flag was accepted and
quietly produced a binding the user did not ask for. It is now rejected with the command that does
work, and the help text offers only the two scopes that mean something here. -
mem doctortold users to delete facts that still worked. Its scope-placement check called a
fact unreachable whenever its capture-time directory was gone, even when the fact was still
reachable from every checkout by repository identity — the casemem recallhandles correctly.
Relocated and genuinely-unreachable facts are now reported separately, and only the latter suggests
mem forget. Confirmed by recalling such a fact from a clone after deleting the original root. -
The installed Codex and Copilot instructions prescribed a command that could not run. The
sharedAGENTS.mdblock told the agent to runmem used <id>... --session-id <session>"passing
the same session id you recalled under", but its own recall command supplies no session id and
those tools have no hook mechanism that would — so the call always failed. The bullet is gone from
the installer and the three integration docs, and a test now fails if the block ever promises
mem usedwithout a session id to reuse. -
Corrected two comments that described the code doing something it does not:
MAX_SCANNED_TURNS
caps how many transcript turns are screened, not how much of the file is read (the whole file is
read and parsed first), and therecall_logretention window has two live readers rather than the
zero its comment claimed. -
The hook path returned no memory at all from a git worktree or second clone.
mem recall
surfaced a project fact from any checkout of the same repository, butmem recall --hint-format
returned an empty header — and that is the pathmem init claude-codeinstalls forSessionStart
andUserPromptSubmit, so it is the one that runs unprompted. The seam's hand-written SELECT
omittedscope_repo, leaving the identity check to compareundefinedand always fail. It failed
open and silently, against a README that promises the fact surfaces "from a git worktree". The same
SELECT had already been fixed once for exactly this reason on a different column; its comment now
states the general rule instead of the one instance. -
mem remembersaid "reaffirmed" while discarding the--anchoryou just gave it. Restating a
fact you had already stored refreshed its clock but dropped any anchor or source reference the
restatement carried, so a fact you had just made re-verifiable stayed caveated forever and could
never reachcontradictedwhen the world changed. The latest statement now wins for those fields,
as it already did for the timestamp, and the CLI says which ones it applied. Omitting a field still
leaves the stored value alone rather than clearing it. -
mem reviewrun from an unrelated directory reported perfectly valid facts as contradicted. It
evaluated every fact's anchor against whatever root it happened to be run from, so a path-scoped
fact bound elsewhere had its anchored file looked for in the wrong tree, not found, and listed under
the heading that invites the user to forget it — whilemem recallfrom the fact's own root
affirmed the same fact. Anchors are now evaluated only for facts actually bound to the root being
asked about. (Recalling a path-scoped fact from a directory above its binding is a related gap
that needs a persisted capture root; it is not addressed here.) -
file-not-containsthrough a symlink asserted the substring was present. mem deliberately
refuses to read through a symlink that escapes the root — but then returnedcontradicted, which
for the negated predicate means "the substring is there", a claim it had no basis for because it
never opened the file. Both forms now returnunverified, matching thefile-exists/file-absent
precedent the file's own header already argued for. The security behaviour is unchanged: the file
is still never read, and the fact is still withheld from ground truth. -
README claimed the
TGMEM/2seam had no consumer, and CLAUDE.md claimed token-goat was one.
Both were wrong in opposite directions: token-goat reads onlymem epoch, while mem's own installed
Claude Code hooks consume the recall seam on every session and every prompt. README also carried a
literal U+FFFD replacement character in that sentence. The guard that exists to catch this class was
scanning neitherAGENTS.mdnorCLAUDE.md, and its pattern missed the phrasing CLAUDE.md used; it
now covers both files and allows for intervening words. -
Removed
isGroundTruthStatus, an exported function with no callers; the constant behind it is used
directly and already carries the distinction its comment described. -
Six shipped claims said mem made no network calls while
mem recallwas sending every query
to a third party. README, AGENTS.md,mem doctor, and themem dreamerror text all described
mem dreamas the only path that leaves the machine — one README paragraph contradicted itself in
consecutive sentences. WithTOKEN_GOAT_MEM_EMBED_URLset, semantic recall embeds the query, and
becausemem init claude-codeinstalls aUserPromptSubmithook that feeds each prompt in as that
query, every prompt typed at the coding tool was being POSTed to the configured endpoint. The
behaviour is legitimate and opt-in; describing it as impossible was not. Every one of those claims
now states what actually leaves the machine, andmem doctor'sembeddings:line discloses it the
way itsdreaming:line already did. -
A query containing a secret was sent to the embeddings endpoint verbatim. Capture has always
refused to store text that trips secret screening, but the recall path applied no such check to the
query — so a pasted key reached the endpoint even though the same string could not have been stored.
A query that trips screening now skips dense ranking entirely; BM25 still ranks it and results still
come back, so the only thing lost is the outbound call. -
A fact could report freshness read off a checkout the user was not in. A project-scoped fact is
in scope for any worktree or second clone of the same repository, but its anchor was still evaluated
against the directory it was captured in. Asking from a worktree that had deleted the anchored file
returnedaffirmedfrom the original tree. The anchor is now evaluated against the querying root
whenever repository identity is what put the fact in scope. -
mem importrefused the cross-machine restore the docs promise. Rejecting any fact whose
recordedscopeRootfell outside--rootclosed a real hole — an imported row could otherwise
name an arbitrary directory and have anchors evaluated there — but it also rejected every fact in a
legitimate export moved to another machine, where the recorded path does not exist and no--root
could satisfy it. A project fact whosescopeRepoidentifies the repository--rootis a checkout
of is now accepted with itsscopeRootrebound to that root. Everything else stays rejected. -
mem embedsent the text of facts the user had deleted. The query behind it filtered on
missing embeddings but never on status, and--allfiltered on nothing at all, so a fact retired
bymem forgetormem review --rejecthad its text re-sent to the embeddings endpoint on every
run. Superseded facts are now excluded from both. -
mem recall --rootwas documented as affecting freshness only. It also decides which project's
facts are returned at all; the help text now says both. -
mem uninstall claude-codecould delete configuration a user wrote themselves. Install
createshooksandhooks.<event>when asettings.jsonhas neither, so uninstall pruned the
containers its own removals had emptied. But an event array that is empty after mem's stamped
entries are removed is indistinguishable from one the user wrote empty in the first place, and a
hand-authored"hooks": {"SessionStart": []}was silently deleted along with the now-empty
hooksobject around it. Uninstall now reads the one-time.token-goat-mem.baksnapshot taken
before mem's first write to answer the question emptiness cannot: did this key exist beforehand.
A missing snapshot indicates mem created the file; an unparseable one is treated as "everything
pre-existed", so pruning fails closed rather than deleting content mem cannot prove it owns. -
mem initandmem uninstallrejected anysettings.jsoncontaining a comment or a trailing
comma. Claude Code's own settings file commonly carries both, and this file already depends on
jsonc-parserto preserve exactly that formatting on the VS Code path -- the Claude Code path was
the one still going through a strictJSON.parse, so it refused to modify the configurations it
most needed to handle. Both paths now share the JSONC parser. -
mem uninstallleft a duplicated marker block behind while reporting success. ACLAUDE.md
orAGENTS.mdthat ended up with two complete back-to-back blocks for one tool -- the realistic
outcome of merging two branches that each ranmem init-- had only its first block stripped.
Removal now loops until no resolvable pair remains. -
A transient
.git/indexread error withheld a correctly-tracked fact. Both catch blocks in
the git-index reader returned "index parsed, definitively empty" for any stat or read failure,
so a Windows file lock from a git GUI, antivirus, or a concurrent git command madegit-tracked
reportcontradicted-- the opposite of the truth -- and the fact was dropped from ground truth.
OnlyENOENTnow means empty; every other error means unverified, matchingpackage-version. -
mem show --jsonreported no successor for a superseded fact that had one. The lookup read
the last audit row for the fact, andmem usedappends one, so recording a use erased the
supersession link from the payload. It now finds the superseding row specifically. -
Two concurrent
mem remembercalls of the same sentence both inserted. The
"have I already stored this" read sat outside the transaction that reaffirms, so both callers saw
nothing to reaffirm -- producing exactly the duplicate the reaffirm path exists to prevent. The
read now happens inside the immediate transaction that acts on it. -
mem facets --backfillwas declared, never read, and absent from the mutual-exclusion guard,
so--backfill --allsilently ran a full re-extract while the ignored flag looked honoured. The
guard and its error message now name all four modes. -
mem import --from-mdhad no file size cap while the JSON path capped at 50 MB. Both now
share the same constant.
Security
mem import --from-jsonaccepted ascopeRootpointing anywhere on disk. The field was
copied verbatim after a bare non-empty-string check -- the validation error text claimed to
expect an absolute path without ever verifying one -- and for aproject-scoped fact it becomes
the root that anchor predicates are evaluated against. A crafted import file could therefore turn
every latermem recallinto a file-existence and content-substring oracle for a directory
outside--root.scopeRootis now required to be absolute and contained by the import root,
reusing the same containment check every anchor argument already goes through; a row that fails
is skipped like any other invalid row rather than aborting the file.--pathwas documented as "resolved against--root" and was not. It used a bare
path.resolve, so--path "../../other-repo"bound a fact to a tree outside the root it was
captured under. All capture paths now enforce containment.SecretDetectedErrorcarried the raw credential. The message was redacted, but the thrown
object retained the full literal, so any caller logging or serializing it echoed the secret back.
The error now carries only the pattern name, field, redacted preview, and length.mem editcopied the prior field value into the audit log without re-screening it, making a
second at-rest copy of anything that got past capture screening. Prior values are now screened
and redacted on the way in. Because the audit row is also what--undorestores from, undoing an
edit whose prior value was redacted now refuses and says so, rather than restoring the redaction
marker in as the fact's text and reporting success.- AWS STS session key ids (
ASIA...) were stored verbatim -- onlyAKIAwas matched, and at 20
characters they sit permanently below the generic entropy floor, so no fallback layer caught them. - The OpenAI key pattern could not match the current
sk-proj-format, stopping four characters
in because-was absent from its character class. password-assignmentmatched inside longer words, flaggingnotpassword=andmypwd=as
credentials. A screener that refuses ordinary text trains users into blanket allowlists, which is
its own security failure.
Changed
- The
mem recall --hint-formatfooter now saysN more in scope, not sentrather than
N more matched, not sent.retrieve()ranks the scoped pool, it does not filter it, so the
count includes facts that never matched the query -- the old wording asserted something untrue. - The transaction-mode guard now catches an inline
db.transaction(...)()invocation. Its regex
keyed on atx-named variable, so the one call site that did not follow that convention was
invisible to it and ran in deferred mode while the guard reported zero offenders. CLAUDE.mdno longer claims there is no CI workflow, andmem dreamno longer claims to be the
only command that sends fact text off the machine (mem embeddoes too).
Fixed
-
mem recall --hint-formatcould not tell "nothing to say" from "held things back". A
response carrying two of six matching decisions was byte-identical to one carrying all six, and a
response for a project with three facts sitting in the review queue was byte-identical -- a bare
TGMEM/2header -- to a response for a project with no memory at all. Both absences are
unfalsifiable from the wire, so a consumer could not distinguish a complete answer from a truncated
one, and a queue could fill indefinitely behind a payload that looked like an empty store. The
single footer line now carries only the clauses that apply:mem show <id> for detailwhen a fact
line was emitted,N more in scope, not sentwhen the per-kind caps dropped results, and
N withheld; mem review to resolve contested/pendingwhen facts were held back. A response with
nothing to follow up on has no footer at all, where before it always carried the same fixed
sentence -- including the review call-to-action, on a store with nothing to review. The withheld
count is store-scoped rather than query-scoped, becauseretrieve()ranks the whole scoped
candidate pool rather than filtering it; that is the honest number for the question the clause
answers ("is something waiting for you"), and the surrounding doc comment says so rather than
leaving a reader to infer it. Footer text is deliberately outside the protocol's version-bump set:
bumping would cost every consumer all hints to protect a line consumers are told not to parse. -
mem edittruncated the one value it exists to preserve. The auditdetailline previewed
both sides of each change at 120 characters, so editing a 223-character fact left 103 characters
recorded nowhere in the store -- while AGENTS.md promised "an edited fact's previous text is
recorded there and nowhere else". The preview is now asymmetric: the prior value is recorded whole,
the new one is still previewed, because the new value is never lost (it is the fact's current text,
one column away in the same row). Building on that,mem edit <id> --undoreverses the most recent
edit --mem review --undo's pattern applied to a command that had none, where walking an edit back
previously meant hand-editing SQLite. The reversal payload is a new nullableaudit_log.prior_json
column scoped to the fields that edit actually touched, so undoing a--textedit cannot clobber a
--scopenobody asked to change back, and an edit row written before the column existed refuses
cleanly instead of restoring nothing and reporting success. -
mem edittreated a fact you typed yourself exactly like one mem inferred. Promotion into
ground truth was already gated hard --captureSuggestedcaps a derived fact's confidence, and only
mem review --promoteactivates a pending one -- but mutation of an already-active fact was not.
Editing asource_type=userfact now requires--force, and the override is recorded in the audit
log. It is a per-invocation override rather than a storedread_onlycolumn, which would be a
second axis of a distinctionsource_typealready makes. The tradeoff is stated rather than hidden:
a derived fact has no equivalent protection through this guard, and there is currently no way to ask
for one. -
Plain
mem recalltold every user to runmem review, on every recall. The trailing line was a
fixed string, so a store with nothing pending, contested, or contradicted still advertised a command
with nothing to do -- the shape of call-to-action that teaches a reader to ignore the line. It now
namesmem reviewonly when a result in that recall is actually withheld. -
mem scan-sessionrecorded a turn number that was wrong and that moved.turnIndexwas
numbered from the start of the scan window rather than the start of the transcript, and the window
is the lastMAX_SCANNED_TURNSturns. So a statement at true turn 249 was filed with
source_ref<transcript>#turn199. Worse than wrong: unstable. The same sentence reported a
different turn on every scan as the transcript grew past the cap, so a reviewer who checked a
pending suggestion's provenance twice got two answers and neither located the sentence. That
pointer is the only thing tying a suggestion back to what was actually said, which is what a
reviewer needs in order to resolve it.scanTranscriptnow offsets the window's indices back to
transcript coordinates. -
mem scan-sessionmissed durable statements that opened with a filler word. Measured against
seven ordinary phrasings of a preference, the^-anchored opener table matched exactly one. The
anchors are right -- they are what keeps "I never got that to work" and "the linter always crashes
on this file" out of the review queue -- but they also rejected "Please always run the linter" and
"So never force-push to master", which say precisely what the anchored form says. Relaxing the
anchor to a substring search would have recovered those and reinstated every false positive the
anchor exists to prevent, so instead a closed list of discourse openers (please,also,so,
ok,note that) may now precede a trigger: the trigger still has to be the very next thing
said, and only those specific words may come before it. Two phrasings that had no trigger at all
were added alongside --rule:andwe should/let'sfollowed byalways/never. The skip
applies to matching only; the stored text stays the whole sentence, filler included, because a
fact whose text was silently edited is a fact the user never said. Same transcript, same
candidates, still no model: seven of seven where it had been one of seven. -
mem recallranked a fact naming an identifier below facts that merely shared its stems.
BM25 reducessrc/retrieval.tstosrc/retriev/ts, so against a three-fact store the fact
that actually named the file came back last, behind two that used those three words in a
sentence. The entity layer already knew the difference --mem facets --list-entitiesextracts
src/retrieval.tsas a single entity on exactly the right fact, andmem recall --entityreturned
it alone -- but nothing consulted that layer unless the caller passed--entity, which requires
already knowing the answer. Recall now extracts entities from the query text with the same
extractor that wrote them at capture time, and fuses an entity-overlap rank list alongside BM25,
usefulness, and embeddings.It is a vote, not an override: a fact that merely carries the identifier does not displace one that
carries it and matches the rest of the query. The list is empty whenever the query names no
identifier, so it cannot vote on queries it has no signal for -- the rule the zero-score BM25 guard
inretrieval.tsalready encodes, after a rank list with no signal was found outvoting one with
signal. Cost is one indexedfact_termslookup per identifier in the query and nothing at all for
a query with none, deliberately not the full-table scan--entitypays for, since this runs on
every recall including the ~150 ms--hint-formatbudget. That agent-facing path is where it
matters most: an agent gets one shot at the context it is handed and never sees what ranked below
the cap, so a mis-ranked identifier is not a worse ordering but a fact the agent never learns.
Added
-
A repeated statement counts as evidence instead of being discarded.
mem scan-sessionskipped
any candidate whose text the store already knew, andmem import --from-mdreported the same skip
-- correct for a fact already settled, but wasteful when the match is stillpending. A preference
the user had restated in four separate sessions sat in the review queue indistinguishable from one
said once, and the evidence that would have told them apart was thrown away at the moment it was
observed. A repeat now records a sighting: one more screened source excerpt and a counter on the
fact, written in the same transaction.mem reviewsorts the pending bucket by it, so the thing
said most often is the thing asked about first. A sighting is evidence for a human, never a
mechanism: it does not promote, does not change status, and does not reach the ground-truth gate.
The rule that a pending fact never auto-promotes -- not on time, not on repetition, not on
confidence -- is unchanged and absolute. Double counting is prevented by excerpt equality rather
than a transcript reference, becausesourcesrecords no locator: the same transcript scanned at
bothStopandPreCompactyields a byte-identical excerpt and counts once, while a genuine
restatement arrives surrounded by different context and counts again. An excerpt that screens
positive for a secret records nothing at all -- under-counting is the safe direction, and there is
no excerpt left to compare against. -
mem reviewnames the fact a pending correction may contradict. A correction filed by the
scanner or bymem suggestcarried no link to whatever it corrects, so promoting it left both
claims live unless the user supplied--subjectand--valueby hand -- and nothing on screen said
that was needed. Each pending correction, and any pending fact that does carry a subject, now prints
the single live fact sharing the most entity and topic terms with it, labelled as one it may
contradict. It reuses the same computationmem show --relatedalready runs, so there is one notion
of relatedness in the tool rather than two. Term overlap establishes that two facts are about the
same thing, never that one negates the other, so this is a label and only a label: it supersedes
nothing, promotes nothing, and changes no status. Only the best match prints, on the same reasoning
the unanchored bucket offers only its first viable anchor -- a review queue is a queue, not a menu. -
mem consolidatecan see across scopes. Its comparability rule puts global and project facts in
structurally disjoint groups, which is right for near-duplicates -- the two surface in different
places, so neither is redundant given the other -- but it also meant a project fact repeating a
global one word for word was never compared to it. Both bind on recall to that root and there is no
text-collision suppression anywhere in retrieval, so the pair double-surfaced on every query and
double-spent the hint budget. A separate exact-text pass now runs beside the near-duplicate
clustering, leaving that clustering and its reasoning untouched: relaxing the shared key would have
let unrelated same-kind facts merge across every project. The global fact always survives, because
widening a project fact's scope is a decisionmem edit --scope globalmakes explicitly and not one
this pass should invent.--cross-projectreports the same shape across two or more projects and
prints that command ready to paste, including the--forcea user-stated fact requires; it is
report-only and has no--apply, because which scope a fact belongs in is the user's judgement.
Matching requires subject and value to agree as well as text: two facts can be worded identically
and mean different things per scope -- adefault_branchofmainglobally andmasterin one
repository is an override, and collapsing it would destroy the override rather than a duplicate. -
The
sourcestable is fed. Its schema, storage API,mem show --jsonsurfacing and gc pruning
have all existed and been tested since they were added, against zero rows: no capture path ever
wrote one, sosources: []meant "mem records no sources at all", and a guard test existed only to
keep that admission honest. Two paths now write a source row in the same transaction as the fact
they explain --mem scan-session, whose excerpt is the user turn the statement was lifted out of,
andmem import --from-md, whose excerpt is<path>:<line>: <raw bullet>. Both are cases where the
raw material is genuinely larger than the fact, so the row answers a question the fact cannot:
where did this come from, and did mem read it right.mem rememberandmem suggest <text>still
write nothing, because there the caller's text is the fact and a source row would echo it back.
Excerpts are truncated to 600 characters and secret-screened before storage; a screened-positive
excerpt is dropped and the fact is still captured, since refusing to store provenance is not a
reason to lose the knowledge. Never the full source content. -
mem reviewshows where a pending fact came from. The queue asked for a promote/reject decision
while showing only mem's own paraphrase, which is the one thing a reviewer cannot check the
paraphrase against. Each pending entry now carries its newest source excerpt, when one exists, under
the fact. Facts captured before sources were fed, and those from paths that write none, print as
they did -- the line is omitted rather than filled with a placeholder. -
mem reviewprints a paste-readymem edit --anchorcommand for facts it can already verify.
Theunanchoredbucket named the problem and left the fix as an exercise: an anchorless fact is
caveated asunverifiedforever, and closing that needs a predicate the user has to compose by
hand. When a fact's text mentions a path that resolves inside its own root andfile-exists
against it readsaffirmedright now, the exact command is printed. A suggestion that would read
contradictedorunverifiedis withheld -- teaching the user the feature is broken is worse than
saying nothing -- and only the first viable candidate is offered, so the bucket stays a queue rather
than a menu. The command carries--forcefor a user-stated fact, becausemem editrefuses one
without it and most of a real store is user-stated: the guard exists to stop an agent rewriting a
user's own words unasked, and this is the user pasting it themselves with the text untouched. -
mem export --format mdrenders the store as a shareable, git-reviewable markdown document.
mem import --from-mdalready read that shape from hand-written notes; nothing produced it. It is
explicitly not a backup, and says so in its own--help: a markdown round trip preserves the fact
text and nothing else -- id, status, confidence, anchor, subject and value are all lost, and
every bullet lands backpending.--format jsonremains the full-fidelity path and remains the
default, so the lossy surface is one a user has to ask for by name. -
mem show --relatedlists the facts sharing the most entity and topic terms with the one being
shown, entity matches weighted above topic matches. The store already indexed those terms for
retrieval andmem consolidatealready compared them for near-duplicates; nothing let a person
walk sideways from one fact to its neighbours. The target itself and superseded facts are excluded,
results are scope-contained to--root, and it is an association aid rather than a ground-truth
channel: pending and contested neighbours do appear, still labelled as such. -
mem init copilot-visual-studioandmem init copilot-jetbrains. Coverage of the Copilot hosts
turned on one file rather than one config per IDE. Copilot CLI, Copilot chat in VS Code and Codex
all readAGENTS.md, which existing writers already produce. Visual Studio does not read it at
all, and JetBrains reads it only for the cloud agent -- local IntelliJ chat reads
.github/copilot-instructions.md. Both new targets write that one shared path, through the same
reference-counted markers, atomic temp-file-and-rename and one-time backup as every other target,
so installing both and removing one leaves the other's block intact. Each ships its own integration
doc, and the docs guard now derives the set of per-tool docs it demands from the tool list itself,
so the next target cannot be added without one. -
mem dreamreports what a configured model thinks follows from several stored facts taken
together -- the one kind of consolidationmem consolidatestructurally cannot do, since Jaccard
over topic terms can tell that two facts restate each other but never that a third thing follows
from both. It is an evaluation surface and nothing more: it writes nothing, there is no--apply,
and the output is a report whose worthwhile lines are kept by typingmem remember. Off unless
TOKEN_GOAT_MEM_DREAM_URLandTOKEN_GOAT_MEM_DREAM_MODELare set, matching howmem embed
already treats an optional model endpoint. It is the only command in the tool that sends fact text
off the machine, so it says so in its own--help, in the error it prints when unconfigured, and
in both docs. Onlyactiveandpinnedfacts are sent: a superseded fact is one the store has
already decided is wrong, and an inference resting on it would carry the store's authority behind
a retracted premise. The endpoint's reply is treated as untrusted input rather than an answer --
every candidate must cite at least two facts that were actually sent, by an index that resolves,
and must not restate a fact already stored; one that fails any check is dropped rather than
printed, so everyfrom:id is onemem showopens. A malformed element is dropped alone rather
than failing the run.mem doctorgained adreaming:line mirroring itsembeddings:one,
because the configuration lives in environment variables and a URL exported once in a shell
profile is otherwise invisible to the person whose facts would be sent; it prints the endpoint
host and never the URL or key, since doctor output is what users paste into an issue. Errors name
the endpoint host and never its URL or key. No--root:
dreaming reasons over the whole live store, and a flag that read as scoping while scoping nothing
would repeat the sharpest edge onmem recall. -
mem review --undo <id>reverses a--reject. Review is a two-key decision made one key at a
time, and reject was the only irreversible one: it marks the factsuperseded, and--promote
refuses anything that is notpendingorcontested, so a mistyped id could not be walked back
through the CLI at all -- only by hand-editing the database or round-tripping amem export. A
review queue whose reject key is unrecoverable is one users are right to hesitate over, which
defeats the queue. The fact returns to the status it actually had, so a rejectedcontestedfact
comes backcontestedrather than being quietly upgraded. Scoped to rejections by name:mem forgetis a considered decision about a fact the user chose to keep, and reversing that is a
different question, so a fact superseded any other way is refused with the mechanism that claimed
it. -
A pinned fact could fall off the recall it was pinned for. With no query -- the shape of the
SessionStarthookmem initinstalls -- every BM25 score ties at zero, the sort falls through to
recency, and the default cap of 20 keeps the newest facts. A pinned fact behind 20 newer ones
silently vanished from the one call the user pinned it for; onlywithheldresults were ever
cap-exempt. Pinned facts now sort first, but only in that zero-signal case: under a real query
relevance still decides, since a pin that also won there would be a ranking cheat code and an
irrelevant pinned fact would displace the one that answers the question. -
Restating a fact reaffirms it instead of duplicating it.
mem rememberhad no dedup, so saying
the same thing twice wrote a second row and left the first one's decay clock running -- the facts a
user cared enough to repeat were exactly the ones drifting below the ground-truth floor, and recall
showed one sentence twice at two confidences. A match now refreshescaptured_atand confidence
and printsreaffirmed. Matching is deterministic and conservative: same normalized text (case
folded, whitespace collapsed, one trailing period dropped), same kind, same scope binding (not
just the scope label), same subject and value -- identical text carrying a different value is a
correction for contradiction resolution to key on, never a repeat to swallow. Onlyactiveand
pinnedfacts are candidates: reaffirming apendingone would promote it without review, and a
supersededone must not be resurrected by a matching sentence.mem suggestnever reaffirms at
all -- its candidates come from file and transcript content, and letting derived text refresh a
user-stated fact's clock would hand aCLAUDE.mdthe power to keep alive a fact nobody restated. -
valid-until <ISO date>anchor predicate, for a fact that is true until a date rather than
until a file changes ("until the v2 migration lands, keep the shim"). Such facts previously had no
anchor available and stayed permanentlyunverified-- caveated forever and never surfaced in
mem reviewas something to resolve. The only predicate that reads no filesystem or git state. A
bareYYYY-MM-DDis read as the end of that day rather than its midnight start, so
valid-until 2026-12-31is still affirmed during the 31st. An unparseable date isunverified
rather thancontradicted-- a typo must never read as "this fact expired" and suppress a true
fact -- and is rejected outright at capture, since anchors.ts would otherwise accept the typo and
caveat the fact forever. -
mem showprints the fact's audit history, andmem editrecords what each field said before.
The audit log had recorded every capture, edit, pin, and status change since the first release and
nothing could read it back: the trail that exists so this tool's output can be trusted was
write-only. It matters most formem edit, which overwrites text in place -- the previous wording
survived nowhere in the store, and the audit row said only which field names changed. Values are
previewed rather than stored whole, so editing a long fact cannot turn one audit row into a second
copy of the store. Deliberately not a version chain: that is a schema migration and a retention
policy bought for a question the audit log can already answer. -
Capture had no path that did not depend on an agent volunteering it -- both hooks
mem init claude-codeinstalled (SessionStart,UserPromptSubmit) are recall paths, so unless the agent obeyed theCLAUDE.mdinstruction block or the user typedmem rememberby hand, a session ended with everything it established forgotten. That made capture the weakest link in a tool whose entire purpose is not forgetting.mem scan-sessioncloses it, wired as a third hook onStop-- the only event that fires after the user has actually spoken, and the only one whose envelope carriestranscript_path. It matches sentences against a fixed table of durable-statement openers (remember that,from now on,always/never,don't,we decided,decision:) and files each match as pending. No model is involved, so the same transcript always yields the same candidates; nothing it produces can be recalled untilmem review --promoteresolves it, and the dedup matches on fact text so a rejected suggestion is never re-filed by a later scan. Takes--transcript <path>as a manual/testing entry point and--quiet(the shapemem initinstalls, since aStophook's stdout lands in the session it just read).Only the human's own text is scanned, which is narrower than it sounds. A transcript stores tool results,
<system-reminder>injections, slash-command payloads and their stdout, relayed subagent reports, and compaction summaries of the assistant's own prior output -- all under the user role, and all but the first as ordinarytextblocks. Treating any of them as speech would let a file mem reads dictate what mem remembers. Each is rejected: tool-result blocks andtoolUseResultenvelopes,isMeta/isCompactSummary/isVisibleInTranscriptOnlyentries, entries whoseorigin.kindis present and nothuman, and blocks carrying a<command-name>/<command-message>/<local-command-stdout>/<task-notification>wrapper;<system-reminder>spans are stripped out of otherwise genuine turns rather than discarding the turn. Both content shapes (bare string and block array) go through one sanitizer -- an earlier revision returned string content unfiltered, which exempted it from every check above.Dogfooded against a real 33 MB session transcript at each step: 28 proposed facts before these channels were excluded, 0 after, with every one of the 28 traced to a channel rather than to the user. Five separate guards, each independently revert-proved.
-
A project-scoped fact was bound to the absolute path it happened to be captured at, so it
vanished from a second clone of the same repository, from every git worktree (a different root by
construction), and from anmem export/mem importonto another machine -- the three cases a
memory tool exists to cover. A newscope_repocolumn records
<normalized git remote>#<root relative to the working tree>alongside the path, and recall
matches a fact whose path binding or identity binding holds.Identity is the remote plus the subpath on purpose: a monorepo has one remote and many project
roots, so remote-only identity would leakpackages/a's decisions intopackages/b. The remote is
normalized sogit@github.com:acme/widget.git,https://github.com/acme/widget, and
ssh://git@github.com/acme/widget.gitproduce one string; several remotes with nooriginis
genuinely ambiguous and yields no identity rather than a guess. Nothing shells out --gitneed
not be installed, and only.git's own files are read (including thecommondirindirection,
without which every worktree reads as remote-less). Identity only ever widens: the path comparison
runs first and is unchanged, and a fact with no identity is matched by path exactly as before.
TOKEN_GOAT_MEM_PROJECT_IDENTITY=pathrestores the path-only binding at both capture and recall,
for two clones that are deliberately not the same project.Contradiction bucketing deliberately still keys on
scope_root: that key decides the persisted
superseded/contestedtransitions, no honest backfill exists for facts captured before this
column, and widening it would rewrite facts across checkouts on the firstmem epoch --gcafter
upgrade. The cost, stated rather than hidden: two facts on one subject captured in two clones are
both in scope and are not detected as rivals. -
mem import --from-md --captured-at <iso>back-dates a whole import run instead of stamping
everything with the moment of the import. ACLAUDE.mdfull of two-year-old conventions imported
today otherwise reads as the newest thing in the store, andcaptured_atdrives both time-decay
and contradiction precedence, so those bullets outranked facts the user actually stated recently.
There is no automatic default because no honest one exists: mtime is reset to checkout time by
git clone, restored backups carry arbitrary ones, and the real answer is the file's last commit
date -- which this codebase deliberately does not shell out to git for. Hence the one-liner in
--help:--captured-at "$(git log -1 --format=%aI -- CLAUDE.md)". A malformed or future value is
rejected once at the CLI boundary with exit 1, rather than being reported once per candidate while
the command still exits 0.