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
+4 -2
View File
@@ -434,9 +434,11 @@ def test_spawn_applies_managed_config(env, fake_launch):
agent = spawn.spawn("proj", "api", task="do it")
wd = agent["working_dir"]
# Disabled connectors are excluded from the generated --mcp-config file.
# Disabled connectors are excluded from the generated --mcp-config file; the
# built-in handler-memory server is always injected ahead of the DB connectors.
mcp = json.loads(open(claude_gen.mcp_config_path(wd)).read())
assert list(mcp["mcpServers"]) == ["github"]
assert list(mcp["mcpServers"]) == ["handler-memory", "github"]
assert mcp["mcpServers"]["handler-memory"]["args"] == ["-m", "handler.mcpserver"]
# Skills synced to the (test-scoped) user-level dir.
skill = os.path.join(str(env["tmp"]), ".claude", "skills", "deploy", "SKILL.md")
assert os.path.exists(skill)