Skip to content

fix: stamp frontmatter when the memory loop writes a context file - #1667

Open
elhoim wants to merge 1 commit into
danielmiessler:mainfrom
elhoim:fix/stamp-context-file-writes
Open

fix: stamp frontmatter when the memory loop writes a context file#1667
elhoim wants to merge 1 commit into
danielmiessler:mainfrom
elhoim:fix/stamp-context-file-writes

Conversation

@elhoim

@elhoim elhoim commented Jul 27, 2026

Copy link
Copy Markdown

Problem

The memory loop appends principal-authored rules to always-loaded LIFEOS/USER/ context files under a ## Memory-System Proposals section. Two writers do it:

  • LIFEOS/PULSE/lib/telegram-proposals.tsapplyProposalEdit() — the chokepoint for both the auto-apply path (MemoryReviewer.ts) and the Telegram yes/edit reply
  • LIFEOS/TOOLS/ProposalGC.ts → the --apply loop that removes provably-redundant entries from the same four files

Neither touches the file's frontmatter. On my install, LIFEOS/USER/CONFIG/OPERATIONAL_RULES.md had accumulated four dated rules (2026-07-23 → 2026-07-27) while its header still read:

provenance: template
last_updated: 1970-01-01T00:00:00Z
last_updated_by: template

Two separate things break:

  1. Freshness lies. last_updated is the write clock in pai-freshness-v1. A file mutated four times reads as never written since epoch.
  2. Containment lies — the sharper one. LIFEOS/PULSE/Tools/ReleaseAudit.ts treats provenance: template as the ship permit for anything under LIFEOS/USER/ ("only template may ship"). A file the memory loop has written personal rules into still presents itself to the release audit as pristine template.

Fix

Adds stampContextWrite(path, by) to LIFEOS/TOOLS/TelosFreshness.ts, called from both writers after a successful write. One atomic read-modify-write that:

  1. sets last_updated / last_updated_by
  2. flips provenance: templateprovenance: customized

provenance-watcher.ts already ships a flipToCustomized() doing half of this, but nothing imports it — it is dead code. I left it alone rather than wiring it up: in-band stamping at the two writers is deterministic and probeable, where a recursive fs watcher is neither (it fires on its own write and needs a host process).

Deliberate limits

  • The provenance key is never invented. A file without one keeps none. ReleaseAudit already treats a missing key as a violation, and inventing one would paper over it.
  • last_reviewed is never touched. Only a principal review bumps that (bumpReviewedTimestamp); a machine write is not a review.
  • Frontmatter is edited line-wise, not re-serialized, so unrelated keys keep their order and quoting in files that ship.
  • Stamping is best-effort and swallows its own errors. The rule landing in the file is the outcome that matters; a stamping failure must never cost the caller the write it just made.
  • customized, not mixed. mixed is arguably more literal for "template body plus principal additions", but every existing writer in the tree (MarkCustomized.ts, edit-handler.ts, provenance-watcher.ts) writes customized for exactly this transition. One meaning per value.

Verification

$ bun LIFEOS/TOOLS/TelosFreshness.ts --selftest

19 checks over temp fixtures — provenance flip on template, no flip on customized/mixed, no key invented when absent, convention and body bytes preserved, last_reviewed/last_reviewed_by preserved verbatim, and both the missing-file and malformed-frontmatter paths returning {changed:false} instead of throwing.

Separately probed end-to-end against a temp fixture: a real applyProposalEdit() call leaves both the new body entry (with its <!-- applied: … --> comment) and a stamped header, and is idempotent across two applies.

Not in this PR

OPERATIONAL_RULES.md is absent from CONTEXT_FRESHNESS_REGISTRY in TelosFreshness.ts, so nothing computes an A–F grade for it even now that its header is honest. Adding it changes the statusline grade denominator, which felt like a separate decision from fixing the write path.

The memory loop appends principal-authored rules to always-loaded USER
context files under `## Memory-System Proposals`, via two writers:

  - `telegram-proposals.ts` → `applyProposalEdit()`, the chokepoint for both
    the auto-apply path (`MemoryReviewer.ts`) and the Telegram yes/edit reply
  - `ProposalGC.ts` → the `--apply` loop that removes redundant entries

Neither touched the file's frontmatter, so a file could accumulate real
content while its header still read:

    provenance: template
    last_updated: 1970-01-01T00:00:00Z

Two things break as a result. The freshness convention (pai-freshness-v1)
reads `last_updated` as the write clock, so a repeatedly-mutated file looks
untouched since epoch. More sharply, `PULSE/Tools/ReleaseAudit.ts` treats
`provenance: template` as the ship permit for anything under `LIFEOS/USER/`
("only template may ship") — so a file the memory loop has written personal
rules into still presents itself to the release audit as pristine template.

Adds `stampContextWrite(path, by)` to `TelosFreshness.ts` and calls it from
both writers. It bumps `last_updated`/`last_updated_by` and flips
`provenance: template` → `customized`, in one atomic read-modify-write.

Deliberate limits:
  - The `provenance` key is never invented. A file without one keeps none —
    the release audit already treats a missing key as a violation, and
    inventing one would paper over it.
  - `last_reviewed` is never touched. Only a principal review bumps that
    (`bumpReviewedTimestamp`); a machine write is not a review.
  - Frontmatter is edited line-wise, not re-serialized, so unrelated keys
    keep their order and quoting.
  - Stamping is best-effort and swallows its own errors: the rule landing in
    the file is the outcome that matters, and a stamping failure must never
    cost the caller the write it just made.

`provenance-watcher.ts` already had a `flipToCustomized()` doing half of this,
but nothing imports it. In-band stamping at the two writers is deterministic
and probeable where a recursive fs watcher is neither, so the watcher is left
alone rather than wired up.

Verify: `bun LIFEOS/TOOLS/TelosFreshness.ts --selftest` (19 checks — flip,
no-flip on `customized`/`mixed`, no key invented, `last_reviewed` preserved,
missing file and malformed frontmatter both non-throwing).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant