fix: stamp frontmatter when the memory loop writes a context file - #1667
Open
elhoim wants to merge 1 commit into
Open
fix: stamp frontmatter when the memory loop writes a context file#1667elhoim wants to merge 1 commit into
elhoim wants to merge 1 commit into
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The memory loop appends principal-authored rules to always-loaded
LIFEOS/USER/context files under a## Memory-System Proposalssection. Two writers do it:LIFEOS/PULSE/lib/telegram-proposals.ts→applyProposalEdit()— the chokepoint for both the auto-apply path (MemoryReviewer.ts) and the Telegramyes/editreplyLIFEOS/TOOLS/ProposalGC.ts→ the--applyloop that removes provably-redundant entries from the same four filesNeither touches the file's frontmatter. On my install,
LIFEOS/USER/CONFIG/OPERATIONAL_RULES.mdhad accumulated four dated rules (2026-07-23 → 2026-07-27) while its header still read:Two separate things break:
last_updatedis the write clock inpai-freshness-v1. A file mutated four times reads as never written since epoch.LIFEOS/PULSE/Tools/ReleaseAudit.tstreatsprovenance: templateas the ship permit for anything underLIFEOS/USER/("onlytemplatemay 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)toLIFEOS/TOOLS/TelosFreshness.ts, called from both writers after a successful write. One atomic read-modify-write that:last_updated/last_updated_byprovenance: template→provenance: customizedprovenance-watcher.tsalready ships aflipToCustomized()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
provenancekey is never invented. A file without one keeps none.ReleaseAuditalready treats a missing key as a violation, and inventing one would paper over it.last_reviewedis never touched. Only a principal review bumps that (bumpReviewedTimestamp); a machine write is not a review.customized, notmixed.mixedis arguably more literal for "template body plus principal additions", but every existing writer in the tree (MarkCustomized.ts,edit-handler.ts,provenance-watcher.ts) writescustomizedfor exactly this transition. One meaning per value.Verification
19 checks over temp fixtures — provenance flip on
template, no flip oncustomized/mixed, no key invented when absent,conventionand body bytes preserved,last_reviewed/last_reviewed_bypreserved 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.mdis absent fromCONTEXT_FRESHNESS_REGISTRYinTelosFreshness.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.