Repository navigation
Dashboard: show when a user memory was rewritten, and accumulate candidate provenance across rewrites #796
Description
Short description
Show a user memory's last-rewrite time, and make from candidates: accumulate instead of freezing at promotion.
What problem does this solve?
Stable user memories are rewritten by the review-user-memories dreamer task. applyReviewVerdict updates a row with UPDATE user_memories SET content = ?, updated_at = ? WHERE id = ?, so content and updated_at change while id, promoted_at, created_at, status and the provenance columns stay put.
The dashboard card never renders updated_at, so a rewritten memory is indistinguishable from one untouched since promotion. Measured on this machine (dashboard 0.20.6, plugin 0.49.13, ~/.local/share/cortexkit/magic-context/context.db):
- memory
#5:promoted_at2026-09-27 14:58,updated_at2026-10-21 03:13:32 (a completedreview-user-memoriesrun). - card rendered on 2026-10-21:
active promoted 23d ago from candidates: 46, 47, 48 23d ago - actual state: content replaced 2 hours ago; 46/47/48 are the promotion candidates only.
The second half of the problem is structural. The update_existing entries are mapped to { memoryId, content } — candidate_ids is dropped and used only for pool consumption. source_candidate_ids / source_candidate_provenance are written once by insertUserMemory and never appended, while the candidate rows themselves are deleted after the run. The evidence that justified the rewrite is therefore gone from both the row and the pool.
Net effect: neither the UI nor the database can answer "was this memory rewritten, when, and on what evidence", even though the rewrite is the operation most likely to change what every future session sees.
Proposed solution
Phase 1 — display + accumulating provenance (no schema change)
UserMemories.tsx.card-meta: renderupdated <relative>next topromotedwhenupdated_at - promoted_at > 60_000(at promotion all three timestamps are written from the samenow, so the threshold is exact and cannot fire on an untouched row).titlecarries the absolute timestamp.- Drop the trailing bare
created_atspan: it is always a duplicate ofpromoted_attoday (insertUserMemorywrites both from onenow), so the third relative time adds noise. source_candidate_idsdisplay: first 8 ids ++N more, with the full list intitle. The list becomes a truthful cumulative union.- Storage:
updateUserMemoryContent(db, id, content, appendedCandidateIds = [])performs an order-preserving, de-duplicating union ofsource_candidate_idsand appends the matchingsource_candidate_provenanceentries. The provenance must be loaded before the candidate rows are deleted — they are deleted a few lines later in the same lease-guarded write, so this works today without changing ordering. The review prompt already includescandidate_idsin eachupdate_existingentry, so nothing in the model contract has to change; the mapper only has to stop discarding the field. - Invariant: if
loadUserMemorySourceProvenance()returns nothing for the appended ids (rows already pruned), keep the previous provenance JSON.serializeUserMemorySourceProvenance()currently returnsnullwhen provenance is empty and ids are non-empty; that path must not be reachable from the update branch. - Optional, same phase: tag provenance entries with
origin: "promotion" | "update"andadded_at. Entries withoutoriginread aspromotion; no migration needed. This lets the UI later say3 promoted · 2 rewritteninstead of one flat list.
Resulting card (before / after):
active promoted 23d ago from candidates: 46, 47, 48 23d ago
active promoted 23d ago · updated 2h ago from candidates: 46, 47, 48, 141, 143, 145, 146 +2 more
Phase 2 (optional) — revision history
A user_memory_revisions table (memory_id, changed_at, previous_content, new_content, run_id, candidate_ids, source), written by both writers (dreamer update and dashboard Edit), enables a History ▾ disclosure with per-revision diffs. Phase 1 can say "rewritten at T"; only phase 2 can say "from what to what".
Phase 2 needs a migration and a look at the mc-module table fence: the Rust module keeps a per-table fingerprint for user_memories (crates/mc-module/src/host_store.rs), so a new column on that table is not a free change. A separate table avoids touching the existing fingerprint, but the migration still has to be reviewed on the Rust side. Phase 1 changes no schema and therefore no fence.
Alternatives considered
- Relabel only (
promoted from candidates:): accurate for today's data but cements the loss — the rewrite evidence is still deleted, and the card still cannot show that anything changed. - Show only
updated_at, keep provenance frozen: minimal, but leaves the source line permanently misleading after the first rewrite. - Enforce one-line-per-observation instead of rewriting: would still need update semantics for merged/refined memories, and loses the same audit trail.
Area
Dashboard
Additional context
Acceptance
- Untouched memories show no
updatedmarker; a rewritten row showsupdated <relative>matchinguser_memories.updated_at. - After a run that rewrites
#5from candidates 141/143/145/146, the card lists46, 47, 48, 141, 143, 145, 146with the promotion batch first, and collapses past 8 as+N more. - Deleting the new candidate rows before the update cannot clear
source_candidate_provenance. - Repeated updates never duplicate an id and keep
updated_atmonotonic. - Harness coverage: the write path is the shared storage helper used by every harness (OpenCode 1/2, Pi, OMP) — no per-harness duplicate of this logic exists — and the dashboard reads the same table directly. The Rust module mirrors and renders
user_memoriesbut does not own promotion into it, so phase 1 has no Rust-side counterpart to change.
Additional context
Verified against master a5c0299 (2026-10-26) and the shipped packages @cortexkit/opencode-magic-context@0.49.23 / dashboard-v0.20.19:
packages/plugin/src/features/magic-context/user-memory/review-user-memories.ts— theupdate_existingmapper dropscandidate_ids; the call passes(id, content)only. The prompt schema a few dozen lines above already asks the model forcandidate_idsper update.packages/plugin/src/features/magic-context/user-memory/storage-user-memory.ts—updateUserMemoryContentwritescontent+updated_atonly;serializeUserMemorySourceProvenancereturnsnullon empty provenance + non-empty ids;insertUserMemorywrites both provenance columns once.packages/dashboard/src-tauri/src/db.rs—UserMemoryalready carriesupdated_at, andget_user_memoriesalready selects it.packages/dashboard/src/lib/types.ts— the frontendUserMemoryinterface already declaresupdated_at.packages/dashboard/src/components/UserMemories/UserMemories.tsx—.card-metarendersstatus,promoted_at,source_candidate_ids,created_atonly. This is the only missing piece for phase 1 step 1–3.
No existing issue covers this (checked the tracker through #795), and user_memory_revisions does not exist anywhere in the repository.
I can prepare the phase-1 PR after a design-approved label; the UI mockup (before/after card plus an optional history panel) and the exact patch sites are ready.
Activity
magic-alfonso commentedon Oct 27, 2026
Thanks for the careful write-up. The code confirms it. In
review-user-memories.ts, theupdate_existingbranch passes only the memory id and the new content toupdateUserMemoryContent, so each update'scandidate_idsare used only to consume the pool. A rewrite therefore leavessource_candidate_idsandsource_candidate_provenanceas they were at promotion, the candidate rows that justified it are deleted, and the dashboard card never showsupdated_at.We'll come back on the phase 1 scope before you start a PR, since it touches what we keep as user-memory provenance and the dashboard is moving into the CortexKit desktop app. Phase 2 (a revision table) needs a migration and won't happen right now.
magic-alfonso commentedon Oct 27, 2026
We've decided not to take this one, either phase. The finding is correct, but user memories are on their way out in their current form.
In the memory profiles and levels work, facts about the person become memories at a global level tied to the user's account, with the same evidence and history as every other memory. That replaces the separate user-memory store, its review task and the user-profile block. It replaces the dashboard too, which is moving into the Alfonso apps. Improving how the current store records rewrites, or adding a card to the current dashboard, would mean building on both just before they're replaced.
The user-memory review keeps working as it does today until then. Thanks for the careful investigation and for offering to build it.