Skip to content

v0.4.1

Choose a tag to compare

@github-actions github-actions released this 16 Sep 14:22
· 127 commits to master since this release

Fixed

  • A store that could not be opened was reported as a project with no memory. buildHintFormat
    wrapped the whole of buildHintFormatUnsafe in one catch, and openStorage is called inside it, so
    a permissions error, a WAL lock or a schema mismatch produced bytes identical to an empty store: the
    bare TGMEM/2 header 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
    at mem 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-session dated every fact from the moment of the scan. The scanner read a transcript
    entry's text and never its timestamp, and the capture call omitted capturedAt entirely -- so a
    statement made in March and scanned in September was stored as having been said in September. Under
    the Stop and PreCompact hooks 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 from captured_at, the
    --stale cutoff is measured from it, recall breaks recency ties on it, and the review queue is
    ordered by it. mem suggest and mem import --from-md already 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's stored_at still
    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 for mem scan-session the 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 a sources row that did not contain the fact at all, and mem 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 same normalizeFactText the 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 --delta suppresses what was already sent, but a
    plain mem recall with a query and --hint-format is 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": matchedQuery is 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 export recorded that a fact
    was superseded but not what superseded it, so after a round trip mem show printed superseded_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 used shipped, recall_log
    carried a used_at column 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 --stale gated 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-run mem 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 bumping TGMEM/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,
    --stable no longer suppresses recall_log writes. It reorders output for reproducible tests,
    which is no reason for the store to forget what it showed.

  • mem init never upgraded an install that was already there. It wrote its marker, saw a mem hook
    already in settings.json and 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-running mem init after upgrading mem
    did nothing to fix it. On this machine a July install carried only SessionStart, without
    --hook-stdin: no recall_log row had ever been written and mem scan-session had never run once,
    a dead feature that read as a code defect rather than an install-age symptom. mem init now
    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 mentions mem does 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 --stale asked a lifetime question of evidence that rotates. The gate read
    "never surfaced by recall", over a recall_log that mem epoch --gc prunes 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 --help and 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-md re-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 by mem scan-session, or typed with
    mem remember, would be filed again as a fresh pending candidate the moment it also appeared in a
    markdown note -- so the two capture paths disagreed about what the store knew, and mem review got
    a question already answered. Import now also checks the store-wide text index, scope-gated by the
    same isBoundToRoot rule recall and scan-session use, so an identical fact bound to an unrelated
    project cannot suppress a candidate here. Such a bullet is reported as skipped (already known),
    distinct from skipped (duplicate), because "you imported this file before" and "mem learned this
    elsewhere" are different things to tell someone.

  • mem init --user quietly configured a project instead. For a tool whose only target is a file
    in the repository -- codex, copilot-cli -- --user was accepted and ignored, and mem's block
    landed in the working directory's AGENTS.md. A user asking to set up their own machine got an
    edit to a repository they may not own, discovered later by git status. Those tools now exit with
    "has no user-level config; omit --user" and write nothing. mem init copilot-vscode --user had the
    same shape with a real answer available: VS Code's keybindings live in the editor's own user
    directory, so --user now installs those alone and leaves the project's tasks.json and
    AGENTS.md untouched.

  • mem import --from-json --dry-run promised imports the real run would refuse. The plan skipped
    both the --root scope binding and secret screening, so a fact whose scopeRoot falls outside the
    import root, or whose text trips the screener, was reported as would-import and 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-session and mem import --from-md
    all extract text and no subject, which makes this the ordinary promotion rather than an exotic one,
    and promoted <id> said nothing about it. mem review --promote now prints the consequence and the
    one command that undoes it, mem edit <id> --subject <key> --value <value>. A keyed promotion says
    nothing extra.

  • mem review named 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 persisted active. mem review listed them as contested and said "resolve with
    --promote/--reject"; mem recall withheld 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 unrelated mem epoch --gc happened to persist the detection. --promote and
    --reject now ask the same question mem review asks, 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 suggest files a
    candidate as pending; stating the same sentence with mem remember wrote a second row and left
    the suggestion queued, so mem review kept asking a question the user had already answered -- and
    the obvious response, --promote, then produced two identical active facts that mem 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 a pending fact never promotes on its own is unchanged: passive
    signals (time, repetition, confidence) still cannot promote one, mem suggest and mem scan-session still cannot, and this path requires the user to state the fact themselves.

  • A UTF-8 BOM made mem init and mem uninstall refuse, and blame the user for it. A
    settings.json (or VS Code tasks.json / keybindings.json) beginning with a byte-order mark --
    what Windows editors and PowerShell's default Out-File/Set-Content write, 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-session threw 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
    as superseded, so A's decision about A suppressed the suggestion in B permanently. The check
    now narrows to facts bound to the scanning root via isBoundToRoot -- 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 SQL LOWER()
    (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 --transcript exited 0 and reported success. mem scan-session --transcript /typo/path.jsonl printed "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/PreCompact scan that cannot find its transcript still fails open and silently, because
    it is background convenience and must never interrupt a session.

  • mem uninstall left husks of its own making, and two paths read the same snapshot by different
    rules.
    Init takes a one-time <file>.token-goat-mem.bak before its first write, and that snapshot
    is the only evidence of what existed beforehand. writeManagedFile treats a snapshot holding only
    mem's own content as no snapshot at all, so a file mem created gets deleted rather than left empty;
    preInstallHooks read the same file and counted any parseable hooks object as the user's. Two
    ordinary sequences fell through the gap. Re-running mem init claude-code after 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-byte CLAUDE.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 what mem init wrote" 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 --user was 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, --user removed the user-level file and left the project's hooks firing.
    mem init --user already 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 a path fact was in scope
    only when the caller passed --context-files naming a file at or under it — and every hook and
    command mem init installs calls mem recall --hint-format --root <dir> with no context files at
    all. So mem remember --path stored facts that plain mem recall listed 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 rule mem recall already 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 a project fact into scope by repository identity — that is how a fact
    captured in the main checkout reaches a worktree — but contradiction bucketing keyed on scope_root
    alone, so facts from two checkouts of the same repository were never compared. --subject package-manager --value pnpm captured in one clone and --value npm in the other left both
    active: recall from either root listed both as current, mem review --summary reported
    contested: 0, and mem epoch --gc resolved 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_repo has 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 consolidate now clusters through the same relation instead of its
    own copy of the rule.

    On upgrade: the first mem epoch --gc after 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 its prior_status and stays visible to mem 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: contradicted is 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, and contradicted stays reachable only for a directory that genuinely holds
      none.
    • valid-until 2026-12-31 expired 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-exists contradict, file-absent affirm, and file-newer-than contradict for whichever
      side could not be stat'ed — and an unreadable directory mid-walk let glob-exists reach
      contradicted while skipping the one place a match could have been. Only ENOENT/ENOTDIR mean
      absence now; every other errno is unverified.
    • 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
      affirmed nor contradicted is honest — a miss explainable purely by casing is now unverified
      on both git-tracked and the literal segments of glob-exists.
  • Three commands kept asking for work no command would ever do. mem doctor's embedding
    coverage counted every fact in its denominator while mem embed refuses to embed a superseded
    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 a fact_terms row, which cannot be told apart from
    "extracted, and the text is entirely stopwords" -- so mem facets --backfill ran, found nothing to
    write, and doctor asked again forever. A facts.terms_checked_at column now records that
    extraction happened rather than leaving it to be inferred from its output. Both denominators now
    describe work that exists.

  • scan-session stored 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 compared text = ? 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 export dropped which model wrote the vectors, so an import silently compared two vector
    spaces.
    The facts.embedding blobs were exported; the embedding_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 carries embeddingMeta, 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 embed or an export written before this release are treated as incomparable and named as such,
    with mem embed --all as the stated way to relabel the store. mem embed --all now 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.ts keeps its own copy of the ranking decision, and that copy did not know
    about unlabelled vectors: mem recall skipped the endpoint and said why, while the same store
    behind mem 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.ts catches 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 dream reasoned from premises mem itself refuses to state. Its pool was every active and
    pinned fact, with no correctness gate — no contradiction resolution, no freshness check. Because
    contradiction outcomes are resolved in memory on each recall but persisted only by mem epoch --gc
    and mem review --reject, two conflicting facts captured with mem remember are both still
    active, so both went to the model as premises while mem recall correctly withheld the loser;
    anchor-contradicted facts 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 as selectVerifiedFacts and shared by both, rather
    than reimplemented beside it — this codebase has been bitten repeatedly by a second hand-maintained
    copy drifting from the first. unverified is still not grounds for exclusion; only contradicted
    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 uninstall deleted files that existed before mem init and 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 empty CLAUDE.md and a .claude/settings.json containing {} 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 like AGENTS.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 --apply reversed 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 as mem pin's help says). Tell
    mem the database is postgres, pin it, later correct yourself to mysql, and one consolidate --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 forget on half of a contested pair left the survivor stranded at
    contested: mem recall surfaced it with no caveat while mem show called it contested, mem list --status active did not list it, and mem pin refused it as "contested, not active" — all at once.
    mem review --undo of 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 with mem review --reject, which already did.

  • mem show named a cause of supersession it could not know. Supersession edges live in the audit
    log, which mem export does not carry, so after an export/import round trip a superseded fact
    reported superseded_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 for supersededBy is corrected to
    match, since null there 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.
    contradicted is 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-version matched 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 yields unverified, which is
    what the README already promised. glob-exists deliberately skips .git and node_modules while
    walking a wildcard segment, then reported contradicted when 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, and contradicted is reserved for a walk that completed and found nothing. And
    file-newer-than, newest-of and package-version returned contradicted when a comparison
    target turned out to be a symlink, which mem refuses to follow: every path predicate now answers
    unverified there. 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 b returning contradicted asserts "a is not newer than b",
    a positive claim about a filesystem mem declined to read. git-tracked already answered
    unverified in the same situation, so the policy is now uniform in the direction it already
    half-held. Simple versions are unaffected: ^1.2.3 still 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-written SELECT over facts, and it has silently omitted a needed
    column three separate times now: prior_status, then scope_repo (which made every
    project-scoped fact invisible from a worktree), and now capture_root — so the capture-root fix
    below worked from mem recall and not from the hook that runs on every prompt, which is the path that matters
    most. The same fact read affirmed when you asked for it and unverified when 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.ts now asserts it instead: every column of facts has to reach
    the seam's SELECT and 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 --undo restored an anchor and left it pointed at the wrong tree. captureRoot was
    missing from the list of fields an edit snapshots, so mem edit --anchor re-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-absent affirmed (nothing is absent from a directory that does not exist)
    and file-exists contradicted, both read off nothing at all, while git-tracked already 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 yield unverified when the root they name is gone. Date-only
    predicates are deliberately unaffected: valid-until reads 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 --root it was captured under, but
    nothing on the fact recorded that root — a path-scoped fact's scope_root holds 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 a file-absent fact as plain ground truth
    while the file plainly existed, and listed a git-tracked fact under the heading inviting the user
    to delete it while the file was tracked: confidently wrong in both directions on the same query.
    mem show had 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
    unverified rather 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 edit re-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 export calls itself a "full-fidelity
    JSON envelope" but omitted last_surfaced_at, while faithfully preserving captured_at — and
    those are precisely the two signals mem consolidate --stale reads 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 carries last_surfaced_at and prior_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. Without prior_status, mem review --undo on a restored
    rejected fact also silently restored it to pending instead of the status it actually held.

  • mem edit --scope project bound the fact to the wrong repository. The edit path wrote
    scope_root and never scope_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_root alone by an explicit, separately documented decision.

  • mem edit --text left the old text's embedding attached, and mem embed could 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; only mem 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 --apply deleted 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 recall still had last_surfaced_at unset and no recall_log row — and --stale reads
    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 --apply superseded it; the next recall
    said "no matching facts". Both recall shapes now stamp last_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-session reported "no new durable statements found" over a broken store. Its
    per-candidate catch swallowed every error, not just the two that mean "this candidate is not
    worth storing" (validation and secret screening). A SqliteError, 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 path silently stored facts bound to a directory. path scope 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 doctor told 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 case mem recall handles 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
    shared AGENTS.md block told the agent to run mem 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 used without 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 the recall_log retention 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, but mem recall --hint-format
    returned an empty header — and that is the path mem init claude-code installs for SessionStart
    and UserPromptSubmit, so it is the one that runs unprompted. The seam's hand-written SELECT
    omitted scope_repo, leaving the identity check to compare undefined and 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 remember said "reaffirmed" while discarding the --anchor you 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 reach contradicted when 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 review run 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 — while mem recall from 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-contains through a symlink asserted the substring was present. mem deliberately
    refuses to read through a symlink that escapes the root — but then returned contradicted, 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 return unverified, matching the file-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/2 seam had no consumer, and CLAUDE.md claimed token-goat was one.
    Both were wrong in opposite directions: token-goat reads only mem 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 neither AGENTS.md nor CLAUDE.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 recall was sending every query
    to a third party.
    README, AGENTS.md, mem doctor, and the mem dream error text all described
    mem dream as the only path that leaves the machine — one README paragraph contradicted itself in
    consecutive sentences. With TOKEN_GOAT_MEM_EMBED_URL set, semantic recall embeds the query, and
    because mem init claude-code installs a UserPromptSubmit hook 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, and mem doctor's embeddings: line discloses it the
    way its dreaming: 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
    returned affirmed from the original tree. The anchor is now evaluated against the querying root
    whenever repository identity is what put the fact in scope.

  • mem import refused the cross-machine restore the docs promise. Rejecting any fact whose
    recorded scopeRoot fell outside --root closed 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 whose scopeRepo identifies the repository --root is a checkout
    of is now accepted with its scopeRoot rebound to that root. Everything else stays rejected.

  • mem embed sent the text of facts the user had deleted. The query behind it filtered on
    missing embeddings but never on status, and --all filtered on nothing at all, so a fact retired
    by mem forget or mem review --reject had its text re-sent to the embeddings endpoint on every
    run. Superseded facts are now excluded from both.

  • mem recall --root was 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-code could delete configuration a user wrote themselves. Install
    creates hooks and hooks.<event> when a settings.json has 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
    hooks object around it. Uninstall now reads the one-time .token-goat-mem.bak snapshot 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 init and mem uninstall rejected any settings.json containing a comment or a trailing
    comma.
    Claude Code's own settings file commonly carries both, and this file already depends on
    jsonc-parser to preserve exactly that formatting on the VS Code path -- the Claude Code path was
    the one still going through a strict JSON.parse, so it refused to modify the configurations it
    most needed to handle. Both paths now share the JSONC parser.

  • mem uninstall left a duplicated marker block behind while reporting success. A CLAUDE.md
    or AGENTS.md that ended up with two complete back-to-back blocks for one tool -- the realistic
    outcome of merging two branches that each ran mem init -- had only its first block stripped.
    Removal now loops until no resolvable pair remains.

  • A transient .git/index read 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 made git-tracked
    report contradicted -- the opposite of the truth -- and the fact was dropped from ground truth.
    Only ENOENT now means empty; every other error means unverified, matching package-version.

  • mem show --json reported no successor for a superseded fact that had one. The lookup read
    the last audit row for the fact, and mem used appends one, so recording a use erased the
    supersession link from the payload. It now finds the superseding row specifically.

  • Two concurrent mem remember calls 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 --backfill was declared, never read, and absent from the mutual-exclusion guard,
    so --backfill --all silently 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-md had no file size cap while the JSON path capped at 50 MB. Both now
    share the same constant.

Security

  • mem import --from-json accepted a scopeRoot pointing 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 a project-scoped fact it becomes
    the root that anchor predicates are evaluated against. A crafted import file could therefore turn
    every later mem recall into a file-existence and content-substring oracle for a directory
    outside --root. scopeRoot is 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.
  • --path was 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.
  • SecretDetectedError carried 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 edit copied 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 --undo restores 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 -- only AKIA was 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-assignment matched inside longer words, flagging notpassword= and mypwd= as
    credentials. A screener that refuses ordinary text trains users into blanket allowlists, which is
    its own security failure.

Changed

  • The mem recall --hint-format footer now says N more in scope, not sent rather 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 a tx-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.md no longer claims there is no CI workflow, and mem dream no longer claims to be the
    only command that sends fact text off the machine (mem embed does too).

Fixed

  • mem recall --hint-format could 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/2 header -- 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 detail when a fact
    line was emitted, N more in scope, not sent when the per-kind caps dropped results, and
    N withheld; mem review to resolve contested/pending when 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, because retrieve() 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 edit truncated the one value it exists to preserve. The audit detail line 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> --undo reverses 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 nullable audit_log.prior_json
    column scoped to the fields that edit actually touched, so undoing a --text edit cannot clobber a
    --scope nobody asked to change back, and an edit row written before the column existed refuses
    cleanly instead of restoring nothing and reporting success.

  • mem edit treated a fact you typed yourself exactly like one mem inferred. Promotion into
    ground truth was already gated hard -- captureSuggested caps a derived fact's confidence, and only
    mem review --promote activates a pending one -- but mutation of an already-active fact was not.
    Editing a source_type=user fact now requires --force, and the override is recorded in the audit
    log. It is a per-invocation override rather than a stored read_only column, which would be a
    second axis of a distinction source_type already 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 recall told every user to run mem 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
    names mem review only when a result in that recall is actually withheld.

  • mem scan-session recorded a turn number that was wrong and that moved. turnIndex was
    numbered from the start of the scan window rather than the start of the transcript, and the window
    is the last MAX_SCANNED_TURNS turns. 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. scanTranscript now offsets the window's indices back to
    transcript coordinates.

  • mem scan-session missed 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: and we should/let's followed by always/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 recall ranked a fact naming an identifier below facts that merely shared its stems.
    BM25 reduces src/retrieval.ts to src/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-entities extracts
    src/retrieval.ts as a single entity on exactly the right fact, and mem recall --entity returned
    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
    in retrieval.ts already encodes, after a rank list with no signal was found outvoting one with
    signal. Cost is one indexed fact_terms lookup per identifier in the query and nothing at all for
    a query with none, deliberately not the full-table scan --entity pays for, since this runs on
    every recall including the ~150 ms --hint-format budget. 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-session skipped
    any candidate whose text the store already knew, and mem import --from-md reported the same skip
    -- correct for a fact already settled, but wasteful when the match is still pending. 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 review sorts 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, because sources records no locator: the same transcript scanned at
    both Stop and PreCompact yields 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 review names the fact a pending correction may contradict. A correction filed by the
    scanner or by mem suggest carried no link to whatever it corrects, so promoting it left both
    claims live unless the user supplied --subject and --value by 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 computation mem show --related already 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 consolidate can 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 decision mem edit --scope global makes explicitly and not one
    this pass should invent. --cross-project reports the same shape across two or more projects and
    prints that command ready to paste, including the --force a 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 -- a default_branch of main globally and master in one
    repository is an override, and collapsing it would destroy the override rather than a duplicate.

  • The sources table is fed. Its schema, storage API, mem show --json surfacing and gc pruning
    have all existed and been tested since they were added, against zero rows: no capture path ever
    wrote one, so sources: [] 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,
    and mem 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 remember and mem 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 review shows 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 review prints a paste-ready mem edit --anchor command for facts it can already verify.
    The unanchored bucket named the problem and left the fix as an exercise: an anchorless fact is
    caveated as unverified forever, 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 and file-exists
    against it reads affirmed right now, the exact command is printed. A suggestion that would read
    contradicted or unverified is 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 --force for a user-stated fact, because mem edit refuses 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 md renders the store as a shareable, git-reviewable markdown document.
    mem import --from-md already 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 back pending. --format json remains the full-fidelity path and remains the
    default, so the lossy surface is one a user has to ask for by name.

  • mem show --related lists 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 and mem consolidate already 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-studio and mem 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 read AGENTS.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 dream reports what a configured model thinks follows from several stored facts taken
    together -- the one kind of consolidation mem consolidate structurally 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 typing mem remember. Off unless
    TOKEN_GOAT_MEM_DREAM_URL and TOKEN_GOAT_MEM_DREAM_MODEL are set, matching how mem 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. Only active and pinned facts 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 every from: id is one mem show opens. A malformed element is dropped alone rather
    than failing the run. mem doctor gained a dreaming: line mirroring its embeddings: 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 on mem 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 fact superseded, and --promote
    refuses anything that is not pending or contested, so a mistyped id could not be walked back
    through the CLI at all -- only by hand-editing the database or round-tripping a mem 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 rejected contested fact
    comes back contested rather than being quietly upgraded. Scoped to rejections by name: mem forget is 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
    SessionStart hook mem init installs -- 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; only withheld results 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 remember had 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 refreshes captured_at and confidence
    and prints reaffirmed. 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. Only active and
    pinned facts are candidates: reaffirming a pending one would promote it without review, and a
    superseded one must not be resurrected by a matching sentence. mem suggest never reaffirms at
    all -- its candidates come from file and transcript content, and letting derived text refresh a
    user-stated fact's clock would hand a CLAUDE.md the 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 permanently unverified -- caveated forever and never surfaced in
    mem review as something to resolve. The only predicate that reads no filesystem or git state. A
    bare YYYY-MM-DD is read as the end of that day rather than its midnight start, so
    valid-until 2026-12-31 is still affirmed during the 31st. An unparseable date is unverified
    rather than contradicted -- 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 show prints the fact's audit history, and mem edit records 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 for mem 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-code installed (SessionStart, UserPromptSubmit) are recall paths, so unless the agent obeyed the CLAUDE.md instruction block or the user typed mem remember by 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-session closes it, wired as a third hook on Stop -- the only event that fires after the user has actually spoken, and the only one whose envelope carries transcript_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 until mem review --promote resolves 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 shape mem init installs, since a Stop hook'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 ordinary text blocks. Treating any of them as speech would let a file mem reads dictate what mem remembers. Each is rejected: tool-result blocks and toolUseResult envelopes, isMeta/isCompactSummary/isVisibleInTranscriptOnly entries, entries whose origin.kind is present and not human, 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 an mem export/mem import onto another machine -- the three cases a
    memory tool exists to cover. A new scope_repo column 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 leak packages/a's decisions into packages/b. The remote is
    normalized so git@github.com:acme/widget.git, https://github.com/acme/widget, and
    ssh://git@github.com/acme/widget.git produce one string; several remotes with no origin is
    genuinely ambiguous and yields no identity rather than a guess. Nothing shells out -- git need
    not be installed, and only .git's own files are read (including the commondir indirection,
    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=path restores 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/contested transitions, no honest backfill exists for facts captured before this
    column, and widening it would rewrite facts across checkouts on the first mem epoch --gc after
    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. A CLAUDE.md full of two-year-old conventions imported
    today otherwise reads as the newest thing in the store, and captured_at drives 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.