Skip to content

Proposal queue: no terminal status for "applied, but to a different file", and terminal statuses are hand-rolled per consumer #1804

Description

@catchingknives

What happened

I worked through a backlog of queued memory proposals in one sitting. Thirteen rows, roughly a month of accumulation. Seven of them were correct captures pointed at the wrong destination file: the reviewer had targeted the always-loaded operational rules file for content that belonged in a skill, in a project file, and in a domain-specific document.

The right outcome for those seven was to write the content where it belonged and close the row. The queue offers no way to say that. The status union in LIFEOS/PULSE/lib/memory-proposals.ts is:

"pending" | "sent" | "accepted" | "rejected" | "edited" | "auto-applied"

accepted is untrue, since the recorded target_file never received the edit. So the only honest-ish option is rejected, and the log now says seven durable user statements were discarded when their content is live in the files where it belongs. The record reads exactly backwards from what happened.

Why this is worth fixing rather than tolerating

There is no runtime consequence today, and that is precisely the trap. The reviewer does not read the queue at all: it has no awareness of what was previously proposed, accepted, or rejected, so a wrong label cannot currently bias anything.

But "feed prior decisions back to the reviewer so it stops re-proposing what the user already ruled on" is an obvious next feature, and it is the natural fix for a reviewer with no proposal memory. The moment someone builds it, these mislabeled rows become its input, and it learns to suppress exactly the proposals the user adopted. The corruption is being written today and consumed later.

This is a silent-wrongness failure rather than a crash. Nothing errors, nothing alerts, and the log stays plausible enough that nobody re-reads it.

The second half: terminal statuses are stringly-typed and hand-rolled, and shipped consumers already disagree

There is no single exported authority for "which statuses are terminal", so every consumer writes its own list. On current main this is not a hypothetical risk; the shipped consumers have already drifted apart:

  1. LIFEOS/PULSE/modules/memory.ts computes pending rows by excluding a literal array, ["auto-applied", "accepted", "rejected", "edited"]. The comment above it records that this exact line was already fixed once, for fix(pulse): pending-proposal badge never clears — only auto-applied counts as resolved #1610, after an earlier version counted resolved rows as pending forever. The fix corrected the values but not the shape.

  2. LIFEOS/PULSE/modules/menubar.ts counts pending as status === "pending" only. It silently drops sent rows, which the dashboard deliberately counts as awaiting a decision. Two surfaces shipped in the same release disagree about what "pending" means.

  3. LIFEOS/TOOLS/MemoryReviewer.ts writes auto-apply-failed, a status that is not in the union at all. The dashboard happens to count it as pending (correctly, per its own comment), but only because unknown strings fall through the exclusion list. Nothing checked that this writer and the type agree.

  4. From a local CLI I built on top of the same library: it wrote approved on its success path, another value outside the union, and gated on approved when checking whether a row was already resolved. Because the dashboard's terminal list does not contain approved, any row resolved that way would be counted as pending forever. That is the same class of bug as fix(pulse): pending-proposal badge never clears — only auto-applied counts as resolved #1610, arriving through a different literal. I am not reporting my CLI as a bug, since it is not shipped code. I am reporting that the library makes the bug easy to write and impossible to catch: a bare string union, no exported terminal set, no helper, no type-level pressure on consumers.

Even the vocabulary confusion shows up in the project's own history: #1610 itself described the human-decision statuses as applied and dismissed, names that have never existed in the union. When the bug report about statuses cannot name the statuses, the strings have no owner.

Suggested fix

  1. Add a terminal status meaning "the content was applied somewhere other than the proposed target", carrying the real destination path. Something like relocated plus a to field.
  2. Give whatever review surface acts on proposals a verb for it, so relocating is one action instead of a manual edit plus a false rejection.
  3. Export the terminal-status set from the module that owns the union, and have every consumer import it:
export const TERMINAL_STATUSES = [...] as const
export function isTerminal(row: ProposalRow): boolean

Then the dashboard, the menu bar, any CLI, and any future reviewer feedback loop agree by construction, and adding a status like relocated is a one-line change rather than a hunt for every hand-rolled array.

Related

#1610 is the same failure mode; this is a request to fix the shape that produced it rather than the values it produced. The missing decision channel (nothing on main can currently write sent, accepted, rejected, or edited at all) is filed separately as #1805, since it is a functional gap rather than a type-design one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions