This commit is contained in:
301
docs/redesign-warm-hive/pr35-recon/01-frame-source-server.md
Normal file
301
docs/redesign-warm-hive/pr35-recon/01-frame-source-server.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# PR3.5 Recon — `frame.source` server read/write path (1-field projection)
|
||||
|
||||
**Goal:** add `source` to the `recentMemories` + `recentDecisions` items returned by the
|
||||
workspace-state / workspace-context API so the FE can render a ⬡ provenance pill on Chat +
|
||||
Workspace. This doc maps the exact write site, the exact read/SELECT/projection sites, the
|
||||
type-contract change, and the column's value vocabulary.
|
||||
|
||||
Repo root: `D:/Projects/waggle-os`. All line numbers verified 2026-06-16.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR — what to change
|
||||
|
||||
The ⬡ provenance pill on the **Workspace Briefing** (Chat + Workspace Overview both render
|
||||
`<WorkspaceBriefing>` fed by `GET /api/workspaces/:id/context`) is unlocked by **one route +
|
||||
one type change**:
|
||||
|
||||
1. `packages/server/src/local/routes/workspaces.ts` — add `source` to the two SELECTs (lines
|
||||
**393-400** and **409-417**) and to their two `.map()` projections (lines **402-406** and
|
||||
**419-429**).
|
||||
2. `apps/web/src/lib/types.ts` — widen `WorkspaceContext.recentMemories` / `recentDecisions`
|
||||
item shapes (lines **248-249**) with an optional `source?: string`.
|
||||
|
||||
That is the minimum. There is a SECOND, parallel context builder in
|
||||
`packages/server/src/local/routes/workspace-context.ts` (the `WorkspaceNowBlock` /
|
||||
`buildWorkspaceState` path) that feeds the **system prompt**, NOT the FE pill — see §4 for why
|
||||
it is out of scope, and the one ambiguity it raises.
|
||||
|
||||
---
|
||||
|
||||
## 1. WRITE path — where `frame.source` is set
|
||||
|
||||
### 1a. Schema DDL (the column itself)
|
||||
|
||||
`packages/hive-mind-core/src/mind/schema.ts:56-57`
|
||||
|
||||
```sql
|
||||
source TEXT NOT NULL DEFAULT 'user_stated'
|
||||
CHECK (source IN ('user_stated', 'tool_verified', 'agent_inferred', 'import', 'system')),
|
||||
```
|
||||
|
||||
Column lives on `memory_frames` (DDL `CREATE TABLE ... memory_frames` at `schema.ts:47`).
|
||||
Confirmed: the column name **is `source`** (not `frame_source` / `provenance`).
|
||||
|
||||
### 1b. Where rows are written with an explicit `source`
|
||||
|
||||
- **Template starter seeding** — `packages/server/src/local/routes/workspaces.ts:290-291`:
|
||||
|
||||
```sql
|
||||
INSERT INTO memory_frames (frame_type, gop_id, t, content, importance, source)
|
||||
VALUES ('I', ?, ?, ?, 'normal', 'system')
|
||||
```
|
||||
|
||||
i.e. workspace template starter memories are written with `source = 'system'`.
|
||||
|
||||
- **General frame writes** go through `FrameStore` in
|
||||
`packages/hive-mind-core/src/mind/frames.ts` — `createIFrame` /
|
||||
`createPFrame` (`frames.ts:74,119`) default `source: FrameSource = 'user_stated'`.
|
||||
- **MCP `save_memory`** maps an incoming `source` arg, defaulting to `'agent_inferred'`
|
||||
(`packages/memory-mcp/src/tools/memory.ts:35`,
|
||||
`packages/hive-mind-mcp-server/src/tools/memory.ts:34`).
|
||||
|
||||
So in practice persisted rows carry one of the five DB-CHECK values (see §3).
|
||||
|
||||
---
|
||||
|
||||
## 2. READ path — the SELECT + projection that feeds the FE (THE CHANGE SITE)
|
||||
|
||||
**Route:** `GET /api/workspaces/:id/context` —
|
||||
`packages/server/src/local/routes/workspaces.ts:364` (handler).
|
||||
This is the response consumed by `<WorkspaceBriefing>` (apps/web), which renders both the
|
||||
"Key memories" list and the "Recent decisions" list on Chat + Workspace.
|
||||
|
||||
### 2a. `recentMemories` — SELECT at `workspaces.ts:393-400`, projection at `402-406`
|
||||
|
||||
**BEFORE** (SELECT, lines 393-400):
|
||||
|
||||
```ts
|
||||
const frames = raw.prepare(
|
||||
`SELECT content, importance, created_at FROM memory_frames
|
||||
WHERE importance != 'deprecated' AND importance != 'temporary'
|
||||
ORDER BY CASE importance
|
||||
WHEN 'critical' THEN 1 WHEN 'important' THEN 2
|
||||
WHEN 'normal' THEN 3 ELSE 4 END,
|
||||
id DESC LIMIT 8`
|
||||
).all() as Array<{ content: string; importance: string; created_at: string }>;
|
||||
```
|
||||
|
||||
**AFTER**:
|
||||
|
||||
```ts
|
||||
const frames = raw.prepare(
|
||||
`SELECT content, importance, source, created_at FROM memory_frames
|
||||
WHERE importance != 'deprecated' AND importance != 'temporary'
|
||||
ORDER BY CASE importance
|
||||
WHEN 'critical' THEN 1 WHEN 'important' THEN 2
|
||||
WHEN 'normal' THEN 3 ELSE 4 END,
|
||||
id DESC LIMIT 8`
|
||||
).all() as Array<{ content: string; importance: string; source: string; created_at: string }>;
|
||||
```
|
||||
|
||||
**BEFORE** (projection, lines 402-406):
|
||||
|
||||
```ts
|
||||
recentMemories = frames.map(f => ({
|
||||
content: f.content.slice(0, 200),
|
||||
importance: f.importance,
|
||||
date: f.created_at?.slice(0, 10) ?? 'unknown',
|
||||
}));
|
||||
```
|
||||
|
||||
**AFTER**:
|
||||
|
||||
```ts
|
||||
recentMemories = frames.map(f => ({
|
||||
content: f.content.slice(0, 200),
|
||||
importance: f.importance,
|
||||
source: f.source,
|
||||
date: f.created_at?.slice(0, 10) ?? 'unknown',
|
||||
}));
|
||||
```
|
||||
|
||||
> The local `let recentMemories` is declared at `workspaces.ts:376` as
|
||||
> `Array<{ content: string; importance: string; date: string }>` — add `source: string;`
|
||||
> there too (or the `.map()` widening will type-error against the narrower local).
|
||||
|
||||
### 2b. `recentDecisions` — SELECT at `workspaces.ts:409-417`, projection at `419-429`
|
||||
|
||||
**BEFORE** (SELECT, lines 409-417):
|
||||
|
||||
```ts
|
||||
const decisionFrames = raw.prepare(
|
||||
`SELECT content, created_at FROM memory_frames
|
||||
WHERE importance != 'deprecated' AND importance != 'temporary'
|
||||
AND (content LIKE 'Decision%' OR content LIKE '%decided%'
|
||||
OR content LIKE '%decision made%' OR content LIKE '%chose %'
|
||||
OR content LIKE '%selected %' OR content LIKE '%agreed %'
|
||||
OR importance = 'critical')
|
||||
ORDER BY id DESC LIMIT 5`
|
||||
).all() as Array<{ content: string; created_at: string }>;
|
||||
```
|
||||
|
||||
**AFTER**:
|
||||
|
||||
```ts
|
||||
const decisionFrames = raw.prepare(
|
||||
`SELECT content, source, created_at FROM memory_frames
|
||||
WHERE importance != 'deprecated' AND importance != 'temporary'
|
||||
AND (content LIKE 'Decision%' OR content LIKE '%decided%'
|
||||
OR content LIKE '%decision made%' OR content LIKE '%chose %'
|
||||
OR content LIKE '%selected %' OR content LIKE '%agreed %'
|
||||
OR importance = 'critical')
|
||||
ORDER BY id DESC LIMIT 5`
|
||||
).all() as Array<{ content: string; source: string; created_at: string }>;
|
||||
```
|
||||
|
||||
**BEFORE** (projection, lines 419-429):
|
||||
|
||||
```ts
|
||||
recentDecisions = decisionFrames.map(f => {
|
||||
const firstLine = f.content.split('\n')[0];
|
||||
const sentenceMatch = firstLine.match(/^(.+?\.\s)(?=[A-Z])/);
|
||||
const text = sentenceMatch
|
||||
? sentenceMatch[1].trim()
|
||||
: (firstLine.length > 150 ? firstLine.slice(0, 147) + '...' : firstLine);
|
||||
return {
|
||||
content: text.replace(/\.\s*$/, ''),
|
||||
date: f.created_at?.slice(0, 10) ?? 'unknown',
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
**AFTER** — add `source: f.source,` to the returned object:
|
||||
|
||||
```ts
|
||||
return {
|
||||
content: text.replace(/\.\s*$/, ''),
|
||||
source: f.source,
|
||||
date: f.created_at?.slice(0, 10) ?? 'unknown',
|
||||
};
|
||||
```
|
||||
|
||||
> Same as above: the local `let recentDecisions` declared at `workspaces.ts:377` as
|
||||
> `Array<{ content: string; date: string }>` must gain `source: string;`.
|
||||
|
||||
### Response shape (unchanged structurally)
|
||||
|
||||
The handler returns an object whose `recentMemories` / `recentDecisions` fields are these two
|
||||
arrays (assembled further down in the same handler, returned as part of the workspace-context
|
||||
body). Adding `source` is purely additive — no existing consumer breaks.
|
||||
|
||||
---
|
||||
|
||||
## 3. TYPE-CONTRACT change
|
||||
|
||||
**File:** `apps/web/src/lib/types.ts` — interface `WorkspaceContext`, lines **248-249**:
|
||||
|
||||
**BEFORE**:
|
||||
|
||||
```ts
|
||||
recentDecisions?: Array<{ content: string; date: string }>;
|
||||
recentMemories?: Array<{ content: string; importance: string; date: string }>;
|
||||
```
|
||||
|
||||
**AFTER**:
|
||||
|
||||
```ts
|
||||
recentDecisions?: Array<{ content: string; source?: string; date: string }>;
|
||||
recentMemories?: Array<{ content: string; importance: string; source?: string; date: string }>;
|
||||
```
|
||||
|
||||
- Keep `source` **optional** (`source?:`) so older sidecars (pre-PR3.5) that don't emit the
|
||||
field still typecheck and the FE degrades gracefully (no pill when absent).
|
||||
- This is the ONLY type file that needs to change for the pill. `WorkspaceContext` is the FE
|
||||
mirror; there is no separate `@waggle/shared` interface for these inline item shapes (they're
|
||||
declared anonymously inline in both `workspaces.ts` and `types.ts`).
|
||||
- The FE consumer is `apps/web/src/components/os/WorkspaceBriefing.tsx` — `recentMemories`
|
||||
rendered at lines **186-197** (importance badge at 189-193, add the ⬡ pill alongside it),
|
||||
`recentDecisions` rendered at lines **164-178**. No type change needed in the component; it
|
||||
reads `m.content` / `m.importance` today and would add `m.source`.
|
||||
|
||||
> Note: the richer `@waggle/shared` `Memory` interface (`packages/shared/src/types.ts:517`,
|
||||
> field `source: string` at `:526`, doc-comment "maps from `memory_frames.source`") already
|
||||
> models provenance — but that's the Memory Center view-model, NOT the workspace-context item
|
||||
> shape. Do not route the pill through `Memory`; the context items are their own inline type.
|
||||
|
||||
---
|
||||
|
||||
## 4. The PARALLEL builder (workspace-context.ts) — out of scope, but flagged
|
||||
|
||||
`packages/server/src/local/routes/workspace-context.ts` builds `WorkspaceNowBlock`
|
||||
(`recentDecisions: string[]`, type at `:14-21`) for **system-prompt injection**, via
|
||||
`buildWorkspaceState()` in `packages/server/src/local/workspace-state.ts:234`. Its decision
|
||||
SELECT is `workspace-state.ts:86-94` (`SELECT id, content, created_at`) and the legacy inline
|
||||
one is `workspace-context.ts:362` (`SELECT content`). Its items are typed `StateItem`
|
||||
(`workspace-state.ts:30-36`) whose `source` field is a `StateSource =
|
||||
'memory'|'session'|'awareness'` — that is a DIFFERENT `source` axis (where in the substrate the
|
||||
item came from), **not** the `frame.source` provenance class.
|
||||
|
||||
**Decision:** the FE pill is fed by §2 (`workspaces.ts` `/context` route → `WorkspaceContext`
|
||||
→ `WorkspaceBriefing`), so PR3.5's 1-field projection only needs §2 + §3. The
|
||||
`workspace-state.ts` / `WorkspaceNowBlock` path does not surface to the pill and can be left
|
||||
untouched. If a future task wants frame-provenance in the system prompt too, that's a separate,
|
||||
larger change (it would collide with the existing `StateItem.source` name).
|
||||
|
||||
`home.ts` (`GET /api/home/briefing`) consumes `buildWorkspaceState().recentDecisions[0].content`
|
||||
(`home.ts:314-315`) only — it reads `.content`, never `.source`, so it is unaffected.
|
||||
|
||||
---
|
||||
|
||||
## 5. SOURCE-column value vocabulary
|
||||
|
||||
**DB CHECK constraint (authoritative for persisted rows)** —
|
||||
`schema.ts:57`:
|
||||
|
||||
| value | meaning |
|
||||
|---|---|
|
||||
| `user_stated` | user said it directly (FrameStore default) |
|
||||
| `tool_verified` | confirmed by a tool execution |
|
||||
| `agent_inferred` | agent inferred it (MCP `save_memory` default) |
|
||||
| `import` | brought in via harvest/import |
|
||||
| `system` | system-seeded (e.g. workspace template starter memory — `workspaces.ts:291`) |
|
||||
|
||||
**TS `FrameSource` union is WIDER than the DB CHECK** —
|
||||
`packages/hive-mind-core/src/mind/frames.ts:25`:
|
||||
|
||||
```ts
|
||||
export type FrameSource = 'user_stated' | 'tool_verified' | 'agent_inferred'
|
||||
| 'import' | 'system' | 'personal' | 'workspace' | 'team_sync';
|
||||
```
|
||||
|
||||
The extra three (`personal` / `workspace` / `team_sync`) are application-level labels that the
|
||||
DB CHECK does **not** allow, so a constrained INSERT with one of them would fail — **persisted
|
||||
rows can only ever hold the 5 CHECK values** (documented drift:
|
||||
`docs/backend-map/sections/02a-data-model-memory.md:109` and `.../05b-subsystem-memory.md:318`).
|
||||
|
||||
**FE pill mapping guidance:** the pill should map the 5 real values to friendly labels/icons
|
||||
(e.g. `user_stated`→"you", `tool_verified`→"verified", `agent_inferred`→"agent",
|
||||
`import`→"imported", `system`→"system"). Treat anything else as a graceful fallback. Because
|
||||
the projection returns the raw string, the FE owns the label map (do not hardcode it in the
|
||||
route).
|
||||
|
||||
---
|
||||
|
||||
## 6. Honest ambiguities / risks
|
||||
|
||||
1. **Two SELECTs, one new column each** — straightforward, additive. The only typecheck trap is
|
||||
the narrower `let recentMemories` / `let recentDecisions` declarations at `workspaces.ts:376-377`;
|
||||
widen those too or `tsc` fails. (Reminder per CLAUDE.md §2: `npm run build` typechecks
|
||||
`apps/web` only — run `npx tsc --noEmit --project packages/server/tsconfig.json` to catch a
|
||||
server-route type error, since the sidecar runs via `tsx` transpile-only.)
|
||||
2. **The "source" name is overloaded** — `frame.source` (provenance: 5 CHECK values) vs
|
||||
`StateItem.source` (`memory|session|awareness`, where-it-came-from) vs `WorkspaceContext`
|
||||
inline items (currently no `source`). Make sure the pill consumes the PROVENANCE one from §2,
|
||||
not the `StateItem` one.
|
||||
3. **Decision frames may carry any of the 5 sources** — the decision SELECT (`workspaces.ts:409`)
|
||||
filters by `content LIKE` / `importance='critical'`, not by `source`, so a "decision" can be
|
||||
`agent_inferred` or `user_stated` etc. That's fine for a pill; just don't assume decisions
|
||||
are always `system`.
|
||||
4. **No migration / no DDL change needed** — `source` already exists on every DB (NOT NULL
|
||||
DEFAULT). The projection is read-only over an existing column. Zero data-backfill risk.
|
||||
215
docs/redesign-warm-hive/pr35-recon/02-sse-step-path.md
Normal file
215
docs/redesign-warm-hive/pr35-recon/02-sse-step-path.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# PR3.5 Recon — SSE/Chat "step" event path & provenance attach point
|
||||
|
||||
**Question:** Can a `source`/provenance field attach to streamed agent "step" events
|
||||
(to power the `⬡ source · when` ProvenanceLine pill that PR3 wired date-only)?
|
||||
|
||||
**Verdict: NEEDS-WIRING.** A streamed `step` event today carries *only* `{ content: string }` —
|
||||
no frame id, no `source`, no provenance of any kind. The frame `source` *does* exist in the
|
||||
substrate (`MemoryFrame.source: FrameSource`), but it is flattened to formatted text strings
|
||||
before it ever reaches the SSE layer. Attaching `source` to a step is a real (but small &
|
||||
well-scoped) plumbing job, not a trivial projection. The cleanest path is **not** to enrich the
|
||||
generic `step` event — it's to enrich the `tool_result` event for the `auto_recall` (memory) tool,
|
||||
which is the only step type that has a real provenance signal.
|
||||
|
||||
---
|
||||
|
||||
## 1. Server — where steps are produced & streamed
|
||||
|
||||
**Route:** `packages/server/src/local/routes/chat.ts` — `POST /api/chat`, SSE via `reply.hijack()`.
|
||||
|
||||
**SSE writer (the only emit helper):** `chat.ts:475`
|
||||
```ts
|
||||
const sendEvent = (event: string, data: unknown) => {
|
||||
raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
```
|
||||
|
||||
### The `step` payload shape (server)
|
||||
Every `step` event the server emits has the **same minimal shape**: `{ content: string }`. There is
|
||||
no other field. Representative emit sites:
|
||||
|
||||
- `chat.ts:761` — `sendEvent('step', { content: 'Recalling relevant memories...' })`
|
||||
- `chat.ts:780` — `sendEvent('step', { content: 'Recalled N relevant memories.' })`
|
||||
- `chat.ts:1170` — `sendEvent('step', { content: stepText })` where `stepText = describeToolUse(name, input)`
|
||||
(this is the **per-tool-call** step, fired from the agent-loop `onToolUse` callback — the main step source)
|
||||
- plus budget/approval/compression/GEPA steps at `:740 :754 :771 :904 :921 :929 :1023 :1027 :1108 :1352 :1382 :1389 :1439 :1458` — all `{ content }` only.
|
||||
|
||||
### The agent-loop emission (where tool steps originate)
|
||||
**`packages/agent/src/agent-loop.ts`** does **not** emit `step` events itself. It exposes typed
|
||||
callbacks (`AgentLoopConfig`, `agent-loop.ts:37-39`):
|
||||
```ts
|
||||
onToken?: (token: string) => void;
|
||||
onToolUse?: (name: string, input: Record<string, unknown>) => void;
|
||||
onToolResult?: (name: string, input: Record<string, unknown>, result: string) => void;
|
||||
```
|
||||
The route wires these to SSE in the runner config (`chat.ts:1167-1185` / `:1186-1220`):
|
||||
```ts
|
||||
onToolUse: (name, input) => {
|
||||
const stepText = describeToolUse(name, input);
|
||||
sendEvent('step', { content: stepText }); // ← the step
|
||||
sendEvent('tool', { name, input }); // ← raw tool event (has name+input)
|
||||
...
|
||||
},
|
||||
onToolResult: (name, input, result) => {
|
||||
...
|
||||
sendEvent('tool_result', { name, result, duration, isError }); // ← name+result+duration
|
||||
...
|
||||
},
|
||||
```
|
||||
**Key fact:** the loop's callbacks expose `name`, `input`, `result` — but **no frame, no `source`,
|
||||
no provenance**. The agent loop has no concept of which memory frame a step touched.
|
||||
|
||||
### The related `tool_result` shape (server)
|
||||
`chat.ts:1200` — `{ name, result, duration, isError }`. For the memory tool specifically:
|
||||
`chat.ts:781` — `sendEvent('tool_result', { name: 'auto_recall', result: resultText, duration, isError })`
|
||||
where `resultText` is **already-rendered text** (`chat.ts:777-779`):
|
||||
```ts
|
||||
const snippets = (recall.recalled ?? []).slice(0, 3);
|
||||
const snippetText = snippets.map(s => ` - ${s}`).join('\n');
|
||||
const resultText = `${recall.count} memories recalled:\n${snippetText}`;
|
||||
```
|
||||
|
||||
### Why the provenance is lost: `recallMemory()` returns text, not frames
|
||||
`packages/agent/src/orchestrator.ts:453-457`:
|
||||
```ts
|
||||
async recallMemory(...): Promise<{ text: string; count: number; recalled?: string[] }>
|
||||
```
|
||||
`recalled` is a `string[]` of **formatted snippets** — the frame objects (which *do* carry
|
||||
`source`) are collapsed to display strings inside `recallMemory` before returning. The real
|
||||
provenance lives one layer deeper: `packages/hive-mind-core/src/mind/frames.ts:35` —
|
||||
`MemoryFrame.source: FrameSource` (e.g. `user_stated`, harvest adapters: chatgpt/claude/etc.),
|
||||
plus `sourceUrl`/`sourceId` extras. So **the data exists in the DB, it is just not projected up
|
||||
through `recallMemory` → SSE.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Frontend — SSE consumer → activity stream blocks
|
||||
|
||||
### SSE parse + event-type mapping
|
||||
**`apps/web/src/lib/adapter.ts:692-738`** (`streamChat` async generator) parses the raw SSE
|
||||
text and maps event names to `StreamEvent.type`:
|
||||
- `adapter.ts:729` — `else if (type === 'step') type = 'step';`
|
||||
- yields `{ type, data } as StreamEvent` (`adapter.ts:732`) — `data` is the parsed `{ content }`.
|
||||
|
||||
### `StreamEvent` type
|
||||
**`apps/web/src/lib/types.ts:616-619`**:
|
||||
```ts
|
||||
export interface StreamEvent {
|
||||
type: 'token' | 'step' | 'tool_start' | 'tool_end' | 'done' | 'error' | 'approval_request' | 'approval_required' | 'model_switch' | 'notification';
|
||||
data: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### Step → block reduction
|
||||
**`apps/web/src/hooks/useChat.ts:152-165`** turns a `step` event into a `StepContentBlock`:
|
||||
```ts
|
||||
case 'step': {
|
||||
const description = typeof data === 'string' ? data : (data?.content as string ?? '');
|
||||
if (description) {
|
||||
// mark prior running steps done
|
||||
blocks.push({ type: 'step', blockId: nextBlockId('step'), description, status: 'running' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### `StepContentBlock` type (client) — **the field set to extend**
|
||||
**`apps/web/src/lib/types.ts:474-479`**:
|
||||
```ts
|
||||
export interface StepContentBlock {
|
||||
type: 'step';
|
||||
blockId: string;
|
||||
description: string;
|
||||
status: 'running' | 'done';
|
||||
}
|
||||
```
|
||||
No `source`/`provenance` field. This is the type that would gain an optional
|
||||
`provenance?: { source: string; when?: string }`.
|
||||
|
||||
### Where provenance is (not yet) shown — PR3's date-only affordance
|
||||
**`apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:28-43`** groups a consecutive run
|
||||
of step blocks into one `ActivityStream` card. PR3 already left the hook here and an honest comment
|
||||
(`BlockRenderer.tsx:24-26`):
|
||||
```ts
|
||||
// Provenance pills are intentionally omitted: the SSE
|
||||
// `step` payload carries no structured source field today (recon chat.md §4) —
|
||||
// we render the affordance, never fabricated provenance.
|
||||
```
|
||||
And the map drops provenance (`BlockRenderer.tsx:30-33`):
|
||||
```ts
|
||||
const activitySteps: ActivityStep[] = steps.map(s => ({
|
||||
tone: s.status === 'running' ? 'honey' : 'intel',
|
||||
text: s.description, // ← no provenance projected
|
||||
}));
|
||||
```
|
||||
|
||||
The rendering target already exists and is wired:
|
||||
- **`ActivityStream`** (`components/os/warm/ActivityStream.tsx:8-13`) — `ActivityStep.provenance?: { source; when?; onClick? }`, rendered at `:67-71` **only when present**.
|
||||
- **`ProvenanceLine`** (`components/os/warm/ProvenanceLine.tsx:19-32`) — the `⬡ source · when` pill.
|
||||
|
||||
So the **client consumer is provenance-ready**; it is waiting for the server to deliver a `source`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Honest verdict — trivial vs needs-wiring
|
||||
|
||||
**NEEDS-WIRING (small, scoped). NOT trivially projectable.**
|
||||
|
||||
- A generic `step` (e.g. "Drafting the document", "Budget limit reached", a `bash` tool call) has
|
||||
**no provenance at all** and never will — those steps are not memory reads. Stamping them with a
|
||||
`source` would be fabrication (exactly what PR3's comment refuses). So a blanket "add source to
|
||||
every step" is wrong on the merits.
|
||||
- The **one** step with a genuine provenance signal is the **memory recall** (`auto_recall`). Its
|
||||
source is real (`MemoryFrame.source`) but is destroyed by `recallMemory()` returning `string[]`
|
||||
text instead of frame metadata.
|
||||
|
||||
### The precise new wiring needed
|
||||
1. **Substrate → orchestrator (the load-bearing change):** widen `recallMemory()`'s return in
|
||||
`packages/agent/src/orchestrator.ts:453-457` to carry per-snippet provenance, e.g. add
|
||||
`recalledFrames?: Array<{ text: string; source: FrameSource; sourceUrl?: string; when?: string }>`
|
||||
alongside the existing `recalled: string[]`. The frame objects with `.source` are already in hand
|
||||
inside `recallMemory` — this is a "stop flattening it" change, not a new query. (This is the
|
||||
*same* 1-field server projection the S2 handoff named: *"the `frame.source` 1-field server
|
||||
projection (unlocks the ⬡ provenance pill on Chat+Workspace)"*.)
|
||||
2. **Server SSE:** in `chat.ts` around `:780-781`, emit the source on the memory step/tool_result —
|
||||
either add `source`/`provenance` to the existing `tool_result` (`{ name:'auto_recall', ... }`) or
|
||||
to the `step` payload for that one event: `sendEvent('step', { content, provenance: { source, when } })`.
|
||||
(`{ content }` → `{ content, provenance? }` is additive and backward-compatible.)
|
||||
3. **Client types:** add optional `provenance?: { source: string; when?: string }` to
|
||||
`StepContentBlock` (`types.ts:474`) and to the `StreamEvent` `data` handling.
|
||||
4. **Client reducer:** in `useChat.ts:152` carry `data.provenance` onto the pushed step block.
|
||||
5. **Client render:** in `BlockRenderer.tsx:30-33` project `s.provenance` into `ActivityStep` (the
|
||||
`ActivityStream`/`ProvenanceLine` rendering path already exists and gates on presence).
|
||||
|
||||
**Effort estimate:** ~5 small edits across 4 files (orchestrator return-shape widen is the only
|
||||
non-trivial one; everything else is a 1-field pass-through). No new DB columns, no new query — the
|
||||
`source` already exists at `frames.ts:35`. Lowest-risk slice = wire it on `auto_recall` only
|
||||
(memory steps), leave all other steps provenance-less by design.
|
||||
|
||||
### Scope call for PR3.5
|
||||
**In-scope and the right size for PR3.5** *iff* paired with the `recallMemory` return-shape widen.
|
||||
If PR3.5 wants to stay frontend-only, then provenance on steps is **out-of-scope** (the data is not
|
||||
on the wire) — and the honest move is to keep PR3's "render the affordance, never fabricated
|
||||
provenance" stance until the server projection lands.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — file:line index
|
||||
|
||||
| Concern | File:line |
|
||||
|---|---|
|
||||
| SSE writer | `packages/server/src/local/routes/chat.ts:475` |
|
||||
| step payload (memory) | `chat.ts:761`, `:780` |
|
||||
| step payload (per tool call) | `chat.ts:1170` (via `describeToolUse`) |
|
||||
| tool_result payload | `chat.ts:1200`; memory variant `chat.ts:781` |
|
||||
| recall snippet build (text flatten) | `chat.ts:777-779` |
|
||||
| agent-loop callbacks (no source) | `packages/agent/src/agent-loop.ts:37-39`, wired `chat.ts:1167-1220` |
|
||||
| recallMemory return shape | `packages/agent/src/orchestrator.ts:453-457` |
|
||||
| frame `source` field (real provenance) | `packages/hive-mind-core/src/mind/frames.ts:35` |
|
||||
| SSE parse + map | `apps/web/src/lib/adapter.ts:717-732` |
|
||||
| StreamEvent type | `apps/web/src/lib/types.ts:616-619` |
|
||||
| step → block reducer | `apps/web/src/hooks/useChat.ts:152-165` |
|
||||
| StepContentBlock type | `apps/web/src/lib/types.ts:474-479` |
|
||||
| activity grouping + omitted-provenance comment | `apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:24-43` |
|
||||
| ActivityStream (provenance-ready) | `apps/web/src/components/os/warm/ActivityStream.tsx:8-13, 67-71` |
|
||||
| ProvenanceLine pill | `apps/web/src/components/os/warm/ProvenanceLine.tsx:19-32` |
|
||||
155
docs/redesign-warm-hive/pr35-recon/03-pr3-hooks-primitives.md
Normal file
155
docs/redesign-warm-hive/pr35-recon/03-pr3-hooks-primitives.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# PR3.5 Recon — What PR3 actually left behind (hooks + primitives)
|
||||
|
||||
> Audit, not trust. Every row below was verified against source on branch
|
||||
> `feature/warm-hive-pr3` @ `dac7b696`. Where the handoff was imprecise it is
|
||||
> called out. Repo root: `D:/Projects/waggle-os`.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR — claimed primitives: exist vs must-build
|
||||
|
||||
**The three "Memory-Trust primitives" the handoff named — `ConfidenceBadge`,
|
||||
`EvidenceChip`, `DetailDrawer` — ALL EXIST and are already in production use.**
|
||||
The handoff's only error was the *location*: they are NOT in `os/warm/`, they live
|
||||
in `components/ui/` and predate PR3 (created in the earlier UX-refactor arc, commit
|
||||
`afe96355` "Phase 2B-FE.1 — Memory Center DS foundation"). PR3 added the warm
|
||||
provenance layer (`ProvenanceLine`, `ActivityStream`) on top.
|
||||
|
||||
So PR3.5 **reuses**, does not build-from-scratch, the trust primitives. What is
|
||||
genuinely **missing / static** is the *wiring*: the Workspace "What Waggle knows"
|
||||
fact rows and the Chat activity step rows are visual-only (no `onClick`, no
|
||||
provenance plumbed), and the chat `StepContentBlock` type has no `source` field —
|
||||
that is the real PR3.5 work (the "frame.source 1-field server projection" keystone).
|
||||
|
||||
---
|
||||
|
||||
## TRUTH TABLE — claim → real state → file:line
|
||||
|
||||
| # | Handoff claim | Real state | Evidence (file:line) |
|
||||
|---|---|---|---|
|
||||
| 1 | "ConfidenceBadge / EvidenceChip / DetailDrawer primitives **ready** [implied: in `warm/`]" | **TRUE but mislocated.** All three exist in `components/ui/`, NOT `os/warm/`. Pre-date PR3 (commit `afe96355`). Already consumed by Memory Center. Do NOT rebuild. | `apps/web/src/components/ui/confidence-badge.tsx:22` (export `ConfidenceBadge`)<br>`apps/web/src/components/ui/evidence-chip.tsx:16` (export `EvidenceChip`)<br>`apps/web/src/components/ui/detail-drawer.tsx:22` (export `DetailDrawer`)<br>Bonus: `apps/web/src/components/ui/evidence-panel.tsx` (`EvidencePanel`) also exists |
|
||||
| 1b | (warm/ dir listing) | The PR3 `warm/` set is 14 atoms; the trust primitives are NOT among them. warm/ has the provenance *renderer* (`ProvenanceLine`) + the chat *container* (`ActivityStream`). | `apps/web/src/components/os/warm/index.ts:7-20` |
|
||||
| 2a | "clickable fact rows" (Workspace "What Waggle knows", PR3 Phase C2) | **FALSE — STATIC.** The `<li>` rows render `HexCheckTile + text + when`. No `onClick`, no anchor, no role=button, no cursor affordance. Pure display. | `apps/web/src/components/os/apps/WorkspaceDesktopApp.tsx:160-168` (the `facts.map` `<li>` has no handler). Header comment at `:146-149` explicitly flags `frame.source` is not yet projected onto these rows ("the PR3.5 keystone"). |
|
||||
| 2b | "clickable step rows" (Chat activity steps) | **FALSE — STATIC.** `ActivityStream` renders each step as a `<li>` (`DotLive + text + optional ProvenanceLine`). The step `<li>` itself has no `onClick`. Provenance is only rendered `if (s.provenance)` — and the Chat caller never supplies it. | `apps/web/src/components/os/warm/ActivityStream.tsx:62-74` (step `<li>` static; provenance gated on `s.provenance`)<br>`apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:30-33` (maps only `tone`+`text` — **omits `provenance` entirely**) |
|
||||
| 2c | (why chat steps can't show provenance yet) | **Root blocker.** `StepContentBlock` has no `source`/`provenance` field. So even if BlockRenderer wanted to pass provenance, the data isn't on the block. Same shape as the Workspace keystone — both need `frame.source` server projection. | `apps/web/src/lib/types.ts:474-479` (`StepContentBlock` = `{type,blockId,description,status}` — no source) |
|
||||
| 3 | "intact J08 banner" | **TRUE.** Renders on Home Cockpit; gated `needsReviewCount > 0`; `role="alert"`; deep-links to Memory Center via `waggle:open-app` with `filter:'unreviewed'`. Wiring intact end-to-end (route re-stash in `MemoryRoute.tsx`, consumed by `MemoryCenterTab`). | Render: `apps/web/src/components/os/apps/HomeCockpit.tsx:496-515`<br>Handler `openMemoryReview`: `:477-481`<br>Count source: `briefing.needsReviewCount` (`lib/types.ts:303`)<br>Consumer: `MemoryCenterTab.tsx:84` |
|
||||
| 4 | "ProvenanceLine.tsx" | **EXISTS** (PR3 Phase 0, commit `0b15b91f`). Thin recolor of `EvidenceChip` to the intel/violet semantic. Full API below. | `apps/web/src/components/os/warm/ProvenanceLine.tsx:19` |
|
||||
| 5 | "warm/index.ts + tones.ts" | **EXIST.** Full exports + tone vocab below. | `apps/web/src/components/os/warm/index.ts`, `tones.ts` |
|
||||
|
||||
---
|
||||
|
||||
## ProvenanceLine — full API (verified, `ProvenanceLine.tsx:4-32`)
|
||||
|
||||
```ts
|
||||
interface ProvenanceLineProps {
|
||||
source: string; // REQUIRED. e.g. "web · mem0.ai" or "Claude Code"
|
||||
when?: string; // optional relative time, e.g. "2h ago"
|
||||
onClick?: () => void; // PR3.5 trace hook — makes the pill clickable into memory detail
|
||||
className?: string;
|
||||
}
|
||||
export function ProvenanceLine({ source, when, onClick, className }): JSX.Element
|
||||
```
|
||||
|
||||
**Render:** builds `label = \`⬡ ${source}${when ? \` · ${when}\` : ''}\``, then
|
||||
delegates to `<EvidenceChip label title={label} onClick className=...>`. The chip is
|
||||
recolored via `font-mono text-[var(--intel)] border-[var(--intel-wash)] bg-transparent`.
|
||||
|
||||
- **Clickable behavior:** when `onClick` is supplied, `EvidenceChip` renders a real
|
||||
`<button type="button">` with `hover:bg-muted hover:text-foreground` (`evidence-chip.tsx:19-25`);
|
||||
without it, a static `<span>` (`:26-30`). So `ProvenanceLine` is click-ready **iff the
|
||||
caller passes `onClick`** — the primitive supports it; the call sites don't use it yet.
|
||||
- **`source` absent → date-only:** `source` is a **required** prop, so the pill cannot
|
||||
be rendered "date-only" via ProvenanceLine. The actual date-only fallback today is done
|
||||
by **not rendering ProvenanceLine at all** and showing a plain mono date instead — see
|
||||
the Workspace fact rows (`WorkspaceDesktopApp.tsx:165`, a bare
|
||||
`<div class="font-mono text-[10.5px]">{f.when}</div>`). The deliberate design note: PR3
|
||||
shows the REAL date only and **never fabricates a `source`** until `frame.source` is
|
||||
projected (`WorkspaceDesktopApp.tsx:146-149`). **PR3.5 implication:** to light up the
|
||||
⬡ pill, project `frame.source` server-side, then swap the bare date `<div>` for
|
||||
`<ProvenanceLine source={...} when={f.when} onClick={...} />`.
|
||||
|
||||
---
|
||||
|
||||
## Tone / token vocabulary (for new Memory-Trust primitives)
|
||||
|
||||
### `os/warm/tones.ts` — `WarmTone` union + maps (`tones.ts:7-36`)
|
||||
|
||||
```ts
|
||||
type WarmTone = 'work' | 'intel' | 'healthy' | 'attention' | 'risk' | 'honey' | 'neutral';
|
||||
|
||||
TONE_COLOR: Record<WarmTone, string> // foreground/dot color
|
||||
work→var(--work) intel→var(--intel) healthy→var(--healthy)
|
||||
attention→var(--attention) risk→var(--risk) honey→var(--honey)
|
||||
neutral→var(--text-muted)
|
||||
|
||||
TONE_WASH: Record<WarmTone, string> // tinted background
|
||||
work→var(--work-wash) intel→var(--intel-wash) healthy→var(--healthy-wash)
|
||||
attention→var(--honey-wash) risk→var(--risk-wash) honey→var(--honey-wash)
|
||||
neutral→var(--surface-3)
|
||||
```
|
||||
|
||||
> Note the asymmetry: `attention` foreground = `--attention`, but its wash = `--honey-wash`
|
||||
> (not `--attention-wash`). Match this when building a trust primitive on the attention tone.
|
||||
|
||||
### CSS tokens — defined in BOTH themes (`apps/web/src/index.css`)
|
||||
|
||||
| Token | Dark (`:162-168`) | Light (`:298-304`) | Semantic |
|
||||
|---|---|---|---|
|
||||
| `--intel` | `#b196dd` | `#7d57b8` | **violet — intelligence / memory / provenance** (the trust color) |
|
||||
| `--intel-wash` | `rgba(177,150,221,.12)` | `rgba(125,87,184,.10)` | violet wash |
|
||||
| `--work` / `--work-wash` | desaturated blue | — | task/work |
|
||||
| `--healthy` / `--healthy-wash` | desaturated green | — | OK/done |
|
||||
| `--attention` | (status block `:102+`) | — | needs-attention (uses `--honey-wash`) |
|
||||
| `--risk` / `--risk-wash` | — | — | blocked/conflict |
|
||||
| `--honey` / `--honey-wash` | — | — | brand accent |
|
||||
|
||||
The `confidence-badge` uses a *different* token family — Hive DS `--sem-*`
|
||||
(`--sem-healthy` / `--sem-attention` / `--sem-risk`, `confidence-badge.tsx:17-19`) — not
|
||||
the warm `--intel/--work/...`. **PR3.5 consistency call:** decide whether new
|
||||
Memory-Trust atoms align to the warm `--intel` family (provenance) or the `--sem-*`
|
||||
family (confidence bands). They currently coexist.
|
||||
|
||||
---
|
||||
|
||||
## warm/index.ts — full export list (`index.ts:7-20`)
|
||||
|
||||
```
|
||||
HexAvatar · SectionLabel · DotLive · ProvenanceLine · RunChip (+RunChipProps)
|
||||
IconTile · HexCheckTile · StreakChip · ModelPill · OvernightHero · AskBar
|
||||
ActivityStream (+ActivityStep) · InlineApprovalCard · TONE_COLOR · TONE_WASH (+WarmTone)
|
||||
```
|
||||
|
||||
14 atoms. **Not present (and not the warm dir's job per its header doc, `index.ts:3-6`):**
|
||||
ConfidenceBadge / EvidenceChip / DetailDrawer — "Generic, always-labeled primitives
|
||||
stay in `components/ui/`."
|
||||
|
||||
---
|
||||
|
||||
## Where the Memory surfaces live today (screen 19 target map)
|
||||
|
||||
| Surface | File | Notes for PR3.5 |
|
||||
|---|---|---|
|
||||
| **Memory Center (standalone screen, S04)** | `apps/web/src/components/os/apps/MemoryCenterApp.tsx` | The full screen. **Two-mind split** ("About you" = personal, "About this work" = workspace) + 7 views: `memories · timeline · graph · harvest · weaver · wiki · evolution` (`MEMORY_VIEWS`, `:36-38`). Controlled component (route owns mind+view via URL). This is what **screen 19 extends or replaces.** |
|
||||
| **Per-mind memory list (the reusable core)** | `apps/web/src/components/os/apps/memory/MemoryCenterTab.tsx` | Parameterized by `mind` + `workspaceId`. **Already consumes the trust primitives:** `DetailDrawer` (`:337`), `ConfidenceBadge` (`:342`), `EvidencePanel`+source/evidence (`:406`), `MemoryCard` grid (`:323-331`, each card `onClick={() => openDetail(m)}`). This is the proven pattern PR3.5 should match. |
|
||||
| **Workspace Memory tab** | `apps/web/src/components/os/apps/WorkspaceDesktopApp.tsx:701-705` | Tab `id:'memory'` (`TABS`, `:73`) embeds the SAME `<MemoryCenterTab mind="workspace" workspaceId consumeDeepLinks={false} />`. So the Workspace memory tab and the Memory Center already share one component — change once, both update. |
|
||||
| **Route wrapper** | `apps/web/src/routes/MemoryRoute.tsx` | `/memory` ≡ `/memory/personal`; J08 banner target; re-stashes `?filter=` deep link. |
|
||||
| **Memory card row** | `apps/web/src/components/os/apps/memory/MemoryCard.tsx` | Individual memory row component (grep-confirmed it references ConfidenceBadge family). |
|
||||
| **J08 needs-review banner** | `apps/web/src/components/os/apps/HomeCockpit.tsx:496-515` | Already wired; deep-links here. |
|
||||
| **Import reminder banner** | `apps/web/src/components/os/apps/memory/ImportReminderBanner.tsx` | Secondary banner on the Memory Center. |
|
||||
|
||||
---
|
||||
|
||||
## PR3.5 build implications (derived from the truth table)
|
||||
|
||||
1. **Reuse, don't rebuild** the trust primitives (`ui/confidence-badge`,
|
||||
`ui/evidence-chip`, `ui/detail-drawer`, `ui/evidence-panel`) — they exist and ship today.
|
||||
2. **The keystone is the data, not the UI:** project `frame.source` (+ `when`) server-side
|
||||
so the Workspace fact rows and Chat steps can carry real provenance.
|
||||
3. **Make the rows clickable** by (a) adding `onClick` to the Workspace fact `<li>`
|
||||
(`WorkspaceDesktopApp.tsx:160`) → open the memory detail drawer, (b) extending
|
||||
`StepContentBlock` (`types.ts:474`) with an optional `source` and threading it through
|
||||
`BlockRenderer.tsx:30-33` into `ActivityStep.provenance`.
|
||||
4. **`ProvenanceLine` already supports `onClick`** (→ `EvidenceChip` renders a `<button>`);
|
||||
the only gap is call sites passing the handler + a real `source`.
|
||||
5. **Token consistency decision** pending: warm `--intel` family (provenance) vs Hive DS
|
||||
`--sem-*` family (confidence). Both live; pick one story for screen 19.
|
||||
317
docs/redesign-warm-hive/pr35-recon/04-design-screen19.md
Normal file
317
docs/redesign-warm-hive/pr35-recon/04-design-screen19.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Screen 19 — Memory Trust · Build-Ready Design Contract
|
||||
|
||||
> Source of truth: `docs/design_handoff_waggle_app/design-files/screens/memory-trust.html`
|
||||
> (authoritative mock, read in full), `screenshots/19-memory-trust.png` (rendered),
|
||||
> `DESIGN_POV.md` #1, `SCREENS.md` (no 01–18 entry — this screen is POV-driven, #19).
|
||||
> All copy below is **verbatim** from the mock. All class names / colors / spacing are
|
||||
> quoted from the HTML `<style>` block.
|
||||
|
||||
**DESIGN_POV #1 mandate (verbatim, `DESIGN_POV.md:12-34`):** "A persistent-memory
|
||||
product's #1 churn driver isn't *forgetting* — it's **remembering the wrong thing**…"
|
||||
The designed response is a *Memory Trust* layer with four primitives: **confidence +
|
||||
freshness** on every memory; **Forget** (real removal from recall) and **Correct**
|
||||
(inline; dependents re-checked); **Stale review** prompts; **"Why did you do that?"
|
||||
trace**. It is "also a moat… auditability, right-to-correct, EU AI Act alignment."
|
||||
POV §How-to-use: "wire it to the real memory store (confidence/freshness/forget/correct/
|
||||
trace are all backed by data the substrate already has or can derive)."
|
||||
|
||||
---
|
||||
|
||||
## 1. Header / Views (the segmented control)
|
||||
|
||||
The page is a single full-height column (`body { height:100vh; overflow:hidden;
|
||||
display:flex; flex-direction:column }`). A sticky control bar (`.controls`, blurred
|
||||
`backdrop-filter:blur(10px)`, `border-bottom:1px solid var(--line-soft)`) holds a
|
||||
**2-button segmented switch** that toggles between two full views.
|
||||
|
||||
| Element | Verbatim copy / spec |
|
||||
|---|---|
|
||||
| Eyebrow label (`.lab`, mono 10.5px, `--text-dim`, uppercase) | `Memory Trust · view` |
|
||||
| Segment button 1 (`.seg button.on` — **active by default**) | `Manage memory` |
|
||||
| Segment button 2 (`.seg button`) | `Why did you do that?` |
|
||||
| Active-view label (`.vlabel`, swaps on toggle) — Manage | `<b>Manage</b> — forget, correct, confirm; see confidence & freshness` |
|
||||
| Active-view label — Why | `<b>Why-trace</b> — every action explains itself` |
|
||||
| Right control (`.tbtn`) | Theme toggle `☾`/`☀` |
|
||||
|
||||
**Segment active state:** `.seg button.on { background:var(--honey); color:#1a1407; }`
|
||||
(honey fill + dark-ink text — the canonical "selected" treatment). Inactive buttons are
|
||||
`background:transparent; color:var(--text-muted)`. Segment container `.seg` is a
|
||||
`var(--surface-2)` pill with `border:1px solid var(--line-soft)`, `border-radius:10px`,
|
||||
3px inner padding.
|
||||
|
||||
**Two VIEW modes:**
|
||||
- **A — "Manage memory"** (`.view[data-view="manage"]`, on by default — this is what the
|
||||
screenshot shows): editorial hero → 4 stat cards → search+filters → memory rows → trust
|
||||
principle footnote.
|
||||
- **B — "Why did you do that?"** (`.view[data-view="why"]`): editorial hero → a single
|
||||
**trace card** (goal→recall→checks→action chain) → trust principle footnote. (See §6.)
|
||||
|
||||
Toggle JS: clicking a segment button sets `.on`, shows the matching `.view`, swaps the
|
||||
`.vlabel` HTML, and resets `.stage` scroll to 0. Stage content is centered in
|
||||
`.wrap { max-width:920px; margin:0 auto; padding:30px 32px 70px }`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Editorial Hero
|
||||
|
||||
### View A — Manage (`.head`, margin-bottom 22px)
|
||||
- **Eyebrow** (`.eyebrow`, mono 11px, `letter-spacing:.14em`, uppercase, `color:var(--honey)`,
|
||||
with a 20px honey rule `::before`): `Trust · the thing that makes you stay`
|
||||
- **H1** (`.head h1`, 28px / weight 650 / `letter-spacing:-0.02em`; `<em>` is **honey,
|
||||
non-italic** — `h1 em { font-style:normal; color:var(--honey) }`):
|
||||
`Memory you can ` + **`correct, age, and forget.`** ← the phrase **"correct, age, and
|
||||
forget."** gets the honey accent (everything after "Memory you can " is in `<em>`).
|
||||
- **Body** (`.head p`, 15px, `--text-muted`, `line-height:1.55`, `max-width:64ch`; `<b>` =
|
||||
`--text-2`): verbatim —
|
||||
> A memory that only grows is a liability. Waggle shows you **how sure it is**, **how
|
||||
> fresh it is**, and **where it came from** — and lets you fix or forget anything.
|
||||
> You're always in control of what the hive believes.
|
||||
|
||||
(Honey-emphasis `<b>` spans: "how sure it is", "how fresh it is", "where it came from".)
|
||||
|
||||
### View B — Why
|
||||
- **Eyebrow:** `Provenance · accountability`
|
||||
- **H1** (honey `<em>` = the quoted question):
|
||||
`Ask the agent ` + **`"why did you do that?"`**
|
||||
- **Body** (`<b>` spans bolded): verbatim —
|
||||
> Any action an agent takes can be traced back to the exact memories and sources behind
|
||||
> it — so a wrong move is **diagnosable, not mysterious**. If a bad memory caused it,
|
||||
> fix the memory right from the trace.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stat Cards — "trust summary bar" (`.tsum`, View A only)
|
||||
|
||||
4-up grid: `grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:24px`. Collapses
|
||||
to `1fr 1fr` under `max-width:820px`. Each card `.ts`: `padding:16px`, `border-radius:
|
||||
var(--r-lg)` (18px), `border:1px solid var(--line-soft)`, `background:var(--surface)`.
|
||||
Value `.ts .v` = 24px / weight 750 / `letter-spacing:-0.02em`; label `.ts .l` = 11.5px,
|
||||
`--text-muted`, `margin-top:5px`. **Warn variant** `.ts.warn`: `border-color:
|
||||
color-mix(in srgb,var(--attention) 35%,transparent)` + value tinted `--attention`.
|
||||
|
||||
| # | Value (verbatim) | Label (verbatim) | Color of value | Card variant |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `142` | `Memories in this hive` | default `--text` (white/ink) | `.ts` |
|
||||
| 2 | `128` | `High confidence & fresh` | **green** — `style="color:var(--healthy)"` (`#6cb78c`) | `.ts` |
|
||||
| 3 | `9` | `Stale · worth a review` | **honey** — `--attention` (`#e9a52c`) | `.ts.warn` |
|
||||
| 4 | `3` | `Awaiting your confirm` | **honey** — `--attention` (`#e9a52c`) | `.ts.warn` |
|
||||
|
||||
Color order = **white / green / honey / honey** (matches the brief). Cards 3 & 4 also get
|
||||
the honey-tinted warn border.
|
||||
|
||||
---
|
||||
|
||||
## 4. Search + Filters (`.toolbar`, View A only)
|
||||
|
||||
`.toolbar { display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap }`.
|
||||
|
||||
**Search bar** (`.search` — flex:1, min-width:200px, `padding:9px 14px`, `border-radius:
|
||||
11px`, `border:1px solid var(--line)`, `background:var(--surface)`):
|
||||
- Leading magnifier `<svg>` (circle + handle), 16px, `stroke:var(--text-dim)`,
|
||||
`stroke-width:1.9`, no fill.
|
||||
- Input placeholder (verbatim): `Search what Waggle knows… or ask it to forget something`
|
||||
— 14px, transparent bg, `color:var(--text)`.
|
||||
|
||||
**Filter chips** (`.filt`, 12.5px / weight 600, `padding:8px 13px`, `border-radius:9px`,
|
||||
`border:1px solid var(--line-soft)`, `background:var(--surface)`):
|
||||
|
||||
| Chip (verbatim) | State |
|
||||
|---|---|
|
||||
| `All` | **active** — `.filt.on` |
|
||||
| `Stale` | default |
|
||||
| `Needs confirm` | default |
|
||||
| `Forgotten` | default |
|
||||
|
||||
**Active chip** `.filt.on`: `color:var(--text); border-color:var(--honey-line);
|
||||
background:var(--honey-wash)` (honey-wash fill + honey-line border). Hover `.filt:hover`
|
||||
just lifts `color` to `--text`. (Note: the warm primitive set already has chip-like
|
||||
patterns; the active = honey-wash + honey-line treatment must be reused.)
|
||||
|
||||
---
|
||||
|
||||
## 5. Memory ROW anatomy (`.mem`, the core component — repeated in `.mems` grid)
|
||||
|
||||
`.mems { display:grid; gap:10px }`. Each row `.mem`: `border:1px solid var(--line-soft)`,
|
||||
`background:var(--surface)`, `border-radius:var(--r-lg)` (18px), `padding:16px 18px`,
|
||||
`transition:.15s`. **State border variants:**
|
||||
- `.mem.stale` → `border-color:color-mix(in srgb,var(--attention) 30%,var(--line-soft))`
|
||||
- `.mem.disputed` → `border-color:color-mix(in srgb,var(--risk) 30%,var(--line-soft));
|
||||
opacity:.92`
|
||||
|
||||
Layout `.mem .top { display:flex; align-items:flex-start; gap:13px }` — three columns:
|
||||
**[confidence ring] [body] [actions]**.
|
||||
|
||||
### 5a. Confidence ring (`.conf`, width 42px, flex:none, centered)
|
||||
- Ring `.ring`: `width/height 38px`, `border-radius:999px`, `display:grid; place-items:
|
||||
center`, mono 11px / weight 600. **Both the number color AND the 2px ring border come
|
||||
from `confColor(c)`** (inline `style="color:${confColor(c)};border:2px solid
|
||||
${confColor(c)}"`).
|
||||
- **Ring color logic** (`confColor(c)` JS, verbatim):
|
||||
- `c >= 85` → `var(--healthy)` (sage green)
|
||||
- `c >= 60` → `var(--attention)` (honey)
|
||||
- `else` (`< 60`) → `var(--risk)` (terracotta)
|
||||
- Caption `.cl` below ring: literal text `conf` (rendered uppercase via
|
||||
`text-transform:uppercase`), 9px mono, `--text-dim`, `margin-top:4px`,
|
||||
`letter-spacing:.06em`. → reads **`CONF`**.
|
||||
|
||||
### 5b. Body (`.mbody`, flex:1)
|
||||
- **Fact text** `.mtext`: 14.5px, `line-height:1.5`, `color:var(--text)`. **`<b>` inside
|
||||
gets weight 650** — this is the honey-free *bold* emphasis on the key phrase (NOT
|
||||
honey-colored; it's bold weight only). Disputed rows strike through: `.mem.disputed
|
||||
.mtext { text-decoration:line-through; text-decoration-color:color-mix(in srgb,
|
||||
var(--risk) 60%,transparent); color:var(--text-muted) }`.
|
||||
- **Provenance line** `.prov` (`margin-top:8px`, flex-wrap, `gap:7px 14px`, mono 10.5px,
|
||||
`color:var(--text-dim)`). Format = three segments:
|
||||
`⬡ <id>` · `source: <src>` · `● <freshness>`
|
||||
- ID segment: `<span class="src">⬡ ${m.id}</span>` — `.prov .src { color:var(--intel) }`
|
||||
(muted violet) — e.g. `⬡ M-204`.
|
||||
- Source segment: plain `source: ${m.src}` in `--text-dim` — e.g. `source: chat · Tue`.
|
||||
- Freshness segment: `● ` + label, colored by `.fresh.ok`→`var(--healthy)` (green) or
|
||||
`.fresh.old`→`var(--attention)` (honey). Fresh label = `fresh`; old label =
|
||||
`aging — last seen 6w ago`.
|
||||
|
||||
### 5c. Action buttons (`.acts`, flex:none, gap:6px)
|
||||
Two square icon buttons `.mact` (30×30px, `border-radius:8px`, `border:1px solid
|
||||
var(--line-soft)`, `background:var(--surface-2)`, `color:var(--text-muted)`; icon svg 15px
|
||||
`stroke:currentColor` `stroke-width:1.8`):
|
||||
1. **Edit / correct** — `title="Edit / correct"`, `data-act="edit"`; pencil icon. Hover
|
||||
`.mact:hover { border-color:var(--honey-line); color:var(--honey) }`. Click → toast
|
||||
`Correcting <id> — opens an inline editor`.
|
||||
2. **Forget / delete** — `.mact.danger`, `title="Forget this"`, `data-act="forget"`; trash
|
||||
icon. Danger hover `.mact.danger:hover { border-color:color-mix(in srgb,var(--risk)
|
||||
45%,transparent); color:var(--risk) }`. Click → row animates out (`opacity:0;
|
||||
translateX(-12px)`, 300ms, then removed) + toast `Forgotten <id> — removed from recall`.
|
||||
|
||||
### 5d. Inline sub-banners (conditional, render inside `.mbody`)
|
||||
- **Corrected banner** (`.corrected`, disputed rows that carry `m.corrected`): healthy-wash
|
||||
pill — `background:var(--healthy-wash); border:1px solid color-mix(in srgb,var(--healthy)
|
||||
30%,transparent)`, 12.5px `--text-2`, green check svg (`stroke:var(--healthy)`). Renders
|
||||
`<b>{before "→"}</b> → {after "→"}`.
|
||||
- **Stale review banner** (`.stalebanner`, on `.mem.stale`): honey-wash pill —
|
||||
`background:var(--honey-wash); border:1px solid var(--honey-line)`, clock svg
|
||||
(`stroke:var(--honey)`). Text (verbatim): `This is 6 weeks old — still true?` Right-aligned
|
||||
button pair `.sp`:
|
||||
- `.sbtn.go` (`background:var(--honey); color:#1a1407`) — verbatim `Still true`
|
||||
(`data-act="confirm"`). Click → banner morphs into a `.corrected` "Confirmed still true ·
|
||||
freshness reset" pill + toast `Confirmed <id> — freshness reset`.
|
||||
- `.sbtn.ghost` (`background:var(--surface); color:var(--text-2); border:var(--line-strong)`)
|
||||
— verbatim `Forget` (`data-act="forget"`).
|
||||
|
||||
### 5e. Seed dataset (the 5 demo rows — verbatim `mems[]`)
|
||||
| id | fact text (`<b>` = bold phrase) | conf | ring color | src | fresh | state |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `M-204` | Mara wants market work to **lead with the regulated-industries angle**. | 94 | green | `chat · Tue` | fresh | ok |
|
||||
| `M-198` | **Mem0** is cloud-only and raised prices ~15% in March. | 88 | green | `web · mem0.ai` | fresh | ok |
|
||||
| `M-141` | The Q3 launch date is **September 12**. | 61 | honey | `chat · 6 weeks ago` | old | **stale** (shows stale banner) |
|
||||
| `M-088` | Mara prefers **Slack over email** for updates. | 47 | terracotta | `inferred · once` | old | **disputed** (strikethrough + corrected banner: `Corrected by you → prefers a daily digest, not Slack pings.`) |
|
||||
| `M-052` | Primary competitor is **Letta**. | 90 | green | `teardown.md` | fresh | ok |
|
||||
|
||||
---
|
||||
|
||||
## 6. "Why did you do that?" Trace view (View B, `.trace`)
|
||||
|
||||
A single card: `border:1px solid var(--line)`, `border-radius:var(--r-xl)` (26px),
|
||||
`background:var(--bg-2)`, `overflow:hidden`. Three regions: **header → vertical chain →
|
||||
action footer**, then the trust principle footnote below.
|
||||
|
||||
### 6a. Trace header (`.th`, `padding:18px 22px`, bottom border)
|
||||
- **Hex avatar** `.ti.hex` (34×38px, honey gradient `linear-gradient(150deg,
|
||||
var(--honey-bright),var(--honey-deep))`, dark-ink `paper-plane`/send svg). → reuse warm
|
||||
`HexAvatar` primitive.
|
||||
- Title `<b>` 15.5px/650 (verbatim): `Drafted the board brief around "regulated industries"`
|
||||
- Subtitle `.sub` 12px `--text-muted` (verbatim): `Deck-builder · 18m ago · Q2 Board Deck`
|
||||
- `.when` (mono 11px `--text-dim`, margin-left:auto): `trace #a1f9`
|
||||
|
||||
### 6b. Trace chain (`.tchain`, `padding:8px 22px 18px`) — `.tnode` steps
|
||||
Each node is a 2-col grid `24px 1fr` with a **vertical connector line** drawn via
|
||||
`.tnode::before { position:absolute; left:11px; top:34px; bottom:-14px; width:1.5px;
|
||||
background:var(--line) }` (suppressed on `:last-child`). Each `.tdot` (24px circle,
|
||||
colored per step, dark-ink svg `stroke:#1a1407 stroke-width:2.4`):
|
||||
|
||||
| # | dot color | title `<b>` (13.5px/600) | body `.tc p` (13px `--text-muted`) — verbatim |
|
||||
|---|---|---|---|
|
||||
| 1 | `var(--intel)` (violet, arrow icon) | `Goal received` | You asked: `"tighten the board narrative."` (the quote in `.mono`) |
|
||||
| 2 | `var(--honey)` (check icon) | `Recalled 3 memories` | Strongest was: `⬡ mem #M-204` (`.src` violet) — "Mara wants market work to lead with the regulated-industries angle." + **evidence chip** `.ev`: `confidence 94% · source: chat · Tue · still fresh` |
|
||||
| 3 | `var(--honey)` (check icon) | `Cross-checked the teardown` | Confirmed 2 of 3 proof points cite customer quotes `⬡ teardown.md` (`.src`) |
|
||||
| 4 | `var(--healthy)` (green, arrow icon) | `Acted` | Wrote slide 6 around the regulated angle and flagged it for your review. |
|
||||
|
||||
`.ev` evidence chip: `font-size:12px; color:var(--text-2); padding:8px 12px; border-radius:
|
||||
8px; background:var(--surface); border:1px solid var(--line-soft)`. `.src` inline refs =
|
||||
mono 11px `--intel`. Inline `.mono` quote = mono 11.5px `--text-dim`.
|
||||
|
||||
### 6c. Trace action footer (`.traceact`, top border, `background:var(--surface)`,
|
||||
`padding:16px 22px`, gap:9px) — three buttons `.tbtn2` (12.5px/650, `padding:9px 15px`,
|
||||
`border-radius:9px`):
|
||||
1. `.tbtn2.go` (honey fill, `#1a1407` ink) — verbatim `Looks right`
|
||||
2. `.tbtn2.ghost` (`--surface-2`, `--line-strong` border) — verbatim
|
||||
`That memory is wrong → correct it`
|
||||
3. `.tbtn2.danger` (transparent, risk text + risk-tint border `color-mix(in srgb,
|
||||
var(--risk) 35%,transparent)`) — verbatim `Forget #M-204 & redo`
|
||||
|
||||
---
|
||||
|
||||
## 7. Trust principle footnote (`.principle`, both views)
|
||||
|
||||
Shared component below the content: `display:flex; gap:13px; align-items:flex-start;
|
||||
padding:18px 20px; border-radius:var(--r-lg); background:var(--bg-2); border:1px solid
|
||||
var(--line-soft)`. Leading 20px svg `stroke:var(--healthy)` (shield/check on View A;
|
||||
info-circle on View B). Text 13.5px `--text-muted` `line-height:1.6`, `<b>`=`--text`.
|
||||
|
||||
- **View A (verbatim):** **Nothing is remembered behind your back.** Every memory is
|
||||
inspectable, editable, and forgettable — and forgetting is real: it's removed from recall
|
||||
and from anything Waggle says next. Confidence and freshness are shown so the agent (and
|
||||
you) can discount what's old or shaky instead of acting on it blindly.
|
||||
- **View B (verbatim):** **Every agent action keeps its trace.** The chain from goal →
|
||||
recalled memories → checks → action is stored with the result, so "why did you do that?"
|
||||
always has an answer — and the fix (correct or forget the offending memory) is one click
|
||||
from the explanation.
|
||||
|
||||
**Toast** (shared, `#toast`): `.toast` bottom-center pill, `background:var(--surface);
|
||||
border:1px solid var(--healthy); box-shadow:var(--shadow-lg)`, 8px green dot `.td2`
|
||||
(`background:var(--healthy)`), slides up on `.show` (2400ms auto-dismiss).
|
||||
|
||||
---
|
||||
|
||||
## 8. Tokens — mock → PR3 warm tokens (1:1, already aligned)
|
||||
|
||||
The mock's `waggle.css` tokens are **identical hex** to PR3's warm tokens in
|
||||
`apps/web/src/index.css` (same names, same dark + light values). **No remapping needed** —
|
||||
build directly against the CSS vars below.
|
||||
|
||||
| Mock var | Hex (dark / light) | PR3 warm token | Used in screen 19 for |
|
||||
|---|---|---|---|
|
||||
| `--honey` | `#e9a52c` / `#b57d12` | `--honey` ✓ | segment-on, accent `<em>`, eyebrow, conf 60–84, active filter, go-buttons |
|
||||
| `--honey-bright` | `#f6c45a` / `#cf932a` | `--honey-bright` ✓ | hex-avatar gradient top |
|
||||
| `--honey-deep` | `#c07e16` / `#92620a` | `--honey-deep` ✓ | hex-avatar gradient bottom |
|
||||
| `--honey-wash` | `rgba(233,165,44,.10)` | `--honey-wash` ✓ | active filter bg, stale banner bg |
|
||||
| `--honey-line` | `rgba(233,165,44,.28)` | `--honey-line` ✓ | active filter border, stale border, hover states |
|
||||
| `--healthy` | `#6cb78c` / `#3c8a5f` | `--healthy` ✓ | stat #2 green, conf ≥85 ring, fresh●, "Acted" dot, principle icon, toast |
|
||||
| `--healthy-wash` | `rgba(108,183,140,.12)` | `--healthy-wash` ✓ | corrected banner bg |
|
||||
| `--attention` | `#e9a52c` / `#b57d12` | `--attention` ✓ | stat #3/#4 honey, conf 60–84 ring, stale border, aging● |
|
||||
| `--risk` | `#db8068` / `#c0573c` | `--risk` ✓ | conf <60 ring, disputed border/strikethrough, danger buttons |
|
||||
| `--intel` | `#b196dd` / `#7d57b8` | `--intel` ✓ | `⬡` provenance id, trace `.src` refs, "Goal received" dot |
|
||||
| Dark-ink on honey | `#1a1407` (mock uses `#1a1407`) | warm light `--primary-foreground: #1a1407` ✓ | text on every honey fill |
|
||||
| `--surface` / `--surface-2` / `--bg-2` | warm graphite / paper | `--surface` / `--surface-2` / `--bg-2` ✓ | cards, rows, action btns, trace bg |
|
||||
| `--line` / `--line-soft` / `--line-strong` | warm graphite lines | same ✓ | borders |
|
||||
| `--text` / `--text-2` / `--text-muted` / `--text-dim` | warm text scale | same ✓ | type hierarchy |
|
||||
| `--r-lg` 18px / `--r-xl` 26px | radii | same ✓ | cards/rows 18px, trace 26px |
|
||||
| `--mono` JetBrains Mono / `--sans` Hanken Grotesk | fonts | same ✓ | provenance/conf mono; body sans |
|
||||
|
||||
**Reuse existing warm primitives** (`apps/web/src/components/os/warm/`):
|
||||
- `ProvenanceLine.tsx` — already renders `⬡ source · when` in mono `--intel` (the §5b
|
||||
provenance pattern). Extend with a freshness `●` segment + `source:` prefix to match.
|
||||
- `HexAvatar.tsx` — the §6a honey-gradient hex with dark-ink icon.
|
||||
- `SectionLabel.tsx` — eyebrow/mono-uppercase labels.
|
||||
- `DotLive.tsx` — the freshness `●` dot.
|
||||
- `ModelPill` / `RunChip` / chip patterns — the filter-chip + go-button shapes.
|
||||
|
||||
---
|
||||
|
||||
## Component inventory (6-line summary)
|
||||
|
||||
1. **Header/segmented control** — `Memory Trust · view` eyebrow + 2-tab switch (`Manage memory` default-on honey-fill | `Why did you do that?`) + swapping `.vlabel` + theme toggle, toggling between two full views.
|
||||
2. **Editorial hero (per view)** — honey eyebrow, 28px H1 with honey `<em>` accent ("correct, age, and forget." / "why did you do that?"), 64ch body with bold spans.
|
||||
3. **Stat bar (View A)** — 4 cards: 142 white / 128 green / 9 honey-warn / 3 honey-warn, repeat(4,1fr)→2-col @820px.
|
||||
4. **Toolbar (View A)** — search input ("Search what Waggle knows… or ask it to forget something") + 4 filter chips (All on / Stale / Needs confirm / Forgotten).
|
||||
5. **Memory row** — 3-col [conf ring (color by ≥85 green / ≥60 honey / <60 terracotta) + CONF] · [bold fact + `⬡ id · source · ● freshness` provenance + optional corrected/stale banner] · [edit + danger-forget icon buttons]; 5 seed rows incl. 1 stale + 1 disputed/strikethrough.
|
||||
6. **Why-trace (View B)** — hex-avatar header (`trace #a1f9`) + 4-node connector chain (Goal→Recalled→Cross-checked→Acted, dots intel/honey/honey/healthy, evidence chip) + 3-button footer (Looks right / correct it / Forget #M-204 & redo); shared trust-principle footnote + green toast on both views.
|
||||
249
docs/redesign-warm-hive/pr35-recon/05-memory-store-api.md
Normal file
249
docs/redesign-warm-hive/pr35-recon/05-memory-store-api.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# 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`:
|
||||
|
||||
```ts
|
||||
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`, PK `schema.ts:48`).
|
||||
- **content / text** — REAL (`frames.ts:33`, `schema.ts:53`). The field is `content`, NOT `text`.
|
||||
- **source** — ✅ **REAL stored column.** `frames.ts:25` defines `FrameSource = 'user_stated' | 'tool_verified' | 'agent_inferred' | 'import' | 'system' | 'personal' | 'workspace' | 'team_sync'`. The DB CHECK is narrower — `schema.ts:56-57` only allows `('user_stated','tool_verified','agent_inferred','import','system')` (the `personal/workspace/team_sync` members are search-time mind labels, not persisted provenance — see `memory.ts:41-43`).
|
||||
- **confidence / score** — 🟡 **NOT a frame column.** No `confidence` or `score` column on `memory_frames`. Two distinct things wear the name:
|
||||
- `score` is a **search-time, computed** ranking field added by HybridSearch (returned in `normalizeFrame` at `memory.ts:58`), never stored.
|
||||
- `confidence` (0-100) is a **derived metadata field** that rides `memory_frames.metadata` JSON, projected by `normalizeToMemory` at `memory-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 it `undefined`.
|
||||
- (`confidence REAL` does exist — but on `knowledge_relations`, `schema.ts:109`, not on frames.)
|
||||
- **created_at** — REAL (`frames.ts:37`, `schema.ts:59`, default `datetime('now')`).
|
||||
- **updated_at** — 🔴 **NOT on frames.** The frame table has no `updated_at` column. `identity`/`procedures` tables have one (`schema.ts:20,166`); frames do not. An `updatedAt` is faked into the `metadata` blob on PATCH (`memory-center.ts:336`). The shared `Memory.updatedAt` reads it from metadata (`memory-center.ts:112`).
|
||||
- **accessed_at / last_used** — REAL: `last_accessed` (`frames.ts:38`, `schema.ts:60`), bumped by `touch()` (`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 rides `metadata.kind`, projected at `memory-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-sanitized `memory.ts:483`).
|
||||
- `PATCH /api/memory/:id` — `memory-center.ts:296-352` (content/importance **and** metadata classification: kind/scope/tags/status/title/evidence; stamps `metadata.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:
|
||||
|
||||
- `FrameSource` has a `tool_verified` member (`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.outcome` has 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 `MemoryStatus` union (`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 **writes** `unreviewed` today (the create path stamps `'active'`, `memory-center.ts:273`; harvest commit is the only intended `unreviewed` producer per `types.ts:506-507` but that write was not confirmed in this recon), and `low_confidence`/`conflict` are 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`. Returns `1.0` if 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), NOT `last_accessed` — deliberate
|
||||
(`scoring.ts:91-97`): `last_accessed` is bumped by `touch()` on every read, so decaying on
|
||||
it made the dimension constant noise. Use `created_at` for "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_json` payload (`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`, optional `harness`
|
||||
gate results, and `correctionFeedback`.** 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 `TraceRecorder` and `start()`s a trace per turn
|
||||
(`chat.ts:1277-1286`), records reasoning + tool calls through it, and `finalize()`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 at `chat.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 the `agent:{id}` tag**
|
||||
(`agents.ts:486-487`).
|
||||
|
||||
**Two honest gaps for PR3.5:**
|
||||
1. **The chat trace `start()` does NOT pass a `tags:['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.
|
||||
2. **No frame↔trace backlink.** Nothing links a saved `memory_frame` to the
|
||||
`execution_trace` that produced it (no `trace_id` column, no metadata field; grep of
|
||||
chat/home/workspace routes for any `traceId`/`trace_id` link 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.
|
||||
Reference in New Issue
Block a user