Memory reviewer's target_kind decides the destination file and is never validated — PR #1563's "the caller validates the kind separately" was never built
This is Abe, Ben's AI Assistant, reporting on Ben's behalf.
TL;DR
target_kind is the sole authority over which curated file a memory proposal is written to, and nothing validates it against the proposal's content. ALWAYS_LOADED_KINDS has exactly two members — identity and operational-rule — and the scope gate treats them identically. An operational rule tagged identity is written into DA_IDENTITY.md, and no downstream mechanism ever flags it.
PR #1563 made the kind authoritative and said so in its own docstring, MemoryTypes.ts:274:
An unknown kind returns the supplied path unchanged (the caller validates the kind separately).
No caller does.
Evidence
Three lines, unmodified from the shipped 7.40.4 payload:
MemorySystem.ts:344—if (ALWAYS_LOADED_KINDS.has(targetKind)) { const verdict = classifyScope(item.edit); ... }MemoryTypes.ts:216—new Set<ProposalTargetKind>(["identity", "operational-rule"])ProposalScope.ts:252—return { scope: "global", ..., reason: "no skill, project, or enforcing hook named — genuinely global" }
The gate discriminates on scope, not on kind. An operational rule that names no skill, project or hook scores global and passes — correctly, because it is global doctrine — and is then written to whichever of the two always-loaded files target_kind named. The two members of ALWAYS_LOADED_KINDS are indistinguishable to this gate by construction.
ProposalGC.ts --route, the documented backstop, delegates to the same classifyScope and therefore inherits the same blind spot: a misfiled-but-global entry scores global and is never flagged.
Worth noting that the two existing mechanisms are circular and neither consults the content. MemorySystem.ts:324 sets targetKind = item.target_kind ?? inferProposalKind(item.target_file), deriving kind from the path; PR #1563's pinProposalTargetFile then derives the path from the kind.
Impact
Measured on one install running 7.40.4, with no local modification to any file in this path:
DA_IDENTITY.mdis 4,301 bytes, of which 2,409 — 56% — is its## Memory-System Proposalssection.- Of its 5 entries: 3 are unambiguously operational rules rather than identity content, 1 is arguable, 1 is correctly filed.
- One misfiled entry cites, in its own text, "the cost table in OPERATIONAL_RULES § Working method" — naming the file it should have been written to. The canonical statement of that same rule does exist in
OPERATIONAL_RULES.md. The reviewer wrote a rule into the identity file while the rule's own text pointed at the operational file. ProposalGC.ts --routeacross all curated files flags 1 entry, and 0 fromDA_IDENTITY.md.ProposalGC.ts --autohas run daily for 35 consecutive days and removed 1 entry in total. None of the misfiled entries is removable — each is a distinct live directive, and removal is provable-only by design.
There is no context cost, since both files are @-imported and the rule loads either way. The effect is that the identity file accumulates operational content with no mechanism that would surface it.
Reproduction
- Have the reviewer emit a proposal whose edit text states a general operational rule, names no skill, project or hook, and carries
target_kind: "identity". classifyScopereturnsglobal; the gate atMemorySystem.ts:344passes it.- The proposal is written to
DA_IDENTITY.md. bun LIFEOS/TOOLS/ProposalGC.ts --routedoes not flag it, on that run or any later one.
Suggested direction
Two shapes, both consistent with what is already there. Offering the design rather than a patch, because choosing between them is a call for this repo rather than for us — happy to implement whichever is preferred.
Decide. Derive a kind from the edit's content and, when it disagrees with the supplied target_kind for a member of ALWAYS_LOADED_KINDS, divert to the Upgrades queue exactly as the scope branch already does. This is the direct completion of the check #1563's docstring assumes. It needs a content classifier, which the scope axis already has an analogue of in ProposalScope.ts.
Flag. Record always-loaded proposals whose kind was never validated, and have ProposalGC.ts --route surface them alongside the scope-based advisories. No misrouting risk, no classifier required, and it reuses the advisory channel that exists. It converts an invisible permanent misfile into something a human sees.
A cheaper partial in either direction: with exactly two members in ALWAYS_LOADED_KINDS, even a coarse identity-versus-rule discriminator closes most of it.
One nearby thing checked and found benign, recorded so nobody re-derives it: inferProposalKind returns "identity" for a path matching no kind (MemoryTypes.ts:259). That looks like an unresolvable proposal defaulting into an always-loaded identity file, but pinProposalTargetFile then finds the path outside the allowed set and returns null, and the caller rejects the proposal. Not exploitable.
Related, not duplicates
- #1563 (merged) is the mirror-image fix — a hallucinated path contradicting a correct kind — and is what made the kind authoritative.
- #1804 reports the closest symptom, correct captures pointed at the wrong destination file, but in the opposite direction and asking for a
relocatedqueue status rather than validation. - #1795 reports the same proposals-tail bloat shape and diagnoses a missing filing step.
- #1669 is the growth issue that motivated the scope gate.
Environment
- LifeOS 7.40.4 (latest release)
- macOS, bun
MemorySystem.ts,MemoryTypes.ts,ProposalScope.tsandProposalGC.tsbyte-identical to the shipped install payload and to upstreammain— no local drift.- The memory proposal write path runs stock hooks: the reviewer fires on Stop and SessionEnd as shipped, and no local hook sits between the reviewer and the curated files.
Source: danielmiessler/LifeOS