v3.11.0
Added
-
Deterministic enforcement hooks, on by default with the Claude Code native-memory
override (ADR-0030). Makes the ADR-0029 doctrine hold for ANY model — old or new,
not just ones that reliably read prose rules:PreToolUseguard (guard-native-memory-write.mjs) — DENIESWrite/Edit/
MultiEdit/NotebookEditcalls that target the native auto-memory directory
(~/.claude/projects/*/memory/), redirecting the model tovault_write_file/
vault_edit_file.Stopnudge (stop-vault-close-reminder.mjs) — when a session did substantive
file work (≥2Write/Edit/MultiEdit/NotebookEditcalls) but never touched the
vault's close-ritual tools, blocks the stop once with a reminder to close out —
with an explicit "ok to skip if nothing's reusable" escape hatch so it never forces
low-value writes. Loop-safe viastop_hook_active.
- Both ship as cross-platform Node scripts (same precedent as the
SessionStarthook),
installed into~/.claude/hooks/and merged idempotently into
~/.claude/settings.json(hooks.PreToolUse,hooks.Stop). - Opt out of just these two with
--no-memory-enforcement(keeps
autoMemoryEnabled:false+ theSessionStarthook); force on under--minimalwith
--memory-enforcement. 19 new tests inclaude-native-memory.test.mjs.
-
Effort-gate hook, on by default with the Claude Code native-memory override,
independent of the enforcement pair above (ADR-0031). Makes a pause real instead of
just announced: aPreToolUsehook (guard-effort-gate.mjs) DENIES a session's 2nd+
substantiveWrite/Edit/MultiEdit/NotebookEditcall until the model has proposed
an effort level (/effort low|medium|high|xhigh|max) and gotten an actual reply from the
user — not just printed a "pausing here" message and kept going. The first substantive
edit of a session is always free (no nagging on one-line fixes), and once satisfied the
gate stays open for the rest of the session. Correctly distinguishes a real user reply
from a tool-result message (Claude Code encodes both astype:"user"), and the marker
text the model must print is taught in-band by the hook's own denial message, no separate
rules-file change needed. Not a token-saving feature — each time it fires it costs at
least one extra turn; its value is letting the user redirect a misscoped task before the
session commits to it, documented as a deliberate trade-off in ADR-0031. Opt out with
--no-effort-gate; force on under--minimalwith--effort-gate. 18 new tests in
claude-native-memory.test.mjs. -
New package
@vkmikc/obsidian-prompt-compiler(obsidian-promptCLI + optional GUI).
Compiles a one-line idea + vault context into an<orchestration_package>XML prompt and
copies it to the clipboard, for pasting into AI tools that don't have the vault's MCP
wired (a web chat, another editor without the wiring). No LLM call: context comes
straight fromvault_observations(already-distilled typed facts) and
vault_hybrid_search, via the same Python bridge the hybrid MCP server uses, with the
search qualified by the project name (+--graph) and snippets capped at 320 chars so a
large real vault's cross-project notes don't drown out the project's own context; when a
project note has no formal observations yet (rich prose instead of- [decision] ...
bullets), falls back to a capped raw excerpt of the note itself rather than coming up
empty. Interactive project picker (PROJECTS/*.mdautocomplete), optional
$EDITOR/$VISUALreview pass before copying, and a reduced XML schema (trimmed from a
generic ~35-tag prompt-engineering catalog down to the leaves that matter for a coding
task — dropped boilerplate self-check tags like<thinking>/<reflect>/<verify>).
Optional GUI (obsidian-prompt-gui, or--install-shortcutfor a hidden-window
Desktop launcher on Windows): a localhost-onlynode:httpserver + a vanilla-JS page
(no framework, no build step — picked over Tauri/Electron for being the lightest/fastest
option) with a live XML preview as you type (debounced) and one-click clipboard copy via
navigator.clipboard. Extractedrag-client.mjsout of
obsidian-memory-mcp/src/hybrid-mcp.mjs(pure refactor, same Python-bridge behavior) so
both packages can reuse it without spawning the MCP stdio server. 34 new tests across the
new package; versioned independently of the rest of the kit (not in
scripts/version.mjs'sMARKERS),private: true, not published.
Fixed
- The sync daemon (
obsidian-memoryd) reported healthy while the vault was
silently not syncing.doctor's alarm keyed only on push failures, so a
failingadd/commit/pull --rebase(e.g. expired git credentials breaking
the rebase) returned an error that was only written to the JSONL log — the
heartbeat stayed fresh, the push counter never moved, anddoctorexited 0
indefinitely while nothing reached the remote.gitSyncnow records the outcome
of the whole cycle: newConsecutiveSyncFailures/LastSyncOK/
LastSyncErrorstate, surfaced bydoctorand alarming at ≥3 consecutive sync
failures (a transient failure that recovers self-clears on the next good sync).
Added regression tests for the pull-failure and stuck-sync paths; normalized the
daemon package withgofmt. npm run setup's verify step now confirms the Python backend actually runs,
not just that the index file exists. Because--install-backend/--build-index
are best-effort, a failed backend install plus a leftoverfts.sqlitepreviously
showed an all-green table while everyvault_hybrid_searchfailed at first use —
the exact "No Python at …" / import-error landmine that leaves memory silently
dead. The new check invokes the backend the way the MCP will
(python -m obsidian_memory_rag --help+ the wiredPYTHONPATH) and points at the
troubleshooting guide when it fails. Fresh-PC quick-check tables gained the same
restart-free CLI check.[[wikilink]]/relation parsing mistook documentation examples for real edges.
audit.py,graphlink.pyandknowledge_graph.pyscanned raw note text for
[[...]]with no Markdown-structure awareness, so a note that documents the
syntax (e.g.`[[target]]`inline, or a fenced snippet showing
- implements [[adr-0014]]) was parsed as a real broken link / real relation.
Newtext_scrub.strip_code_regions(blanks fenced code blocks + inline code
spans, preserving line/char offsets) is now applied before every wikilink/
relation/observation scan, plus a fence-aware heading check inchunking.py
(a#-prefixed line inside a code fence is real code, not a section heading).
Verified against a real ~200k-token vault: false-positive broken links dropped
14→2, bogus relations dropped 40→36. The fence regex needs\r?$, not just
$— CRLF-terminated notes silently defeated the naive version.- Every MCP tool response was pretty-printed JSON (
JSON.stringify(v, null, 2))
— ~15-25% wasted tokens on every search/relations/observations/report call for
an LLM consumer that gets no benefit from indentation. Now compact. session-start-vault-context.mjshad no cap on the_meta/index.mddump it
injects into every session unconditionally — already ~10KB/~2500 tokens on a
modest 69-note vault, and grows with every project added. Capped at 4000 chars
with avault_read_filepointer for the rest; also added the standard
isEntryPointimport guard (was missing, unlike every other managed hook).vault_read_filehad no size cap on a whole-file read (nohead/tail) —
one large note could return unbounded tokens. Default cap 200,000 chars
(configurable viamaxChars), with a truncation notice pointing at head/tail.memory_extract_candidatesran its per-bullet dedup lookups serially (one
Python subprocess spawn at a time) and silently swallowed backend failures
as "no duplicate found." Now parallelized (Promise.all) and surfaces a
backendErrorper candidate so a broken index/Python env can't masquerade as
a confirmed-new bullet.build_report(duplicates=True)instantiated the embedder twice — once in
ensure_freshto actually build vectors, again just to read.name(a second
ONNX model load whenfastembedis the active embedder). New
resolve_embedder_name()computes the identity string without constructing
the model.audit_vault'soversized/broken_linkslists had no cap, unlike the
rest of the memory-report hygiene indices (stale_notes/orphan_notes/
hub_noteswere already capped) — a messy vault could return hundreds of
entries verbatim. Capped (default 100) with an accompanying_totalcount.
Documentation
- New troubleshooting section — "Hybrid search and the Python backend (Claude
Code)" (docs/en/troubleshooting.md+docs/es/troubleshooting.md), with a
Mermaid decision tree and a two-command health check (claude mcp list+
a livevault_hybrid_search). Covers the Claude-Code-specific failure modes the
previous docs (Cursor-centric) didn't: the hybrid MCP missing/Failed to connect, an orphaneduvPython venv (No Python at …), an unbuilt index, and
an MCP registered against a stale second clone. ARCHITECTURE.mdand the "how it works" guides updated to reflect the code-span
scrubbing, the compact-JSON tool responses, and thevault_read_filesize cap.