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>
154 lines
6.7 KiB
SQL
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;
|