danielmiessler / danielmiessler/LifeOS

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

Open
#2,077 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
19k
Forks
2.5k
Avg merge
8d 17h
Merged PRs (30d)
1

Description

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(["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.md` is 4,301 bytes, of which 2,409 — 56% — is its `## Memory-System Proposals` section.
- 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 --route` across all curated files flags 1 entry, and 0 from `DA_IDENTITY.md`.
- `ProposalGC.ts --auto` has 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

1. 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"`.
2. `classifyScope` returns `global`; the gate at `MemorySystem.ts:344` passes it.
3. The proposal is written to `DA_IDENTITY.md`.
4. `bun LIFEOS/TOOLS/ProposalGC.ts --route` does 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 `relocated` queue 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.ts` and `ProposalGC.ts` byte-identical to the shipped install payload and to upstream `main` — 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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading MemorySystem.ts around lines 324 and 344, then inspect MemoryTypes.ts, ProposalScope.ts, and ProposalGC.ts --route. Reproduce the mismatch described in the issue and trace how target_kind reaches the curated-file write path. Done should mean the chosen validation or advisory behavior catches a global proposal whose content conflicts with its always-loaded target kind.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, typescript
Domain
ai, backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.