.agents/skills/ux-audit/references/example/memory.md
A real run of this skill against the Memory module, 2026-07 (LOBE-11150) — a personal-AI-memory
management area: a home persona dashboard + five isomorphic list surfaces
(identities / contexts / preferences / experiences / activities), each with a filter bar, grid /
timeline views, a right-side detail panel, and an "Analyze" flow that extracts memory from chat
history. Surfaces under src/routes/(main)/memory/**, backed by
src/store/userMemory/slices/{home,identity,context,preference,experience,activity,base}. Use it
as a template for the output shape, not current-state truth (re-verify before citing).
Surface class: personal AI memory / personalization store — benchmark against ChatGPT "Manage memories", Gemini personalization, Mem0, Personal.ai, CRM "customer 360". Layers run: L1 (static / code) ✅. L2 / L3 ⏳ not run.
Headline: two axes fail. (1) The read/write error side has no handling at all — one root
cause (every useUserMemoryStore fetch resolves only on success, no onError) hangs list pages on
a permanent skeleton, the home page as a false-empty, and detail panels as a permanent
blank; the edit modal spins forever on a failed save. (2) The surface-class trust-repair /
data-control axis is absent — for a store of data about the user there is no way to correct a
wrong memory (only blind-edit or delete), no retain-without-use (pause/exclude), no export, and no
undo — exactly what this product class is judged on, and exactly what a code-only read is blind to.
| Pattern (family) | Where | Rating | Note |
|---|---|---|---|
| Overview + Detail (nav) | home → 5 list tabs → right-panel detail | ✅ | clean drill; sidebar Nav.tsx |
| Empty-as-onboarding (growth) | list/home empty → MemoryEmpty + MemoryAnalysis CTA | ⚠️ | activities empty has no CTA (gap X1) |
| Loading Skeleton (feedback) | list Loading.tsx reuses card chrome; DetailLoading | ✅ | chrome-reusing skeleton (home uses full-page) |
| Failure + Retry (feedback) | every fetch (5 lists + persona/tags + detail + load-more) | — abs. | systemic root cause (gap A) |
| Search / filter (read) | per-tab FilterBar; q → queryMemories server-side | ✅ | server-side search — no false-empty on unfetched |
| Lists at scale (read) | grid + timeline via Virtuoso, page-12 load-more | ✅ | virtualized + load-more spinner |
| Selection visibility (read §1.3) | card click → ?xId= + right panel | — abs. | no scroll-into-view, no active highlight (gap D) |
| Live/polling (read §1.7) | analysis task poll (useTask.ts) | ⚠️ | failed poll not distinct from running (gap E) |
| Entity lifecycle — delete (act) | *Dropdown delete → confirmModal (danger) | ⚠️ | confirm ok; no undo, no failure toast |
| Entity lifecycle — edit (act/edit) | EditableModal → EditorModal | ⚠️ | failed save spins forever (gap B); volatile draft |
| Entity lifecycle — correct / add | — | — abs. | can't mark-wrong or add manually (gap C, X3) |
| Wide-blast confirm (act §3.7) | PurgeButton (delete ALL) | ⚠️ | one-click danger, no type-to-confirm (gap C) |
| Provenance (source link) | SourceLink.tsx → chat topic | ✅ | the one class-norm we ship (backward-only) |
| Data control (export / pause) | — | — abs. | no export, no off-switch, no undo (gap C) |
| Cross-tab consistency (semantic) | 5 list tabs | ⚠️ | activities diverges; sort vanishes on timeline |
This surface is strong on the read happy-path and on one write flow (analysis), but weak on error handling and data-control — the gaps in §3 rank against that mixed baseline. Keep the following; don't "fix" them.
DateRangeModal submit state machine. Double-submit guard + try/catch/finally +
success toast + close (MemoryAnalysis/DateRangeModal.tsx:42-60). Load-bearing: this is the exact
✅ contrast to gap B's EditorModal no-finally trap — the model every memory write should copy
(reset busy in finally, toast on catch). → landed as ux Feedback §4.2 ✅.q is passed straight to queryMemories
(contexts/index.tsx:57-62, mirrored per tab), so search queries the full set — no client-filter
false-"no results" over the loaded page. Load-bearing: search correctness at scale.
→ landed as ux Read §1.2 ✅.SourceLink provenance. Each item links back to its source chat topic
(SourceLink.tsx:16-40, rendered in every *RightPanel). Load-bearing: the one class-norm we
ship on the trust axis ("why does it know this") — it anchors the axis gap C says is otherwise
absent, so keeping and extending it (backward source → forward "used in N chats") is the move.features/GridView, features/TimeLineView); page-2 shows a footer skeleton (uncached SWR key →
isLoading). Load-bearing: lists-at-scale — the load-more path itself is sound; only its failure
is silent (part of gap A).Loading.tsx keeps card container / border / radius and
swaps only text); single-item delete confirm (*Dropdown.tsx:33-44, danger confirmModal);
analysis running / error Alert (Status.tsx). Solid, not standout — keep.🔴 A — No onError / error state anywhere; every fetch resolves only on success. Systemic:
each slice sets xSearchLoading=false / xInit=true only in onSuccess with no onError
(context/action.ts:88, activity/action.ts:90, identity/action.ts:125, preference/action.ts:91,
experience/action.ts:88; persona/tags home/action.ts:27,50; detail base/action.ts:238). No
*Error field exists in any initialState. Consumers hang differently:
showLoading = xSearchLoading || !xInit
(contexts/index.tsx:80); on failure the flag never flips → skeleton forever, no retry.isLoading (resolves after retries) then falls to
MemoryEmpty when !persona && !roles.length ((home)/index.tsx:52-63) → a failed load reads
as "analyze to get started", inviting redundant re-analysis.{data, isLoading}; content is set only in
the isLoading / data branches (ContextRightPanel.tsx:32-34 + 4 twins) → error and
not-found both render an empty panel body.context/action.ts:44-55).
→ Feedback §4.2 + Read §1.1 (error before empty; error ≠ not-found). The module's flagship ❌.🔴 B — Edit save failure: the modal OK button spins forever, no error, no retry, edit discarded
on close. EditorModal onOk runs setConfirmLoading(true) → await onConfirm → setConfirmLoading(false)
with no try/catch/finally (src/features/EditorModal/index.tsx:35-40), and updateMemory has
no .catch / failure toast (base/action.ts:157-236). A rejected write leaves OK permanently
loading; closing / reloading loses the edit. → Feedback §4.2 (awaited write, no finally), Act §3.1.
🔴 C — Surface-class trust-repair & data-control axis is absent (data about the user). For a store of what the AI believes about you, the class contract is: correct a wrong belief, keep a fact without using it, take your data out, undo a mistake. None exist:
*Dropdown.tsx:47-61); edit
is a blind single-field overwrite, and a deleted fact can be re-extracted identically.status column but no UI on/off (no global "memory off", no per-item pause/exclude).crud.ts exposes only get/update/delete/deleteAll; no export in UI.purgeAllMemories (base/action.ts:54) are hard, no recovery;
Purge all is a one-click danger confirmModal with no type-to-confirm (PurgeButton.tsx:34).
→ new ux rule (Act §3.9) + Act §3.7 (wide-blast). L1 is structurally blind to these (no file:line).🟠 X1 — activities empty state has no CTA, and its toolbar drops Analysis.
activities/features/List/index.tsx:33 renders <MemoryEmpty> with no children (siblings pass
<MemoryAnalysis/>); activities/index.tsx:85 is <ActionBar showPurge> (siblings add
showAnalysis). So Activities has no path to Analysis at all and an empty tab dead-ends. →
Read §1.1, "consistency is semantic".
🟠 D — Deep-linked / restored active item isn't scrolled into view, and cards have no active
highlight. grep scrollIntoView|scrollToIndex across the memory tree = 0; Virtuoso mounts at
scrollTop=0 (TimeLineView/index.tsx, GridView/index.tsx); cards take no active prop. A
?contextId= below the fold = zero list feedback. → Read §1.3 (the no-anchor case).
🟠 E — Analysis flow: failed run has no retry, and a failed poll is indistinguishable from
running. Status.tsx:49-71 shows an error Alert with no Retry; useTask.ts keeps last SWR
data on a poll error so a failed refresh keeps a stale running frame. → Act §3.1, Read §1.7.
🟠 F — No global cross-layer search. Each tab searches only itself; searchMemory /
retrieveMemory exist in the service (services/userMemory/index.ts) but aren't wired to any UI,
so "find everything about X" means repeating the query in 5 tabs. → Read §1.8 (class norm).
🟠 G — Manual "Add memory" is impossible from the UI though the backend supports it.
add*Memory / createIdentity exist (services/userMemory/index.ts) but no button calls them. →
Act §3.4 (lifecycle: create entry point).
🟠 H — Sort silently vanishes on switch to timeline (4 tabs). Sort <Select> renders only in
grid mode and the chosen sort is dropped on toggle with no signal (contexts/index.tsx:52,106). →
"consistency is semantic".
🟠 I — Home persona is read-only and dead-ends. The persona editor + edit trigger are
commented out (Persona/index.tsx:16-35, (home)/index.tsx:27,36); persona sections / role
tags don't link into the detail tabs. → Grow §5.3, Act §3.4.
🟡 J — Edit draft is volatile; silent discard on close. Source is in-memory
editingMemoryContent; destroyOnHidden + onCancel=clearEditingMemory discards with no
dirty-check (EditableModal.tsx:9,28). → Edit §2.1 (lighter for modals).
🟡 K — Score / usage signals inconsistent. Confidence shows for experiences only;
scoreImpact / scoreUrgency / scorePriority and accessedCount / lastAccessedAt are stored
but never surfaced. → consistency; class norm (recency/usage signals).
🟡 L — No bulk-select; only "one" or "everything". Between per-item delete and Purge-all there is no multi-select. → Act §3.1 (bulk ⇄ single parity).
🟡 M — No per-layer explanation / onboarding. Five tabs with only generic empty copy; users get no definition of identities vs contexts vs … → Grow §5.1.
🟡 N — DateRangeModal allows an unbounded all-null range submit with no confirm of scope
(DateRangeModal.tsx:42-49); the analysis trigger isn't hard-locked during an in-flight task
(AnalysisTrigger.tsx:19). → friction / Act §3.1.
userMemory store onSuccess-only / no-onError pattern (gap A)
EditorModal onOk no-finally write trap (gap B).PurgeButton one-click wide-blast, no type-to-confirm (gap C).DateRangeModal's submit machine (guard + try/catch/finally + toast) is
the in-repo ✅ the write-side-finally rule now cites, directly beside the gap-B ❌.MemoryEmpty reads as a real page; the persona / tag-cloud layout; dark mode.?xId= below the fold to
confirm gap D; force a delete/update failure to confirm the missing toasts.