Add agent memory: linked note store, MCP server, recall hook, and graph UI

The distilled knowledge layer over the raw log/transcript history, inspired by
TencentDB-Agent-Memory's memory-hub model, adapted to handler's invariants
(state lives only in the database; workers stay stateless and disposable):

- memory_notes + memory_links tables (migration 0014) with a portable DAL:
  scoped listing/search (project + global), idempotent linking, and a one-read
  graph. Deleting an agent nulls attribution but keeps its notes; deleting a
  project removes its notes and edges, leaving global knowledge intact.
- /memory API routes: reads on the normal token, note/link authoring on the
  admin token, plus GET /memory/graph for the dashboard.
- Bundled handler-memory MCP server (python -m handler.mcpserver), a
  dependency-free stdio JSON-RPC implementation injected into every launch's
  --mcp-config ahead of the DB connectors and allowlisted in generated
  settings, exposing memory_search / memory_get / memory_save / memory_link.
  Identity and DATABASE_URL arrive via the spawn env, same as hooks.
- SessionStart recall hook: injects the most recent notes in scope as
  additional context at session start, best-effort, never blocking.
- Memory page in the web UI: a hand-rolled force-directed SVG graph of the
  note web (colored by kind, hover highlights, click-through details), plus
  note/link authoring — the app's first visualization, no chart dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqkoYPX8NAo1V2KyXr1pk
This commit is contained in:
Claude
2026-08-04 13:49:47 +00:00
parent 682ea20ffc
commit dc80e7bc28
53 changed files with 1949 additions and 54 deletions
+31
View File
@@ -209,6 +209,37 @@ export interface ClaudePermissions {
base_allow: string[];
}
/* ---- agent memory (the Memory page's note graph) ---- */
export type NoteKind = "fact" | "decision" | "gotcha" | "runbook";
export interface MemoryNote {
id: number;
project_id?: string | null; // null = global note
agent_id?: number | null; // authoring agent; null = operator-authored
title: string;
body: string;
kind: string;
tags?: string[] | null;
created_at: string;
updated_at: string;
}
export interface MemoryLink {
id: number;
src_note_id: number;
dst_note_id: number;
relation: string;
created_by_agent_id?: number | null;
created_at: string;
}
/* Everything the graph view draws, in one response (GET /memory/graph). */
export interface MemoryGraph {
notes: MemoryNote[];
links: MemoryLink[];
}
export interface SharedContext {
key: string;
value: string;
+1
View File
@@ -18,6 +18,7 @@ export const NAV_ROUTES: NavRoute[] = [
{ key: "servers", href: "/servers", label: "Git Servers" },
{ key: "activity", href: "/activity", label: "Activity" },
{ key: "shared", href: "/shared", label: "Shared" },
{ key: "memory", href: "/memory", label: "Memory" },
{ key: "claude", href: "/claude", label: "Claude" },
];