Releases: jojoprison/mnemo
Release list
v1.2.17
Added
- A private-leak guard in the linter — a link into someone's vault can no longer ship. Two
[[wikilink]]s pointing at the maintainer's own Obsidian notes reached a public PR during the v1.2.14 review and were caught by hand; nothing in the repo guarded that class.scripts/lint-skills.pynow sweeps every shipped text file and fails on links that name a concrete note. A blanket ban was impossible — mnemo is a plugin about Obsidian and its docs carry 60+ legitimate examples — so the rule passes only shapes that cannot name a real note (placeholders like[[{hub_name}]]and[[Session — …]], single ASCII words like[[wikilinks]], regex fragments) plus the invented names listed in the newscripts/wikilink-allowlist.txt. Adding a name there is a review decision, not a formality: the file says so, andscripts/test-lint-wikilinks.py(22 tests) fails on entries that no longer appear anywhere, so the list stays a review surface instead of debris. The same pass flags absolute home paths while leaving~/and${VAR}forms alone. - Two properties of that guard are worth naming, because both are how this class hides. The sweep reads whole files: the second link caught by hand was wrapped across two source lines, and a line-by-line scan would have called that file clean. And a sweep matching nothing at all fails instead of reporting clean, since a wrong root, extension list, or regex is indistinguishable from a spotless repo. The guard caught its own documentation while it was being written, and it caught five machine-local paths that an earlier hand-run
grephad missed — that grep had searched for one specific username and returned an honest, meaningless zero.
Fixed
- 49 documentation claims that no longer matched the code, found by an adversarial audit of every doc against the implementation and confirmed one by one (of 50 findings, verification rejected exactly one). The dominant failure was not the code moving ahead of the docs — it was a fact corrected in one file and left standing in its neighbour: the RU and ZH README sections still said "all fields are optional" long after the EN section was fixed in v1.2.5,
CONTRIBUTING.mdcarried a stale copy of a gate whose canonical list lives inTESTING.md, anddocs/health.mdshowed a report format the skill stopped printing. - Six places across all three README languages claimed nothing runs without your pick.
review --fullhas been the documented exception since v1.2.8 — the flag itself is the consent — andreview.full.autoConnectlets its connect step apply links unprompted. Both are now stated wherever the absolute was. The RU and ZHconnectrows carried the same absolute and now carry the exception too. CONTRIBUTING.mddescribed a skill that fails the mandatory gate. Its "Adding a New Skill" steps named neither the required## Portable pathssection nor the literal invocation marker, both enforced by the gate, and implied an eighth skill is a drop-in addition when the canonical set is pinned in three separate places. All of it is now named, with the registries listed.skills/session/SKILL.mdcontradicted itself abouthandoff.maxKB— 40 KB in one line, 56 KB fifty lines earlier. The code says 56; 40 turns out to be the legacyarchive-handofffallback used only when no value is passed, andconfig-schema.mdnow records both.- Seven CHANGELOG compare-links pointed at four tags that exist neither locally nor on origin (
v0.1.0,v0.4.0,v0.5.8,v0.8.0), each rendering a GitHub 404. The neighbouring links now span the gaps, with a dated note explaining why those four releases have no link. TESTING.md— the manual-gate document — described nothing that shipped in v1.2.16 and still stamped itself v1.2.15, the exact staleness v1.2.15 had fixed. It now coverscontext-window.py --explain, health's Step 7.55, and the new guard, and its skill-routing claim matches the frontmatter (context: forkis on one skill, not three). Smaller corrections landed inconfig-schema.md(the reserve formula omitted the second clamp; a repo-relative command does not resolve for an installed plugin; a model alias was pinned to a version),docs/setup.md(three config blockssetupwrites were absent),docs/health.md(four steps missing from the contract table), and one-line omissions in theask,connect,review,save, andsessionguides.
v1.2.16
Added
context-window.py --explain— why the autocompact nudge is quiet, and what to set.hooks.autocompactNudgefails silently by design: when the window cannot be established it stays quiet, which is indistinguishable from "nothing to warn about". The new read-only diagnostic says which case you are in, names the source the window came from (env/settings/cache/model-default), and reports the threshold that follows from it.healthruns it (new Step 7.55, only when the nudge is enabled) andsetuppasses on its hint when the user turns the nudge on.- It answers the two questions people get wrong. The threshold is not the window: Claude Code compacts at
window − min(max_output, 20000) − 13000, so asking for compaction at 460000 means setting 493000 — setting 460000 compacts at 427000. And a value above the model's context window does nothing: on a 200k model460000resolves to200000, which looks identical to a working setting. Both are reported explicitly, the second as aclamped: trueflag.
Changed
- mnemo still ships no default for
autoCompactWindow, and this is now written down as a decision rather than an omission. It is Claude Code's setting, not mnemo's; measured against the model table in the runtime, 8 of 14 models have a 200k window where any larger value has no effect at all, while on the 6 native-1M models a "sensible" value would halve someone's usable context without being asked. mnemo reports what to set and never writes it.
v1.2.15
Fixed
- CI ran 7 of the 18 test suites.
.github/workflows/skill-lint.ymlnamed each suite as its own step, so a test that shipped with a feature was never executed until someone remembered to add a step —test-context-window.pyandtest-autocompact-nudge.py, both released in 1.2.14, had never run in CI at all. It now loops overscripts/test-*.py, and fails when the glob matches fewer than 10 files, because a glob that matches nothing would otherwise report a green gate on zero tests. docs/review.mdclaimed the session scan "never falls back to another task". It does: with no runtime session id it borrows the newest Codex rollout sharing the working directory. 1.2.14 made that visible with aSOURCE:line rather than removing it, so the doc was the only place in the repo directly contradicting the code. Thereviewskill now also checks that line and treats a borrowed scan as unavailable instead of auditing another task's work as your own.- The gate command list lived in three hand-kept copies (
TESTING.md,CONTRIBUTING.md,docs/codex.md), each naming a different subset and two missingverify-release.py.TESTING.md§ Automated gate is now the single canonical list and uses the same glob as CI; the other two point at it. config.example.jsonwas missing keyssetupwrites —handoff,hot,review.full,hooks.hotDigest— while README tells you to copy that file as your working config. The full schema inconfig-schema.mdhad the same gap, and its "defaults are: …" line read as exhaustive while omittinghotDigest=true.README.md's project tree had drifted: nomnemo-autocompact-nudge.sh, nocontext-window.py, nodepth-contract.md/vault-conventions.md. The README config example also carried nohooksblock at all, so a reader never learned the nudges exist or that both blocking ones are opt-in.head -32still truncated the session scan.render()reaches 43 lines at worst — 44 with aSOURCE:line — andWORTH_SAVING_SIGNALSprints last, so it was lost on an active session. Raised tohead -44.- The CHANGELOG entries for 1.2.13 were in Russian, which put them into the body of a public GitHub Release. Translated; the surrounding trigger-phrase quotes stay as they are.
Changed
TESTING.mdgains manual checks V19-V23 for the 1.2.14 session-id fix and the autocompact nudge, and its header now tracks the current version (it read v1.2.6).docs/BACKLOG.mdgains a P4 card forPreCompactas a second leg toautocompactNudge, recording why it was deliberately not used as a replacement: atPreCompacttime the agent is not running, so it cannot invoke the close-out the nudge exists to trigger. Filed so the option stays visible instead of being re-proposed as new. The card also notes thatlint-skills.pyhas no[[wikilink]]rule — two links to private notes reached a public PR during the 1.2.14 review and were caught by hand.
v1.2.14
Added
hooks.autocompactNudge— warn before autocompact silently drops raw context. New opt-in (default false) Stop hookhooks/mnemo-autocompact-nudge.shblocks once per severity level as the session closes in on the point Claude Code compacts (warn ~50k / critical ~10k tokens remaining) and recommends/mn:review --full. The window follows Claude Code's own chain — envCLAUDE_CODE_AUTO_COMPACT_WINDOW,settings.autoCompactWindow, then its per-model~/.claude.jsoncache read by the active model's key — validated to the[100k, 1M]range Claude Code accepts, then clamped to the model's context window, because a configured value is a ceiling request and not the threshold: a 460k setting on a 200k session never takes effect. With nothing configured the standard 200k window applies only where it is provable — when Claude Code's 1M-access cache shows the account has no large-context access — so a normal install gets a working nudge with no configuration, while a large-context account stays silent rather than be warned at 15% full (the transcript records the model id without its[1m]suffix, so the two are indistinguishable). Bands are measured from where compaction actually happens,W − min(max_output, 20k) − 13k; measured fromW,criticalwould sit past a point the session can never reach. The anti-loop marker lowers when the level drops, so a second compaction in the same session still warns — a compaction changes neithersession_idnor transcript path. Silent whenautoCompactEnabledis false, when save and session have both already run (the same gatestopNudgeuses, so the two hooks never nag together), and when usage exceeds the resolved window, which can only mean the window is wrong. Claude Code only — no-op on Codex. Seedocs/design-decisions.md, "Proactive nudges via hooks".
Fixed
- Session scans read the session id Claude Code actually exports.
session-scan.pylooked forCLAUDE_SESSION_ID, which Claude Code never sets in a child process — it only expands that name as a${...}placeholder inside skill text. Every model-invoked scan therefore fell through to the newest Codex rollout sharing the cwd and rendered it as the caller's own session, or, with no Codex installed, reportedSESSION_ID: not availableand leftsessionandreviewworking blind. It now readsCLAUDE_CODE_SESSION_IDfirst (CLAUDE_SESSION_IDstays a fallback), and prints aSOURCE:line whenever a scan does borrow an unattributed Codex rollout, so borrowed numbers can never pass for your own. - The prewarm cache is finally read back. The warm is keyed on
(jsonl, session_id); because the hook seeded the legacy name and the model-invoked scan resolved a different one, the two keys diverged and the warmed entry was never hit — soTESTING.md's "first review is near-instant thanks to prewarm" was untrue on Claude Code. The hook now seeds the canonical name, and its run gate recognises a session carried only by the environment (hook stdin withoutsession_id) instead of skipping the warm silently. - Cross-runtime recall detects Claude Code from the bash-tool environment.
runtime-memory.pyrecognised onlyCLAUDE_SESSION_ID/CLAUDE_PLUGIN_ROOT, neither of which exists there, so an omittedruntimeplaceholder degraded quietly to zero hits. It now also acceptsCLAUDE_CODE_SESSION_IDandCLAUDECODE. Related:CODEX_HOMEno longer outranks a live Claude session — it only names a config directory, so exporting it no longer makes a Claude Code user look like a Codex one; live thread markers still win.
v1.2.13
Changed
- The handoff index rotates by calendar, not by line count.
handoff.keepDays(default 31) is now the rule;handoff.maxLinesis gone. At the measured pace of a live vault — 7.5 sessions/day, 234 in 31 days — the old 180-line ceiling silently delivered ~24 days under a config that readkeepDays: 31.handoff.maxKB(56, up from 40) is a backstop underneath the window, sized from measurement: 234 lines × ~195 B ≈ 46 KiB, so a normal month fits with ~20% headroom and the note still opens in a single read.handoff.hardCapBytesfolded intomaxKB, so the config carries three knobs where it carried five. - When the window does not fit, the file says so. A month busier than
maxKBallows drops its oldest pointers and states it in one> _overflow:line. A window that silently holds less than it promises is the failure this whole reform exists to remove; it must not reappear as a silent truncation. - Every index line is re-clipped, including ones written by hand. An inline open-item list (
· open 4 (a · b · c) ·) is now recognised and trimmed — measured at 754 B and 881 B on the live vault against a 200 B ceiling. The[[Session — …]]link is still never truncated (median 106 B, max 177 B on that vault): only the project label gives way.
Added
-
relink-orphan-pointers.py— a pointer must have a live target. A migrated block with no[[Session — …]]produced a pointer reading(без session-заметки). That was cosmetic while the index kept everything; under a calendar window it is data loss, because eviction removes the block's last inbound link. Now such pointers are repointed at the exact archive part holding the block (10 of 186 blocks on the live vault, carrying 170 open items). The migration no longer creates them in the first place. -
backfill-tails-from-archive.py— recently-archived tails move into their own session note. Pre-reform sessions wrote unfinished work into the shared handoff, so after migration those tails sat in cold parts the digest deliberately does not read. This is a deliberately narrow revisit of "don't rewrite the user's notes": it appends only genuinely missing items — measured, 105 of 136 recent tails already existed in their note under different wording, so 31 were appended, each marked with its block's date. Coverage is fuzzy on purpose (a literal comparison reports 0% where the truth is 77%), which also makes a second run a no-op. -
The handoff maintenance scripts now ship with the plugin.
migrate-handoff-to-index.py,split-handoff-archive.py,restore-handoff-from-bak.pyand the two above moved from the repo intoplugins/mnemo/scripts/, andhealthStep 7.6 offers them in dependency order when it finds a handoff still in block format. An installed user previously had no migration path at all: their handoff was in block format while the newsessionwrote pointers. -
setup's config template carries thehandoff,hotandhooks.hotDigestkeys, so a fresh install learns about the window, the digest and their bounds instead of silently inheriting defaults. -
docs/design-decisions.md— раздел про handoff, которого не было: почему индекс, что отвергнуто (архивировать сильнее, триаж ради размера, сжатие блоков, переписывание чужих заметок) и почему миграция остаётся отдельной ручной операцией. Non-goalhot.mdпереписан: отвергнут файл-кэш в волте, отгружен эфемерный вычисляемый дайджест.
Fixed
- Откат миграции был no-op.
restore-handoff-from-bak.pyвыбирал бэкап по mtime, а повторный прогон миграции пишет бэкап УЖЕ мигрированного файла — «отмена» восстанавливала индекс поверх индекса. Теперь кандидат обязан выглядеть до-миграционным (содержит## YYYY-MM-DDблоки либо крупнее текущего файла), иначе скрипт требует явный--stamp. - Повторный
split-handoff-archive.py --applyзатирал уже разложенные части —.bakделался только хабу, блоки прошлого прогона исчезали. Теперь при существующих частях скрипт отказывается работать без--force, а с ним бэкапит каждую часть. Плюс размер части проверяется ДО добавления блока (одна часть выходила на 204 819 B при потолке 204 800). handoff-index-upsertмолча терял указатель, если handoff ещё в блочном формате и больше потолка: цикл усечения выбрасывал и только что вставленную строку, возвращаяok. Теперь строка, ради которой сделан вызов, неприкосновенна, а неподъёмный легаси-файл получает явную ошибку «migrate first».migrate --keep-blocks Nпри повторном прогоне дублировал указатели — гейт «блоков не осталось» не срабатывал, пока часть блоков намеренно оставлена.- Существующие строки индекса не переклипывались — правило длины применялось только к новой строке, поэтому запись от старой версии или руками жила вечно (на живом vault — 754 B при потолке 200).
- Шаблон отчёта
healthукрал хвост content-lint: блок📮 Handoffбыл вставлен между заголовком и деталями, и правило о вердиктах читалось как часть handoff. Плюс Step 7.6 честно помечен legacy-only — на мигрированном handoff он видит 0 блоков. - Сиблинги контракта досвипаны:
session-template.md,session/SKILL.md(«unfinished threads become handoff items») и хардкодmax_kb: 40 / keep_days: 14вместо конфиг-плейсхолдеров.
v1.2.12
Changed
- An unfinished tail now belongs to its own session note; the handoff gets a pointer.
sessionStep 5 anddepth-contract.mdpreviously routed open threads into the shared handoff. Measured on a live vault, that left only 9% of fresh open items present in their own session note (34% for older ones) — the handoff had become the sole home of forward state, which is precisely how it reached 805 KiB and then could be neither read nor shrunk. A pointer line is bounded by the number of sessions; a copied tail is bounded by nothing.hot-scan(v1.2.11) collects the tails from where they now live, so the digest keeps working unchanged.
Added
handoff-index-upsertaction invault-write.py— writes one idempotent pointer line per session (- 2026-07-25 · mnemo · open 3 · [[Session — …]]), keyed on the session link so a mid-task checkpoint refreshes its own line instead of appending a twin. This also repairs a contract that was already broken: Step 5 demanded "the exact old section copied from read", but a large handoff read comes back truncated to a preview in which no complete section appears. Bounds —handoff.maxLines(180),handoff.maxLineBytes(200),handoff.hardCapBytes(38912) — are enforced in bytes, and trimming sacrifices the project label, never the[[Session — …]]link (a cut wikilink is a dead link). The block path (archive-handoff,mode=blocks) is untouched, along with its post-v1.1.11 regression suite.scripts/migrate-handoff-to-index.py— one-shot migration,--dry-runby default. Moves blocks verbatim and whole into the archive (never checkbox-extracted: 90 flat bullets and ~200 prose lines on the live file carry live state with no- [ ]), never edits a session note, never deletes, writes.bakfirst, and verifies afterwards that every migrated block is byte-present in the archive. Measured on the live handoff: 826 916 B → 35 910 B (−95.7%), 186 pointer lines.scripts/restore-handoff-from-bak.py— the rehearsed undo. The vault has no version control, so the migration's rollback is written and exercised before the migration may run; a restore itself saves a.pre-restorecopy. Verified end-to-end on a copy of the live file: migrate → restore returns it byte-for-byte (md5 match).scripts/split-handoff-archive.py— splits the cold archive into per-month notes plus a small hub. A cold file is not harmless: at 717 KiB it is past the 256 KB read limit, so "it's still in the archive" was a promise nothing could keep, and it matched nearly every content search while being unopenable. A month is not automatically a readable unit either — June alone was 425 KB — so oversized months split into numbered parts under--max-part-bytes(default 200 KB).- 43 new tests across
test-handoff-index.py,test-migrate-handoff.py, andtest-split-archive.py, pinning the safety contract itself: dry-run writes nothing, blocks move verbatim, nothing is lost, backups precede writes, restore is byte-exact. Mutation-tested: counting characters instead of bytes, or dropping the hard cap, each fails the suite.
v1.2.10
Added
review --fullcan auto-apply connect's links (opt-in,review.full.autoConnect, default false). When the flag is on, the--fullchain'sconnectstep writes its suggested links without a per-suggestion prompt (connect Step 5.5) and reports every write — so closing a session no longer stops on "apply these? (y/N)". The user typing--fulland setting the flag is the consent, mirroringreview.lint.autoStampReviewed: it's the second, default-off exception to the non-destructive principle (docs/design-decisions.md). A standalone/mn:connect/$mnemo:connectnever auto-applies regardless of the flag, and the default install is byte-for-byte unchanged (connect stays suggest-only). Dual-runtime by construction — the behavior lives in the sharedSKILL.mdprose that both Claude Code and Codex read; the Claude-onlymodel:frontmatter is untouched.test-skill-write-contracts.pypins the new invariants (flag default false, standalone never auto-applies, verify pass still never links).
v1.2.9
Changed
- Stop nudge now recommends the one-command close-out
/mn:review --fullinstead of listing/mn:saveand/mn:sessionseparately — aligning the automatic end-of-session reminder with the v1.2.8 one-command close-out (Codex:$mnemo:review --full). The gating is unchanged: opt-inhooks.stopNudge, worth-saving signals ≥3, save/session not yet run, blocks at most once per session.
Fixed
- Stop nudge over-firing in Codex — the anti-loop governor keyed only on the Stop payload's
session_id, which a Codex Stop payload can omit or vary, so the once-per-session marker failed to dedup and the nudge could fire on every Stop. It now falls back toCODEX_THREAD_ID/CODEX_SESSION_IDfor a stable per-thread key. Newscripts/test-stop-nudge.py(7 cases) pins the--fullrecommendation, both runtimes' syntax, once-per-session dedup, the Codex thread-key fallback, and the silence conditions (below-threshold signals, save+session already ran, recursion guard).
v1.2.8
Added
/mn:review --full— one-command session close-out. An explicit--fullflag turnsreviewinto the whole end-of-session ritual with no per-skill prompt: the flag itself is consent (not the implicit autorun removed in v0.16.0 — the user types it). It anchors on the session's origin (reconstructs the first request, measures drift: discussed vs wanted vs did), audits the arc, chains save → session → [focus text] → connect, then runs a read-only verify pass.healthis excluded (heavy — manual). Folds the four manual "be thorough / did we capture everything / what's left / find hidden links" prompts into built-in phases. Plain/mn:reviewis byte-for-byte unchanged (audit + one interactive offer, never auto-runs).references/depth-contract.md— thoroughness by routing, not volume. A standing contract--fullinjects into itssave/sessionsteps (andsessionreads as its default bar): business-logic / pains / how-the-user-thinks route intosave's typedprinciple/pain/stanceatoms, links toconnect, unfinished work to the handoff — never a "capture everything" blob. Encodes depth = structure (the v1.2.7 rule) across the whole close-out.- Grounded, idempotent verify guardrails. The verify pass cites only external facts (git diff, orphans,
session-scan, a Step-0 snapshot), never self-grades; its non-orphan check is binary and never rewards link count (orphans delegate toconnect; verify never links); it REPORTS a missing prod/e2e verification as an unchecked gap (memory-not-CI — it never runs QA); and a re-run on an unchanged session prints "already in order" and stops.
Changed
sessionis thorough-by-routing and self-checks its own note. Step 1 carries a standing depth-routing default (pointing at the depth-contract) so even a standalone/mn:sessionroutes atom-worthy material tosaveinstead of swelling the narrative; Step 7 adds an own-note self-check (dup / MOC / orphan / delegation). The cross-skill palace audit stays inreview --full.review's description gains the--fulltriggers;evals/trigger-eval.jsonadds P9/P10 (positives) + N8 (near-miss);test-skill-write-contracts.pypins the new--fullinvariants (flag = consent not autorun, chain order, grounded-binary-never-links verify, depth-contract routing).
v1.2.7
Added
- First-class "how you think" capture (
principle/pain/stance) —savegains a semantic sub-type for the user's business logic, pains, and decision stance, piggybacking on theinsightrole (notaxonomy_rolesschema change, so it works on every existing vault without re-setup). These are human-authored/confirmed atomic claims — never an agent-generated dossier — recorded with a searchablekind:field so[kind:pain]is enumerable in/mn:ask. - Typed body slots — each note's body now follows a template for its semantic type:
decision→ one Y-statement (context / choice / rejected / goal / trade-off / because);gotcha/ business rule → GIVEN / WHEN / THEN + Because + Fails-when;principle/pain/stance→ JTBD (Job / Pain / Done-well / Anti-goal);fact/insight→ claim-title + BLUF first line + evidence. "Poured-in" polish achieved by structure, not verbosity. - Optional
aliases:retrieval keys — EN/RU synonyms or a short name so a cross-language or short query still reaches an atom (a polyglot-vault search key, not authored content).
Changed
savesplits deep material into atoms, never one blob — a new Step 0b atomicity gate requires a claim-shaped title plus abecauserationale per typed note (hard-gated fordecision/ actionable rule /principle·pain·stance; a soft nudge for a plainfact). Material carrying ≥2 separable claims becomes ≥2 notes plus an optional synthesis, never one exhaustive note. This keeps point-precise retrieval intact and stays inside the human-authored, non-destructive principle — no auto-ingest. Thesavedescription andagents/openai.yamlpresentation are updated with the new triggers;evals/trigger-eval.jsonadds W4 positives and a near-miss.