Skip to content

Audit and publish the corpus

Terminal window
memhtml doctor # thirteen findings; ten of them decide healthy
memhtml doctor --fix # repairs the two that need no judgement call
memhtml publish # regenerate index.html listings and sitemap.xml, and commit

A healthy store answers with every finding list empty:

{
"apiVersion": "1",
"type": "doctor.report",
"data": {
"root": "/home/you/memhtml",
"healthy": true,
"dangling": [],
"orphanAccessRows": [],
"inboxDepth": 0,
"inboxCrowded": false,
"inboxTaskDepth": 0,
"inboxTasksCrowded": false,
"overdueTasks": [],
"staleBlockers": [],
"untypedEntities": [],
"untypedEntityTotal": 0,
"stuckSleepRuns": [],
"warnings": [],
"unparseable": [],
"indexFresh": true,
"indexHeadSha": "4e232759bfad745b0445ecd83cc9883c30a0c426",
"headSha": "4e232759bfad745b0445ecd83cc9883c30a0c426",
"embedModelMatches": true,
"storedEmbedModel": "cohere.embed-v4:0@1024",
"configuredEmbedModel": "cohere.embed-v4:0@1024",
"vectorCoverage": 1,
"vectorCoverageFloor": 0.95,
"vectorCoverageLow": false,
"chunks": 9402,
"embeddings": 9402,
"vectorCoverageRemedy": null,
"dirty": []
}
}

Every list is present and possibly empty, so a parser never has to branch on a missing key. healthy is true when every check comes back clean.

Finding Meaning Fix
dangling A <link> points at a path the tree does not hold. --fix rewrites it to the archive path, or drops it when the target is gone.
orphanAccessRows A state.access row whose path left the tree. --fix prunes it.
inboxCrowded Over 20 active memories in areas/inbox/, which means the placement rules stopped matching what agents write. Re-place them, or revisit the rules.
inboxTasksCrowded Over 10 open tasks in areas/inbox/tasks/: work with no project. Drain it. A task inbox is a queue, and this one is filling up.
overdueTasks An open task whose memhtml-due has passed. Doctor is the only surface that reads due_at, because search excludes a task by default and every sleep phase skips it.
staleBlockers A blocks edge whose blocker is archived or absent. Decide whether the blocked task is ready. Each file on its own is valid, and only the pair is wrong.
untypedEntities A memhtml-entity meta written as a bare name, so it indexes under the unknown type. Name a type in the producer. unknown:checkout-api is not reachable by --entity service:checkout-api.
stuckSleepRuns A sleep_runs row still running whose branch is gone or whose start is over 20 hours old: a killed run. memhtml sleep run reaps it at start, and memhtml sleep run --dry-run does too. Doctor never writes that table.
warnings An element outside the closed vocabulary. The file still indexes. Author’s intent, and doctor will not guess at it.
unparseable A file the parser refuses. It is absent from the index. Read the violations with memhtml read <path>, fix the file, then memhtml index rebuild.
indexFresh: false The index describes an older commit. memhtml index update --embed.
embedModelMatches: false The stored vectors came from a different embedding model than the configured one. Delete the database and rebuild: see rebuild the index.
vectorCoverageLow: true Under vectorCoverageFloor (default 0.95, MEMHTML_VECTOR_COVERAGE_FLOOR) of the chunks carry a vector in the configured space while the plane is in use: some vector exists, or an embedder is configured. Search drops the vector arm and reports degraded: true with vectorCoverage. A store with zero vectors and no embedder is lexical-only and stays healthy. vectorCoverageRemedy names it: memhtml index embed to backfill, or memhtml index rebuild --embed. Set MEMHTML_EMBED=off if the store is meant to stay lexical-only.

The two inbox thresholds are INBOX_WARN_DEPTH 20 and INBOX_TASK_WARN_DEPTH 10 (apps/cli/src/doctor.ts:69, apps/cli/src/doctor.ts:78).

