v4.5.0
Fixed
-
execa 10 reverted to ^9.6.1 — it broke the kit's own Node 20 floor. The Dependabot
bump (#83) landed even thoughexeca@10declaresengines.node >=22and calls
Set.prototype.union(Node 22+) at import time, crashing every execa-importing test
file on CI's Node-20 leg (TypeError: TEXT_ENCODINGS.union is not a function) — the
exact regression that leg exists to catch. All four workspaces pinned back to^9.6.1
and the stale nestednode_modules/execa@10lockfile entries deduped away. execa can
ride to 10 when the kit'senginesfloor moves to 22 — deliberately, not via a bump. -
The token-saver's Bash compaction was provably losing diagnostic DETAIL. The new
adversarial-fixture gate (test/compact-diagnostics.test.mjs: real 650–1,100-line
failure logs with the decisive error buried mid-stream) caught it before any live A/B:
the rescue pass kept lines matching the diagnostic regex but dropped their neighbors —
theTS2339:detail under anERROR in …header, thestate.go:161stack frame under
WARNING: DATA RACE.compactTextnow rescues context blocks (1 line before,
3 after each match, overlaps merged,[...]separators, cap raised 40→60 lines) so a
diagnosis-from-compacted-output is possible. The gate stays in CI to keep it that way. -
Docs no longer under-report the tool surface. The installer
--help,ARCHITECTURE.md
anddocs/{en,es}/how-it-works.mddescribed obscura-web as 2 tools (it registers 8 —
obscura_fetch/_fetch_many,obscura_search,obscura_research+_start/_status/_stop,
obscura_consolidate) and vkm-downloads as 2 (it registers 6 — addsprobe_mirrorsand
download_start/_status/_cancel). The hybrid MCP README now carries the authoritative
22-tool table, and a newtool-doc-drift.test.mjsgate parses the registrations in
hybrid-mcp.mjsand fails the build if the README table and the code ever diverge again —
same philosophy as the schema-budget gate: doc drift a reader can't detect becomes a red build. -
Residual "v3 kit" self-descriptions in
docs/{en,es}/faq.mdandtroubleshooting.mdupdated
to the v4 identity (historical version anchors like "since v3.8.1" are kept — they're history,
not drift). ADR-0046's body no longer claims a "Proposed" status for a deletion that shipped in
4.0.0, and the ADR index now surfaces ADR-0050's amendment instead of a bare "Accepted". -
A tagged release published to npm having run zero tests.
ci.ymltriggers only on
push-to-main andpull_request, and neither fires on a tag push — sorelease.ymlreached
npm publishgated on nothing but the version, changelog-section and license-mirror checks.
workflow_dispatchwas worse: it releases whatevermaincurrently is, equally untested.ci.yml
is nowworkflow_call-able andrelease.ymlrequires the whole matrix at the ref being released. -
A skipped
npm-publishjob reports green, and the guard that would have said so is inside it.
The job's condition matched the fullowner/name, so a repo rename silently disarmed publishing
while the workflow still reported success — the exact failure that shipped 4.2.0 and 4.3.0 late,
and this repo has already been renamed once (obsidian-memory-kit→create-vkm-kit). Now keyed
on the owner.releasealso takes aconcurrencygroup (never cancel-in-progress — aborting
mid-publish is worse than queueing) so two dispatches cannot race the samenpm publish. -
markdownlint never saw any file inside a dot-directory.
**/*.mddoes not match them, so
.agents/rules/*.md,.continue/rules/*.mdand.github/PULL_REQUEST_TEMPLATE.mdwere silently
unlinted — which is also why.markdownlintignorelisted a.githubfile the glob could never
have reached. All ten were already clean; the gate now actually covers them. -
The
basic-memorypin CI verifies is no longer hand-copied.scripts/mcp-smoke.mjsimports
BASIC_MEMORY_VERSIONinstead of repeating the literal, so the smoke test can no longer certify a
server users never receive — the drift its own comment warned about but did not prevent. -
The
agent.tomlschema check no longer rides on an unpinned interpreter.tomllibis stdlib
only from 3.11 and thelintjob had nosetup-python; it is now pinned to 3.12 like the other
Python jobs. The network-boundlinksandmcp-smokejobs get timeouts instead of the 6h default. -
engines: node >=20was declared in five manifests, advertised in the README badge, and
verified nowhere — every CI job pinned Node 24 and there is no runtime guard.test-nodenow
includes one ubuntu leg on the declared floor, enough to keep the claim honest without tripling
the matrix. -
Latent test flakes, fixed at the mechanism rather than by widening a tolerance.
hybrid-mcp.test.mjs'scleanup()restoredBASIC_MEMORY_HOMEbut never closed the client or
server, leaking a connected pair per call (~15 per run); since the vault is resolved lazily from
the environment on every call, a surviving server that handled anything after the restore would
resolve against the ambient vault — the developer's real one. It now closes both, before
restoring the env. Andollama-resources.test.mjsassertedelapsed < 1000msagainst a closed
port as a proxy for "it short-circuited" — a proxy a loaded runner, a GC pause, or a firewall
that DROPs rather than RSTs can blow. It now pointsensureOllamaServerat a reachable fake
Ollama and asserts it still returns false: an implementation that probed would find
/api/versionanswering and return true, so only a real short-circuit passes. Deterministic, and
a stronger claim than the timing check ever made. -
The installer reported its own version as
v2 / v3. A hardcoded banner string on a 4.x kit —
the one place every user sees a version was the one place guaranteed to be wrong. It now prints
readKitVersion(), already the source of truth for--check-updateand the sidecar's
kitVersion. Relatedly,--version/-vused to fall through to the interactive wizard, so
the standard way to answer "which version are you on?" started an install; it now prints the
version and exits. -
The issue and PR templates were from the deleted v1 project, and blocked filing.
bug_report
required a PowerShell version ($PSVersionTable) in a repo with no.ps1file, required a
Cursor version, asked forDoctor.ps1/Vault-Doctor.ps1output (neither exists), and made
"I readdocs/troubleshooting.md" a required checkbox for a path that does not exist — the
real file isdocs/en/troubleshooting.md.feature_requestrequired agreeing that "this is not a
runnable codebase" and that "scripts live inside the prompt as literal text", both false for a
repo with a Go daemon, eight npm workspaces and a Python package. Rewritten around what actually
ships: kit version via--version, the real component list, the six agent surfaces,vkm-doctor
output, and Windows/macOS/Linux rather than Windows-only. -
CONTRIBUTING.md's "local checks" claimed to mirror CI and did not. It omittedversion.mjs check,lint,typecheck,license:sync:check,linkcheck, theagent.tomlparse and the
whole Node test suite, while listingnpx lychee— lychee is a Rust binary, so that command
installs an unrelated npm package. Now thelintjob verbatim and in order, with an explicit
note about which gates (lychee, gitleaks, govulncheck, mcp-smoke, the benches) are CI-only and
why. The PR template's checklist points at it instead of drifting a third variant. -
version.mjscould never fix — or even report — a drifted-ldflagsversion. The Go daemon
carries the kit version twice (var versionand the example-ldflagsin the build comment),
and they were one marker whosereadlooked only at the first. Three things then compounded:
setskips a file whosereadalready matches, so oncevar versionwas right the second
replace never ran again; the "refusing partial write" guard compares the whole file, so a no-op
second replace is invisible whenever the first one changed something; andcheckonly ever
inspected whatreadreturned. The documented build command could print a stale version forever
with bothsetandchecksilent. Now two independent markers — surveyed, checked and written
separately — with a regression test that also pins every marker'sread/writeas inverses, so
a regex that writes a shape its own reader cannot parse can't create self-inflicted drift. -
First query against a cold vault could crash with
database is locked.store.connect()set
PRAGMA journal_mode=WALbeforePRAGMA busy_timeout, but the ordering was never the real
problem: converting a database into WAL takes a brief EXCLUSIVE lock on a sqlite code path
(pagerExclusiveLock) that does not consult the busy handler, sobusy_timeoutdoes not
apply to that one statement however large it is or how early it is set. Measured: with
busy_timeout=3000already in effect, a contended transition raises in 0.000s, while the same
pragma against an already-WAL database returnswalin 0.008s taking no lock at all. That second
measurement is why a bounded retry is the fix and not a band-aid — the only way to lose this race
is another connection converting the same database, so once any one of them wins, every later
attempt is a lock-free no-op and the loop exits immediately. The trigger was never exotic:
assembleContextfans out three rag processes over one vault at once, so the first query against
a fresh index has three processes creating the same database concurrently. It surfaced as a flaky
vkm-specpipeline test in CI, but it was a user-facing cold-start bug, not a test artifact.
connect()now also closes the handle if setup fails partway instead of leaking it. -
The old npm name never forwarded, and four releases of docs said it did.
release.ymlno
longer publishes@vkmikc/create-obsidian-memory, and every user-facing claim about it is
corrected: what the registry actually serves under that name is the last real v3 kit, 3.15.0
(published 2026-07-09, all sixteen versions deprecated with npm's generic message) — the
forwarding shim ADR-0041/0050 designed exists only in-tree and was never published. Two causes,
both invisible from the repo: the granularNPM_TOKENgrants write on the new name only, and npm
answers an unauthorized publish with404 … could not be found or you do not have permission,
which reads like a missing package; and deprecation being per-version means publishing 4.x to
that name would have created a non-deprecated latest, quietly reviving the name the deprecation
retired. The shim package stays in-tree (version-locked, tested) but unpublished; both READMEs,
the package README and both migration guides now say deprecated, frozen on the v3 kit, does not
forward, repoint your scripts. ADR-0050 carries the amendment and the lesson: the source of
truth for what users receive is the registry packument, never the source tree.
Added
- End-to-end smoke (
scripts/e2e-smoke.mjs, new CI jobe2e-smoke): real installer
→ real Python index → real hybrid MCP over stdio → search a seeded fact →
vault_write_file→ reindex → find the written note. The wiring proof no
per-component bench provides. - Latency floor:
bench-recallnow measures per-query search wall-clock (p50/p95/
mean, index build excluded) and supports--assert-p95-ms; CI gates at a loose 500 ms
(measured ~3 ms on the fixture corpus) — an accidental-O(n²) detector, not a hardware
benchmark. - Neural retrieval floor (
.github/workflows/nightly-benchmarks.yml, nightly +
dispatch): the same labelled corpus on the fastembed embedder, with the
pin-failures lever arm — the measured gate the semantic upgrade never had. Floors
provisional until the first green run; then ratcheted in a reviewed PR. - Token-economy ratchet:
--assert-answered0.95 → 1.0 (measured: 100% of
labelled queries answered under passage-first at k=5 — the gate now pins it). VKM_DEFAULT_LIMITenv override for the two search tools' default hit count in
hybrid-mcp.mjs— exists for A/B-benchmarking ADR-0034's 10-hit default
(token-quality eval) without touching schema text; behavior unchanged when unset.- Spec-validator self-test (
test/validate-spec-selftest.test.mjs): the vkm-spec
grader passes a reference good spec and catches six seeded defects by name
(mutation-style), plus the XML envelope path — a grader that can't discriminate
grades nothing. - First live benchmark round, with committed raw data (2026-07-21, Haiku 4.5 +
Sonnet 5 subjects, results under each eval'sresults/2026-07-21-round1/):- Triggering accuracy: 100% hit-rate, 0% false-positives for all four skills on
both models (104 gradings, ES+EN, incl. everynonedistractor). - token-quality-ab #5 (compact-tool-output): delta 0.0 on both models — the
compacted log (~81% smaller) lost zero diagnostic ability vs the raw log (18
gradings/model). Pre-registered verdict: KEEP. Measured on the hook as fixed by
the compact-diagnostics gate — the pipeline caught the defect deterministically and
the live A/B confirms the repair. - discipline-bench (upgraded skill, n=3/cell): explicit-contract task saturates at
100 everywhere (no harm); under-specified task lifts Haiku 47.0 → 91.7 (+44.7)
with Sonnet stable at 83 — the disciplined small model again beats the stock cells.
- Triggering accuracy: 100% hit-rate, 0% false-positives for all four skills on
/vkm-specgrew from a 33-line monolith into a full skill (Anthropic
skill-authoring practices: progressive disclosure, worked example, executable feedback
loop): rewritten SKILL.md with a copyable checklist, trigger phrases in the description
and a degradation ladder;references/spec-template.md— the orchestration template
the description always promised, now an actual file;references/field-guide.md
(weak-vs-strong example per field);examples/worked-example.md(vague idea →
validated, approved spec); andscripts/validate_spec.mjs, a zero-dep validator
(six sections, 3–7 testable requirements, per-constraint source citations or an
explicit(assumption)marker, ≤600-char current_state, ≥2 binary criteria, vague-word
detection) with fix-me error messages — it doubles as the deterministic grader for the
upcoming spec-bench./vkm-disciplinegets executable evidence:scripts/evidence-gates.shdetects and
runs the project's own gates (npm test/lint/typecheck,go test+ gofmt, pytest,
cargo, make test) and prints one pass/fail block — step 5 ("Show it works") now has a
tool instead of prose;examples/dial-examples.mdadds two complete worked passes
(trivial rename vs irreversible table drop) for the dial, the skill's hardest
calibration; description rewritten to third person with trigger terms.- Skills triggering-accuracy eval (
evals/skills-triggering/): 52 labelled ES+EN
prompts (10 should-trigger per skill + 12nonedistractors, including the
obscura_research-vs-/vkm-research near-miss), a runner that builds the listing from the
real template frontmatter, and deterministic grading with per-skill gates (hit ≥ 0.9,
false-positive ≤ 0.1). Modes:--emit-prompts/--gradefor external subjects,
--provider apifor direct runs. - Architecture deep dive (
docs/en/architecture-deep-dive.md+docs/es/arquitectura-a-fondo.md):
the full as-built walkthrough — system flowchart, five per-operation sequence diagrams (recall,
write, close ritual, sync, research), a mind map of the kit's channels, a decision map tracing
every load-bearing behavior to its ADR, the condensed 22+8+6 tool surface, and an ownership map
of who writes what. All 14 mermaid blocks are render-verified; the tool tables are covered by the
tool-doc-drift.test.mjsgate (now checking the deep-dives in both languages, not just the MCP
README). Linked from both READMEs, both doc indexes andARCHITECTURE.md. - Spanish mirrors for the last English-only user docs:
docs/es/observabilidad.mdand
docs/security/mcp-remote-rce.es.md, plus a newdocs/security/README.{md,es.md}index with
the kit's threat model in one paragraph. Both doc indexes now link Observability and Security —
full ES/EN parity across every user-facing doc.
Security
- The shipped daemon was carrying 10 reachable vulnerabilities. Not merely present in the
module graph —govulnchecktraced them to real call paths acrossgo-git/v5(6),circl(2)
andx/crypto(1), plus one Go stdlib advisory. Every one had a published fix. Nothing in the
repo was positioned to notice: Dependabot covered onlygithub-actions, and no vulnerability
scanner ran at all, whileci.yml's banner presented SHA-pinning as the supply-chain control —
which governs how actions are fetched, not what the dependency trees contain.go-git→ v5.19.1
andcircl→ v1.6.3 (lettingx/crypto/x/netfloat up rather than pinning them to their exact
minimums, which silently downgradedgo-gitpast four of its own fixes). Reachable count:
10 → 1, the survivor being a stdlibcrypto/tlsadvisory fixed by the toolchain, not by
go.mod. Agovulncheckjob now gates this — a hard gate, notcontinue-on-error, since a
security check that cannot fail the build is the silent-pass antipattern. - Dependabot now covers all four ecosystems that ship here (
npm,gomod,pip,github-actions)
instead of only the last.
Changed
- Building the daemon from source now needs Go 1.25+ (was 1.22), the floor
go-gitv5.19
requires. Only affects contributors compilingobsidian-memoryd. - The
typecheckgate no longer overstates itself. It advertised "strict TS + checkJs over
shipped JS", but the checkJs half inheritedstrict: falsethroughtsconfig.eslint.json— a
file that self-described as "Not a gate" while being the gate's actualcompilerOptionssource.
Rather than flipstrict: true(measured: 944 errors, 601 of them implicit-anyparameters —
a codebase-wide JSDoc decision, not a fix), every strict sub-flag measured at zero cost is now
enabled explicitly:noImplicitThis,alwaysStrict,strictBindCallApply,strictFunctionTypes,
noImplicitOverride,noFallthroughCasesInSwitch. Each is a ratchet — it pins a property the code
already has. The three still off are recorded with their exact cost,strictNullChecks(37) being
the best next candidate.strictFunctionTypesrequired one real fix:applyUpdatePlan's
writeSidecarImplwas declared as takingmanifest: unknown, which is unsound rather than
lenient — parameters are contravariant, so a wider declared parameter promises callers something
the default impl (which dereferencesmanifest.assets) cannot honour. scripts/mcp-smoke.mjs's@modelcontextprotocol/sdkimport is now a declared root
devDependency. It had been resolving purely by npm hoisting it out of three private
workspaces — dropping or renaming any of them, or installing without hoisting, would have
broken the job withERR_MODULE_NOT_FOUND.scripts/linkcheck.mjsfinally runs in CI. It validates relative links and#anchor
fragments against real heading slugs — coverage the lychee job does not provide — and was
written, wired intopackage.json, and then never invoked by any workflow. Turning it on
immediately caught a cross-platform hazard:CLAUDE.md,.clinerulesand
.github/copilot-instructions.mdare symlinks toAGENTS.mdso each agent tool finds its
own filename, and following one re-resolves AGENTS.md's relative links from the alias's
directory —./docs/en/install.mdbecomes.github/docs/en/install.md. It passed on Windows
and failed only on Linux CI, because a Windows checkout without symlink support stores those
three as ordinary ~12-byte files containing the target path.walk()now skips symlinks: an
alias's links are already validated once at the target's real location, the only place its
relative paths mean anything.