Skip to content

Releases: liza-studio/skillmem

0.11.0 — the trust boundary, closed on every channel; 40 review rounds

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 17 Sep 07:06

Two independent reviewers (one on the Claude side, one on the GPT side) read the
whole codebase for the first time, then re-read every fix, forty rounds deep,
each round hunting for what the previous round's fixes broke. The first pass
found its P1s not in the recap hook everyone had been staring at but in the
parts nobody had reviewed end to end: the HTTP server, body files, packs.

Trust boundary

  • skillmem trust and --untrust refuse to run without a terminal, and
    init --claude-code adds "Bash(skillmem trust*)" to permissions.deny.
    Both are safeguards against an agent running the command, not an owner
    authentication: a process with write access to the database can still set
    the columns. README says which is which.
  • Unapproved memory is framed by one renderer wherever a body, snippet or
    history entry reaches a model — MCP mem_get/mem_search/mem_recall,
    HTTP /get//search//recall, CLI recall (text and JSON), hooks — with
    the title (and a history entry's old title) inside the frame; a previous
    version is framed whatever the current one's approval — approval belongs to
    the words that were approved. Listings (mem_list, /list) carry origin
    and a trusted flag with raw titles. mem_get no longer returns a raw body next
    to a trust_warning key.
  • HTTP /write on an existing slug demands the same permission /update
    does: resubmitting a public rule's exact text as private used to reassign
    its author and visibility and keep the owner's approval. /learn requires
    write_public for public skills. A soft-deleted slug is not free for anyone.
  • CLI write/learn from a process without a TTY record origin=agent, not
    owner; HTTP writes record origin=agent instead of unknown; auto-memory
    imports default to agent.
  • Pack import never overwrites a row that is not that pack's own; SKILL.md
    symlinks and files outside the pack are ignored; git clone --; aggregate
    caps (500 files / 4 MB); a removed pack reinstalls. Vault attachments must
    resolve inside the vault.

Data integrity

  • Body files written from now on are <slug>__<hash>[-<db>]+<content32>.md:
    namespaced per file-backed database (two databases under one home no longer
    share a newly written file) and content-addressed with 128 bits (a new body
    is a new file, so publish-before-commit is safe and an outer rollback cannot
    leave a row pointing at someone else's text). Pre-0.11 files are not renamed
    and stay shared if two databases referenced one. Orphans in a database's own
    namespace are collected on the nightly decay run and HTTP /decay, under
    the database write lock, with a 60 s grace; a non-lock SQLite error there
    now propagates instead of reading as "nothing to do".
  • kind is validated on write — [a-z0-9_-][a-z0-9_ -]{0,31} after
    lower-casing, trimming and collapsing whitespace — and existing rows are
    normalised once on the next open (a write) — case, runs of space, tab,
    CR, LF, VT, FF and NBSP; a pre-0.11 visibility is lower-cased and space-trimmed, and one
    outside public/shared/private becomes private, so old rows stay
    updatable. Filters are
    case-insensitive; a filter nothing can match returns nothing. Export refuses
    any path outside its destination (kind="../../x" used to write there).
  • Export keeps a per-database manifest and prunes the files it wrote last
    time. Use one destination per database: two databases exporting the
    same kind/slug into one directory overwrite each other, and the first
    0.11 export over a pre-release manifest adopts and prunes that whole list.
    Strength, pin and the access/confirmed/failure counters are written to
    frontmatter now, and a body is written verbatim, so a dump→restore keeps
    exact slugs (a_b and a-b no longer merge), LF bodies byte for byte (so
    re-importing over an identical approved row keeps its approval — approval
    itself never travels in a dump), counters, pins (when the dump records
    them) and a recorded originunknown included. Importing a plain
    Obsidian note keeps what the row earned.
  • A same-text write through HTTP, MCP or the CLI applies only the metadata the
    caller actually sent (library callers such as pack and vault imports keep
    their file-is-authoritative behaviour — a migrated file still sets kind and
    visibility; same-text provenance changes only when restoration is
    explicitly enabled, i.e. a skillmem dump or a file carrying strength:): a
    retried mem_write without a kind no longer turns a trusted skill into a
    note, a same-text mem_learn no longer flips a private skill public, and an
    explicit topics: [] really revokes a shared audience (the lexical index
    follows). A write onto a soft-deleted slug is refused on every channel (it
    used to say OK and stay invisible); a dump restore or a pack reinstall
    revives it. An ordinary update keeps the strength the row earned (only a
    restore — a skillmem dump, or a file carrying strength: — sets it); restem indexes full
    document bodies; reinforce is one relative UPDATE (concurrent confirmations
    no longer lose each other). Known: reinforce is not idempotent — a
    retried call counts as new evidence; evidence ids are a later release.
  • Decay: a fresh skill is measured from its creation, not from "never used";
    one decay step per threshold (the threshold is at least one day), so a job
    run twice does not compound; the
    lifecycle sweep runs even when nothing decays (the CLI used to skip it on
    those runs, so skills sitting at the floor were never archived).
  • History chain: changed_at is clamped monotonic on every history write
    (a clock stepped back no longer reads as tampering). A row stamped in the
    future pins later stamps to it until real time catches up — by design; a
    warning is logged once when the gap exceeds a day.
  • init_schema no longer writes on every open (hooks stalled behind any writer).

Hooks

  • The Stop→skillmem migrate hook is gone: it imported the alphabetically
    first project's memory directory on every turn. init --claude-code with
    any hooks mode but none removes an existing one (backup written) and
    installs the deny rule; --hooks minimal means exactly that and nothing
    else. Hand-written memory: skillmem migrate --source <dir>.
  • Recall context is budgeted per section before framing (a frame can no longer
    be cut in half) and the seen-ledger lists exactly what was emitted (any slug
    without whitespace or ]).
  • Recap: publication fails closed without its lock; Stop recaps of one
    session are serialised by a per-session lock, and the SessionEnd recap waits
    up to 5 s for an in-flight Stop recap and then proceeds anyway (publication
    is compare-and-swap, so neither can clobber the other); Claude Code's
    synthetic string turns stay out of the summary.

CLI / MCP / scheduling

  • skillmem --db X init ... writes SKILLMEM_DB=X (absolute) into every
    agent's MCP entry — Claude Code, Codex, Cursor, Windsurf, Gemini CLI,
    opencode. Claude Code, Cursor, Windsurf, Gemini CLI and opencode update an
    existing entry's database in place (JSON, rewritten atomically). Codex's
    hand-written TOML is never edited in place: an existing entry keeps its
    database and init prints the one line to set by hand
    (SKILLMEM_DB = "X" under [mcp_servers.skillmem.env]). uninstall
    removes the Codex table only when the result provably equals the old file
    minus that table; otherwise (including a file it cannot parse) it leaves
    the file alone and says so in warnings. uninstall also removes a
    skillmem hook from a settings.json group it shares with other hooks. Without --db an existing entry is left alone everywhere.
    init and uninstall write config files atomically, through a symlink
    to its target, with mode kept (JSON and TOML alike). --db reaches scheduled jobs (re-run
    schedule install after upgrading). uninstall --purge-db removes the
    DB, its -wal/-shm, its namespaced body files and the legacy-named
    files it references (a second database referencing the same legacy file
    loses it — split them first).
  • HTTP /write and /learn with check_conflicts reported duplicate
    candidates from every agent's records, private ones included — a 409 that
    quotes another agent's private title is a read through the trust boundary.
    Candidates are now the top BM25 matches among the records the writer may
    read
    (the scan walks the BM25 order past hidden rows until it has scored
    its five, so no number of hidden rows crowds out the writer's own
    duplicate). MCP is one principal; unchanged.
  • A same-text write that changes ttl_days now moves freshness_until with
    it, and over HTTP an explicit ttl_days: null clears both; before, the new
    TTL was stored and the deadline never came. Re-sending the same TTL does
    not renew; MCP and the CLI cannot clear a TTL (null is not sent, 0 is
    refused) — 0.11.1.
  • /update and mem_update mark the rewritten text origin=agent: an
    owner-authored row edited by an agent kept origin=owner, so the owner
    would re-trust words they never wrote (approval was already dropped).
  • HTTP /search, /list and /recall used to cut the page before the
    visibility filter, so another agent's records could crowd the caller's own
    out of the answer (an empty 200 while /get found the row); the filter
    now runs inside the ranking — every match is ranked once and the first
    limit rows the caller may read come back, bodies read only for those.
    /get lists in links_in only the backlink sources the caller may read —
    a private record's slug used to show on the public record it linked to.
  • import-vault no longer follows a symlinked note out of the vault (the
    rule attachments and packs already had): zshrc.md -> ~/.zshrc in a
    cloned vault used to land the real file's text in the database.
  • scrub is idempotent: a value already re...
Read more

v0.10.5 — a checked-in Docker build, and an honest serverInfo

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 15 Sep 20:43
  • A Dockerfile, checked in. Catalogues build a container to score an MCP server, and a server whose inferred build fails is kept out of their search results. This one is explicit and verified on Ubuntu: the image builds at 297MB, initialize answers with the real version, and tools/list returns all 9 tools over stdio. Memory lives in the /data volume, so a restart keeps it. --build-arg EXTRAS='[semantic]' includes the vector path.
  • The MCP server reports its own version. serverInfo carried the SDK's 1.30.0, so a registry listing or a client's debug log showed a version this package has never had.

pip install -U skillmem

v0.10.4 — a clean install starts clean

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 15 Sep 20:29

Found while verifying a real pip install skillmem on Ubuntu: a brand-new database reported lexical_reindex_pending: true, which would send the nightly job on a pointless pass and makes skillmem doctor look alarming on a clean install. The flag is now set only when there is something stored to rebuild.

Together with 0.10.3 (the lexical index now keeps two-character tokens — db, py, js, ci — and the rebuild runs from the nightly job or skillmem reindex-lexical, never inside a hook) this closes the lexical-recall defect end to end.

Verified on Ubuntu from PyPI: install, fresh database, learn, recall by full path and by db.py with the embedder disabled, and the tool-recall hook emitting the untrusted frame.

pip install -U skillmem

v0.10.2 — a file path has to match lexically too

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 15 Sep 19:31

If you installed with a plain pip install skillmem (no semantic extra), recall was silently dead for Edit, Write and NotebookEdit. The FTS query was split on whitespace, so /work/analysis.ipynb became a single phrase token that matched nothing — and tool-recall passes the edited file's path as its query. The embedder masked it locally; CI, which installs without the extra, did not.

Queries are now tokenised the way the documents are, and the regression test runs with MEM_SEMANTIC=0 so the semantic layer cannot hide it again.

Also in 0.10.1: the README explains what 0.10.0 changed and how this differs from memory products built for conversational or user memory, with the measured retrieval numbers next to the claim; server.json, plugin.json and .claude-plugin/plugin.json were still pinned at 0.9.2, which would have pointed MCP Registry installers at a version that still contains the recursion fixed in 0.9.3.

pip install -U skillmem

v0.10.0 — provenance, and trust the owner grants

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 14 Sep 21:11

Memory now carries where it came from, and trust is something the owner grants.

The loop this closes was open: an external text — a README, a web page — reaches a transcript, a model distils it into a note, and the note comes back in the next session under a heading that reads like your own rules. A document could also talk an agent into saving a rule through mem_learn, and that rule then looked exactly like one a human wrote.

  • origin on every memory (schema v10): owner, agent, imported, derived, unknown. Writers declare it; nothing guesses.
  • Trust is explicit. trusted_at is set only by the owner — skillmem trust <slug> — and editing an approved memory's text drops the approval with it. A CLI write approves itself only from a TTY, because an agent can call the CLI through Bash as easily as a person can type it.
  • One frame, applied at read time, on every channel: auto-recall, tool-recall, session-history, mem_recall, mem_get, cat, and inject (approved titles only; the rest reported as a count). Unapproved memory arrives inside a marked block that says it is data, not instructions, with its provenance on the line (origin=derived session=…, origin=imported pack=…). Written into the note body the frame would not survive — recall collapses newlines, snippets cut the middle, and a summary can carry its own closing backticks.
  • The summariser runs caged, or not at all: --tools "" and --strict-mcp-config are mandatory, and a CLI that does not understand them gets skip:unsafe-cli instead of a run without them.
  • Migration is additive, inside one transaction, after a copy of the database into <data>/backups/pre-v10-*.db, with columns re-checked under the lock. 8901 rows in 0.45s here. Rows whose tags cannot be parsed keep origin=unknown: unreadable provenance is not guessed and not approved.
  • Grandfathering, stated plainly: existing owner and agent rows are approved by the migration (trusted_by='migration-v10') — the alternative is that every rule you have relied on for months arrives unapproved the morning after an upgrade. Imported packs and transcript summaries are not: 7809 summaries stayed unapproved on the machine this was built on.
  • export/import keep provenance. A file may lower its own origin but never claim owner, and trust is never importable.

What this does not do: a frame makes the boundary legible; it does not guarantee a model ignores an instruction sitting inside data. The guarantee comes from the reader having no tools — which is why the summariser has none.

Reviewed by GPT-6 over four passes (it found the inject hole, the json_each failure that turned a pack into a trusted rule, and the unatomic migration) with each claim verified against a live install. Every fix has a test that is red without it: 212 passing.

pip install -U skillmem

v0.9.9 — compare-and-swap the recap, exclude kinds in the query

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 14 Sep 19:06

A third review pass reproduced two defects in code shipped this week.

  • Publishing a recap is compare-and-swap now. The freshness check and the file replacement were two steps: a Stop that passed the check before SessionEnd wrote the final recap would replace it afterwards. The basis is re-read inside a short publish lock — milliseconds, not the model call — so the late writer sees the final note and stands down.
  • Excluding session recaps from search happened after the candidate pool, and that pool is capped at 50 per signal: a wall of recaps filled it and the search returned nothing while a matching skill sat just below (--kind skill found it). Kinds are excluded inside the ranking query now — exclude_kinds on search and hybrid_rank_ids.

Both were reproduced before the fix, and each test is red without it.

pip install -U skillmem

v0.9.7 — a trust boundary, and a way to see what the hooks did

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 14 Sep 17:00
  • Memory arrives with a trust boundary. Anything this machine did not author — an imported pack, or a note distilled from a transcript that may itself quote a web page — is injected under its own header saying plainly that it is data, not instructions. A stored instruction is still an instruction, and it used to land in the same block as the user's own rules.
  • skillmem search hides session recaps by default (--notes brings them back). They accumulate one per session and reached 90% of the words in one database, so an unfiltered search returned the diary instead of the rules.
  • skillmem recap [TRANSCRIPT] [--force] writes a recap on demand — the Stop hook is rate-limited, and this is how you save a session's closing minutes without waiting. With no argument it picks the current project's newest transcript.
  • skillmem hooks-status shows what the hooks have actually been doing: runs, skips, failures, the last line per hook, and the state directory. Hooks swallow their own errors so they can never break a session — which also means one that silently stopped working looks exactly like one with nothing to do.
  • A manual recap no longer inherits SessionEnd's budget. Its first live run timed out at 45s: only the real SessionEnd event lives inside that 60s ceiling. A manual run gets the full timeout, and the SessionEnd one trims its input to 20KB, because a truncated recap beats one that times out.

pip install -U skillmem

v0.9.5 — four silent defects from the review

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 14 Sep 16:05

An outside review of the hook path (GPT-6 reading the code, a second model verifying each claim against the live install) turned up four defects. Every one of them failed quietly.

  • Notebook edits get recall again. tool-recall read file_path, but Claude Code sends notebook_path for NotebookEdit — the query came out empty and the hook returned nothing at all for every notebook edit.
  • Session notes are linked to their session again. The recap writes metadata.source_session; the importer only knew originSessionId/sessionId, so imported session notes landed with a null session — 7793 of 7793 on the machine where this surfaced.
  • The MCP guard's count told the truth. It reported len(actual) of len(expected), so a config with extra servers could claim "12 of 10 expected connected" in the same breath as listing one as missing.
  • The per-session recall ledger moved to the private state dir. On Linux without TMPDIR it sat in a world-writable /tmp, where a neighbour could pre-create the file and mute someone's recall; stale ledgers are now pruned after seven days instead of accumulating one file per session forever.

Every fix has a test that was checked by reverting the fix.

pip install -U skillmem

v0.9.4 — the recap costs what it should

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 14 Sep 16:01

0.9.3 stopped the Stop hook recursing. This release stops it being expensive, and closes what a full review of the recap path turned up.

Stop fires after every assistant turn, not when the session closes — so the recap ran per turn: a model call each time, and a near-identical note every few minutes.

  • At most one call per session per SKILLMEM_RECAP_MIN_INTERVAL (default 600s), and one note per session per day that a later recap rewrites.
  • The limit counts attempts, not successes — a failing model left no trace, so every following turn bought another call.
  • A non-zero exit is no longer stored as memory. Any output over 100 characters (a usage-limit message, a stack trace) became a note and overwrote a good recap.
  • SessionEnd gets its own recap, not rate-limited, so a session's closing turns still reach memory. skillmem init wires it; existing installs pick it up by re-running init.
  • The parallel-slot semaphore is back (SKILLMEM_RECAP_MAX_PARALLEL, default 2), lost in the rename. O_EXCL locks reclaimed by age, so SIGKILL cannot wedge a slot.
  • The child runs lean: --strict-mcp-config (it booted every MCP server per call) and --no-session-persistence (it left a transcript on disk per call — a gigabyte on one machine). Dropped automatically on a CLI too old to know them.
  • Notes are written atomically; a crash mid-write left half a note behind.
  • A typo in SKILLMEM_RECAP_TIMEOUT no longer breaks every commandint() ran at import time.
  • Tests stopped writing into the developer's live state directory, where a stamp from one test silently debounced the next.

pip install -U skillmem

v0.9.3 — the recap no longer recaps itself

Choose a tag to compare

@mrpetrukovich mrpetrukovich released this 14 Sep 15:36

session-recap shells out to claude -p, and that child is a Claude Code session like any other: its own Stop hook fired the same recap, whose child fired it again. On one machine the chain went from 6 recaps a day to 4083 — 3901 ghost sessions, a gigabyte of transcripts and a burned subscription quota, while the garbage summaries fed themselves back into the next session's context.

The child now inherits SKILLMEM_NO_RECAP=1, the way it did before the rename. The old test only proved the flag works when set from outside, so the regression shipped green; the new one asserts the flag reaches the child.

Anyone on 0.9.0–0.9.2 should upgrade: pip install -U skillmem.

Also in this release (unreleased since 0.9.2): third-party skill pack imports (skillmem skills add), strength earned by outside evidence instead of self-report (schema v9), mem_pin for rules that must never fade, and init support for Cursor, Windsurf, Gemini CLI and opencode.