17 KiB
PR3.5 Memory-Trust — Real Memory-Store API Surface
Recon for the warm-Hive PR3.5 Memory-Trust arc. Question: what can the Memory-Trust UI wire to as it exists today, vs what must be added — and what it must never fabricate.
Repo:
D:/Projects/waggle-os. Substrate:packages/hive-mind-core/src/mind/. Server surface:packages/server/src/local/routes/. All claims below are quoted file:line. Brutally honest split: PR3.5 must not invent data the store doesn't hold.
TL;DR Capability Matrix
| Feature | Verdict | Where the truth lives |
|---|---|---|
| source (provenance class) | ✅ REAL (stored column) | memory_frames.source |
| forget (delete) | ✅ REAL (store + 2 routes) | FrameStore.delete() + DELETE /api/memory/:id |
| correct (edit content) | ✅ REAL (store + 2 routes) | FrameStore.update() + PATCH /api/memory/:id |
| freshness / staleness | 🟡 DERIVABLE (compute fn exists; not surfaced) | computeTemporalScore() over created_at / last_accessed |
| confidence | 🟡 DERIVABLE / PARTIAL (metadata blob; only set at harvest) | memory_frames.metadata.confidence (B2 heuristic) |
| trace ("why did you do that?") | 🟡 DERIVABLE (rich store + 1 route; not per-frame) | execution_traces + GET /api/agents/:id/traces |
| confirm / verified status | 🔴 MUST-BUILD (no per-frame confirm concept) | absent — see §4 |
One-line split: source, forget, correct are REAL and route-exposed today. freshness, confidence, and trace are DERIVABLE (the signals exist in the substrate but are not projected onto the Chat/Workspace surfaces, and confidence is only populated on the harvest path). A per-memory "confirm / needs-confirm / verified" status is MUST-BUILD — it does not exist.
1. The FRAME shape
Canonical row type — packages/hive-mind-core/src/mind/frames.ts:27-48:
export interface MemoryFrame {
id: number;
frame_type: FrameType; // 'I' | 'P' | 'B'
gop_id: string;
t: number;
base_frame_id: number | null;
content: string;
importance: Importance; // critical|important|normal|temporary|deprecated
source: FrameSource; // see below
access_count: number;
created_at: string;
last_accessed: string;
content_hash?: string | null; // dedup
metadata?: string; // JSON blob (Phase 2B) — provenance/classification
}
DDL backing it — schema.ts:47-73 (memory_frames). Every field above is a real
stored column; metadata is TEXT NOT NULL DEFAULT '{}' (schema.ts:65).
Per-field verdict against the requested set:
- id — REAL (
frames.ts:28, PKschema.ts:48). - content / text — REAL (
frames.ts:33,schema.ts:53). The field iscontent, NOTtext. - source — ✅ REAL stored column.
frames.ts:25definesFrameSource = 'user_stated' | 'tool_verified' | 'agent_inferred' | 'import' | 'system' | 'personal' | 'workspace' | 'team_sync'. The DB CHECK is narrower —schema.ts:56-57only allows('user_stated','tool_verified','agent_inferred','import','system')(thepersonal/workspace/team_syncmembers are search-time mind labels, not persisted provenance — seememory.ts:41-43). - confidence / score — 🟡 NOT a frame column. No
confidenceorscorecolumn onmemory_frames. Two distinct things wear the name:scoreis a search-time, computed ranking field added by HybridSearch (returned innormalizeFrameatmemory.ts:58), never stored.confidence(0-100) is a derived metadata field that ridesmemory_frames.metadataJSON, projected bynormalizeToMemoryatmemory-center.ts:100(typeof meta.confidence === 'number' ? meta.confidence : undefined). It is only populated on the harvest path (harvestConfidence, see §"confidence" below). Curated/agent/quick-capture writes leave itundefined.- (
confidence REALdoes exist — but onknowledge_relations,schema.ts:109, not on frames.)
- created_at — REAL (
frames.ts:37,schema.ts:59, defaultdatetime('now')). - updated_at — 🔴 NOT on frames. The frame table has no
updated_atcolumn.identity/procedurestables have one (schema.ts:20,166); frames do not. AnupdatedAtis faked into themetadatablob on PATCH (memory-center.ts:336). The sharedMemory.updatedAtreads it from metadata (memory-center.ts:112). - accessed_at / last_used — REAL:
last_accessed(frames.ts:38,schema.ts:60), bumped bytouch()(frames.ts:184-191).access_count(frames.ts:36) is the use counter. - decay / staleness — 🟡 NOT stored; DERIVABLE. No decay column. Decay is a pure compute over timestamps in
scoring.ts(see §5). - type / kind — split brain:
frame_type('I' | 'P' | 'B') is the substrate-internal kind (incremental/patch/branch), REAL (frames.ts:29).importance(critical|important|normal|temporary|deprecated) is the closest stored "salience" axis, REAL (frames.ts:35).- The product-facing
MemoryKind(fact|decision|task|preference|strategy|learning|goal|entity,shared/types.ts:354-356) is NOT a column — it ridesmetadata.kind, projected atmemory-center.ts:91, defaulting to'fact'when absent.
Verdict — {confidence, freshness/recency, source}:
- source → REAL stored field.
- freshness/recency → DERIVED (no column; computed from
created_at/last_accessed). - confidence → metadata-blob field, REAL-but-sparse (only harvest sets it); treat as DERIVABLE/PARTIAL for any non-harvested frame.
2. FORGET (delete) — ✅ REAL
Store: FrameStore.delete(id): boolean — frames.ts:321-334. Hard delete; cleans up
FTS, vec, KG entity links, and nullifies self-referential FKs. There is no
tombstone — it's a real row removal. Also deleteByContentPrefix (frames.ts:343-354).
Routes that expose it:
DELETE /api/memory/frames/:id—memory.ts:571-607(legacy frame-id contract; workspace-first then personal fallback).DELETE /api/memory/:id—memory-center.ts:390-411(bare-id contract, hard delete,mind-strict).
Both emit an audit event (eventType: 'memory_delete', memory.ts:601, memory-center.ts:402).
Suggested approach: wire the Memory-Trust "Forget" action straight to DELETE /api/memory/:id?mind=…. No new backend.
3. CORRECT (edit content) — ✅ REAL
Store: FrameStore.update(id, content, importance?) — frames.ts:281-301. Updates the
row, FTS index, vec index, and maintains content_hash. Returns the updated frame.
Routes:
PUT /api/memory/frames/:id—memory.ts:466-533(content + importance; XSS-sanitizedmemory.ts:483).PATCH /api/memory/:id—memory-center.ts:296-352(content/importance and metadata classification: kind/scope/tags/status/title/evidence; stampsmetadata.updatedAt).
Suggested approach: "Correct this memory" → PATCH /api/memory/:id. It already supports
editing content and reclassifying. No new backend.
4. CONFIRM / "needs confirm" / verified status — 🔴 MUST-BUILD (mostly)
There is NO per-frame "confirmed / needs-confirmation / verified" concept. Honest accounting of the near-misses:
FrameSourcehas atool_verifiedmember (frames.ts:25,schema.ts:57) — but that's a provenance class set at write time (this fact came from a verified tool call), not a user-confirmation lifecycle. It's never toggled after creation.execution_traces.outcomehas a'verified'value (schema.ts:223,execution-traces.ts:20) — but that's about an agent run passing a verifier gate, not a memory being confirmed.- The shared
MemoryStatusunion (shared/types.ts:514-515) has'unreviewed' | 'low_confidence' | 'conflict'— the vocabulary for a review lifecycle exists and is already projected (memory-center.ts:84-85) and filterable (memory-center.ts:198). But: nothing writesunreviewedtoday (the create path stamps'active',memory-center.ts:273; harvest commit is the only intendedunreviewedproducer pertypes.ts:506-507but that write was not confirmed in this recon), andlow_confidence/conflictare explicitly noted as "may be derived at recall-time rather than persisted" (types.ts:510-511) — i.e. not implemented.
Verdict: A "Confirm" / "Needs your confirmation" affordance is MUST-BUILD, but cheaply:
the metadata.status field + the MemoryStatus union are the rails. Add a
POST /api/memory/:id/confirm that sets metadata.status='active' (clearing unreviewed),
mirroring the existing /archive route (memory-center.ts:354-386). The "needs confirm"
queue = GET /api/memory?status=unreviewed (already works — memory-center.ts:198).
Do NOT show a "verified ✓" badge unless the frame is genuinely source==='tool_verified' or status was explicitly set — anything else is fabrication.
5. STALE / freshness / decay — 🟡 DERIVABLE (not surfaced)
Real signal exists as pure compute, not a stored flag — scoring.ts:
computeTemporalScore(iso)—scoring.ts:52-63. Returns1.0if within 7 days (RECENCY_BOOST_DAYS,scoring.ts:38), else exponential decay with a 30-day half-life (HALF_LIFE_DAYS,scoring.ts:37):Math.pow(0.5, daysSince / 30).- Decay anchors on
created_at(write time), NOTlast_accessed— deliberate (scoring.ts:91-97):last_accessedis bumped bytouch()on every read, so decaying on it made the dimension constant noise. Usecreated_atfor "age". - The substrate also auto-prunes by age in
FrameStore.compact()— temporary frames > 30d, deprecated > 90d (frames.ts:366-389) — a real staleness policy, but a background sweep, not a per-frame badge.
Verdict: "Stale · worth a review" is DERIVABLE today with zero new storage:
compute daysSince(created_at) (or call computeTemporalScore) FE-side or in a thin route.
A reasonable "stale" threshold: temporal score below ~0.5 ≈ older than one half-life (~30d),
or simply created_at older than N days for importance ∈ {normal, temporary}.
Constraint: created_at IS projected on the legacy /api/memory/frames and /search
responses (memory.ts:56), so the FE already has the input. Do NOT invent a "freshness %"
that implies stored decay — present it as "last touched / age", computed honestly.
6. "WHY DID YOU DO THAT?" trace — 🟡 DERIVABLE (rich; not per-frame)
A real, rich execution-trace store exists — execution-traces.ts + execution_traces
DDL (schema.ts:215-233). Per "unit of agent work" it records:
outcome(success|corrected|abandoned|verified|pending,execution-traces.ts:20),cost_usd,duration_ms,created_at/finalized_at,session_id/persona_id/workspace_id/model(execution-traces.ts:75-88).- A structured
trace_jsonpayload (TracePayload,execution-traces.ts:48-72):input,output,reasoning[](step text + ts),toolCalls[](tool, args, result, ok, durationMs, ts —execution-traces.ts:22-36),artifacts[],tokens, optionalharnessgate results, andcorrectionFeedback. This is exactly the data a "why did you do that?" panel needs.
Production wiring (it IS populated for chat):
- The chat loop creates a
TraceRecorderandstart()s a trace per turn (chat.ts:1277-1286), records reasoning + tool calls through it, andfinalize()s with outcome+output+tokens (chat.ts:1413-1424); aborted/errored turns finalize as'abandoned'(chat.ts:1648-1649,1713). - Corrections downgrade a prior trace via
markCorrected()(execution-traces.ts:297-310, feedback noted atchat.ts:1651).
Route exposure:
GET /api/agents/:id/traces—agents.ts:469-505. Returns id/ts/session/workspace/model/ outcome/cost/durationMs/tools[] per trace. It filters by theagent:{id}tag (agents.ts:486-487).
Two honest gaps for PR3.5:
- The chat trace
start()does NOT pass atags:['agent:…'](chat.ts:1279-1285), so the agent-traces route's tag filter will not surface conversational traces. The richest traces (chat reasoning + tool calls) are written but not addressable by that route. - No frame↔trace backlink. Nothing links a saved
memory_frameto theexecution_tracethat produced it (notrace_idcolumn, no metadata field; grep of chat/home/workspace routes for anytraceId/trace_idlink returned nothing). So "why is THIS specific memory here?" can't be answered from the trace store today — you can only show "what the agent did in this turn/session", not "the decision that wrote this frame".
Verdict: Trace data is DERIVABLE and rich for the session/turn granularity, but a
per-frame "why" requires MUST-BUILD plumbing (a trace_id on the frame metadata at
write time, plus a GET /api/memory/:id/trace resolver). For PR3.5, the cheap honest win
is a session/turn-scoped trace view (reasoning steps + tool calls) — and either (a) add the
agent: tag to chat start(), or (b) add a thin GET /api/sessions/:id/traces reading
traceStore.queryParsed({ sessionId }) (the store already supports it, execution-traces.ts:328).
Do NOT synthesize a "reason" string for a frame that has no linked trace.
7. SERVER ROUTES exposing memory to apps/web
Two plugins. Legacy frame ops (memory.ts) + shared-Memory-entity contract
(memory-center.ts). Quoted with method + path + file:line:
packages/server/src/local/routes/memory.ts
| Method | Path | Line | Notes |
|---|---|---|---|
| GET | /api/memory/search?q&scope&limit&workspace&since&until |
memory.ts:126 |
HybridSearch; returns normalized frames incl. computed score, source |
| GET | /api/memory/frames?workspace&limit&since&until |
memory.ts:194 |
recent frames, no query needed (Memory tab initial load) |
| POST | /api/memory/frames |
memory.ts:245 |
direct write; optional entity extraction; XSS-sanitized; dedup |
| GET | /api/memory/stats?workspace&scope |
memory.ts:397 |
counts only (mind-isolation: ?scope=all-minds opt-in, memory.ts:425-438) |
| PUT | /api/memory/frames/:id |
memory.ts:467 |
CORRECT — edit content/importance |
| PATCH | /api/memory/frames/:id/access |
memory.ts:539 |
atomic access_count++ (touch) |
| DELETE | /api/memory/frames/:id |
memory.ts:571 |
FORGET — hard delete |
| POST | /api/quick-capture |
memory.ts:614 |
Home quick-capture → frame (+ awareness task row) |
packages/server/src/local/routes/memory-center.ts (shared Memory shape)
| Method | Path | Line | Notes |
|---|---|---|---|
| GET | /api/memory?mind&kind&status&scope&q&minConfidence&limit |
memory-center.ts:167 |
list as Memory; status/confidence filters live here |
| GET | /api/memory/:id |
memory-center.ts:215 |
one memory, normalized |
| POST | /api/memory |
memory-center.ts:235 |
curated create (sets metadata.kind/scope/status='active'/confidence) |
| PATCH | /api/memory/:id |
memory-center.ts:296 |
CORRECT — content + reclassify; stamps updatedAt |
| POST | /api/memory/:id/archive |
memory-center.ts:355 |
reversible Archive (status='archived') — template for a /confirm route |
| DELETE | /api/memory/:id |
memory-center.ts:390 |
FORGET — hard delete, mind-strict |
| POST | /api/memory/merge |
memory-center.ts:416 |
merge ≥2 → concat + archive originals (C11) |
Projection note (load-bearing for the warm-Hive provenance pill): the legacy
memory.ts normalizeFrame DOES carry source provenance (memory.ts:48-49), and
memory-center.ts normalizeToMemory carries source + sourceUrl/sourceId/confidence
(memory-center.ts:97-100). BUT the Chat and Workspace context surfaces do not read
those routes — workspace-context.ts:283-284 selects only content, importance, created_at
from memory_frames, omitting source. This is the "frame.source 1-field server
projection" gap flagged in the S2 handoff — adding source (and created_at, already there)
to that projection is the MUST-BUILD that unlocks the ⬡ provenance pill on Chat + Workspace.
PR3.5 build guidance (do-not-fabricate checklist)
| UI affordance | Wire to | New work |
|---|---|---|
| ⬡ provenance pill (source) | Memory.source from /api/memory ✅; for Chat/Workspace context add source to workspace-context.ts:283 SELECT |
1-field projection (MUST-BUILD, tiny) |
| Forget button | DELETE /api/memory/:id ✅ |
none |
| Correct / edit | PATCH /api/memory/:id ✅ |
none |
| "Stale · review?" | compute from created_at (already projected) via computeTemporalScore |
FE compute / thin helper (DERIVABLE) |
| Confidence chip | Memory.confidence (only present on harvested frames) |
show only when present; never default a number (PARTIAL) |
| Confirm / needs-confirm | POST /api/memory/:id/confirm (set metadata.status) + GET /api/memory?status=unreviewed |
new route mirroring /archive (MUST-BUILD, cheap) |
| "Why did you do that?" | GET /api/agents/:id/traces (session-level) |
per-frame "why" needs a trace_id backlink (MUST-BUILD); session/turn view is DERIVABLE |
Hard rule: confidence, freshness, and trace-reason are the three places PR3.5 could silently fabricate. Confidence is sparse (harvest-only) → hide when absent. Freshness has no stored decay → present as honest age, not a stored %. Per-frame "why" has no backlink → only show a reason when a real linked trace exists.