@clude/sdk 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "@clude/sdk",
3
+ "version": "3.0.1",
4
+ "description": "Persistent memory SDK for AI agents — Stanford Generative Agents architecture on Supabase + pgvector",
5
+ "main": "dist/sdk/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "require": "./dist/sdk/index.js"
9
+ },
10
+ "./schema": "./supabase-schema.sql",
11
+ "./local": {
12
+ "require": "./dist/mcp/local-store.js"
13
+ },
14
+ "./mcp": {
15
+ "require": "./dist/mcp/server.js"
16
+ }
17
+ },
18
+ "bin": {
19
+ "clude": "dist/cli/index.js"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "supabase-schema.sql",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build:publish": "node scripts/build-publish.mjs",
29
+ "prepublishOnly": "pnpm --filter @clude/shared build && pnpm --filter @clude/brain build && node scripts/build-publish.mjs",
30
+ "typecheck": "tsc --noEmit",
31
+ "dashboard": "cd apps/dashboard && pnpm run dev",
32
+ "dashboard:build": "cd apps/dashboard && pnpm run build"
33
+ },
34
+ "keywords": [
35
+ "ai-agent",
36
+ "memory",
37
+ "cognitive-architecture",
38
+ "generative-agents",
39
+ "solana",
40
+ "supabase",
41
+ "pgvector",
42
+ "anthropic",
43
+ "claude",
44
+ "embeddings",
45
+ "dream-cycle",
46
+ "sdk",
47
+ "persistent-memory",
48
+ "agent-memory",
49
+ "llm",
50
+ "mcp",
51
+ "long-term-memory",
52
+ "memory-system",
53
+ "vector-search",
54
+ "knowledge-graph",
55
+ "rag",
56
+ "langchain",
57
+ "ai-memory"
58
+ ],
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/sebbsssss/cludebot.git"
62
+ },
63
+ "homepage": "https://clude.io",
64
+ "bugs": {
65
+ "url": "https://github.com/sebbsssss/cludebot/issues"
66
+ },
67
+ "author": "sebbsssss",
68
+ "license": "MIT",
69
+ "engines": {
70
+ "node": ">=18.0.0"
71
+ },
72
+ "dependencies": {
73
+ "@ai-sdk/anthropic": "^3.0.64",
74
+ "@ai-sdk/google": "^3.0.55",
75
+ "@ai-sdk/openai": "^3.0.49",
76
+ "@ai-sdk/xai": "^3.0.75",
77
+ "@anthropic-ai/sdk": "^0.39.0",
78
+ "@huggingface/transformers": "^4.1.0",
79
+ "@modelcontextprotocol/sdk": "^1.26.0",
80
+ "@openrouter/ai-sdk-provider": "^2.3.3",
81
+ "@privy-io/node": "^0.12.0",
82
+ "@solana/web3.js": "^1.98.4",
83
+ "@supabase/supabase-js": "^2.95.3",
84
+ "ai": "^6.0.142",
85
+ "better-sqlite3": "^12.9.0",
86
+ "bs58": "^6.0.0",
87
+ "dotenv": "^16.4.0",
88
+ "express": "^4.21.0",
89
+ "express-rate-limit": "^8.2.1",
90
+ "jose": "^6.1.3",
91
+ "multer": "^2.1.1",
92
+ "murmurhash3js": "^3.0.1",
93
+ "node-cron": "^3.0.3",
94
+ "pino": "^9.0.0",
95
+ "sqlite-vec": "^0.1.9",
96
+ "tweetnacl": "^1.0.3",
97
+ "twitter-api-v2": "^1.29.0",
98
+ "unpdf": "^1.4.0",
99
+ "vercel-minimax-ai-provider": "^0.0.2",
100
+ "zod": "^4.3.6"
101
+ },
102
+ "devDependencies": {
103
+ "@types/express": "^4.17.25",
104
+ "@types/node": "^22.19.11",
105
+ "@types/node-cron": "^3.0.11",
106
+ "esbuild": "^0.28.0",
107
+ "tsx": "^4.19.0",
108
+ "typescript": "^5.6.0"
109
+ },
110
+ "optionalDependencies": {
111
+ "utf-8-validate": "^5.0.10"
112
+ }
113
+ }
@@ -0,0 +1,531 @@
1
+ -- Run this in the Supabase SQL Editor to create all required tables
2
+
3
+ CREATE TABLE IF NOT EXISTS wallet_links (
4
+ id BIGSERIAL PRIMARY KEY,
5
+ x_handle TEXT UNIQUE NOT NULL,
6
+ x_user_id TEXT UNIQUE NOT NULL,
7
+ wallet_address TEXT NOT NULL,
8
+ verified_at TIMESTAMPTZ DEFAULT NOW()
9
+ );
10
+
11
+ CREATE TABLE IF NOT EXISTS processed_mentions (
12
+ tweet_id TEXT PRIMARY KEY,
13
+ feature TEXT NOT NULL,
14
+ response_tweet_id TEXT,
15
+ conversation_id TEXT,
16
+ author_id TEXT,
17
+ processed_at TIMESTAMPTZ DEFAULT NOW()
18
+ );
19
+ CREATE INDEX IF NOT EXISTS idx_processed_conv ON processed_mentions(conversation_id) WHERE conversation_id IS NOT NULL;
20
+ CREATE INDEX IF NOT EXISTS idx_processed_conv_author ON processed_mentions(conversation_id, author_id) WHERE conversation_id IS NOT NULL;
21
+
22
+ -- Migration for existing deployments:
23
+ -- ALTER TABLE processed_mentions ADD COLUMN IF NOT EXISTS conversation_id TEXT;
24
+ -- ALTER TABLE processed_mentions ADD COLUMN IF NOT EXISTS author_id TEXT;
25
+ -- CREATE INDEX IF NOT EXISTS idx_processed_conv ON processed_mentions(conversation_id) WHERE conversation_id IS NOT NULL;
26
+ -- CREATE INDEX IF NOT EXISTS idx_processed_conv_author ON processed_mentions(conversation_id, author_id) WHERE conversation_id IS NOT NULL;
27
+
28
+ CREATE TABLE IF NOT EXISTS opinion_commits (
29
+ id BIGSERIAL PRIMARY KEY,
30
+ tweet_id TEXT NOT NULL,
31
+ question TEXT NOT NULL,
32
+ answer TEXT NOT NULL,
33
+ answer_hash TEXT NOT NULL,
34
+ solana_signature TEXT,
35
+ created_at TIMESTAMPTZ DEFAULT NOW()
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS rate_limits (
39
+ key TEXT PRIMARY KEY,
40
+ count INTEGER DEFAULT 0,
41
+ window_start TIMESTAMPTZ DEFAULT NOW()
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS price_snapshots (
45
+ id BIGSERIAL PRIMARY KEY,
46
+ price_usd DOUBLE PRECISION NOT NULL,
47
+ volume_24h DOUBLE PRECISION,
48
+ market_cap DOUBLE PRECISION,
49
+ recorded_at TIMESTAMPTZ DEFAULT NOW()
50
+ );
51
+
52
+ CREATE INDEX IF NOT EXISTS idx_price_snapshots_recorded ON price_snapshots(recorded_at);
53
+
54
+ -- ============================================================
55
+ -- MEMORY SYSTEM: The Cortex
56
+ -- Episodic, Semantic, Procedural, and Self-Model memory
57
+ -- with hybrid vector + keyword retrieval
58
+ -- ============================================================
59
+
60
+ -- Enable trigram similarity for fuzzy text search
61
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
62
+
63
+ -- Enable pgvector for semantic similarity search
64
+ CREATE EXTENSION IF NOT EXISTS vector;
65
+
66
+ CREATE TABLE IF NOT EXISTS memories (
67
+ id BIGSERIAL PRIMARY KEY,
68
+ memory_type TEXT NOT NULL CHECK (memory_type IN ('episodic', 'semantic', 'procedural', 'self_model', 'introspective')),
69
+ content TEXT NOT NULL,
70
+ summary TEXT NOT NULL,
71
+ tags TEXT[] DEFAULT '{}',
72
+ concepts TEXT[] DEFAULT '{}', -- structured concept ontology labels
73
+ emotional_valence REAL DEFAULT 0, -- -1 (negative) to 1 (positive)
74
+ importance REAL DEFAULT 0.5, -- 0 to 1
75
+ access_count INTEGER DEFAULT 0,
76
+ source TEXT, -- trigger: mention, market, consolidation, reflection, emergence
77
+ source_id TEXT, -- tweet ID, event ID, etc.
78
+ related_user TEXT, -- X user ID if relevant
79
+ related_wallet TEXT, -- wallet address if relevant
80
+ metadata JSONB DEFAULT '{}',
81
+ created_at TIMESTAMPTZ DEFAULT NOW(),
82
+ last_accessed TIMESTAMPTZ DEFAULT NOW(),
83
+ decay_factor REAL DEFAULT 1.0, -- type-specific decay (episodic=0.93, semantic=0.98, procedural=0.97, self_model=0.99)
84
+ evidence_ids BIGINT[] DEFAULT '{}', -- IDs of memories that support this one (Park et al. 2023)
85
+ solana_signature TEXT, -- Solana tx signature if committed on-chain
86
+ embedding vector(1024), -- vector embedding for semantic similarity search
87
+ hash_id TEXT, -- collision-resistant hash ID (Beads-inspired)
88
+ compacted BOOLEAN DEFAULT FALSE, -- whether this memory has been compacted
89
+ compacted_into TEXT, -- hash_id of the memory this was compacted into
90
+ encrypted BOOLEAN DEFAULT FALSE, -- whether content is client-side encrypted
91
+ encryption_pubkey TEXT, -- Solana pubkey that encrypted this memory
92
+ owner_wallet TEXT, -- Solana pubkey of the memory owner
93
+ event_date TIMESTAMPTZ DEFAULT NULL, -- explicit event date extracted from content (temporal indexing)
94
+ event_date_precision TEXT DEFAULT NULL CHECK (event_date_precision IN ('day', 'week', 'month', 'year')),
95
+ ts_summary tsvector GENERATED ALWAYS AS (
96
+ setweight(to_tsvector('english', COALESCE(summary, '')), 'A') ||
97
+ setweight(to_tsvector('english', COALESCE(LEFT(content, 2000), '')), 'B')
98
+ ) STORED -- auto-generated tsvector for BM25-like full-text search
99
+ );
100
+
101
+ -- Memory fragments: granular vector decomposition for precision retrieval
102
+ -- Each memory is split into semantic fragments (summary, content chunks, tag context)
103
+ -- so vector search matches against precise sub-memory content rather than the whole blob.
104
+ CREATE TABLE IF NOT EXISTS memory_fragments (
105
+ id BIGSERIAL PRIMARY KEY,
106
+ memory_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
107
+ fragment_type TEXT NOT NULL, -- 'summary', 'content_chunk', 'tag_context'
108
+ content TEXT NOT NULL,
109
+ embedding vector(1024),
110
+ created_at TIMESTAMPTZ DEFAULT NOW()
111
+ );
112
+
113
+ -- Dream logs: consolidation, reflection, emergence sessions
114
+ CREATE TABLE IF NOT EXISTS dream_logs (
115
+ id BIGSERIAL PRIMARY KEY,
116
+ session_type TEXT NOT NULL CHECK (session_type IN ('consolidation', 'reflection', 'emergence', 'compaction', 'decay', 'contradiction_resolution')),
117
+ input_memory_ids BIGINT[] DEFAULT '{}',
118
+ output TEXT NOT NULL,
119
+ new_memories_created BIGINT[] DEFAULT '{}',
120
+ created_at TIMESTAMPTZ DEFAULT NOW()
121
+ );
122
+
123
+ -- Indexes for memory retrieval
124
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type);
125
+ CREATE INDEX IF NOT EXISTS idx_memories_tags ON memories USING GIN(tags);
126
+ CREATE INDEX IF NOT EXISTS idx_memories_concepts ON memories USING GIN(concepts);
127
+ CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance DESC);
128
+ CREATE INDEX IF NOT EXISTS idx_memories_user ON memories(related_user);
129
+ CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at DESC);
130
+ CREATE INDEX IF NOT EXISTS idx_memories_decay ON memories(decay_factor);
131
+ CREATE INDEX IF NOT EXISTS idx_memories_summary_trgm ON memories USING GIN(summary gin_trgm_ops);
132
+ CREATE INDEX IF NOT EXISTS idx_memories_evidence ON memories USING GIN(evidence_ids);
133
+ CREATE INDEX IF NOT EXISTS idx_memories_owner ON memories(owner_wallet);
134
+ CREATE INDEX IF NOT EXISTS idx_memories_event_date ON memories(event_date) WHERE event_date IS NOT NULL;
135
+ CREATE INDEX IF NOT EXISTS idx_memories_ts_summary ON memories USING GIN (ts_summary);
136
+
137
+ -- Vector similarity indexes (HNSW for fast approximate nearest neighbor)
138
+ CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories USING hnsw (embedding vector_cosine_ops);
139
+ CREATE INDEX IF NOT EXISTS idx_fragments_embedding ON memory_fragments USING hnsw (embedding vector_cosine_ops);
140
+ CREATE INDEX IF NOT EXISTS idx_fragments_memory_id ON memory_fragments(memory_id);
141
+
142
+ CREATE INDEX IF NOT EXISTS idx_dream_logs_type ON dream_logs(session_type);
143
+ CREATE INDEX IF NOT EXISTS idx_dream_logs_created ON dream_logs(created_at DESC);
144
+
145
+ -- ============================================================
146
+ -- VECTOR SEARCH FUNCTIONS
147
+ -- Used by the hybrid retrieval system in memory.ts
148
+ -- ============================================================
149
+
150
+ -- Semantic search across memory-level embeddings with metadata filtering
151
+ CREATE OR REPLACE FUNCTION match_memories(
152
+ query_embedding vector(1024),
153
+ match_threshold float DEFAULT 0.3,
154
+ match_count int DEFAULT 10,
155
+ filter_types text[] DEFAULT NULL,
156
+ filter_user text DEFAULT NULL,
157
+ min_decay float DEFAULT 0.1,
158
+ filter_owner text DEFAULT NULL,
159
+ filter_tags text[] DEFAULT NULL
160
+ )
161
+ RETURNS TABLE (id bigint, similarity float)
162
+ LANGUAGE plpgsql AS $$
163
+ BEGIN
164
+ RETURN QUERY
165
+ SELECT m.id, (1 - (m.embedding <=> query_embedding))::float AS similarity
166
+ FROM memories m
167
+ WHERE m.embedding IS NOT NULL
168
+ AND m.decay_factor >= min_decay
169
+ AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
170
+ AND (filter_user IS NULL OR m.related_user = filter_user)
171
+ AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
172
+ AND (filter_tags IS NULL OR m.tags && filter_tags)
173
+ AND (1 - (m.embedding <=> query_embedding)) > match_threshold
174
+ ORDER BY m.embedding <=> query_embedding
175
+ LIMIT match_count;
176
+ END;
177
+ $$;
178
+
179
+ -- Temporal-aware semantic search with date range filtering (Exp 9)
180
+ -- Uses COALESCE(event_date, created_at) so memories with explicit event dates
181
+ -- are preferred, but all memories fall back to creation time.
182
+ CREATE OR REPLACE FUNCTION match_memories_temporal(
183
+ query_embedding vector(1024),
184
+ match_threshold float DEFAULT 0.3,
185
+ match_count int DEFAULT 20,
186
+ start_date timestamptz DEFAULT NULL,
187
+ end_date timestamptz DEFAULT NULL,
188
+ filter_types text[] DEFAULT NULL,
189
+ filter_user text DEFAULT NULL,
190
+ min_decay float DEFAULT 0.1,
191
+ filter_owner text DEFAULT NULL,
192
+ filter_tags text[] DEFAULT NULL
193
+ )
194
+ RETURNS TABLE (id bigint, similarity float)
195
+ LANGUAGE plpgsql AS $$
196
+ BEGIN
197
+ RETURN QUERY
198
+ SELECT m.id, (1 - (m.embedding <=> query_embedding))::float AS similarity
199
+ FROM memories m
200
+ WHERE m.embedding IS NOT NULL
201
+ AND m.decay_factor >= min_decay
202
+ AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
203
+ AND (filter_user IS NULL OR m.related_user = filter_user)
204
+ AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
205
+ AND (filter_tags IS NULL OR m.tags && filter_tags)
206
+ AND (1 - (m.embedding <=> query_embedding)) > match_threshold
207
+ AND (start_date IS NULL OR COALESCE(m.event_date, m.created_at) >= start_date)
208
+ AND (end_date IS NULL OR COALESCE(m.event_date, m.created_at) <= end_date)
209
+ ORDER BY m.embedding <=> query_embedding
210
+ LIMIT match_count;
211
+ END;
212
+ $$;
213
+
214
+ -- BM25-ranked full-text search using tsvector/tsquery (Exp 8)
215
+ -- Replaces ilike-based keyword search with stemming and TF-IDF ranking.
216
+ CREATE OR REPLACE FUNCTION bm25_search_memories(
217
+ search_query text,
218
+ match_count int DEFAULT 20,
219
+ min_decay float DEFAULT 0.1,
220
+ filter_owner text DEFAULT NULL,
221
+ filter_types text[] DEFAULT NULL,
222
+ filter_tags text[] DEFAULT NULL
223
+ )
224
+ RETURNS TABLE (id bigint, rank float)
225
+ LANGUAGE plpgsql AS $$
226
+ DECLARE
227
+ tsquery_val tsquery;
228
+ BEGIN
229
+ tsquery_val := plainto_tsquery('english', search_query);
230
+ IF tsquery_val IS NULL OR tsquery_val = ''::tsquery THEN
231
+ RETURN;
232
+ END IF;
233
+ RETURN QUERY
234
+ SELECT m.id, ts_rank_cd(m.ts_summary, tsquery_val, 32)::float AS rank
235
+ FROM memories m
236
+ WHERE m.ts_summary @@ tsquery_val
237
+ AND m.decay_factor >= min_decay
238
+ AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
239
+ AND (filter_types IS NULL OR m.memory_type = ANY(filter_types))
240
+ AND (filter_tags IS NULL OR m.tags && filter_tags)
241
+ ORDER BY rank DESC
242
+ LIMIT match_count;
243
+ END;
244
+ $$;
245
+
246
+ -- ============================================================
247
+ -- MEMORY ASSOCIATION GRAPH
248
+ -- Bidirectional, typed, weighted links between memories.
249
+ -- Enables multi-hop retrieval, conflict detection, and
250
+ -- associative reasoning (A-Mem / MAGMA / SYNAPSE inspired).
251
+ -- ============================================================
252
+
253
+ CREATE TABLE IF NOT EXISTS memory_links (
254
+ id BIGSERIAL PRIMARY KEY,
255
+ source_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
256
+ target_id BIGINT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
257
+ link_type TEXT NOT NULL CHECK (link_type IN (
258
+ 'supports', -- evidence-backed
259
+ 'contradicts', -- conflicting information
260
+ 'elaborates', -- adds detail to existing memory
261
+ 'causes', -- causal chain
262
+ 'follows', -- temporal sequence
263
+ 'relates', -- general semantic association
264
+ 'resolves', -- contradiction resolution outcome
265
+ 'happens_before', -- temporal ordering
266
+ 'happens_after', -- temporal ordering
267
+ 'concurrent_with' -- temporal co-occurrence
268
+ )),
269
+ strength REAL DEFAULT 0.5,
270
+ created_at TIMESTAMPTZ DEFAULT NOW(),
271
+ UNIQUE(source_id, target_id, link_type)
272
+ );
273
+
274
+ CREATE INDEX IF NOT EXISTS idx_links_source ON memory_links(source_id);
275
+ CREATE INDEX IF NOT EXISTS idx_links_target ON memory_links(target_id);
276
+ CREATE INDEX IF NOT EXISTS idx_links_type ON memory_links(link_type);
277
+ CREATE INDEX IF NOT EXISTS idx_links_strength ON memory_links(strength DESC);
278
+
279
+ -- 1-hop traversal: get all memories linked to a set of IDs (both directions)
280
+ CREATE OR REPLACE FUNCTION get_linked_memories(
281
+ seed_ids BIGINT[],
282
+ min_strength FLOAT DEFAULT 0.1,
283
+ max_results INT DEFAULT 20,
284
+ filter_owner TEXT DEFAULT NULL
285
+ )
286
+ RETURNS TABLE (
287
+ memory_id BIGINT,
288
+ linked_from BIGINT,
289
+ link_type TEXT,
290
+ strength FLOAT
291
+ )
292
+ LANGUAGE sql AS $$
293
+ SELECT DISTINCT ON (ml.target_id, ml.link_type)
294
+ ml.target_id AS memory_id,
295
+ ml.source_id AS linked_from,
296
+ ml.link_type,
297
+ ml.strength::float
298
+ FROM memory_links ml
299
+ JOIN memories m ON m.id = ml.target_id
300
+ WHERE ml.source_id = ANY(seed_ids)
301
+ AND ml.target_id != ALL(seed_ids)
302
+ AND ml.strength >= min_strength
303
+ AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
304
+ UNION
305
+ SELECT DISTINCT ON (ml.source_id, ml.link_type)
306
+ ml.source_id AS memory_id,
307
+ ml.target_id AS linked_from,
308
+ ml.link_type,
309
+ ml.strength::float
310
+ FROM memory_links ml
311
+ JOIN memories m ON m.id = ml.source_id
312
+ WHERE ml.target_id = ANY(seed_ids)
313
+ AND ml.source_id != ALL(seed_ids)
314
+ AND ml.strength >= min_strength
315
+ AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
316
+ ORDER BY strength DESC
317
+ LIMIT max_results;
318
+ $$;
319
+
320
+ -- 2-hop traversal: from seeds, get connected memories up to depth 2
321
+ CREATE OR REPLACE FUNCTION get_memory_graph(
322
+ seed_ids BIGINT[],
323
+ min_strength FLOAT DEFAULT 0.2,
324
+ max_results INT DEFAULT 30
325
+ )
326
+ RETURNS TABLE (
327
+ memory_id BIGINT,
328
+ hop INT,
329
+ link_type TEXT,
330
+ strength FLOAT
331
+ )
332
+ LANGUAGE plpgsql AS $$
333
+ DECLARE
334
+ hop1_ids BIGINT[];
335
+ BEGIN
336
+ RETURN QUERY
337
+ SELECT g.memory_id, 1 AS hop, g.link_type, g.strength
338
+ FROM get_linked_memories(seed_ids, min_strength, max_results) g;
339
+
340
+ SELECT ARRAY_AGG(DISTINCT g.memory_id)
341
+ INTO hop1_ids
342
+ FROM get_linked_memories(seed_ids, min_strength, max_results) g;
343
+
344
+ IF hop1_ids IS NOT NULL AND array_length(hop1_ids, 1) > 0 THEN
345
+ RETURN QUERY
346
+ SELECT g2.memory_id, 2 AS hop, g2.link_type, g2.strength * 0.5 AS strength
347
+ FROM get_linked_memories(hop1_ids, min_strength, max_results / 2) g2
348
+ WHERE g2.memory_id != ALL(seed_ids)
349
+ AND g2.memory_id != ALL(hop1_ids);
350
+ END IF;
351
+ END;
352
+ $$;
353
+
354
+ -- Hebbian reinforcement: boost link strength for co-retrieved memories
355
+ CREATE OR REPLACE FUNCTION boost_link_strength(
356
+ memory_ids BIGINT[],
357
+ boost_amount FLOAT DEFAULT 0.05
358
+ )
359
+ RETURNS INTEGER
360
+ LANGUAGE plpgsql AS $$
361
+ DECLARE affected INTEGER;
362
+ BEGIN
363
+ UPDATE memory_links
364
+ SET strength = LEAST(1.0, strength + boost_amount)
365
+ WHERE source_id = ANY(memory_ids)
366
+ AND target_id = ANY(memory_ids);
367
+ GET DIAGNOSTICS affected = ROW_COUNT;
368
+ RETURN affected;
369
+ END;
370
+ $$;
371
+
372
+ -- Find unresolved contradiction pairs (no 'resolves' link spanning both memories)
373
+ CREATE OR REPLACE FUNCTION get_unresolved_contradictions(
374
+ max_pairs INT DEFAULT 3,
375
+ filter_owner TEXT DEFAULT NULL
376
+ )
377
+ RETURNS TABLE (
378
+ link_id BIGINT,
379
+ source_id BIGINT,
380
+ target_id BIGINT,
381
+ strength FLOAT
382
+ )
383
+ LANGUAGE sql AS $$
384
+ SELECT
385
+ ml.id AS link_id,
386
+ ml.source_id,
387
+ ml.target_id,
388
+ ml.strength::float
389
+ FROM memory_links ml
390
+ JOIN memories ms ON ms.id = ml.source_id AND ms.decay_factor > 0.1
391
+ JOIN memories mt ON mt.id = ml.target_id AND mt.decay_factor > 0.1
392
+ WHERE ml.link_type = 'contradicts'
393
+ AND (filter_owner IS NULL OR ms.owner_wallet = filter_owner)
394
+ AND (filter_owner IS NULL OR mt.owner_wallet = filter_owner)
395
+ AND NOT EXISTS (
396
+ SELECT 1 FROM memory_links r1
397
+ JOIN memory_links r2 ON r1.source_id = r2.source_id
398
+ WHERE r1.link_type = 'resolves'
399
+ AND r2.link_type = 'resolves'
400
+ AND r1.target_id = ml.source_id
401
+ AND r2.target_id = ml.target_id
402
+ )
403
+ ORDER BY ml.strength DESC, ml.created_at DESC
404
+ LIMIT max_pairs;
405
+ $$;
406
+
407
+ -- Fragment-level semantic search with deduplication to parent memory.
408
+ -- Returns the highest similarity fragment per memory, enabling precision
409
+ -- matching against individual facts/sentences rather than whole memories.
410
+ CREATE OR REPLACE FUNCTION match_memory_fragments(
411
+ query_embedding vector(1024),
412
+ match_threshold float DEFAULT 0.3,
413
+ match_count int DEFAULT 10,
414
+ filter_owner text DEFAULT NULL
415
+ )
416
+ RETURNS TABLE (memory_id bigint, max_similarity float)
417
+ LANGUAGE plpgsql AS $$
418
+ BEGIN
419
+ RETURN QUERY
420
+ SELECT f.memory_id, MAX((1 - (f.embedding <=> query_embedding))::float) AS max_similarity
421
+ FROM memory_fragments f
422
+ JOIN memories m ON m.id = f.memory_id
423
+ WHERE f.embedding IS NOT NULL
424
+ AND (1 - (f.embedding <=> query_embedding)) > match_threshold
425
+ AND (filter_owner IS NULL OR m.owner_wallet = filter_owner)
426
+ GROUP BY f.memory_id
427
+ ORDER BY max_similarity DESC
428
+ LIMIT match_count;
429
+ END;
430
+ $$;
431
+
432
+ -- ============================================================
433
+ -- AGENT DASHBOARD: Orchestration & Monitoring
434
+ -- Multi-agent registry, task management, activity audit log
435
+ -- ============================================================
436
+
437
+ -- Agent registry
438
+ CREATE TABLE IF NOT EXISTS dashboard_agents (
439
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
440
+ name TEXT NOT NULL,
441
+ type TEXT DEFAULT 'claude_code' CHECK (type IN ('claude_code', 'script', 'webhook', 'clude_bot', 'content', 'research', 'dev', 'testing', 'design_audit', 'customer_journey')),
442
+ status TEXT DEFAULT 'offline' CHECK (status IN ('online', 'offline', 'paused', 'error')),
443
+ description TEXT,
444
+ config JSONB DEFAULT '{}',
445
+ heartbeat_url TEXT,
446
+ heartbeat_interval_ms INTEGER DEFAULT 300000,
447
+ last_heartbeat_at TIMESTAMPTZ,
448
+ budget_monthly_usd NUMERIC(10,2) DEFAULT 0,
449
+ budget_used_usd NUMERIC(10,2) DEFAULT 0,
450
+ budget_reset_at TIMESTAMPTZ,
451
+ created_at TIMESTAMPTZ DEFAULT NOW(),
452
+ updated_at TIMESTAMPTZ DEFAULT NOW()
453
+ );
454
+
455
+ CREATE INDEX IF NOT EXISTS idx_dashboard_agents_status ON dashboard_agents(status);
456
+ CREATE INDEX IF NOT EXISTS idx_dashboard_agents_type ON dashboard_agents(type);
457
+
458
+ -- Task/ticket system
459
+ CREATE TABLE IF NOT EXISTS dashboard_tasks (
460
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
461
+ agent_id UUID REFERENCES dashboard_agents(id) ON DELETE SET NULL,
462
+ title TEXT NOT NULL,
463
+ description TEXT,
464
+ status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
465
+ priority TEXT DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high', 'critical')),
466
+ parent_task_id UUID REFERENCES dashboard_tasks(id) ON DELETE SET NULL,
467
+ metadata JSONB DEFAULT '{}',
468
+ created_at TIMESTAMPTZ DEFAULT NOW(),
469
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
470
+ completed_at TIMESTAMPTZ
471
+ );
472
+
473
+ CREATE INDEX IF NOT EXISTS idx_dashboard_tasks_agent ON dashboard_tasks(agent_id);
474
+ CREATE INDEX IF NOT EXISTS idx_dashboard_tasks_status ON dashboard_tasks(status);
475
+ CREATE INDEX IF NOT EXISTS idx_dashboard_tasks_priority ON dashboard_tasks(priority);
476
+
477
+ -- Immutable activity/audit log
478
+ CREATE TABLE IF NOT EXISTS dashboard_activity (
479
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
480
+ agent_id UUID REFERENCES dashboard_agents(id) ON DELETE SET NULL,
481
+ action TEXT NOT NULL,
482
+ details JSONB DEFAULT '{}',
483
+ cost_usd NUMERIC(10,4) DEFAULT 0,
484
+ created_at TIMESTAMPTZ DEFAULT NOW()
485
+ );
486
+
487
+ CREATE INDEX IF NOT EXISTS idx_dashboard_activity_agent ON dashboard_activity(agent_id);
488
+ CREATE INDEX IF NOT EXISTS idx_dashboard_activity_action ON dashboard_activity(action);
489
+ CREATE INDEX IF NOT EXISTS idx_dashboard_activity_created ON dashboard_activity(created_at DESC);
490
+
491
+ -- ============================================================
492
+ -- Chat Billing: balances, top-ups, per-message usage
493
+ -- ============================================================
494
+
495
+ CREATE TABLE IF NOT EXISTS chat_balances (
496
+ wallet_address TEXT PRIMARY KEY,
497
+ balance_usdc NUMERIC(20,8) NOT NULL DEFAULT 0,
498
+ total_deposited NUMERIC(20,8) NOT NULL DEFAULT 0,
499
+ total_spent NUMERIC(20,8) NOT NULL DEFAULT 0,
500
+ updated_at TIMESTAMPTZ DEFAULT NOW()
501
+ );
502
+
503
+ CREATE TABLE IF NOT EXISTS chat_topups (
504
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
505
+ wallet_address TEXT NOT NULL,
506
+ amount_usdc NUMERIC(20,8) NOT NULL,
507
+ chain TEXT NOT NULL DEFAULT 'solana',
508
+ tx_hash TEXT UNIQUE,
509
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'failed')),
510
+ created_at TIMESTAMPTZ DEFAULT NOW(),
511
+ confirmed_at TIMESTAMPTZ,
512
+ reference TEXT
513
+ );
514
+
515
+ CREATE INDEX IF NOT EXISTS idx_chat_topups_wallet ON chat_topups(wallet_address, created_at DESC);
516
+ CREATE INDEX IF NOT EXISTS idx_chat_topups_tx ON chat_topups(tx_hash);
517
+ CREATE INDEX IF NOT EXISTS idx_chat_topups_reference ON chat_topups(reference);
518
+
519
+ CREATE TABLE IF NOT EXISTS chat_usage (
520
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
521
+ wallet_address TEXT NOT NULL,
522
+ conversation_id UUID REFERENCES chat_conversations(id) ON DELETE SET NULL,
523
+ message_id UUID,
524
+ model TEXT NOT NULL,
525
+ tokens_prompt INTEGER,
526
+ tokens_completion INTEGER,
527
+ cost_usdc NUMERIC(20,8) NOT NULL DEFAULT 0,
528
+ created_at TIMESTAMPTZ DEFAULT NOW()
529
+ );
530
+
531
+ CREATE INDEX IF NOT EXISTS idx_chat_usage_wallet ON chat_usage(wallet_address, created_at DESC);