Repository navigation
Releases
v1.36.0
Added — confidence write-back from grounding reconciliation (#364, Phase 2 of #245)
The Phase-1 mk grounding report (read-only, #245) gains its destructive companion: mk grounding --apply writes a reconciled confidence back onto the actionable review/promote atoms. Gated on human_edit events (#247) — without that signal a blind write-back could clobber a confidence a human deliberately set. MINOR (new flags + exported engine).
- Engine
src/reconcile.ts(reconcileGrounding,reconciledConfidence): reusescomputeGrounding/classifyQuadrant(no re-derived scoring). The reconciled value is an asymmetric convex pull of the prior toward the grounding score —reconciled = clamp(prior + α·(grounding − prior), 0, 1)— with α_neg = 0.08 > α_pos = 0.03 (SmartVector, arXiv 2604.20598) so disconfirmation (review: pull down) moves faster than confirmation (promote: pull up). The small step means repeated passes converge an atom toward its grounded value and stop once it crosses the quadrant threshold, rather than snapping onto the diagonal. - Provenance-aware skip: atoms carrying a
human_editevent are skipped (human-asserted confidence) unless--override— this is the dependency on #247. Each write emits anatom_reconciledaudit event (added toEVENT_ACTIONS+MUTATION_ACTIONS; carries the post-write snapshot, so replay reconstructs it and compaction keeps the latest).--dry-runpreviews; onlyreview/promoteatoms that pass the actionability guards and aminDeltafloor (0.005) are written. - CLI (
mk grounding --apply [--dry-run] [--override] [--agent-id] [--session-id]):--apply --jsonemits{ scanned, candidates, applied, skipped_human_edit, skipped_below_min_delta, dry_run, changes[] }(distinct from the advisory report shape). Plainmk groundingis unchanged — still read-only, no index. - Exports (
src/index.ts):reconcileGrounding/reconciledConfidence+ReconcileOptions/ReconcileResult/ReconcileChange+DEFAULT_ALPHA_NEG/DEFAULT_ALPHA_POS/DEFAULT_MIN_DELTA. - Completes #245 step 4 (the deferred write-back half). +14 tests (
test/reconcile.test.ts+mk grounding --applyCLI cases);contracts.md§2.1 +sdk-reference.mdgrounding section +public-api-surfacesnapshot updated.
Added — human_edit events: close the dark-signal gap in provenance tracking (#247)
human_edit had been in EVENT_ACTIONS since v1 but was never emitted — direct filesystem edits (via $EDITOR, the Edit tool, or any non-mk path) bypassed the event system entirely, leaving the strongest correction signal in the store (a human directly validating or correcting an atom) invisible to the log. This wires both paths. MINOR (new public CLI command + flag + exported engine functions).
- Forward path —
mk edit <atom-id>(src/edit.ts, engineeditAtom+ CLIsrc/cli/edit.ts): opens the atom in$EDITOR, hashes the file before/after, and on change re-serializes through the canonical writer (so the on-disk bytes match the recorded snapshot) and emits a provenancedhuman_editevent carrying a diff summary (hash_before/hash_after,lines_added/lines_removed) inmeta. No-op (no event) when saved unchanged; refuses encrypted (SECRET) atoms (editing ciphertext is meaningless). Injectable editor runner (EditorRunner) for testing.--dry-run/--jsonlikemk execute. - Backward path —
mk reflect --backfill-human-edits(src/provenance.ts,detectUnprovenancedWrites+backfillHumanEdits): detects writes that already happened off-band, labelled by confidence —content-diff(high: current serialized form, normalized through the current serializer to cancel format drift, differs from the snapshot in the last recorded mutation event; skipped for SECRET, whose snapshots are encrypted) andtimestamp-heuristic(low:updated_atis newer than the last mutation event). Bulkmk doctor --fixmigration passes cluster on a single same-secondupdated_at— that signature (≥3 candidates) is detected and excluded from backfill. Synthetic events are taggedmeta.synthetic = true+ their detection confidence. Opt-in (off by default in plainmk reflect) because content-diff also surfaces legitimate-but-eventlessmkwrites (relate/relink/enrich-relations); emission must be deliberate.ReflectResultgains optionalunprovenanced_writes/human_edits_backfilled(present only with the flag, so the default result shape is unchanged). human_editadded toMUTATION_ACTIONS(src/schema.ts) — it carries a full post-edit snapshot, so replay reconstructs the edited state and compaction keeps the latest per atom_ref. This is also what makes the backward detector idempotent: an emittedhuman_editbecomes the atom's latest snapshot baseline, so a second pass re-detects nothing.- Exports (
src/index.ts):editAtom+EditOptions/EditResult/EditorRunner;detectUnprovenancedWrites/backfillHumanEdits+UnprovenancedWrite/DetectionConfidence/BackfillOptions/BackfillResult. - Unblocks the grounding reconciliation write-back (#364 / #245-246), which weights human corrections 2.67× over silent accepts and was operating without the disconfirmatory signal.
- +17 tests (
test/edit.test.ts,test/provenance.test.ts,test/edit-cli.test.ts);schema-v2/public-api-surfacesnapshots updated.
Changed — releases now publish CHANGELOG notes + an Announcements discussion
release.yml's main-package job now derives its GitHub-release notes from the curated CHANGELOG.md ## [X.Y.Z] section (via a new shared scripts/changelog-section.sh) instead of PR auto-notes, and posts a linked announcement in the public repo's Discussions Announcements category (gh release create --notes-file … --discussion-category "Announcements"; needs the new discussions: write workflow permission). scripts/sync-to-public.sh was migrated onto the same extractor so the CHANGELOG-section logic lives in exactly one place.
scripts/changelog-section.sh(new) — extract one version's CHANGELOG section;--body-onlyomits the heading. +9 tests (test/changelog-section.test.ts)..github/workflows/release.yml— release notes from CHANGELOG; Announcements discussion;discussions: write.scripts/sync-to-public.sh— reuse the shared extractor (sibling-relative resolution viaSCRIPT_DIR).
CI / release-infra only — no version bump.
Fixed — Dependabot auto-merge workflow could not read status checks
The .github/workflows/dependabot-auto-merge.yml poll loop queries the PR's statusCheckRollup, which GitHub resolves through commit.statusCheckRollup. Reading that nested rollup needs checks: read and statuses: read, but the workflow's permissions: block granted only contents: write + pull-requests: write. Under the Dependabot-context GITHUB_TOKEN (undeclared permissions default to none), the GraphQL drill-down was denied with Resource not accessible by integration, so the job failed on its first poll and every Dependabot PR fell back to manual merge (e.g. #239).
.github/workflows/dependabot-auto-merge.yml— addschecks: readandstatuses: readto the workflowpermissions:block. Norun:step or trigger change.
No library code, tests, or build outputs change — CI-infrastructure-only fix, no version bump.
v1.35.0
Changed — requireExistingDir helper de-duplicates the CLI existence-check boilerplate (arch-review #369 slice)
CLI-internal refactor, no behaviour change, no public API (PATCH-class). 18 command handlers in src/cli/mk.ts opened with the same if (!fs.existsSync(<dir>)) { exitWithError(\Memory directory not found: ${
block (13 with theRun "mk init" first. hint, 6 without — deliberately left out of the #360 helper to avoid message drift). Introduced a CLI-internal **requireExistingDir(dir, json, { hint })** helper and collapsed all 18 to one call each, preserving each site's message **byte-identically** (hintdefault on;{ hint: false }for the 6 no-suffix sites). Themk doctorsite is intentionally **not** routed through it — it exits **2** with custom JSON/console output, a different contract. Behaviour proven unchanged by the existing CLI subprocess e2e tests (which assert the exactMemory directory not found` message + exit code on a missing dir); full suite green at 2013. One concrete slice of the #369 mk.ts-thinning umbrella, which stays open (per-command option→engine translation + inline cron templating remain).
Added — RECALL_DEBUG recall ranking observability (arch-review #371)
PATCH-class, opt-in diagnostic, no public API (no ContextBundle field, stderr only). Recall produced a final ranking with no way to see why atom X outranked atom Y. Setting RECALL_DEBUG=1 now emits a per-atom score breakdown to stderr, sorted by final score desc: task path prints fts/specificity/length/coverage/semantic/recency/type_weight/conf_factor/graph_boost/final (slotted into the existing scoreTaskAtoms loop); the no-task constitution path prints status/status_priority/recency/updated_at. OFF by default with zero overhead — a single env read per recall; no row allocation or string building when disabled (verified by a no-leak test). Deliberately stderr-only (not a ContextBundle field) to keep it off the public surface; a programmatic field can be a later MINOR if a consumer needs it. +3 tests (test/recall-observability.test.ts); public-api-surface snapshot unchanged.
Fixed — events.ndjson owner-only after bootstrap/merge + store-permissions audits the NDJSON sidecars (arch-review #389)
PATCH-class (no public API). Two parts:
- Security fix —
events.ndjsonwas rewritten world/group-readable aftermk bootstrapandmk merge.appendEvent/compactLogwrite the event log0o600(#138), butbootstrapEvents(src/bootstrap.ts) andmergeStores(src/merge.ts) rewrote the whole log viawriteFileAtomic(logPath, ndjson)without a mode → it landed at the umask default (0o644), exposing SECRET-derived atom snapshots/evidence until the nextappendEvent. Both now pass0o600. (Surfaced by the code review of part 2.) - Doctor coverage.
store-permissionsaudited.memory-index.db+ SECRET atom files but not the NDJSON sidecarsevents.ndjson(#138) andtriples.ndjson(#370/#380).probe()now checks both when present;fix()repairs them to0o600(it already iterates violations, andformatViolationrenders non-atom paths by basename — a drift prints e.g.triples.ndjson has mode 640, expected 600).
+4 POSIX-guarded tests (doctor-store-checks probe incl. a bootstrapEvents regression that the un-fixed write path would have reddened; doctor-fix-checks repair). Full suite green at 2010.
Tests — MCP client↔server transport E2E + test-layer taxonomy (arch-review #391)
Test + docs, no source change → no version bump. Until now no test exercised a real MCP transport: handlers were called in-process, but the StdioServerTransport + server.connect() bootstrap and the JSON-RPC tool/resource schema marshalling over the wire were untested. New test/mcp-transport-e2e.test.ts wires a real SDK Client to the McpServer over an InMemoryTransport linked pair (same construction path as src/mcp/server.ts: registerTools/registerResources → connect) and asserts end-to-end: listTools returns the registered tools with marshalled input schemas; callTool mk_remember round-trips and the atom lands on disk; listResources/readResource('memory://decisions') return view content incl. the PLACEHOLDER (EISDIR) path; and an isolated-mode call routes the write into the agent store. The server bootstrap surfaced no bug. File-level 20s timeout (vi.setConfig) absorbs connect-handshake latency under full-suite parallel load. Also added a test-layer taxonomy to test/README.md (unit / engine-integration / CLI-subprocess-e2e / MCP-transport-e2e) with the #390 coverage caveat. +5 tests (2001 → 2006).
Added — coverage tooling: @vitest/coverage-v8 + npm run test:coverage (arch-review #390)
Infra/tooling, no source change → no version bump. The repo had no coverage measurement, so every figure in the architecture review was an estimate. Added @vitest/coverage-v8 (dev dep) and a test:coverage script backed by a dedicated vitest.coverage.config.ts (v8 provider; text/json-summary/html reporters; coverage/ gitignored). A non-gating coverage CI job runs it report-only (parallel to test, so no added wall-clock on the gating path); a blocking threshold is a deliberate later decision once the baseline is trusted.
The coverage config excludes the 17 subprocess CLI e2e test files (they spawn node dist/cli/mk.js — the child isn't instrumented, so they add zero in-process coverage and time out under instrumentation/load). Consequence: src/cli/** and src/mcp/server.ts under-report here (exercised end-to-end by the excluded suite, just invisibly to v8) — a measurement artifact documented in CONTRIBUTING.md + the config header, not a real gap.
Real in-process baseline (2026-06-17): Lines 72.28% (5186/7174), Statements 71.6%, Functions 78.57%, Branches 61.15% (3012/4925) — replacing #358's estimates. Engine modules (src/scoring/*, src/index/*, recall, etc.) are well-covered; the 0%-line entries are all src/cli/* command modules (the documented subprocess artifact). Also bumped the wander-frontier-cap 1200-atom stress test's explicit timeout 15s→30s so it clears the bar under instrumentation.
Changed — split index-db.ts into internal src/index/* modules behind a facade (arch-review #368)
Internal refactor, no public API change (PATCH-class). src/index-db.ts (1297 lines bundling connection lifecycle, FTS, embeddings, relations, and reindex/indexing) is now a thin re-export facade over internal modules under src/index/: connection.ts (owns the single process-global SQLite connection cache + schema + openIndex/closeIndex/closeAllIndexes/indexExists), indexing.ts (atoms-table write/read: reindex/indexAtom/removeFromIndex/queryIndex/indexStats/getAllAtomIds + the internal bodyHash/extractTitle helpers), embeddings.ts (KNN vector storage/retrieval), fts.ts (atom + episode full-text search + term-frequency/corpus helpers), and relations.ts (typed graph edges). Every other module obtains its handle from connection.ts, so there is exactly one connection cache — the correctness-critical constraint. No triples.ts module: triple write/read already live in src/triples.ts/src/triples-sidecar.ts and the reindex reconcile stays inline in indexing.reindex. All ~44 importers and the public barrel (src/index.ts) keep importing from ./index-db.js unchanged; public-api-surface snapshot unchanged; full suite green at 2001 (no test-count change). Heaviest of the arch-review items; landed after the #379 reconcile fix so the union-reconcile was fixed in the old shape before the split.
Tests — CLI error/branch + extract/export-obsidian/mcp-resources coverage (arch-review #358)
Test-only, no source or behaviour change. Closes the honest coverage gaps the architecture review flagged: src/cli/extract.ts (~4% — its validation branches all fire before the LLM spawn), src/cli/export-obsidian.ts (~32% — transformAtom had only the CodeQL YAML-escape tests), and src/mcp/resources.ts (~44% — registerResources + the safeReadView PLACEHOLDER fallback were uncovered). New test/cli-extract-errors.test.ts (subprocess; log-not-found, memory-dir-not-found, invalid --max-atoms/--skip-lines/--max-input-chars, plain + --json contracts), new test/export-obsidian-cli.test.ts (transformAtom classification/tags/relations/wikilink-resolution branches + export-obsidian subprocess e2e incl. --include-archived/--json), and extended test/mcp-resources.test.ts (explicit PLACEHOLDER via an EISDIR read error + a registerResources mock-server wiring test). Review pass also added the resolveAtomId Case-2 slug-match branch test and tightened two pre-existing weak typeof-only assertions. +21 tests (1980 → 2001).
Changed — resolveContextDir helper de-duplicates CLI dir+agent boilerplate (arch-review #360)
CLI-internal refactor, no behaviour change, no public API change (PATCH-class). 16 command handlers in src/cli/mk.ts opened with the identical resolveDir(opts.dir, getAgent()) boilerplate, re-threading the --agent isolation lookup at every site. Introduced a small CLI-internal resolveContextDir(opts) helper (a literal wrapper around resolveDir(opts.dir, getAgent())) and collapsed all 16 sites to resolveContextDir(opts). --dir is a per-command option defaulting to ./memory, so opts.dir is always a string. The separate if (!fs.existsSync(memoryDir)) exitWithError(...) pattern (which varies per command in message/JSON-flag) is left untouched to avoid behaviour drift. Behaviour is byte-for-byte identical; proven by the existing CLI integration suite. No new test (trivial literal wrapper); public-api-surface snapshot unchanged.
Changed — `entity_tripl...
v1.34.0
Docs — contracts / lifecycle / troubleshooting pages + README audit (#306, #310, #311, #315)
Documentation batch closing the fleet-day knowledge gaps (no code change, no version bump):
docs/contracts.md(new, #310) — the draft-visibility matrix (status ×auto-extractedtag ×--include-drafts→ recall/render), the--jsonoutput shapes (pointing at the #301 Zod schemas) +recall_statussemantics, and the FTS sanitization ruleset (what's stripped, what causesfts_unavailable).docs/lifecycle.md(new, #311) — the atom status state machine (incl. superseded vs archived vs expired and its recall behaviour), the tiered auto-promotion table (incl. #309 procedure executed-once), tag semantics (reservedauto-extracted, the #262 whitespace rule), and the hand-authored-vs-auto-extracted draft visibility asymmetry.docs/troubleshooting.md(new, #315) — five fleet failure modes with symptom/cause/fix/verify: better-sqlite3 node-gyp rebuild, cronPATH/MK_BIN(references the #345 fix),claude loginexpiry silently no-op'ingmk extract,fts_unavailablepunctuation, and theEMBEDDING_API_KEY-vs-OPENAI_API_KEYkey-set-but-0-vectors trap.- README audit (#306) — most items were already current from prior audits (qualified test count, correct
--task/--embeddistinction, documented key fallback); added a NanoClaw/OpenClaw one-line gloss and links to the three new docs.
Added — --json output contracts as exported Zod schemas (#301)
Every mk --json output is an implicit contract integrators learn by trial-and-error (the seeds_used mis-parse is the canonical cost). Zod is already a runtime dep, so the contracts are now explicit, exported, and test-enforced. New src/schemas.ts exports Zod schemas (+ inferred types) for the four integrator-friction commands, from the package barrel: RecallOutputSchema (ContextBundle), DoctorOutputSchema (healthy/issue_count/issues/checks/fixes), RememberOutputSchema, EvalOutputSchema (fixtures/ok/exit_code), plus the building blocks (AtomOutputSchema, DoctorCheckResultSchema, EvalResultSchema, EvalQueryResultSchema). Derived from the actual output shapes; .passthrough() + optional so additive fields don't break consumers. +5 tests (test/schemas.test.ts) run the real CLI and validate its output against each schema — the schema is the authoritative oracle, so it can't drift. The full human-readable contracts page is the #310 docs batch (it references these). New public exports → MINOR-class (rolls into v1.34.0).
Added — tag-whitespace hygiene: tag-format doctor check + mk remember warning (#262, absorbs #264)
Tags in the mk convention are single hyphen-separated tokens. A space-joined list emitted as one YAML item (tags: ["AIRE peer-review N-version …"] — from an observer LLM, or a quoted mk remember --tags "foo bar") is valid YAML but ONE opaque token that silently breaks FTS tag queries + tag filtering. Two delivery points share one isValidTag() predicate (src/format.ts): (1) a new tag-format mk doctor check (src/doctor/checks/tag-format.ts, warn) flags any atom whose scope.tags[] contains a whitespace token, naming the atom + tag; (2) mk remember warns at write time (non-fatal — still writes) when a --tags value contains whitespace, with a tag_warning field in --json mode. Both sides check the normalized view (normalizeTags) so they agree: a space-joined "foo bar" is flagged, a comma-list "foo, bar" (split cleanly) is not. No fix() (splitting is ambiguous → human review) — note this means a store carrying a legacy whitespace tag now makes mk doctor exit 1 (warn), surfaced by the cron self-canary, until the tag is hand-split. +7 tests (test/tag-whitespace.test.ts). Absorbs #264 (its mk remember half). Internal check + warning, isValidTag exported from format.ts — PATCH-class.
Added — mk lint store-composition skew check + --strict (#316)
Store composition is a retrieval-quality variable, not just hygiene: a belief-dominated store buries facts in recall (Mai's 89%-belief store sat a loud belief in the top-3 of 8/11 golden queries). mk lint (knowledge-health) gains a composition check over active atoms: warns on belief monoculture (beliefs > 80% of active atoms, citing the type histogram + recall-degradation risk) and on a composition gap (a core type — fact/decision/procedure/preference — with 0 active atoms). Only engages at ≥ 20 active atoms so fresh / lifecycle-seed-only / small stores aren't false-flagged. New mk lint --strict exits 1 when any warning is present (default mk lint still always exits 0 — no exit-code change without the flag). Engine findCompositionSkew (src/lint.ts), new LintFinding.category: 'composition'. +7 tests (test/lint-composition.test.ts). New CLI flag, no exported-API change — rolls into the v1.34.0 MINOR.
Fixed — cron wrapper memory-dir validation (host-vs-container) (#347)
mk init --cron bakes the given memory-dir into the wrapper; if generated with a container path while the systemd/cron timer fires on the host, mk render -d "$MEMORY_DIR" fails "Memory directory not found" and the nightly silently goes dark (the other half of the Taj incident; PATH was #345). Two layers: (1) mk init --cron now warns (non-fatal) when the baked memory-dir doesn't exist on the generating host — catching the container-vs-host mistake early; (2) a new wrapper-memory-dir mk doctor check (src/doctor/checks/wrapper-memory-dir.ts, reuses discoverWrappers + parseGeneratedHeader) flags any installed wrapper whose baked # mk:memory-dir doesn't resolve on the host it runs on, so doctor (and the cron self-canary) catch it where it matters. +4 tests. Internal check + a generation warning, no public API change — PATCH-class.
Changed — mk_context_bundle / checkpoint() now embeds the query when a key is set (#323)
mk_context_bundle (the documented primary session-start/handoff retrieval) → checkpoint() used the synchronous FTS-only recall path and never embedded the query, even with EMBEDDING_PROVIDER + EMBEDDING_API_KEY set — only mk_recall took the semantic path, so conceptual queries missed at session start. checkpoint() is now async and routes recall through recallWithEmbeddings / recallIsolatedWithEmbeddings (same embed-then-FTS-fallback as mk recall --embed): with a key + task it embeds the query; with no key or no task it degrades silently to FTS — no regression for offline/no-key/no-task. Callers updated to await (mk_context_bundle MCP handler, mk checkpoint CLI, the openclaw plugin's bundle + pre-compaction hooks). +3 deterministic tests (test/checkpoint.test.ts) asserting the embed path engages with a key + task and is skipped otherwise; the live golden-query improvement is verified operator-side with a real key. Observable behavior change on a public retrieval path → MINOR-class (sets v1.34.0). checkpoint()'s return type changes to Promise<CheckpointResult>.
v1.33.2
Fixed — mk init --cron wrapper resolves mk via MK_BIN (group-npm nightly no longer goes dark) (#345)
The generated nightly sync wrapper set PATH to node's dir + ~/.local/bin, but not the agent's actual mk location. On a group-npm-only host (mk is a local dep at …/node_modules/.bin/mk — the MK_BIN, in neither of those dirs) bare mk wasn't found, so reflect/reindex/render all failed with mk: command not found and the render went stale (Taj's nightly was dark 34h; sibling of #340). mk init --cron now bakes MK_BIN — the same var mk upgrade resolves, read from the env where the operator runs it — into the wrapper as a runtime-overridable default (MK_BIN="${MK_BIN:-<baked>}") and prepends its dir to the front of PATH, so the wrapper invokes the agent's own binary even when the cron/systemd timer hands it a clean env with no MK_BIN exported. Baked into the # mk:mk-bin= header so --update preserves it; falls back to node-dir + ~/.local/bin when no MK_BIN is known. +5 tests (test/cron-template.test.ts) incl. a smoke-run that resolves bare mk against the baked value under a stripped PATH with no runtime MK_BIN (the real cron case) + a runtime-override case. No public API change — PATCH-class. (The issue's secondary — mk init --cron baking a container memory-dir that doesn't exist on the host — is tracked separately in #347.)
v1.33.1
Post-v1.33.0 bugfixes from the fleet deploy. No public API change — PATCH.
Fixed — mk upgrade now upgrades a group-npm local-dependency MK_BIN (#340)
mk upgrade's installer (shipped in #331) assumed the global layout (MK_BIN = <prefix>/bin/mk → npm install -g --prefix <prefix>). The nanoclaw fleet's MK_BIN is a local dependency (<pkgroot>/node_modules/.bin/mk symlinked into node_modules/memory-kernel); there the -g --prefix <node_modules> install wrote to <node_modules>/lib/node_modules/… and left the symlinked package — the binary the agent actually runs — untouched (caught by verify-agent-version → FAIL, never a false PASS, but it didn't upgrade). New pure resolveInstallPlan(mkBin, version) detects the layout: a node_modules/.bin/mk path → a local npm install memory-kernel@<ver> run from the owning package root (updates the dep the symlink resolves to); otherwise the global -g --prefix path (unchanged). +3 tests (test/upgrade-installer.test.ts), including a regression that replicates the group-npm .bin/mk-symlink layout with a fake npm and asserts the symlinked binary truly upgrades (red under the old installer).
Fixed — shell scripts run on macOS bash 3.2 (no more mapfile false failures) (#341)
scripts/privacy-scan.sh and scripts/docs-hygiene-check.sh used the bash-4 builtin mapfile, which is absent on the macOS-default /bin/bash 3.2 → mapfile: command not found (exit 127) → 28 false test failures for Mac contributors (Linux CI on bash 4+ stayed green). Both now use a portable while IFS= read -r x; do arr+=("$x"); done < <(...) loop. Added test/shell-portability.test.ts — a static guard that fails if any scripts/*.sh reintroduces a bash-4-only builtin (mapfile/readarray/declare -A/case-conversion expansions); deterministic and CI-meaningful (the existing script tests pass on bash 4+ regardless). Verified clean under /bin/bash 3.2.
v1.33.0
Added — procedure "executed-once" promote signal (#309)
Completes the #274 tiered-promotion story. #274 correctly held procedure drafts but left when they promote unresolved — a procedure is only trustworthy once it has been executed at least once (it describes how the agent actually works, not how it aspires to).
executed_atfrontmatter field (AtomFrontmatter+ Zod schema + serializer key-order) — first-confirmed-execution timestamp. Optional/additive; existing atoms validate unchanged.mk execute <atom-id>(enginemarkExecuted,src/cli/execute.ts) stampsexecuted_at, emits anatom_updatedevent (meta.operation = 'execute'), and reindexes. Idempotent — preserves the first execution time.--dry-run+--json. The session-end extractor (#268) can populate the same field downstream.autoPromote(src/reflect.ts) gains aprocedurebranch: promote (status-only, draft → active) whenexecuted_atis set AND confidence ≥ 0.7 AND no contradiction — no age gate (execution is the signal). Aspirational, never-executed procedures stay in draft.- New exports
markExecuted+ types. +9 tests (test/execute.test.ts, procedure cases intest/reflect-tiered-promotion.test.ts). MINOR-class (new command + field + export; folds into the v1.33.0 batch).
Added — eval cadence: delta-only post-sync alert + weekly digest (#266)
Builds on mk eval (#300). Watching the absolute recall pass rate is noisy; the cadence turns a stream of mk eval runs into a signal.
- Engine
src/eval-cadence.ts(summarizeEvalRun+decideCadence, internal — not barrel-exported): reduces anmk eval --jsonrun to a per-category + overall pass-rate snapshot, then compares the latest against a rolling N-day baseline (default 7d). Fires only when a category drops > 10pp (regression) or improves > 15pp (worth re-baselining); silent within thresholds and on a first observation. Emits a stable weekly digest regardless. Thresholds + window configurable;nowMsinjectable for deterministic tests. +8 tests (test/eval-cadence.test.ts). scripts/eval-cadence.mjs— I/O wrapper: appends the latest run to a JSONL history, runs the engine, prints the digest, and exits non-zero withALERTlines on a fired alert (so a post-sync hook can gate).--digestreports without appending.docs/eval-cadence.md— runbook for wiring it post-sync + weekly (operator-gated; not deployed to live agents).
Engine internal + script + docs — no public API/CLI change → PATCH-class (folds into the v1.33.0 batch). Per-agent golden fixtures + live baselines were run on the local store copies and reported on the issue (private — never committed); the FTS-only floor confirmed the golden sets are embedding-dependent (an embed key is required for a meaningful baseline; the KNOWLEDGE-grep category scores without one).
Added — mk upgrade: host-side one-command agent upgrade (#331)
Upgrading an agent was a manual multi-step dance with a wrong-binary footgun (a host can have several mks — global, the group-npm MK_BIN the agent runs, a dev clone — at different versions), a non-idempotent re-seed, a cron-wrapper regen, and a doctor run against the right dir. v1.32.0 had an agent report "done" while silently stale.
mk upgrade --to <ver>(enginesrc/upgrade.ts, CLIsrc/cli/upgrade.ts) runs the whole sequence and prints one PASS/FAIL (exit 1 on FAIL): resolve the agent binary (--mk-bin/MK_BIN, never PATH) → installmemory-kernel@<ver>at its npm prefix → verify it reports<ver>→ idempotent re-seed (#329) → regenerate the cron wrapper (--cron-wrapper) → gate onmk doctor(incl. #330 seed-set-freshness). PASS = no error-severity doctor issues (warnings inform, don't block).- Explicitly host-side — the agent can't upgrade its own in-container binary; run on the host, from the target version (
npx memory-kernel@<ver> upgrade --to <ver>). The seed bodies + canonical slug set + doctor gate all come from the runningmk, so averify-runnerstep hard-fails when the runner's version ≠--to(preventing a re-seed of the wrong version's set — the v1.32.0 incident); version comparisons tolerate a leadingv.mk doctoris how the agent subsequently knows it took. - The three external effects (npm install / version probe / cron regen) are injectable, so the orchestration / gating / dry-run logic is fully tested with no network and no live-agent contact. New exports
runUpgrade+ types. +7 tests (test/upgrade.test.ts). MINOR-class (new command + export; folds into the v1.33.0 batch).
Added — mk doctor: seed-set-freshness check + agent (MK_BIN) version reporting (#330)
Nothing in mk doctor caught a stale or incomplete lifecycle seed set — during the v1.32.0 adoption an agent re-seeded the old set and doctor was green; the staleness was only caught by manually diffing the store against origin/main. "Upgrade done" now means "doctor green."
seed-set-freshness(new check,src/doctor/checks/seed-set-freshness.ts) compares the store's active lifecycle SET (atoms taggedsession-loop, keyed by slug + type — the same identityseedLifecyclereconciles on) against the canonical set shipped with the running kernel (canonicalLifecycleSet(), #329). Set, not count —8 stale + 3 dupes = 11passes a count check but fails here. Error on missing/duplicate entries (naming them); info on extra entries (a slug removed in this version, or a user's ownsession-loop-tagged atom —session-loopis an unreserved tag, so extras are advisory and never flip the exit code); info on a store with no lifecycle atoms (plain non-agent stores aren't hard-failed) or an exact match. This is the acceptance gate behindmk upgrade(#331). PurediagnoseSeedSet()is unit-testable without a store. New exportcanonicalLifecycleSet().mk-versionnow also reports the agent binary the cron wrapper runs viaMK_BIN, not justmkon PATH — a host can have severalmks at different versions, so doctor makes the agent-relevant one unambiguous. New purediagnoseMkBin()helper; MK_BIN mismatch / unrunnable → warn.- +37 tests (
test/doctor-seed-set-freshness.test.ts, MK_BIN cases intest/doctor-integration-health.test.ts). Adds one public export (canonicalLifecycleSet) — MINOR-class (folds into the v1.33.0 batch alongside #329).
Added — mk seed --lifecycle: idempotent lifecycle seeding + seeds shipped in the package (#329)
Re-seeding the canonical lifecycle atoms was not idempotent (generateAtomId always appends a unique counter+nonce, so mk remember --slug is not an upsert), and the canonical seeds didn't ship in the npm tarball — together these caused a silent stale seed during the v1.32.0 fleet adoption (one agent re-seeded the old 8-atom 1.28.3 set and reported "done").
mk seed --lifecycle -d <dir>(enginesrc/seed.ts, CLIsrc/cli/seed.ts) reconciles a store to the canonical set idempotently: it matches existing atoms on the stable slug segment of their id (extractIdSlug()— present on legacy atoms too, no new frontmatter field) and, per canonical slug, creates / supersedes-and-recreates / no-ops / dedups in place. Running it twice leaves exactly one active atom per slug with zero duplicates.--dry-runreports planned actions without writing;--jsonemits the structuredSeedResult.- Single source of truth:
skills/mk-memory-setup/seed-atoms/lifecycle/manifest.json({version, atoms:[{file,type,slug,tags}]}) describes the 11 canonical atoms; consumed bymk seed, the rewrittenseed-lifecycle.shwrapper, and (forthcoming #330) the doctor seed-set-freshness check. - B2: added
skills/mk-memory-setup/seed-atomstopackage.jsonfiles, so the canonical bodies + manifest + wrapper ship with the release — a freshnpm i -g memory-kernelcan seed without cloning the repo. - New exports:
seedLifecycle/loadLifecycleManifest/canonicalLifecycleSlugs/resolveSeedDir/extractIdSlug/normalizeSlug(src/index.ts). +11 tests (test/seed-lifecycle.test.ts).
New public command + exported engine — MINOR-class. Prerequisite for the mk upgrade helper (#331).
Fixed — mk doctor falsely errored on relations to drifted-filename atoms (#327)
atom-frontmatter's buildAllIds() registered each known atom id from the filename, so a file whose name had drifted from its id — e.g. a legacy doubled <id>-<id>.md archive file — was registered under the wrong id, and an inbound relation to the real id was reported as a broken-relation-ref error (kept mk doctor red on a healthy store; seen live on Mai @ v1.32.0). buildAllIds now also reads the authoritative frontmatter id: from each file (via parseFrontmatter, throw-guarded), in addition to the basename — widening the known-id set only suppresses false positives. +2 tests (test/doctor-atom-frontmatter.test.ts). No public API change.
Added — mk eval: first-class golden-query recall runner with pass/fail exit codes (#300)
Promotes the host-side mk-golden-eval.py to a real mk eval command so recall quality can gate CI and serve as a post-sync canary:
- Exit codes —
0all fixtures pass (pass_rate >= threshold),1any below threshold,2runner error (missing store, malformed fixture). - YAML fixtures (
<store>/eval/*.yamlby default;--fixture <path>override). A query passes when ≥1expectatom id is in the top-K recall (suffix-drift-tolerant);expect_contentgrepsKNOWLEDGE/**for doc-category queries (docs aren't atoms). Optional per-fixturethreshold/top_k;--threshold/--top-koverride. - Embeddings auto-engage only when a key + vectors exist (
--no-embedforces FTS); keyless runs are deterministically FTS-only, so CI needs no live API calls. - New exports:
runEval/runFixture/loadFixtures...
v1.32.1
Security
esbuildoverride → 0.28.1 — pins transitiveesbuild(viatsxandvite) to^0.28.1viaoverridesinpackage.json. Closes Dependabot alerts #39 (low: arbitrary file read on Windows dev server) and #40 (high: missing binary integrity verification in Deno module). Neither vulnerability is exploitable in this project's use (no Deno, no dev server in production), but the override is a zero-cost defensive fix.npm auditnow reports 0 vulnerabilities. No library code, tests, or public API changed — PATCH only.
v1.32.0
Round 1 of the epic #294 "operational correctness & self-diagnosis" pass: fix what was silently wrong on the live agents and make the integration self-diagnosing.
Fixed — mk recall: apostrophe fts_unavailable + beliefs outranked facts/prefs (#283)
Two recall-quality bugs surfaced by the #266 golden-eval baselines:
- Apostrophe →
fts_unavailable— the FTS query sanitizer (searchFts,src/index-db.ts) stripped. , ; ? !(#214) but not the apostrophe'(the FTS5 string-literal delimiter), so any query withTaj's/user'sraised an FTS5 syntax error → caught → null →recall_status: fts_unavailable(silent recall failure). Now strips'plus the two common typographic apostrophes (’‘); re-audited the remaining FTS5 syntax chars (all already stripped). - Beliefs buried facts/prefs — the recall-time type-weight lever (
DEFAULT_TYPE_WEIGHTS) was too weak (belief0.8vs fact1.0): a semantically-broad belief still outranked grounding facts in ~8/11 golden queries. Strengthened to belief/entity_summary0.6, fact/preference1.1(fact/belief ratio 1.25× → 1.83×); ordering invariants preserved. Validated on local Mai+Taj copies (apostrophe queries nowmatch; facts surface). +4 tests.
Changed — integration docs hardened: --embed on recall examples + verify-blocks (#296)
The integration guides predated embeddings and steered agents into FTS-only recall. Updated docs/cli-integration.md, docs/nanoclaw-integration.md, docs/host-integration-doctrine.md, and README.md:
--embedon themk recallexamples (CLI docs); documented the FTS-only path as omitting--embed— there is no--no-embedflag.- Key clarification:
EMBEDDING_API_KEYis read first; theOPENAI_API_KEY/VOYAGE_API_KEYfallback is provider-matched (OPENAI only withEMBEDDING_PROVIDER=openai, etc.) — the ambiguity behind the "key set, 0 vectors" outage. - Version-assert (
mk --version) added to the top of each setup flow. - Verify-block at the end of each setup section —
mk doctor(binary / key-source / vectors / smoke-recall / sync-liveness, from #305) + an--embedsmoke recall — the 30-second deployment-seam check. mk reindex --embedadded to the NanoClaw nightly sync example (+ a pointer to the hardenedmk init --cronwrapper, #303).
Docs only — no library code, no version bump.
Changed — lifecycle seed atoms: --embed + version-assert + 3 new disciplines (#297)
/mk-memory-setup's seeded lifecycle atoms inherited the integration blind spots that caused the 2026-06-11 fleet failures. Fixed at the source:
01-session-start— the canonical recall command now includes--embed(FTS-only path = omit--embed; there is no--no-embedflag), and amk --versionassert is added at session start (catch a stale binary before it silently breaks recall semantics).- Three new procedure atoms (
09–11), so the most common failure modes are structurally recallable rather than relying on in-context prose: verify claims about your own memory/config against the tool (not memory); supersede capability atoms in the same change as an infra change; every repeated action sequence becomes aprocedureatom (never buried in a belief body).
Lifecycle seed count 8 → 11 (10 procedures + 1 constraint): seed-lifecycle.sh SEEDS table, lifecycle/README.md, README, and the CLAUDE.md convention note updated; docs/agent-session-loop.md kept in sync (new "Operating Disciplines" section + the --embed/version-assert session-start change). Seed script validated end-to-end (by_type: {constraint: 1, procedure: 10}). Skills/docs only — no library code or version bump.
Changed — mk init --cron wrapper hardened with field lessons (#303)
generateCronWrapper now encodes the operational fixes the fleet's hand-patched sync scripts carry, so new adopters don't inherit the silent-failure class that left a nightly sync dark for 6 days:
- Fail-soft — dropped the bare
set -euo pipefail; nowset -uo pipefail+ astep()helper that runs each step guarded (a non-fatal failure, e.g. a no-match grep exiting 1, is logged + counted but never aborts the whole sync). - PATH self-add —
export PATH="$(dirname "$(command -v node …)"):$HOME/.local/bin:$PATH"so cron's minimal environment findsmk. mk reindex --embedbetween reflect and render so new atoms get vectors (pairs with #305'sembeddings-vectors-fresh).- Self-canary — a non-fatal
mk doctorafter sync surfaces 0-vectors / stale-sync / version drift in the cron log, with a per-run warning summary.
Generated script validated with bash -n. Tests updated to the fail-soft contract + new guarantees (test/cron-template.test.ts). No new public API — PATCH-class wrapper-output change.
Added — mk doctor integration-health checks (#305)
Five additive, read-only checks so mk doctor can answer the post-upgrade / post-incident questions (surfaced by the v1.29→1.30→1.31 upgrade chain):
mk-version— warns when a stalemkon PATH shadows the running kernel version.embedding-key-source— reports which env var supplied the embedding key (EMBEDDING_API_KEYor theOPENAI_API_KEY/VOYAGE_API_KEYfallback; last-4 only, never the value); warns if a provider is set but no key resolves. Backed by a new exportedresolveEmbeddingKeySource()insrc/embeddings.tsthat mirrorsgetEmbeddingConfig's resolution order (no drift).embeddings-vectors-fresh— warns on the unambiguous "key set, 0 vectors" stall; partial coverage is info (SECRET/PERSONAL atoms are never embedded).smoke-recall— read-only FTS probe (no egress, no schema migration); opt-in embedding smoke viaMK_DOCTOR_SMOKE_EMBED=1.sync-liveness— reports time since last reindex (.memory-index.dbmtime); only warns when a freshness SLA is declared viaMK_SYNC_MAX_AGE_HOURS(idle/manual stores aren't false-flagged); folds in the closed #284.
All checks are store-aware (skipWhen: ['store'] where they touch the index) and degrade gracefully on a corrupt index (warn, never a hard error). mk-version's PATH shell-outs are timeout-bounded (5s).
All checks open the index read-only so a plain mk doctor never migrates the schema (that stays store-schema's --fix job). Boundary: doctor = integration health; composition-skew stays in mk lint (#316). +18 tests (test/doctor-integration-health.test.ts).
v1.31.0
mk wander seed-selection fixes — surfaced by the first live wander dry-runs on real stores (Taj, Jun 11). Auto-seeds now reflect graph connectivity (citation weight) and span clusters (type diversity) instead of pulling session noise from one monoculture.
Added — mk wander type-diverse auto-seed selection (#281)
In a type-monoculture store (Taj's is ~90% beliefs), autoSeeds() returned the global top-N — all beliefs from one tight cluster — so the walk never escaped it and produced intra-cluster redundancy instead of the cross-cluster surprise spreading activation is meant to surface (observed live, Jun 11: 14/17 activated atoms from a single Apr-18–22 belief arc, zero cross-cluster collisions).
src/wander.ts—autoSeeds()now draws seeds round-robin across atom types (best-ranked belief, then best fact, then best decision, …) so the seed set spans multiple clusters and the walk can bridge between them. Within each type, the citation-primary order from #280 is preserved. Reduces to plain top-N when only one type is present. Default on via the newWanderOptions.diverseSeeds(defaulttrue); CLI--no-diverse-seedsreverts to plain top-N. +3 tests (test/wander.test.ts).src/cli/mk.ts— newmk wander --no-diverse-seedsflag.
New public API (WanderOptions.diverseSeeds field + --no-diverse-seeds CLI flag) — sets the MINOR. (PR #286)
Out of scope (noted on #281): embedding-distance ("semantic surprise") collision scoring. Tier-1
wanderis by-design LLM-free / pure-SQLite; embedding-backed scoring belongs to a future Tier-2 wander, not this change. The observed problem (walk stuck in one cluster) is fully addressed by type-diverse seeding.
Fixed — mk wander auto-seed selection was recency-biased, not citation-weighted (#280)
When no seeds were supplied, autoSeeds() ranked candidates purely by ACT-R base-level activation, ln(citations+1) − 0.5·ln(ageDays). Because ageDays is clamped to ≥0.01, an atom written in the current session earns a ≈+2.3 recency spike — enough to outrank a 28×-cited foundational atom. The result: wander consistently pulled the most-recently-touched atom as a seed regardless of graph connectivity, injecting session noise into spreading activation (observed live on Taj's store, Jun 11).
src/wander.ts—autoSeeds()now ranks citation-primary: by rawcitation_countdescending, withbase_activationonly breaking ties among equally-cited atoms. A cited atom always outranks an uncited one as a seed. With no citation data (table unpopulated, or file-scan mode) the ranking degrades gracefully to pure recency. +2 regression tests (test/wander.test.ts). (PR #285)
Docs
docs/agent-session-loop.md— addedmk wanderintegration instructions for the agent session loop (#282, PR #288).
v1.30.0
Draft-atom lifecycle (#274) — the mk-side prerequisite for #268's session-end extract. Two halves: a recall/render visibility gate (Gap 1) and tiered promotion in reflect (Gap 2).
Added — auto-extracted drafts excluded from recall/render by default (#274 Gap 1)
Session-end extract (#268) lands status: draft atoms tagged auto-extracted. Those are unvetted, but recall ranked draft just below active, so they entered live context (and #267's recall-inject) immediately. Recall, the SQLite index query, and fill-mode render now exclude status: draft atoms that carry the auto-extracted tag by default.
- Scoped to the
auto-extractedtag, not all drafts — hand-authored draft beliefs (the developmental-arc resting state, held in draft by Gap 2) still render. An explicitstatuses: ['draft']filter still surfaces them for inspection. - New opt-in:
mk recall --include-drafts,RecallQuery.include_drafts, and themk_recallMCP tool'sinclude_draftsparam (CLI / library / MCP parity). - Three enforcement points kept in sync via a shared
AUTO_EXTRACTED_TAGconstant (src/types.ts, also used by themk extractproducer andmk consolidate) + a sharedisUnvettedDraft()predicate:recall.tsfilterAtoms(file-scan path),index-db.tsqueryIndex(SQL path),render.tsfill-mode. Isolated recall inherits the exclusion (routes throughrecall()). - +6 tests (
test/recall-draft-visibility.test.ts). (PR #276)
Changed — tiered draft promotion in reflect (#274 Gap 2)
Replaced the old belief → fact @ confidence 0.9 auto-promote (which converted the over-produced type — the opposite of the monoculture-fix intent) with type-tiered, status-only promotion (draft → active; type unchanged, no file rename):
- fact / preference / decision: promote after 48h if
confidence ≥ 0.7and no contradiction with an existing active atom of the same type/scope (reuses thedetectConflictsheuristic). - open_question: promote immediately (additive, no quality risk).
- belief: held in draft (over-produced + re-extraction drift — review-gated).
- procedure: held in draft (interim; the "executed-once" tool-trace signal is a separate sub-task — aspirational procedures must not auto-activate).
Promotion strips the auto-extracted tag (matching mk consolidate, so a reflect-promoted atom isn't left looking unvetted). The contradiction check shares a single atomsConflict() predicate + CONFLICT_CONFIDENCE_GAP constant with detectConflicts (no drift between the promotion gate and the detector). The atom_promoted event now carries { from_status, to_status, type } (was { from_type, to_type }). +11 tests (test/reflect-tiered-promotion.test.ts); ~10 existing tests that asserted the old belief→fact rule updated to the new semantics. Closes #274; unblocks #268's fork-side MK_EXTRACT_ON_END=1. (PR #277)