From a1b121b6cf2038209e411906175968e720627329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Alves?= Date: Mon, 27 Apr 2026 22:38:44 -0300 Subject: [PATCH] Add memory hovercards to consolidation view --- convex/memoryRecords.ts | 16 +++ debug/src/components/ConsolidationPanel.tsx | 126 ++++++++++++++++++-- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/convex/memoryRecords.ts b/convex/memoryRecords.ts index 256152ee..aed4666d 100644 --- a/convex/memoryRecords.ts +++ b/convex/memoryRecords.ts @@ -89,6 +89,22 @@ export const getByIds = query({ }, }); +export const getByMemoryIds = query({ + args: { memoryIds: v.array(v.string()) }, + handler: async (ctx, args) => { + const uniqueIds = [...new Set(args.memoryIds)].slice(0, 100); + const out = []; + for (const memoryId of uniqueIds) { + const record = await ctx.db + .query("memoryRecords") + .withIndex("by_memory_id", (q) => q.eq("memoryId", memoryId)) + .unique(); + if (record) out.push(record); + } + return out; + }, +}); + export const vectorSearch = action({ args: { embedding: v.array(v.float64()), limit: v.optional(v.number()) }, handler: async (ctx, args): Promise; score: number; record: any }>> => { diff --git a/debug/src/components/ConsolidationPanel.tsx b/debug/src/components/ConsolidationPanel.tsx index f46965fd..6c71f7a5 100644 --- a/debug/src/components/ConsolidationPanel.tsx +++ b/debug/src/components/ConsolidationPanel.tsx @@ -3,6 +3,8 @@ import { useQuery } from "convex/react"; import { api } from "../../../convex/_generated/api.js"; import { useSocket, type SocketEvent } from "../lib/useSocket.js"; +const MEMORY_ID_RE = /mem_[a-z0-9]+_[a-z0-9]+/gi; + type Phase = | "loaded" | "proposing" @@ -512,6 +514,12 @@ function ReasoningSection({ run, isDark }: { run: any; isDark: boolean }) { } catch { /* invalid JSON */ } + const memoryIds = memoryIdsFrom(details); + const memoryRecords = useQuery(api.memoryRecords.getByMemoryIds, { memoryIds }); + const memoryById = new Map((memoryRecords ?? []).map((m: any) => [m.memoryId, m])); + const renderMemoryText = (text = "") => ( + + ); if (!details || !details.proposals?.length) { return ( @@ -590,11 +598,12 @@ function ReasoningSection({ run, isDark }: { run: any; isDark: boolean }) { {p.type === "merge" && ( <>
- keep: {p.keep} + keep:{" "} + {renderMemoryText(p.keep)}
absorb:{" "} - {(p.absorb ?? []).join(", ")} + {renderMemoryText((p.absorb ?? []).join(", "))}
{p.rewriteContent && (
- newer: {p.newer} + newer:{" "} + {renderMemoryText(p.newer)}
older:{" "} - {(p.older ?? []).join(", ")} + {renderMemoryText((p.older ?? []).join(", "))}
)} {p.type === "prune" && ( <>
- memoryId: {p.memoryId} + memoryId:{" "} + {renderMemoryText(p.memoryId)}
{p.reason && (
- reason: {p.reason} + reason:{" "} + {renderMemoryText(p.reason)}
)} @@ -654,7 +666,7 @@ function ReasoningSection({ run, isDark }: { run: any; isDark: boolean }) { > JUDGE{" "} - {d.rationale} + {renderMemoryText(d.rationale)}
)} @@ -665,6 +677,106 @@ function ReasoningSection({ run, isDark }: { run: any; isDark: boolean }) { ); } +function memoryIdsFrom(details: any): string[] { + if (!details) return []; + return [...new Set(JSON.stringify(details).match(MEMORY_ID_RE) ?? [])]; +} + +function MemoryText({ + text, + memoryById, + isDark, +}: { + text: string; + memoryById: Map; + isDark: boolean; +}) { + const parts = text.split(MEMORY_ID_RE); + const ids = text.match(MEMORY_ID_RE) ?? []; + return ( + <> + {parts.map((part, idx) => ( + + {part} + {ids[idx] && ( + + )} + + ))} + + ); +} + +function MemoryIdHover({ + memoryId, + memory, + isDark, +}: { + memoryId?: string; + memory?: any; + isDark: boolean; +}) { + if (!memoryId) return null; + const muted = isDark ? "text-slate-500" : "text-slate-400"; + const panel = isDark + ? "bg-slate-950 border-slate-700 text-slate-200 shadow-black/40" + : "bg-white border-slate-200 text-slate-800 shadow-slate-900/15"; + const pill = + memory?.lifecycle === "active" + ? isDark + ? "bg-emerald-500/10 text-emerald-300 border-emerald-500/20" + : "bg-emerald-50 text-emerald-700 border-emerald-200" + : memory?.lifecycle === "archived" + ? isDark + ? "bg-amber-500/10 text-amber-300 border-amber-500/20" + : "bg-amber-50 text-amber-700 border-amber-200" + : isDark + ? "bg-rose-500/10 text-rose-300 border-rose-500/20" + : "bg-rose-50 text-rose-700 border-rose-200"; + + return ( + + + {memoryId} + + + + ); +} + function SummaryStat({ label, value,