Skip to the content.

memshelf — Decision log

Newest last. Earlier entries were made while the design lived as RFC-0001 in docshelf-mcp.

Date Decision By
2026-07-13 RFC draft created. Form = companion project over docshelf (not a docshelf subpackage, not a prompt-only pattern); primary surface v1 = Claude Code / Cowork. draft, author-merged (ignatenkofi/docshelf-mcp#42)
2026-07-13 Portability requirement: core/episode format/on-disk shelf contain nothing host-specific; three-ring model (core → MCP+CLI surfaces → host adapters); triggers are adapter territory; prompts are core assets. author note, merged (ignatenkofi/docshelf-mcp#43)
2026-07-13 Prior-art survey (LANDSCAPE.md): niche confirmed open — no project combines episodes+digests+git+INDEX navigation. Design amendments: mechanical eviction, injection budgets/KV-cache discipline, prompt-injection-on-recall defense, platform-collision defense, subagent-deposit trigger (v2), memory-tool adapter ring. Direction kept, scope sharpened. draft, author-merged (ignatenkofi/docshelf-mcp#44)
2026-07-13 Name + placement resolved: memshelf, repo ignatenkofi/memshelf-mcp (PyPI memshelf / memshelf-mcp checked free). Repo created by author; seeded from the RFC. author
2026-07-13 Storage defaults reworked per author’s git concerns: modes plain / git-local (default — auto-commit, no remote configured) / git-remote (opt-in, private-only, doctor-enforced, autopush: false). Accidental-exfiltration threat handled by absence of a remote, not absence of git. “Store inside Claude” (artifacts/attachments) assessed: unsuitable as canonical store (breaks portability), adopted as optional read-mirror idea (M3 experiment). author + draft
2026-07-13 Positioning: hero scenarios added to MANIFEST — (1) the weeks-long dialog that must not die (primary wedge), (2) context advisor: surface where the window goes, flag shelvable episodes (promoted into M2; also the onboarding moment), (3) archive as raw material: tags/graph/retrospectives/fork-a-thread (M3). Many-repos overload noted as advisor diagnosis territory, not a direct memshelf fix. author + draft
2026-07-13 Import mode adopted (author: “выгрузить весь диалог на полку”): whole-dialog retro-shelving = segmentation into episodes + one session digest, mode: import in the ledger. Distinct from the “transcript archiver” non-goal — segmentation and digests are mandatory, raw excerpts optional, the raw transcript itself is input-only and never committed. M0 does it manually via the skill; memshelf_import is an M1 candidate. author + draft
2026-07-13 Transparent token accounting adopted (author request, mirroring docshelf’s measured-savings ethos): ledger.tsv per shelf, one row per shelve; memshelf_stats in M1; derived metrics (standing cost / shelved mass / compression / recall cost per question) defined in docs/M0.md. author + draft
2026-07-13 M0 launched. Kit: prompt-only /shelve skill (live + import modes) and recall-rule snippet in adapters/claude-code/; protocol in docs/M0.md. Case A material: author’s long sqst homework-review dialog (Opus 4.7 era) as retro-import; episodes depersonalized per sqst PII policy (roles/codes, no student identifiers), raw transcript stays outside all repos. M0 shelf uses a private remote (cloud sessions are ephemeral) — a documented deviation from the git-local default. author + draft
2026-07-22 Claude Code plugin hooks scoped to what shell hooks can actually do (#11): a hook is a shell command, not the LLM, and per the current hook contracts SessionStart can inject context while PreCompact cannot and SessionEnd runs after the agent stops. So the plugin ships SessionStart → inject INDEX.md (recall bootstrap) and SessionEnd/PreCompact → mechanical shelf push (MEMSHELF_AUTOPUSH, off by default). The ROADMAP’s “PreCompact shelve / SessionEnd session digest” is revised: both need the LLM, so they stay agent-driven (the /shelve skill + recall rule), not hooks. author + draft
2026-07-22 Server topology: separate MCP process (closes open question 3 / #28). memshelf-mcp ships its own FastMCP stdio server (server.py, mirroring docshelf’s), not an in-process registration alongside docshelf tools — shipped this way in the M1 slices and confirmed as the decision: the core imports docshelf as a library (one dependency), while the server stays independently versioned, installable, and testable; users who want both attach two config entries. Revisit only if the two-entry config proves a real adoption burden. draft, per shipped M1
2026-07-22 memshelf_stats reports claimed AND realized economy (#6; closes the Case B verdict’s measurement gap). Claimed = ledger-derived (standing cost = tokens(INDEX) + Σ digest tokens; shelved mass = Σ approx_tokens_in; compression = mass / standing). Realized = an opt-in recall log (recall --log → recall-log.tsv), where each fetch’s saving = the episode’s original mass − tokens actually fetched. The true fetch-hit rate (share of past-work questions closed by a shelf fetch vs re-reading the repo / owner re-explaining) needs a denominator that can’t be captured mechanically, so stats reports the measurable side (recall count, episodes recalled, realized tokens saved) and stays honest about the rest. chars/4 estimator, tokenizer-independent. author + draft
2026-07-31 Derived shelf files become bot-rendered; notes moves into the episode (#58; adopts project-atlas ADR 0007). Parallel shelve runs collide in four files at once — INDEX.md, ledger.tsv, docs/*/.meta.json, stats.svg — because every writer regenerates all of them. Target shape (M2): shelve writes only the episode file; ledger.tsv becomes derivable once its one non-derivable column (notes) lives in the episode frontmatter; INDEX/ledger/.meta/stats are rendered by a bot on main behind a PR guard on the derived paths. The conflict class then cannot occur by construction rather than being resolved after the fact. memshelf resolve (shipped, live run 2026-07-30) stays as the fallback for hand-merges and for shelves that keep the current layout. Rejected: .gitattributes merge=union — GitHub’s server-side merge does not run custom drivers, the same reason the atlas turned it down; and push-straight-to-main on shelve, which breaks the draft-PR discipline of night shifts without closing the two-simultaneous-push race. author + draft
2026-08-01 Context advisor: heuristics only; the window breakdown is an input, not something the tool hunts for (#14, closes open question 7). A library cannot see the window it is asked about, and a parser for a host’s /context output would work on exactly one host and rot with its next release. So the advisor reuses the split shelve and rollup already have: the model reports what only the model knows (labels, rough sizes, which topics are closed), and the tool contributes the four things a self-assessment cannot — memshelf’s own standing cost, measured; verification of every “already shelved” claim against the actual episodes (a false claim is refused loudly, since acting on it destroys work); arithmetic net of what shelving adds forever (~200 tokens per episode, so nothing under 2000 is proposed at all); and a deterministic ranking, without which the M2 exit criterion “proposals accepted, not overridden” cannot be measured. Proposals only — the tool writes nothing. Rejected: harness-parsing in core (host-bound, and portability rule 2 puts it in an adapter filling the same input); inferring “already shelved” from a title match (it becomes a hint, never an auto-drop). author-planned, draft
2026-08-14 A packaged host gets one global shelf through $MEMSHELF_SHELF_PATH; an explicit shelf_path still wins. Claude Desktop configures an extension only through the environment, so a bundle cannot ask the model to remember a path — but a shelf per project is the whole point of the portability rules, and one global setting would flatten that. Precedence resolves it: a call that names shelf_path uses it (a project pins its own shelf by saying so in its instructions), a call that names none falls back to the variable (the global shelf, and the case for a chat outside any project), and neither present is an error naming the variable rather than a write to "". Resolution lives in the input models, so the MCP surface and any future caller of tools.py inherit it; the CLI keeps --shelf required, which is an asymmetry left open deliberately rather than settled by default. Rejected: a user_config value the host injects into every call (hosts do not rewrite tool arguments), and reading the setting once at import (a host may set it after the process starts). author + draft
2026-08-15 The CLI honours $MEMSHELF_SHELF_PATH too, and announces when it did (#86, settles the asymmetry the row above left open). The variable working for the MCP tools while --shelf stayed required reads as a bug to whoever sets it, and the CLI is the documented portability surface — “anything that can run a shell command can drive the shelf” — so it is the one most likely to be scripted around, where an environment variable is the natural way to say which shelf. The objection to honouring it (an implicit shelf in a script is a footgun) is answered by removing the silence rather than the fallback: a resolved default prints memshelf: shelf from $MEMSHELF_SHELF_PATH: … on stderr, where it cannot corrupt piped JSON. One resolver, shared with the input models, so the two surfaces cannot drift apart. Rejected: documenting the split as deliberate (it makes the CLI the odd surface out for no benefit a script can use), and an argparse default= (frozen at import; a host may set the variable after the process starts). night shift, draft
2026-09-10 Archive reuse (#18) ships as CLI verbs, not MCP tools; cross-episode links are bare ids, not a link syntax. tags / graph / retro / fork / mirror are pure functions of the files on disk, and each is either a report a person reads outside a session or (fork) the first message of a different session — nothing a tool inside the session would do better, while every published tool costs every turn its description (#111, 320-char cap and the descriptions * 4 < total guard). The graph reads references the way the dogfood shelf already writes them: an episode names another by its id inside Decisions / Open threads (110 live episodes, 0 [[wiki]] links, ids mentioned up to 9× each), matched as whole tokens so …-auth does not fire inside …-auth-followup. fork wraps INDEX and every episode in the recall data envelope so the receiving session treats them as material, and refuses unknown ids or missing sections rather than emitting a document with a hole. mirror answers open question 8: the mirror is a generated static page; publishing it (claude.ai artifact, gist, a phone’s Files app) is a host action outside the tool, so no adapter and no vendor binding. Rejected: a memshelf_fork MCP tool (prefix cost for a document the session itself never consumes; recall already covers the in-session case); [[wikilinks]] as the link syntax (would have to be taught and back-filled — the ids are already there); an artifact-publishing adapter (vendor-bound, breaks portability principle 9; the page is enough). night shift, draft
2026-09-10 The embedding sidecar (#17) lives outside the shelf, fuses by rank, and is an optional extra built from the CLI — search keeps its signature and the tool surface does not grow. A shelf is a git repository whose every file is meant to be read and diffed; a derived vector blob inside it would ride into every commit and every clone, so the index is a JSON file under the state directory keyed by the resolved shelf path, rebuilt from the episodes (incrementally, by file (mtime, size)) and droppable at any time. Fusion is reciprocal rank, not a score mix: grep scores are occurrence counts and cosines live in [-1, 1], and no scale calibrates one against the other while ranks do; the fused score stays an integer so the hit shape is unchanged. The model is model2vec static embeddings (no GPU, no service, one process, ~3 s to load once) behind memshelf-mcp[semantic], because the base install must stay light and a shelf without the extra must behave byte-for-byte as before — which is also why $MEMSHELF_SEMANTIC=off exists and why search only goes hybrid when all three of extra, index, and switch are present. Building is a CLI verb, not a tool: it is a one-off per shelf that a session never needs to run, and every published tool is charged to every turn (#111). Measured, dogfood shelf, 30 paraphrase / cross-language / keyword queries: grep 1/30 in the top 5, hybrid 16/30 (21/30 in the top 10). Rejected: the index inside the shelf (see above); a vector database or an embedding service (a dependency to run and a place for the shelf’s text to leave the machine — portability principle 9); auto-building on first search (a multi-second surprise inside a tool call, and a write the caller did not ask for); a memshelf_semantic_build tool (prefix cost for a one-off). night shift, draft
2026-09-25 Served-code freshness is said in the tool’s own response, not only by doctor (#125, #158; dogfood shelf thread 2026-09-10). served-code-differs has been a doctor finding since #125, and both later incidents were still found sideways, because nobody runs doctor when a tool merely looks buggy. So the server hashes its own package once per process and the reference checkout (the resolution doctor uses: $MEMSHELF_CHECKOUT, else memshelf-mcp next to the shelf) once per path until HEAD moves or five minutes pass, and prepends a warning — same code, both hashes, both paths — as the first key of every envelope, success and error alike, at the one place responses are composed. Three outcomes stay distinct: differs → the warning; same → nothing; unknown → nothing per call, one line in the initialize instructions naming MEMSHELF_CHECKOUT. MEMSHELF_FRESHNESS_WARNING=0 is the opt-out for a host where the copy is meant to differ, and the instructions then say it is off. A new key rather than warnings, which already means three shapes across the tools. Rejected: a per-call freshness-unknown warning (noise with no fix attached on every machine without sources); re-hashing per call (~1.2 ms × 2, avoidable); an initialize-only line (read once at the handshake, while the stale answer is read per call); prepending into the tools’ own warnings lists (three shapes, no uniform slot). night shift, draft