stuckSleepRuns counts toward healthy for the same reason orphanAccessRows does: a row that says running about a process that is gone is a false statement in the ledger. Each entry names the runId, its branch, its startedAt, and branchExists, which is false when the branch was deleted (the row is stuck on that alone) and true when the row is stuck on age alone and memhtml sleep resume <run-id> could still finish it. The age bound is SLEEP_RUN_STALE_AFTER_MS, 20 hours (packages/sleep/src/contract.ts): a bound on how long one run can still be executing, several times a full run’s length and four hours short of a day so a once-a-day caller always reaps the previous run on the next start. A row that is young and whose branch exists is a run executing right now and is not listed.

overdueTasks, staleBlockers and untypedEntities are the three findings excluded from healthy. The first two describe work that has fallen behind rather than a corpus that is wrong, and folding them in would make healthy: false the normal state. untypedEntities is excluded because unknown is a supported storage type: a bare name costs reachability, not correctness, and gating on it would turn a hand-authored corpus red for writing its metas the way the format allows. untypedEntities is capped at UNTYPED_ENTITY_SAMPLE 20 with the distinct count reported beside it as untypedEntityTotal, so a truncated sample cannot be read as the whole set.

Search excludes a task memory by default and every sleep phase skips it, so memhtml doctor is the only command that reports a task past its due date. If you use tasks, put this command on your cron.

--fix rewrites dangling hrefs and prunes orphanAccessRows. Everything else needs a decision doctor is not entitled to make: a crowded inbox might be a bad day or bad placement rules, an element outside the vocabulary might be exactly what the author meant, and an unparseable file needs a human to read the violations.

--fix reuses the repair the sleep integrity phase already implements, which splices the changed bytes in place instead of writing the file out again through the serializer. The reason goes past avoiding a second implementation: a repair routed through the serializer would move the content hash of every file it touched, which would then read as a body change in the next memhtml sleep review and would re-embed the file for nothing.

Under --fix the report gains a repaired object: hrefs rewritten (pointed at the target’s archive path), hrefs dropped (the target has no file anywhere), failedWrites, prunedAccessRows, and the commitSha the href repairs landed in — null when nothing was rewritten (apps/cli/src/doctor.ts:156).

A repair counts only when its bytes reached disk. failedWrites lists the source paths whose repaired bytes could not be written, and those findings are still open: they stay in dangling, they are absent from rewritten and dropped, and they are never staged, so a re-run retries them. Both halves of that matter — staging an unchanged file would put the pre-repair bytes into a commit whose subject claims they were repaired, and counting it would report a finding as settled while it is still open. A non-empty failedWrites is the one part of a --fix report that needs a second look, usually at permissions on the paths it names.

Terminal window
memhtml publish
{
"apiVersion": "1",
"type": "publish.report",
"data": {
"root": "/home/you/memhtml",
"artifacts": 5,
"written": 5,
"paths": [
"index.html",
"resources/index.html",
"resources/infra/index.html",
"resources/runbook/index.html",
"sitemap.xml"
],
"commitSha": "f1242b5e861ce09efb7fc1dfafd00f95b446215d"
}
}

memhtml publish regenerates the per-directory index.html listings and sitemap.xml and commits them. It is deterministic to the byte (apps/cli/src/publish.ts:10), so two runs over an unchanged corpus write nothing and commit nothing, reporting written: 0 and commitSha: null. That is what makes it safe on the cron.

Those artifacts are what make the store browsable: open $MEMHTML_ROOT/index.html in a browser and walk the corpus with no tooling at all.

.gitattributes marks index.html and sitemap.xml with merge=ours, and that attribute needs the per-clone merge.ours.driver config that memhtml init sets. If a merge has already written conflict markers into a generated file, leave the file itself alone.

Finish the merge however git needs you to, then run memhtml publish and let it overwrite the artifact. The generator is the authority for a derived file, so an edit you make by hand only produces a version the next publish discards.