Files
synapbus/internal/storage/schema/028_memory_consolidation.sql
T
Algis DumbrisandClaude Opus 4.7 da827c03b3 feat(020): foundational — migration 028, dispatch tokens, owner resolver
Adds the SQL substrate (6 tables + memory_status view) and the helpers
every user story depends on:
- migration 028_memory_consolidation.sql + smoke test
- internal/messaging/memory_config.go (env-flag plumbing)
- internal/messaging/dispatch_tokens.go (32-byte rand, 15m TTL, single-job-bound)
- internal/messaging/memory_channels.go (open-brain / reflections-* / is_memory flag)
- internal/agents/owner.go (OwnerFor with sentinel errors)

Deviations from spec, all documented in code:
- owner_id is stored as INTEGER FK to users; OwnerFor converts to the
  string scope-key the new tables use.
- MemoryChannel is a local struct to avoid an import cycle between
  internal/channels and internal/messaging.
- channels.metadata column does not exist yet; IsMemoryChannel honors
  it conditionally so MemoryChannelIDs can extend trivially when added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:55:33 +03:00

154 lines
6.7 KiB
SQL

-- 028_memory_consolidation.sql — proactive memory + dream worker.
--
-- Adds six tables backing per-(owner, agent) core memory, typed memory
-- links, dream-worker consolidation jobs, owner-pinned memories,
-- single-use dispatch tokens, and a 24h rolling audit ring of what was
-- injected into each tool response. Adds one derived view
-- `memory_status` over `memory_consolidation_jobs.actions` so retrieval
-- can filter out soft-deleted / superseded memories without touching
-- the hot `messages` table.
--
-- `owner_id` is stored as TEXT (string form of `agents.owner_id`) so
-- that retrieval can carry the value through unchanged from
-- `auth.ContextAgent(ctx)`; the auth middleware already exposes it as a
-- string via `trace.ContextWithOwnerID`.
--
-- All `CREATE TABLE` / `CREATE INDEX` statements use `IF NOT EXISTS`
-- so this migration is safe to re-run. CHECK constraints enforce enum
-- columns. The partial unique index on
-- `memory_consolidation_jobs(owner_id, job_type)` guarantees at most
-- one in-flight job per (owner, job_type).
-- 1. Per-(owner, agent) identity-and-context blob.
CREATE TABLE IF NOT EXISTS memory_core (
owner_id TEXT NOT NULL,
agent_name TEXT NOT NULL,
blob TEXT NOT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by TEXT NOT NULL,
PRIMARY KEY (owner_id, agent_name)
);
CREATE INDEX IF NOT EXISTS idx_memory_core_owner ON memory_core(owner_id);
-- 2. Directed typed edges between two message ids.
CREATE TABLE IF NOT EXISTS memory_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
src_message_id INTEGER NOT NULL,
dst_message_id INTEGER NOT NULL,
relation_type TEXT NOT NULL CHECK (relation_type IN (
'refines', 'contradicts', 'examples', 'related',
'duplicate_of', 'superseded_by',
'mention', 'reply_to', 'channel_cooccurrence'
)),
owner_id TEXT NOT NULL,
created_by TEXT NOT NULL,
metadata TEXT NOT NULL DEFAULT '{}',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(src_message_id, dst_message_id, relation_type)
);
CREATE INDEX IF NOT EXISTS idx_memory_links_owner ON memory_links(owner_id);
CREATE INDEX IF NOT EXISTS idx_memory_links_src ON memory_links(src_message_id);
CREATE INDEX IF NOT EXISTS idx_memory_links_dst ON memory_links(dst_message_id);
CREATE INDEX IF NOT EXISTS idx_memory_links_type ON memory_links(relation_type);
-- 3. Audit log of dispatched dream jobs and their resulting actions.
CREATE TABLE IF NOT EXISTS memory_consolidation_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id TEXT NOT NULL,
job_type TEXT NOT NULL CHECK (job_type IN (
'reflection', 'core_rewrite', 'dedup_contradiction', 'link_gen'
)),
status TEXT NOT NULL CHECK (status IN (
'pending', 'dispatched', 'running', 'succeeded', 'partial', 'failed', 'expired'
)) DEFAULT 'pending',
trigger_reason TEXT NOT NULL,
dispatch_token TEXT,
harness_run_id TEXT,
actions TEXT NOT NULL DEFAULT '[]',
summary TEXT,
error TEXT,
lease_until DATETIME,
started_at DATETIME,
finished_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_consolidation_owner_status
ON memory_consolidation_jobs(owner_id, status);
CREATE INDEX IF NOT EXISTS idx_consolidation_lease
ON memory_consolidation_jobs(lease_until)
WHERE status = 'running';
CREATE UNIQUE INDEX IF NOT EXISTS idx_consolidation_in_flight
ON memory_consolidation_jobs(owner_id, job_type)
WHERE status IN ('pending', 'dispatched', 'running');
-- 4. Owner-pinned message ids that bypass the relevance floor.
CREATE TABLE IF NOT EXISTS memory_pins (
owner_id TEXT NOT NULL,
message_id INTEGER NOT NULL,
pinned_by TEXT NOT NULL,
note TEXT,
pinned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (owner_id, message_id)
);
CREATE INDEX IF NOT EXISTS idx_memory_pins_owner ON memory_pins(owner_id);
-- 5. Single-use, owner-bound, job-bound dispatch tokens.
CREATE TABLE IF NOT EXISTS memory_dispatch_tokens (
token TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
consolidation_job_id INTEGER NOT NULL,
issued_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
used_at DATETIME,
revoked_at DATETIME,
FOREIGN KEY (consolidation_job_id) REFERENCES memory_consolidation_jobs(id)
);
CREATE INDEX IF NOT EXISTS idx_memory_tokens_job
ON memory_dispatch_tokens(consolidation_job_id);
-- 6. 24-hour rolling audit ring of what was injected for each tool call.
CREATE TABLE IF NOT EXISTS memory_injections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id TEXT NOT NULL,
agent_name TEXT NOT NULL,
tool_name TEXT NOT NULL,
packet_size_chars INTEGER NOT NULL,
packet_items_count INTEGER NOT NULL,
message_ids TEXT NOT NULL,
core_blob_included BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_memory_injections_owner_time
ON memory_injections(owner_id, created_at);
-- 7. Derived view: memory_status. Tells retrieval whether a message is
-- active / soft_deleted / superseded based on completed consolidation
-- jobs' action records. Owners can override via a future restore
-- action (out of scope here).
CREATE VIEW IF NOT EXISTS memory_status AS
WITH actions AS (
SELECT
j.owner_id,
json_extract(act.value, '$.target_message_id') AS message_id,
json_extract(act.value, '$.tool') AS tool,
json_extract(act.value, '$.args.keep_id') AS keep_id,
json_extract(act.value, '$.args.b_id') AS superseded_by,
json_extract(act.value, '$.args.reason') AS reason,
j.finished_at AS at
FROM memory_consolidation_jobs j, json_each(j.actions) act
WHERE j.status IN ('succeeded', 'partial')
)
SELECT
message_id,
owner_id,
CASE
WHEN MAX(CASE WHEN tool='memory_supersede' THEN at END) IS NOT NULL THEN 'superseded'
WHEN MAX(CASE WHEN tool='memory_mark_duplicate' AND keep_id != message_id THEN at END) IS NOT NULL THEN 'soft_deleted'
ELSE 'active'
END AS status,
MAX(CASE WHEN tool='memory_supersede' THEN superseded_by END) AS superseded_by,
MAX(CASE WHEN tool='memory_mark_duplicate' AND keep_id != message_id THEN at END) AS soft_deleted_at,
MAX(reason) AS reason
FROM actions
GROUP BY message_id, owner_id;