Skip to content

Dashboard: show when a user memory was rewritten, and accumulate candidate provenance across rewrites #796

Closed as not planned

Description

@ICEY16360

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_at 2026-09-27 14:58, updated_at 2026-10-21 03:13:32 (a completed review-user-memories run).
  • 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)

  1. UserMemories.tsx .card-meta: render updated <relative> next to promoted when updated_at - promoted_at > 60_000 (at promotion all three timestamps are written from the same now, so the threshold is exact and cannot fire on an untouched row). title carries the absolute timestamp.
  2. Drop the trailing bare created_at span: it is always a duplicate of promoted_at today (insertUserMemory writes both from one now), so the third relative time adds noise.
  3. source_candidate_ids display: first 8 ids + +N more, with the full list in title. The list becomes a truthful cumulative union.
  4. Storage: updateUserMemoryContent(db, id, content, appendedCandidateIds = []) performs an order-preserving, de-duplicating union of source_candidate_ids and appends the matching source_candidate_provenance entries. 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 includes candidate_ids in each update_existing entry, so nothing in the model contract has to change; the mapper only has to stop discarding the field.
  5. Invariant: if loadUserMemorySourceProvenance() returns nothing for the appended ids (rows already pruned), keep the previous provenance JSON. serializeUserMemorySourceProvenance() currently returns null when provenance is empty and ids are non-empty; that path must not be reachable from the update branch.
  6. Optional, same phase: tag provenance entries with origin: "promotion" | "update" and added_at. Entries without origin read as promotion; no migration needed. This lets the UI later say 3 promoted · 2 rewritten instead 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 updated marker; a rewritten row shows updated <relative> matching user_memories.updated_at.
  • After a run that rewrites #5 from candidates 141/143/145/146, the card lists 46, 47, 48, 141, 143, 145, 146 with 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_at monotonic.
  • 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_memories but 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 — the update_existing mapper drops candidate_ids; the call passes (id, content) only. The prompt schema a few dozen lines above already asks the model for candidate_ids per update.
  • packages/plugin/src/features/magic-context/user-memory/storage-user-memory.ts — updateUserMemoryContent writes content + updated_at only; serializeUserMemorySourceProvenance returns null on empty provenance + non-empty ids; insertUserMemory writes both provenance columns once.
  • packages/dashboard/src-tauri/src/db.rs — UserMemory already carries updated_at, and get_user_memories already selects it.
  • packages/dashboard/src/lib/types.ts — the frontend UserMemory interface already declares updated_at.
  • packages/dashboard/src/components/UserMemories/UserMemories.tsx — .card-meta renders status, promoted_at, source_candidate_ids, created_at only. 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

  1. magic-alfonso commented on Oct 27, 2026

    @magic-alfonso

    Thanks for the careful write-up. The code confirms it. In review-user-memories.ts, the update_existing branch passes only the memory id and the new content to updateUserMemoryContent, so each update's candidate_ids are used only to consume the pool. A rewrite therefore leaves source_candidate_ids and source_candidate_provenance as they were at promotion, the candidate rows that justified it are deleted, and the dashboard card never shows updated_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.

  2. magic-alfonso commented on Oct 27, 2026

    @magic-alfonso

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestNew feature or capability request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions