Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{
"name": "mnemo",
"description": "7 memory skills for Claude Code and Codex via Obsidian — vault audit, knowledge search, bounded cross-runtime recall, connection discovery, session handoff, memory routing, review orchestration, and setup",
"version": "1.2.12",
"version": "1.2.13",
"source": "./plugins/mnemo",
"category": "productivity",
"keywords": [
Expand Down
30 changes: 29 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

## [1.2.13] - 2026-07-27

### Changed

- **The handoff index rotates by calendar, not by line count.** `handoff.keepDays` (default **31**) is now the rule; `handoff.maxLines` is 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 read `keepDays: 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.hardCapBytes` folded into `maxKB`, so the config carries **three** knobs where it carried five.
- **When the window does not fit, the file says so.** A month busier than `maxKB` allows 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.py` and the two above moved from the repo into `plugins/mnemo/scripts/`, and `health` Step 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 new `session` wrote pointers.
- **`setup`'s config template carries the `handoff`, `hot` and `hooks.hotDigest` keys**, so a fresh install learns about the window, the digest and their bounds instead of silently inheriting defaults.

- **`docs/design-decisions.md` — раздел про handoff**, которого не было: почему индекс, что отвергнуто (архивировать сильнее, триаж ради размера, сжатие блоков, переписывание чужих заметок) и почему миграция остаётся отдельной ручной операцией. Non-goal `hot.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` вместо конфиг-плейсхолдеров.

## [1.2.12] - 2026-07-25

### Changed
Expand Down Expand Up @@ -965,7 +992,8 @@ Frontmatter now includes `session_id: {CLAUDE_SESSION_ID}` — disambiguates sam
- `config.example.json`
- MIT License

[Unreleased]: https://github.com/jojoprison/mnemo/compare/v1.2.12...HEAD
[Unreleased]: https://github.com/jojoprison/mnemo/compare/v1.2.13...HEAD
[1.2.13]: https://github.com/jojoprison/mnemo/compare/v1.2.12...v1.2.13
[1.2.12]: https://github.com/jojoprison/mnemo/compare/v1.2.10...v1.2.12
[1.2.10]: https://github.com/jojoprison/mnemo/compare/v1.2.9...v1.2.10
[1.2.9]: https://github.com/jojoprison/mnemo/compare/v1.2.8...v1.2.9
Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,18 +266,21 @@ mnemo doesn't force a note structure. Change `taxonomy` to match yours:

## Cross-Session Continuity

The killer feature. When a session ends, `/mn:session` writes a handoff note:
The killer feature. When a session ends, `/mn:session` writes the open tails into that session's own note:

```markdown
## Pending
## Next steps / pending
- [ ] Check orphans after mass note creation
- [ ] Update MOC — AI Research with 3 new notes
```

…and adds **one pointer line** to `Meta — Session Handoff`, which is an index of the last 31 days, not a store:

## Context
- Researched WeChat agent ecosystem, all saved in Session — 2026-03-23
```markdown
- 2026-03-23 · research · open 2 · [[Session — 2026-03-23 WeChat agent ecosystem]]
```

The next session reads this and picks up where you left off.
The next session opens with the **digest**: a SessionStart hook collects still-open tails from recent session notes and injects a byte-capped, project-scoped summary — so you pick up where you left off without anyone having to open a file. (The handoff note itself is never read at startup: past ~256 KB nothing can read it, which is exactly how the old "write the tails into the handoff" contract failed.)

## Requirements

Expand Down Expand Up @@ -319,6 +322,13 @@ mnemo/
│ │ ├── session-scan.py # JSONL parser (Claude + Codex) with incremental cache
│ │ ├── skills-discover.py # Auto-discovery across Claude/Codex skill paths
│ │ ├── review-candidates.py # type-aware staleness scan for /mn:health
│ │ ├── hot-scan.py # open-tails digest injected at SessionStart
│ │ ├── handoff-resolver.py # report-only handoff triage (legacy block format)
│ │ ├── migrate-handoff-to-index.py # one-shot: blocks → pointer index (dry-run default)
│ │ ├── split-handoff-archive.py # cold archive → readable per-month parts
│ │ ├── relink-orphan-pointers.py # pointers with no target → their archive part
│ │ ├── backfill-tails-from-archive.py # archived tails → their own session note
│ │ ├── restore-handoff-from-bak.py # undo for the migration, content-picked backup
│ │ └── check-cm-version.sh # claude-mem cache inspector
│ └── hooks/ # Harness hooks
│ ├── hooks.json # Codex-safe shared SessionStart + Stop baseline
Expand Down Expand Up @@ -515,7 +525,7 @@ cp config.example.json ~/.mnemo/config.json

## Непрерывность между сессиями

Киллер-фича. `/mn:session` записывает handoff-заметку — следующая сессия подхватывает с того места. Больше никакого «а что я вчера делал?»
Киллер-фича. `/mn:session` пишет открытые хвосты в **саму session-заметку**, а в `Meta — Session Handoff` кладёт одну строку-указатель (индекс за последний 31 день, не хранилище). Следующая сессия открывается с **дайджеста**: SessionStart-хук собирает незакрытые хвосты из свежих заметок и инжектит краткую сводку по твоему проекту. Больше никакого «а что я вчера делал?»

## Требования

Expand Down
2 changes: 2 additions & 0 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Priorities: 🔴 P1 must-do · 🟠 P2 valuable · 🟡 P3 nice · ⚪ P4 someda

Nothing is rewritten; the output is a worklist. Natural home is **`mn:health`** as one more check (it already owns report-only detects and the research-gap candidates), which also keeps detects in one house instead of scattering them across skills.

**What already exists — do not rebuild it (added 2026-07-27).** `handoff-resolver.py` (v1.2.11) already ships the *anchor* half of this idea: it extracts Linear keys and PR numbers from open items, separates an item's **own** anchors from ones inherited from its header (inherited counting inflated resolvability 27.6% → 60.6%), and reports a worklist without ever writing. `health` Step 7.6 is its report-only host. So this card's remaining, genuinely new part is narrower than it reads: **the git side** — mapping "paths touched since the note's `date`" to the notes that mention them. Reuse the resolver's anchor parsing and its report shape; do not write a second anchor extractor, and do not widen this card back into "a general staleness detector" — type-aware staleness already ships in `health` Steps 7/7.5.

**On-philosophy.** It is the *report-only half* — the exact pattern already shipped for web-search imputation (declined as a writer, shipped as `health` Step 8.5 research-gap candidates): point at the gap, never fill it. Non-destructive holds (no note is authored or overwritten), human-authored holds (the user judges and edits), in-agent holds (reads the repo mnemo is already running in — no external service, no crawl). Contrast with the ambitious version below, which is where the cost sits.

**Open questions.**
Expand Down
Loading
Loading