Skip to content

v4.5.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 20:38
16c9a74

Fixed

  • execa 10 reverted to ^9.6.1 — it broke the kit's own Node 20 floor. The Dependabot
    bump (#83) landed even though execa@10 declares engines.node >=22 and 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 nested node_modules/execa@10 lockfile entries deduped away. execa can
    ride to 10 when the kit's engines floor 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 —
    the TS2339: detail under an ERROR in … header, the state.go:161 stack frame under
    WARNING: DATA RACE. compactText now 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
    and docs/{en,es}/how-it-works.md described 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 — adds probe_mirrors and
    download_start/_status/_cancel). The hybrid MCP README now carries the authoritative
    22-tool table, and a new tool-doc-drift.test.mjs gate parses the registrations in
    hybrid-mcp.mjs and 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.md and troubleshooting.md updated
    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.yml triggers only on
    push-to-main and pull_request, and neither fires on a tag push — so release.yml reached
    npm publish gated on nothing but the version, changelog-section and license-mirror checks.
    workflow_dispatch was worse: it releases whatever main currently is, equally untested. ci.yml
    is now workflow_call-able and release.yml requires the whole matrix at the ref being released.

  • A skipped npm-publish job reports green, and the guard that would have said so is inside it.
    The job's condition matched the full owner/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-kitcreate-vkm-kit). Now keyed
    on the owner. release also takes a concurrency group (never cancel-in-progress — aborting
    mid-publish is worse than queueing) so two dispatches cannot race the same npm publish.

  • markdownlint never saw any file inside a dot-directory. **/*.md does not match them, so
    .agents/rules/*.md, .continue/rules/*.md and .github/PULL_REQUEST_TEMPLATE.md were silently
    unlinted — which is also why .markdownlintignore listed a .github file the glob could never
    have reached. All ten were already clean; the gate now actually covers them.

  • The basic-memory pin CI verifies is no longer hand-copied. scripts/mcp-smoke.mjs imports
    BASIC_MEMORY_VERSION instead 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.toml schema check no longer rides on an unpinned interpreter. tomllib is stdlib
    only from 3.11 and the lint job had no setup-python; it is now pinned to 3.12 like the other
    Python jobs. The network-bound links and mcp-smoke jobs get timeouts instead of the 6h default.

  • engines: node >=20 was 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-node now
    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's cleanup() restored BASIC_MEMORY_HOME but 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. And ollama-resources.test.mjs asserted elapsed < 1000ms against 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 points ensureOllamaServer at a reachable fake
    Ollama and asserts it still returns false: an implementation that probed would find
    /api/version answering 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-update and the sidecar's
    kitVersion. Relatedly, --version / -v used 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 .ps1 file, required a
    Cursor version, asked for Doctor.ps1 / Vault-Doctor.ps1 output (neither exists), and made
    "I read docs/troubleshooting.md" a required checkbox for a path that does not exist — the
    real file is docs/en/troubleshooting.md. feature_request required 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 omitted version.mjs check, lint, typecheck, license:sync:check, linkcheck, the agent.toml parse and the
    whole Node test suite, while listing npx lychee — lychee is a Rust binary, so that command
    installs an unrelated npm package. Now the lint job 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.mjs could never fix — or even report — a drifted -ldflags version. The Go daemon
    carries the kit version twice (var version and the example -ldflags in the build comment),
    and they were one marker whose read looked only at the first. Three things then compounded:
    set skips a file whose read already matches, so once var version was 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; and check only ever
    inspected what read returned. The documented build command could print a stale version forever
    with both set and check silent. Now two independent markers — surveyed, checked and written
    separately — with a regression test that also pins every marker's read/write as 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=WAL before PRAGMA 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, so busy_timeout does not
    apply to that one statement however large it is or how early it is set. Measured: with
    busy_timeout=3000 already in effect, a contended transition raises in 0.000s, while the same
    pragma against an already-WAL database returns wal in 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:
    assembleContext fans 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-spec pipeline 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.yml no
    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 granular NPM_TOKEN grants write on the new name only, and npm
    answers an unauthorized publish with 404 … 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 job e2e-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-recall now 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-answered 0.95 → 1.0 (measured: 100% of
    labelled queries answered under passage-first at k=5 — the gate now pins it).
  • VKM_DEFAULT_LIMIT env 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's results/2026-07-21-round1/):
    • Triggering accuracy: 100% hit-rate, 0% false-positives for all four skills on
      both models (104 gradings, ES+EN, incl. every none distractor).
    • 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.
  • /vkm-spec grew 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); and scripts/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-discipline gets executable evidence: scripts/evidence-gates.sh detects 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.md adds 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 + 12 none distractors, 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/--grade for external subjects,
    --provider api for 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.mjs gate (now checking the deep-dives in both languages, not just the MCP
    README). Linked from both READMEs, both doc indexes and ARCHITECTURE.md.
  • Spanish mirrors for the last English-only user docs: docs/es/observabilidad.md and
    docs/security/mcp-remote-rce.es.md, plus a new docs/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 — govulncheck traced them to real call paths across go-git/v5 (6), circl (2)
    and x/crypto (1), plus one Go stdlib advisory. Every one had a published fix. Nothing in the
    repo was positioned to notice: Dependabot covered only github-actions, and no vulnerability
    scanner ran at all, while ci.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
    and circl → v1.6.3 (letting x/crypto/x/net float up rather than pinning them to their exact
    minimums, which silently downgraded go-git past four of its own fixes). Reachable count:
    10 → 1
    , the survivor being a stdlib crypto/tls advisory fixed by the toolchain, not by
    go.mod. A govulncheck job now gates this — a hard gate, not continue-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-git v5.19
    requires. Only affects contributors compiling obsidian-memoryd.
  • The typecheck gate no longer overstates itself. It advertised "strict TS + checkJs over
    shipped JS", but the checkJs half inherited strict: false through tsconfig.eslint.json — a
    file that self-described as "Not a gate" while being the gate's actual compilerOptions source.
    Rather than flip strict: true (measured: 944 errors, 601 of them implicit-any parameters —
    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. strictFunctionTypes required one real fix: applyUpdatePlan's
    writeSidecarImpl was declared as taking manifest: unknown, which is unsound rather than
    lenient — parameters are contravariant, so a wider declared parameter promises callers something
    the default impl (which dereferences manifest.assets) cannot honour.
  • scripts/mcp-smoke.mjs's @modelcontextprotocol/sdk import 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 with ERR_MODULE_NOT_FOUND.
  • scripts/linkcheck.mjs finally 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 into package.json, and then never invoked by any workflow. Turning it on
    immediately caught a cross-platform hazard: CLAUDE.md, .clinerules and
    .github/copilot-instructions.md are symlinks to AGENTS.md so 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.md becomes .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.