Skip to content

fix(distill/skills): enumerate every MEMORY*.md; resolve memory dir via the engine probe - #59

Merged
s-annam merged 1 commit into
mainfrom
fix/57-58-enumeration-and-link-probe
Aug 1, 2026
Merged

fix(distill/skills): enumerate every MEMORY*.md; resolve memory dir via the engine probe#59
s-annam merged 1 commit into
mainfrom
fix/57-58-enumeration-and-link-probe

Conversation

@s-annam

@s-annam s-annam commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Two modules were still deriving by hand facts the engine already owns, and lane routing (#53) turned both into silent, unsafe-direction bugs. Both now ask the engine.

Closes #57
Closes #58

#57 — distill reads every MEMORY*.md, not just the root

plugins/memory_distill.py read only the root MEMORY.md and treated every other MEMORY*.md as a durable page. Both assumptions were correct-by-accident before #53:

  • Lane indexes were tokenized as pages. A lane index is a dense bag of every hook in that lane, so feeding it to _page_token_set made load_coverage report almost any topic in that lane as already-covered — and distill suppressed genuine suggestions. Silent, no error, invisible in the output.
  • Hooks resolved only for root-pointed pages. Any page whose pointer now lives in MEMORY-<lane>.md got "" as its hook, so it carried a systematically thinner token set than root-pointed ones. Under Two-tier index: route pointers to lane indexes, make MEMORY.md a map of content #53's default routing that is most pages, so it was the common case rather than the edge case.

Both predicates now delegate to memory_reindex.page_files() / index_files() — the single home for the page-vs-index rule as of #56 — and the local rich-link-only regex is replaced by read_lines() + parse_pointers(), which also resolves bare - slug.md — hook pointers the old regex missed. _SKIP_PAGE_NAMES was deleted rather than extended, so the local rule cannot drift back.

Four regression tests, verified failing against git show HEAD:plugins/memory_distill.py in a scratch tree — not by reasoning. The headline one fails exactly as predicted: assert 5 == 3, with the two extra sets being the lane indexes.

#58 — curate resolves its memory dir via the engine probe

skills/okfmem-curate/SKILL.md Phase 1 derived the dir with SLUG=$(pwd -P | sed 's|/|-|g'). That replaces only the separator, but encode_root also encodes the Windows drive colon (C:\Users\you\okfmemC--Users-you-okfmem), so on Windows the slug matched nothing, Phase 1's own guard fired, and /okfmem-curate could not run at all without an explicit path argument — while reporting a missing directory when the directory existed and the slug was wrong. A raw cwd-to-slug transform also can't see the registry, so renames broke on POSIX too.

It now calls okfmem init --project-link-state (shipped #48), which resolves the name through the registry. Each probe state gets its own message; unlinked names okfmem init as the remedy. The explicit-path escape hatch is unchanged.

Caught by pre-PR adversarial review, fixed here

  • The probe block collapsed to the store's projects root. awk '{print $2}' yields empty for not-a-repo, no-claude, and for okfmem off PATH — a configuration install.sh explicitly supports. MEM_DIR became $STORE/projects/, a real directory that passes any -d guard, so Phases 2–3 reported on the wrong tree and Phase 4 asked the user to approve deletions derived from it. The same block in skills/okfmem-save/SKILL.md had the identical defect with a larger blast radius, because okfmem-save writesSTATE.md and memory pages would have landed in the projects root, where nothing auto-loads them. Fixed in both: read -r STATE NAME (so a bare state word can never become a project name, which ${LINK_STATE#* } would have allowed), two separate probe assignments, the dir assigned only on the linked branch, and a fifth "engine unreachable" table row. Every downstream call site carries a ${VAR:?...} guard so an unresolved dir refuses instead of failing open — reindex --report "" otherwise auto-resolves to the cwd project and prints a plausible report.
  • Collateral from that fix: the dir is now the symlink's target rather than the symlink, so the unchanged readlink "$MEM_DIR" git-recoverability probe printed nothing and exited 1 on every correctly linked store. Curate would have told the user their deletions were unrecoverable 100% of the time on the happy path, including in Phase 4's hard approval gate — the same shape as Save-state badge stuck 'unsaved': SAVE_BASH_RE misses path- and interpreter-prefixed okfmem sync #46/fix(badge): recognize path- and interpreter-prefixed okfmem sync as a save #47, where a benign-direction misfire firing always killed the signal. Replaced with git -C "$MEM_DIR" rev-parse --show-toplevel, which tests the property instead of a proxy and also covers the explicit-path escape hatch that readlink never handled.

Test plan

  • python3 scripts/check-leaks.py exits 0 — 69 tracked files, no leaks; added prose eyeballed for private strings the gate can't judge (only ~/okfmem, ~/okfmem-store, $OKFMEM_STORE placeholders)
  • python3 -m pytest tests/318 passed / 1 skipped, unchanged across every review round
  • ruff check clean on both changed Python files (ruff format --check flags both; confirmed pre-existing on HEAD, advisory in CI)
  • The shipped Phase 1 fenced blocks were extracted programmatically from the markdown and executed under zsh 5.9 and bash 3.2 across linked / unlinked / not-a-repo / no-claude / off-PATH / engine-unreachable / bare linked with no name / a project name containing a space
  • Guarded engine import verified in all three branches, including plugins/ copied outside the repo with no engine anywhere — one stderr line, exit 2, no traceback
  • Full OS-matrix verify — the CI gate

Known, deliberate

memory_reindex.NON_PAGE_NAMES is {STATE.md, CONTEXT.md}, while the deleted _SKIP_PAGE_NAMES also listed SESSIONS.md and README.md. Neither is ever seeded into a projects/<name>/ dir (_seed_store_project writes only MEMORY.md + STATE.md; the store's README.md sits at the projects root, which is not scanned), and an old-vs-new enumeration diff across all 14 projects of a real store showed 12 files newly excluded — all genuine lane indexes — and 0 newly included. Re-adding a local filter would reinstate the predicate this PR exists to delete; widening NON_PAGE_NAMES would change reindex --verify orphan semantics and status inventory counts for every other caller. Flagged rather than silently decided.

Provenance

Stage Model Effort
Implementation — #57 (distill enumeration) Claude Opus 5 ultra
Implementation — #58 (curate link probe) Claude Sonnet 5 medium
Adversarial review — round 1 Claude Opus 5 high
Review fixes — round 1 Claude Opus 5 ultra
Adversarial review — round 2 Claude Opus 5 high
Review fixes — round 2 + orchestration + PR Claude Opus 5 session default
Verification CI verify pending

Adversarial review

Two bounded rounds, reviewer independent of the implementers and prompted to break the change rather than bless it. Both rounds returned clean: false.

Round 1 — 1 blocking

skills/okfmem-curate/SKILL.md — the probe collapsed MEM_DIR to the store's projects root. awk '{print $2}' yields the empty string for not-a-repo, no-claude, and for okfmem off PATH. Reproduced end-to-end: env PATH=/usr/bin:/binLINK_STATE=[]MEM_DIR=[$STORE/projects/] → directory exists, guard passes, curate proceeds against the whole store. A regression — pre-change the sed resolved correctly on POSIX in that case. The reviewer also found the identical block in skills/okfmem-save/SKILL.md, where the blast radius is larger because that skill writes STATE.md and pages.

Fixed in both files. The fix subagent tested and then rejected all three parts of the reviewer's suggested snippet: ${LINK_STATE#* } returns a single-word value unchanged, so not-a-repo would have become the project name — replaced with read -r STATE NAME, which leaves NAME empty instead. $(a || b) became two separate assignments so an error string cannot concatenate onto a valid state. The dir is initialised empty and assigned only on the linked branch. Verified by extracting the fenced blocks from the shipped markdown and executing them against fake engines across 8 conditions.

Round 2 — 1 blocking (collateral from the round-1 fix)

skills/okfmem-curate/SKILL.md — the git-recoverability probe went dead and lied at the deletion gate. The fix changed MEM_DIR from the harness symlink to the symlink's target, but the following line was still readlink "$MEM_DIR", which prints nothing and exits 1 on a real directory. Reproduced: new path → readlink exit=1 (skill concludes "not git-backed"), old path → prints the target, ground truth → the dir is inside ~/okfmem-store. Per the skill's own prose that means every curate run on a correctly linked store told the user recovery was harder, and Phase 4's hard approval gate carried "this is not recoverable via git" — a benign-direction misfire firing 100% of the time, the same shape as #46/#47.

Fixed with git -C "$MEM_DIR" rev-parse --show-toplevel — the property, not a proxy — which also covers the explicit-path escape hatch readlink never handled. Verifying that fix surfaced a second fail-open: git -C "" rev-parse returns the cwd repo, so an unresolved dir would have reported the wrong repository as the recovery mechanism. Hence the ${VAR:?...} guard at all 8 downstream call sites (6 curate, 2 okfmem-save), which also closes the reviewer's related finding that reindex --report "" auto-resolves to the cwd project and prints a plausible report.

Verified rather than trusted

Round 2 independently re-checked round 1's clears rather than inheriting them: lines[p.line - 1] is aligned by construction (read_lines splits on b"\n", parse_pointers sets lineno = i + 1 over that same list, rstrip("\r") preserves length); index_files() returns root-first by construction, so setdefault's root-wins precedence is a guarantee rather than filesystem luck, and it is an improvement over the old last-wins; syntax == "external" can never match a basename, so the skip cannot drop a real hook; _find_engine_root() probes the local root first, so $OKFMEM_ENGINE/~/okfmem cannot silently supply a different engine than the dispatcher intends.

Both rounds checked the Windows/platform-literal class individually per assertion — the class that bit this repo on #14/#16/#17, #41, #42, and three times on #56. Clean: _write pins newline="\n", every fixture path uses os.path.join, expected counts derive from page_files() rather than literals, and there is no byte-count assertion in the added code.

Surviving nits — for the human reviewer

  1. skills/okfmem-curate/SKILL.md:25 still describes the target as ~/.claude/projects/<project-slug>/memory/, the pre-change path shape. Same directory via the symlink, so not wrong, but stale next to the new Phase 1.
  2. plugins/memory_distill.py:78-82 — the guarded-import message names memory_reindex.py while except ImportError catches any transitive failure, so the sentence can pair two different module names. The real exception text is interpolated, so it stays diagnosable.
  3. plugins/memory_distill.py — the engine root is now inserted only when absent from sys.path, which leaves plugins/ ahead of it rather than behind. No colliding module name exists today.
  4. plugins/memory_distill.py:238 — a bare pointer whose hook contains an inline link keys both targets to the same line, so the linked page inherits a hook that is not its own. Heuristic-only, and the regex it replaced had the same class with last-wins instead of first-wins.
  5. skills/okfmem-curate/SKILL.md:60-63 claimed the engine-path hedge matched Phase 2's; Phase 2 in fact uses only the python3 ~/okfmem/okfmem form. Comment corrected in this PR.

Follow-up, not in this PR

The reviewer found the same restated predicate #57 exists to delete in two engine modules the issues did not name: memory_backfill.py:36 and memory_consolidate.py:49 both carry a local SKIP_NAMES = {"MEMORY.md","STATE.md","CONTEXT.md"}. Reproduced: 11 real lane indexes on a live machine pass that filter and reach process_file, which would stamp decay frontmatter into a lane index; consolidate would then become eligible to decay/archive it. Latent today only because both bail on files with no frontmatter. Same one-line fix (page_files(d)), but engine code with different blast radius — filed separately rather than expanded into this PR.

…ia the engine probe

Two modules were still deriving, by hand, facts the engine already owns — and
lane routing (#53) turned both into silent, unsafe-direction bugs.

`plugins/memory_distill.py` read only the root `MEMORY.md`. A lane index is a
dense bag of every hook in that lane, so feeding it to `_page_token_set` made
`load_coverage` report almost any topic as already-covered and distill suppressed
genuine suggestions; separately, any page whose pointer moved to a lane index got
an empty hook string and a systematically thinner token set. Both predicates now
delegate to `memory_reindex.page_files()` / `index_files()`, and the local
rich-link-only regex is replaced by `read_lines()` + `parse_pointers()`, which
also resolves bare `- slug.md — hook` pointers.

`skills/okfmem-curate/SKILL.md` derived its memory dir with
`SLUG=$(pwd -P | sed 's|/|-|g')`. That replaces only the separator, but
`encode_root` also encodes the Windows drive colon, so curate could not run at
all on Windows without an explicit path argument — and it blamed a missing
directory rather than its own slug. It now asks `okfmem init
--project-link-state`, which resolves the name through the registry and so
handles renames on POSIX too.

Pre-PR adversarial review caught two further defects, both fixed here: the probe
block collapsed the memory dir to the store's projects root whenever no name was
printed (including when `okfmem` is off `PATH`, a supported install), in curate
and in the okfmem-save block it was ported from — where the same collapse would
have written `STATE.md` and pages into the projects root; and the follow-on fix
left `readlink` testing a symlink that is no longer the resolved path, which
reported "not recoverable via git" on every correctly linked store.

- plugins/memory_distill.py: page/index enumeration + hooks via memory_reindex;
  _SKIP_PAGE_NAMES deleted; guarded engine import
- tests/test_distill.py: lane-routed store fixture + 4 regression tests
- skills/okfmem-curate/SKILL.md: engine probe, per-state branching, git-backed
  check via rev-parse, ${VAR:?} guards at every call site
- skills/okfmem-save/SKILL.md: same probe hardening (write path)

Closes #57
Closes #58
@s-annam
s-annam merged commit 204cba5 into main Aug 1, 2026
3 checks passed
@s-annam
s-annam deleted the fix/57-58-enumeration-and-link-probe branch August 1, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant