4.7 KiB
P3 — D2 Two-Mind Memory Center: adversarial review record (2026-06-11)
Workflow: 6 dimension finders (races / contract / security / parity / tests / ux) → per-finding adversarial refuter. 36 agents. 19 confirmed / 11 refuted. First run was VOID (all finders hit the session limit and returned nothing — recorded here because an empty "confirmed" list from a dead run is indistinguishable from a clean one unless you check the failures block).
The HIGH (confirmed independently by 5 of 6 dimensions)
Cross-mind fall-through on mutations — candidateStores pushed the personal store
unconditionally after the workspace store, and PATCH/archive/DELETE/merge acted on the first store
resolving the id. Frame ids collide across the per-mind SQLite DBs (observed on real data: id 36 in
both minds), so a stale workspace row whose id had vanished from the workspace store would resolve
to — and hard-delete/patch/merge-archive — an unrelated PERSONAL memory, returning 200. The path
became reachable for the first time in this phase (pre-P3, the UI never sent workspace on these
mutations). Verifier note: the "two concurrent surfaces" trigger is weak in the single-window app,
but background deletion lanes (W4.3 replace-on-update, compaction, cleanup_frames MCP) + open stale
lists make it ordinary anyway.
Fix: mind-strict resolution. candidateStores(workspace, mind?) — when the caller declares
mind, exactly that store is consulted, no fallback; parseMind 400s on invalid values. All five
single-memory routes + merge accept mind; the adapter threads it; MemoryCenterTab (the only
caller) always declares it. Legacy no-mind fall-through preserved and PINNED as back-compat.
Server pins: workspace-mind PATCH/archive/DELETE succeed; personal-only id + mind=workspace →
404 with the personal frame intact; merge never resolves its id set in personal; 400 contracts.
Other confirmed findings → dispositions
| # | Sev | Finding | Disposition |
|---|---|---|---|
| 2 | MED | mutate()'s closured await load() beats the loadSeq guard — old-mind/old-filter rows can win after a mid-mutation mind/filter switch |
FIXED: post-mutation refetch rides a reloadTick state through the load effect (always current props); pinned with a deferred-archive + mid-flight switch test |
| 3,15 | LOW/MED | In-flight old-mind response can commit into the reset→next-load window | FIXED: mind-switch reset effect bumps loadSeq (invalidates in-flight commits) |
| 5 | LOW | Reset/load keyed on raw workspaceId — personal-mind instances wiped on workspace churn |
FIXED: keyed on effective wsParam |
| 18 | LOW | Misleading "No memories match these filters." during the 250ms debounce window after a switch | FIXED: reset effect sets loading=true |
| 7 | MED | Audit events stamped the REQUEST's workspaceId, not the resolved mind | FIXED: all five audit sites attribute c.mind === 'workspace' ? workspace : 'personal' (incl. POST-create unknown-workspace fallback) |
| 4,19 | LOW | ?filter= stash strands when landing on a legacy tab; replays later |
FIXED: route stashes only when the landing view is Memories |
| 10 | LOW | URL keeps advertising ?filter= after the one-shot stash is consumed |
FIXED: route strips the param post-consumption (replace); refresh/share can't re-seed a stale filter |
| 12 | MED | J08 cold-load route stash unpinned | FIXED: route-level pins (stash + strip; legacy-tab drop) |
| 13 | MED | No server test exercised ANY workspace-mind mutation | FIXED: mutation suite added (see HIGH pins) |
| 14 | MED | Cross-mind fall-through unpinned | FIXED: HIGH pin above |
| 11,17 | LOW | Workspace pill could announce pressed+disabled simultaneously; pills lacked group semantics | FIXED: aria-pressed only when enabled; role="group" labeled |
| 16 | LOW | Tab bar lacked tablist semantics | FIXED: role="tablist"/role="tab"/aria-selected |
Refuted (11) — examples
XSS via the moved dangerouslySetInnerHTML (renderSimpleMarkdown's two defenses hold); path
traversal via the workspace param (getWorkspaceMindDb resolves from an allowlisted map);
mind+filter interaction double-counting (per-store getRecent precedes the merge slice exactly as
before); WorkspaceDesktopApp uncontrolled-mode regression (import is side-effect-free); StrictMode
double-stash (ref-latched); several parity claims (banner gating, context-rail wiring, KG scope
selector all carried over).
Behavior change vs the pre-review build (review happened before anything was committed)
?filter= no longer survives in the URL after consumption, and a mind switch no longer carries it
(one-shot intent semantics) — the original "filter survives the switch" route pin was replaced
accordingly.