Skip to content

ADR-0007: Hot-Memory Read-State Contract

Status

Accepted

Context

planning/memory_design.md defines .palee/hot.md as a derived working-memory view that must be tolerant of external edits (Obsidian, sync daemons, manual text editing). ADR-0006 established the write side of that contract — resetHotMemory, rebuildHotAndIndex, updateHotMemory in src/storage/memory.ts, all behind the src/storage/index.ts facade.

The read side was never owned. Until #130, src/cli/session.ts hand-rolled its own parseFrontmatter on hot.md content at six call sites, and each site re-implemented its own notion of what a "bad" read looked like. When those sites were consolidated behind a single readHotMemory() accessor (#130), the classification itself became an exported contract (HotMemoryReadState) without ever being formally decided — first flagged in #145 review.

One semantic question was left implicit: which palee_schema values count as supported. The pre-accessor code checked truthiness (!frontmatter.palee_schema), so palee_schema: 2, true, and "1" all classified as ok. That is inconsistent with the rest of the codebase: palee migrate rejects non-v1 schemas as unrecognized, ADR-0005's migration path converges notes on palee_schema: 1, and every hot-memory writer pins version 1 (HotMemoryData).

Decision

readHotMemory(vaultPath) classifies .palee/hot.md into exactly five states and enforces the accessor boundary:

  1. Five-state classification (HotMemoryReadState):

    • ok — frontmatter parses and carries palee_schema: 1.
    • schema-invalid — frontmatter parses but palee_schema is absent or not the supported version. Strict equality (=== 1) rejects 2, true, and "1"; tolerant fields are still exposed for policy-free inspection.
    • no-frontmatter — no parsable fences (absent, or ---\n--- empty fence).
    • corrupt — fences present, YAML parse failed.
    • missing — file does not exist (ENOENT).
  2. Accessor boundary (unchanged from #130): ENOENT maps to missing; all other filesystem errors are thrown, so callers keep deciding whether to swallow; unknown frontmatter keys are ignored, never rejected; no rebuild, no mutation, no output. The read joins .palee/hot.md directly rather than via getPaleeDir() (which creates the directory) — a read must never have that side effect.

  3. Session policy stays in the CLI layer, deliberately un-normalized because the flows are intentionally different:

    • topic resolution swallows every read failure → null
    • session start rebuilds corrupt/schema-invalid, tolerates no-frontmatter without rebuilding, propagates non-ENOENT errors to the command catch (exit 5)
    • session draft / session end keep their own age/skew rules (24h/no-future; no age limit + 60s future-skew clamp)
  4. Compatibility boundary: only palee_schema: 1 is supported. A v2 (or foreign) hot.md is schema-invalid, so session start rebuilds it from the newest confirmed session rather than trusting derived state it cannot interpret — matching memory_design.md's rule that a missing or invalid hot.md is rebuilt, and ADR-0005's stance that unknown schema versions are not silently consumed.

Consequences

Positive

  • The derived file's schema finally has a single read-side owner; adding a hot.md field touches memory.ts and types.ts, not six scattered parsers.
  • session start no longer trusts a v2 hot.md whose fields it may misinterpret; it self-heals via the documented rebuild path.
  • Callers keep their deliberately different policies — the accessor is classification, not a hidden session-policy object.

Negative / Tradeoffs

  • session start rebuilds a hot.md that a hypothetical future schema version wrote. That is intended (rebuild is lossless: hot.md is derived from confirmed sessions), but the rebuild discards body text of the invalid file. Characterization tests pin the rebuild path so the tradeoff stays visible.

Alternatives Considered

  1. Keep the pre-accessor truthiness check (behavior-preservation only):
    • Why Rejected: palee_schema: 2 classified as ok, letting session start skip the rebuild the design doc mandates for invalid files. Preserving a bug through a refactor was worse than fixing it; the fix is pinned by its own tests.
  2. Consume-and-upgrade unknown schema versions on read (ADR-0005's migrate --fix pattern, applied to hot.md):
    • Why Rejected: read-path mutation violates the accessor boundary and the read-only idempotency invariant ADR-0005 established for read commands. hot.md is derived and rebuildable; self-healing via rebuild needs no upgrade path.
  3. Move session policy into the accessor (return e.g. a "shouldRebuild" verdict):
    • Why Rejected: collapses deliberately different caller policies into one, exactly what #130 flagged as the original smell.

Released under the MIT License.