@jhizzard/termdeck 0.9.0 → 0.10.0
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 +1 -1
- package/packages/client/public/graph.html +104 -0
- package/packages/client/public/graph.js +683 -0
- package/packages/client/public/index.html +1 -0
- package/packages/client/public/style.css +427 -0
- package/packages/server/src/graph-routes.js +555 -0
- package/packages/server/src/index.js +21 -0
- package/packages/server/src/preflight.js +82 -0
- package/packages/server/src/rag.js +138 -0
- package/packages/server/src/setup/mnestra-migrations/009_memory_relationship_metadata.sql +126 -0
- package/packages/server/src/setup/mnestra-migrations/010_memory_recall_graph.sql +147 -0
- package/packages/server/src/setup/rumen/migrations/003_graph_inference_schedule.sql +49 -0
|
@@ -46,10 +46,28 @@ class RAGIntegration {
|
|
|
46
46
|
this.db = db;
|
|
47
47
|
this.supabaseUrl = config.rag?.supabaseUrl || null;
|
|
48
48
|
this.supabaseKey = config.rag?.supabaseKey || null;
|
|
49
|
+
this.openaiApiKey = config.rag?.openaiApiKey || process.env.OPENAI_API_KEY || null;
|
|
49
50
|
this.enabled = !!(config.rag?.enabled && this.supabaseUrl && this.supabaseKey);
|
|
50
51
|
this.syncInterval = config.rag?.syncIntervalMs || 10000;
|
|
51
52
|
this._syncTimer = null;
|
|
52
53
|
|
|
54
|
+
// Sprint 38 / T3 — graph-aware recall toggle. When true, the recall()
|
|
55
|
+
// method routes through the new memory_recall_graph RPC (vector seed +
|
|
56
|
+
// graph expansion + combined re-rank). When false (default), it
|
|
57
|
+
// delegates to the existing mnestra-bridge vector path. The half-life
|
|
58
|
+
// mirrors the SQL function default (30 days) but is exposed here so
|
|
59
|
+
// callers can override it without re-deploying the migration.
|
|
60
|
+
this.graphRecall = config.rag?.graphRecall === true;
|
|
61
|
+
this.graphRecallDepth = Math.max(1, Math.min(5, config.rag?.graphRecallDepth ?? 2));
|
|
62
|
+
this.graphRecallK = Math.max(1, Math.min(50, config.rag?.graphRecallK ?? 10));
|
|
63
|
+
this.graphRecallRecencyHalflifeDays = config.rag?.graphRecallRecencyHalflifeDays ?? 30;
|
|
64
|
+
|
|
65
|
+
// Bridge reference for the vector-only recall path. Wired in by index.js
|
|
66
|
+
// after the bridge is created so we avoid duplicating the embed → RPC
|
|
67
|
+
// plumbing here. Optional: if absent, recall() with graphRecall=false
|
|
68
|
+
// throws a helpful error instead of silently returning empty.
|
|
69
|
+
this._bridge = null;
|
|
70
|
+
|
|
53
71
|
// Table configuration matching Josh's multi-layer schema
|
|
54
72
|
this.tables = {
|
|
55
73
|
sessionMemory: config.rag?.tables?.session || 'mnestra_session_memory',
|
|
@@ -374,6 +392,126 @@ class RAGIntegration {
|
|
|
374
392
|
this._statusWriteAt.clear();
|
|
375
393
|
}
|
|
376
394
|
|
|
395
|
+
// Sprint 38 / T3 — wire the mnestra-bridge so vector-only recall delegates
|
|
396
|
+
// to the existing direct/webhook/mcp path instead of duplicating the embed
|
|
397
|
+
// pipeline here.
|
|
398
|
+
setBridge(bridge) {
|
|
399
|
+
this._bridge = bridge || null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Sprint 38 / T3 — graph-aware recall entry point. Returns the same shape
|
|
403
|
+
// as bridge.queryMnestra: { memories: [...], total }. Routes through the
|
|
404
|
+
// memory_recall_graph RPC when graphRecall is enabled, otherwise falls
|
|
405
|
+
// back to the bridge's vector path.
|
|
406
|
+
//
|
|
407
|
+
// options: { project?, searchAll?, sessionContext?, cwd?, depth?, k? }
|
|
408
|
+
async recall(query, options = {}) {
|
|
409
|
+
if (this.graphRecall) {
|
|
410
|
+
return this._recallViaGraph(query, options);
|
|
411
|
+
}
|
|
412
|
+
return this._recallViaVectorOnly(query, options);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async _recallViaVectorOnly(query, options) {
|
|
416
|
+
if (!this._bridge) {
|
|
417
|
+
throw new Error('RAGIntegration.recall: no bridge wired (call setBridge first)');
|
|
418
|
+
}
|
|
419
|
+
return this._bridge.queryMnestra({
|
|
420
|
+
question: query,
|
|
421
|
+
project: options.project,
|
|
422
|
+
searchAll: !!options.searchAll,
|
|
423
|
+
sessionContext: options.sessionContext,
|
|
424
|
+
cwd: options.cwd
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Direct REST call to memory_recall_graph (migration 010). Mirrors the
|
|
429
|
+
// bridge.queryDirect pattern: OpenAI embedding → Supabase RPC. Stays in
|
|
430
|
+
// rag.js so callers don't need to know which mnestra mode the bridge is
|
|
431
|
+
// using; graph recall is always direct-against-Postgres because the RPC
|
|
432
|
+
// doesn't ship as a Mnestra MCP tool yet (Sprint 38 / T1 wires the
|
|
433
|
+
// related MCP tools — graph recall lives here for now).
|
|
434
|
+
async _recallViaGraph(query, options) {
|
|
435
|
+
if (!this.supabaseUrl || !this.supabaseKey) {
|
|
436
|
+
throw new Error('graphRecall: supabaseUrl/supabaseKey not configured');
|
|
437
|
+
}
|
|
438
|
+
if (!this.openaiApiKey) {
|
|
439
|
+
throw new Error('graphRecall: OPENAI_API_KEY not configured');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const project = options.searchAll ? null : (options.project || null);
|
|
443
|
+
const depth = options.depth ?? this.graphRecallDepth;
|
|
444
|
+
const k = options.k ?? this.graphRecallK;
|
|
445
|
+
|
|
446
|
+
const embeddingRes = await fetch('https://api.openai.com/v1/embeddings', {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
headers: {
|
|
449
|
+
'Authorization': `Bearer ${this.openaiApiKey}`,
|
|
450
|
+
'Content-Type': 'application/json'
|
|
451
|
+
},
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
model: 'text-embedding-3-large',
|
|
454
|
+
input: query,
|
|
455
|
+
dimensions: 1536
|
|
456
|
+
})
|
|
457
|
+
});
|
|
458
|
+
if (!embeddingRes.ok) {
|
|
459
|
+
const err = await embeddingRes.text();
|
|
460
|
+
console.error('[rag:graph] embedding failed:', err);
|
|
461
|
+
throw new Error('graphRecall: embedding generation failed');
|
|
462
|
+
}
|
|
463
|
+
const embeddingData = await embeddingRes.json();
|
|
464
|
+
const embedding = embeddingData.data[0].embedding;
|
|
465
|
+
|
|
466
|
+
console.log(`[rag] using graph recall path project=${project ?? 'ALL'} depth=${depth} k=${k}`);
|
|
467
|
+
|
|
468
|
+
const rpcBody = {
|
|
469
|
+
query_embedding: `[${embedding.join(',')}]`,
|
|
470
|
+
project_filter: project,
|
|
471
|
+
max_depth: depth,
|
|
472
|
+
k
|
|
473
|
+
};
|
|
474
|
+
const rpcRes = await fetch(`${this.supabaseUrl}/rest/v1/rpc/memory_recall_graph`, {
|
|
475
|
+
method: 'POST',
|
|
476
|
+
headers: {
|
|
477
|
+
'Content-Type': 'application/json',
|
|
478
|
+
'apikey': this.supabaseKey,
|
|
479
|
+
'Authorization': `Bearer ${this.supabaseKey}`
|
|
480
|
+
},
|
|
481
|
+
body: JSON.stringify(rpcBody)
|
|
482
|
+
});
|
|
483
|
+
if (!rpcRes.ok) {
|
|
484
|
+
const err = await rpcRes.text();
|
|
485
|
+
console.error(`[rag:graph] RPC failed ${rpcRes.status}:`, err);
|
|
486
|
+
throw new Error(`graphRecall: memory_recall_graph RPC failed (${rpcRes.status})`);
|
|
487
|
+
}
|
|
488
|
+
const rows = await rpcRes.json();
|
|
489
|
+
return {
|
|
490
|
+
memories: rows.map((m) => ({
|
|
491
|
+
content: m.content,
|
|
492
|
+
// graph recall doesn't return source_type; preserve the bridge's
|
|
493
|
+
// shape by returning null so consumers don't crash on chip render.
|
|
494
|
+
source_type: m.source_type ?? null,
|
|
495
|
+
project: m.project,
|
|
496
|
+
// The bridge consumers read `similarity`; pass final_score so they
|
|
497
|
+
// see the combined (vector × edge × recency) signal as the badge.
|
|
498
|
+
// Also expose the underlying scores for callers that want to split
|
|
499
|
+
// them out (graph viz, debugging).
|
|
500
|
+
similarity: m.final_score,
|
|
501
|
+
depth: m.depth,
|
|
502
|
+
vector_score: m.vector_score,
|
|
503
|
+
edge_weight: m.edge_weight,
|
|
504
|
+
recency_score: m.recency_score,
|
|
505
|
+
path: m.path,
|
|
506
|
+
// memory_recall_graph doesn't return created_at — depth-N neighbors
|
|
507
|
+
// come from the graph walk, not a direct timestamp pull. Caller can
|
|
508
|
+
// re-fetch via memory_get if they need it.
|
|
509
|
+
created_at: m.created_at ?? null
|
|
510
|
+
})),
|
|
511
|
+
total: rows.length
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
377
515
|
// Live-toggle for the dashboard RAG settings panel (Sprint 36 T3 Deliverable A).
|
|
378
516
|
// Re-evaluates eligibility — flipping `enabled: true` without configured
|
|
379
517
|
// Supabase creds is a no-op so the live integration never claims to be on
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
-- 009_memory_relationship_metadata.sql
|
|
2
|
+
--
|
|
3
|
+
-- Sprint 38 (T1) — Knowledge graph substrate.
|
|
4
|
+
-- Adds graph-edge metadata columns to memory_relationships, expands the
|
|
5
|
+
-- relationship_type vocabulary from 5 to 8 values, and ships a recursive-CTE
|
|
6
|
+
-- traversal function (expand_memory_neighborhood) for N-hop neighborhood
|
|
7
|
+
-- queries.
|
|
8
|
+
--
|
|
9
|
+
-- Idempotent: safe to re-run.
|
|
10
|
+
--
|
|
11
|
+
-- Pre-existing state (verified against petvetbid 2026-04-27 17:25 ET):
|
|
12
|
+
-- memory_relationships has 749 live edges. The migration adds nullable
|
|
13
|
+
-- columns and a wider CHECK; no existing row violates the new constraint.
|
|
14
|
+
--
|
|
15
|
+
-- The original CHECK on relationship_type is anonymous (defined inline in
|
|
16
|
+
-- migration 001), so its name is auto-generated (e.g., memory_relationships_check1).
|
|
17
|
+
-- We can't rely on a known name for DROP CONSTRAINT; the DO block below
|
|
18
|
+
-- introspects pg_constraint and drops any CHECK on this table whose
|
|
19
|
+
-- definition references "relationship_type IN" — preserving the separate
|
|
20
|
+
-- (source_id <> target_id) CHECK.
|
|
21
|
+
|
|
22
|
+
-- ── 1. Metadata columns ──────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
alter table memory_relationships
|
|
25
|
+
add column if not exists weight float,
|
|
26
|
+
add column if not exists inferred_at timestamptz,
|
|
27
|
+
add column if not exists inferred_by text;
|
|
28
|
+
|
|
29
|
+
-- ── 2. Expand relationship_type CHECK vocabulary ─────────────────────────
|
|
30
|
+
|
|
31
|
+
do $$
|
|
32
|
+
declare
|
|
33
|
+
c record;
|
|
34
|
+
begin
|
|
35
|
+
for c in
|
|
36
|
+
select con.conname
|
|
37
|
+
from pg_constraint con
|
|
38
|
+
join pg_class cls on cls.oid = con.conrelid
|
|
39
|
+
where cls.relname = 'memory_relationships'
|
|
40
|
+
and con.contype = 'c'
|
|
41
|
+
and pg_get_constraintdef(con.oid) ilike '%relationship_type%'
|
|
42
|
+
loop
|
|
43
|
+
execute format('alter table memory_relationships drop constraint %I', c.conname);
|
|
44
|
+
end loop;
|
|
45
|
+
end
|
|
46
|
+
$$;
|
|
47
|
+
|
|
48
|
+
alter table memory_relationships
|
|
49
|
+
add constraint memory_relationships_relationship_type_check
|
|
50
|
+
check (relationship_type in (
|
|
51
|
+
'supersedes',
|
|
52
|
+
'relates_to',
|
|
53
|
+
'contradicts',
|
|
54
|
+
'elaborates',
|
|
55
|
+
'caused_by',
|
|
56
|
+
'blocks',
|
|
57
|
+
'inspired_by',
|
|
58
|
+
'cross_project_link'
|
|
59
|
+
));
|
|
60
|
+
|
|
61
|
+
-- ── 3. Indexes for graph traversal ───────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
create index if not exists memory_relationships_weight_idx
|
|
64
|
+
on memory_relationships(weight)
|
|
65
|
+
where weight is not null;
|
|
66
|
+
|
|
67
|
+
create index if not exists memory_relationships_inferred_at_idx
|
|
68
|
+
on memory_relationships(inferred_at)
|
|
69
|
+
where inferred_at is not null;
|
|
70
|
+
|
|
71
|
+
-- ── 4. expand_memory_neighborhood — recursive-CTE N-hop traversal ────────
|
|
72
|
+
--
|
|
73
|
+
-- Returns one row per (memory_id, depth) reachable from start_id within
|
|
74
|
+
-- max_depth hops. Edges are undirected for traversal purposes (source_id
|
|
75
|
+
-- and target_id treated symmetrically) — agents linking memories don't
|
|
76
|
+
-- always think directionally, and graph-aware recall benefits from
|
|
77
|
+
-- bidirectional reachability.
|
|
78
|
+
--
|
|
79
|
+
-- Cycle-safe: the path[] array tracks visited nodes; we only follow an
|
|
80
|
+
-- edge if its other endpoint is not already in path. Without this check,
|
|
81
|
+
-- the CTE would loop indefinitely on any cycle in the graph.
|
|
82
|
+
--
|
|
83
|
+
-- edge_kinds[] mirrors path: position i in edge_kinds is the relationship_type
|
|
84
|
+
-- of the edge that brought us to position i+1 in path. Caller can filter
|
|
85
|
+
-- downstream by inspecting edge_kinds (e.g., drop rows whose path traversed
|
|
86
|
+
-- a 'contradicts' edge).
|
|
87
|
+
--
|
|
88
|
+
-- Performance note: with ~5K memory_items and ~750 edges (current scale),
|
|
89
|
+
-- recursive expansion to depth 2 returns in <50ms unsharded. At >50K edges
|
|
90
|
+
-- consider materializing precomputed neighborhoods.
|
|
91
|
+
|
|
92
|
+
create or replace function expand_memory_neighborhood(
|
|
93
|
+
start_id uuid,
|
|
94
|
+
max_depth int default 2
|
|
95
|
+
)
|
|
96
|
+
returns table (
|
|
97
|
+
memory_id uuid,
|
|
98
|
+
depth int,
|
|
99
|
+
path uuid[],
|
|
100
|
+
edge_kinds text[]
|
|
101
|
+
)
|
|
102
|
+
language sql stable
|
|
103
|
+
as $$
|
|
104
|
+
with recursive neighborhood as (
|
|
105
|
+
select
|
|
106
|
+
start_id as memory_id,
|
|
107
|
+
0 as depth,
|
|
108
|
+
array[start_id] as path,
|
|
109
|
+
array[]::text[] as edge_kinds
|
|
110
|
+
union all
|
|
111
|
+
select
|
|
112
|
+
case when r.source_id = n.memory_id then r.target_id else r.source_id end,
|
|
113
|
+
n.depth + 1,
|
|
114
|
+
n.path || (case when r.source_id = n.memory_id then r.target_id else r.source_id end),
|
|
115
|
+
n.edge_kinds || r.relationship_type
|
|
116
|
+
from neighborhood n
|
|
117
|
+
join memory_relationships r
|
|
118
|
+
on (r.source_id = n.memory_id or r.target_id = n.memory_id)
|
|
119
|
+
where n.depth < max_depth
|
|
120
|
+
and not (
|
|
121
|
+
case when r.source_id = n.memory_id then r.target_id else r.source_id end
|
|
122
|
+
= any (n.path)
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
select memory_id, depth, path, edge_kinds from neighborhood;
|
|
126
|
+
$$;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
-- Mnestra v0.3 — memory_recall_graph (Sprint 38 / T3)
|
|
2
|
+
--
|
|
3
|
+
-- Graph-aware recall. Two-stage retrieval:
|
|
4
|
+
--
|
|
5
|
+
-- Stage 1: vector recall via match_memories() — top-K nearest neighbors
|
|
6
|
+
-- in embedding space (existing RPC; tombstone filters inherited).
|
|
7
|
+
-- Stage 2: graph expansion via expand_memory_neighborhood() — for each
|
|
8
|
+
-- stage-1 seed, walk the relationship graph to max_depth.
|
|
9
|
+
--
|
|
10
|
+
-- Re-rank the union of (stage 1 ∪ stage 2) by a combined signal:
|
|
11
|
+
--
|
|
12
|
+
-- final_score = vector_score × edge_weight × recency_score
|
|
13
|
+
--
|
|
14
|
+
-- where:
|
|
15
|
+
-- vector_score = cosine similarity of seed memory to query (0..1)
|
|
16
|
+
-- edge_weight = mean of memory_relationships.weight along the path,
|
|
17
|
+
-- defaulting to 0.5 for edges T2 hasn't classified yet
|
|
18
|
+
-- (the 749 pre-T2 edges all start with weight = NULL).
|
|
19
|
+
-- recency_score = exp(-age_seconds / (30 × 86400)) (30-day half-life)
|
|
20
|
+
--
|
|
21
|
+
-- Initial (depth=0) results are seeded with edge_weight = 1.0 (no path).
|
|
22
|
+
--
|
|
23
|
+
-- Hard dependencies:
|
|
24
|
+
-- • migration 009 must have run (introduces memory_relationships.weight
|
|
25
|
+
-- and the expand_memory_neighborhood function).
|
|
26
|
+
-- • migration 001 (match_memories, memory_items, memory_relationships).
|
|
27
|
+
--
|
|
28
|
+
-- Rerun-safe: CREATE OR REPLACE.
|
|
29
|
+
|
|
30
|
+
create or replace function memory_recall_graph (
|
|
31
|
+
query_embedding vector(1536),
|
|
32
|
+
project_filter text default null,
|
|
33
|
+
max_depth int default 2,
|
|
34
|
+
k int default 10
|
|
35
|
+
)
|
|
36
|
+
returns table (
|
|
37
|
+
memory_id uuid,
|
|
38
|
+
content text,
|
|
39
|
+
project text,
|
|
40
|
+
depth int,
|
|
41
|
+
vector_score float,
|
|
42
|
+
edge_weight float,
|
|
43
|
+
recency_score float,
|
|
44
|
+
final_score float,
|
|
45
|
+
path uuid[]
|
|
46
|
+
)
|
|
47
|
+
language sql stable
|
|
48
|
+
as $$
|
|
49
|
+
with initial as (
|
|
50
|
+
-- Stage 1: top-K vector recall. match_memories already filters
|
|
51
|
+
-- is_active / archived / superseded_by / project, so depth-0 hits
|
|
52
|
+
-- are tombstone-clean. match_threshold=0.0 returns the full top-K
|
|
53
|
+
-- (we rank by combined signal, not by raw similarity threshold).
|
|
54
|
+
select
|
|
55
|
+
mm.id as memory_id,
|
|
56
|
+
mm.content,
|
|
57
|
+
mm.project,
|
|
58
|
+
0 as depth,
|
|
59
|
+
mm.similarity as vector_score,
|
|
60
|
+
mi.created_at,
|
|
61
|
+
array[mm.id] as path
|
|
62
|
+
from match_memories(query_embedding, 0.0, k, project_filter) mm
|
|
63
|
+
join memory_items mi on mi.id = mm.id
|
|
64
|
+
),
|
|
65
|
+
expanded as (
|
|
66
|
+
-- Stage 2: graph expansion. For each stage-1 seed, walk the graph to
|
|
67
|
+
-- max_depth via T1's expand_memory_neighborhood RPC. Carry the seed's
|
|
68
|
+
-- vector_score forward (we do NOT re-embed neighbors — the assumption
|
|
69
|
+
-- is "if A is relevant to the query and B is connected to A, then B
|
|
70
|
+
-- inherits some of A's relevance, attenuated by the path weight").
|
|
71
|
+
select
|
|
72
|
+
n.memory_id,
|
|
73
|
+
mi.content,
|
|
74
|
+
mi.project,
|
|
75
|
+
n.depth,
|
|
76
|
+
i.vector_score,
|
|
77
|
+
mi.created_at,
|
|
78
|
+
n.path,
|
|
79
|
+
coalesce(
|
|
80
|
+
(
|
|
81
|
+
-- Average of edge weights along the path (cycle-safe: T1's CTE
|
|
82
|
+
-- guarantees no repeats). Pairwise lookup over consecutive path
|
|
83
|
+
-- elements, treating the relationship as undirected so A→B and
|
|
84
|
+
-- B→A both count.
|
|
85
|
+
select avg(coalesce(r.weight, 0.5))
|
|
86
|
+
from generate_series(1, array_length(n.path, 1) - 1) as g
|
|
87
|
+
join memory_relationships r
|
|
88
|
+
on (r.source_id = n.path[g] and r.target_id = n.path[g + 1])
|
|
89
|
+
or (r.source_id = n.path[g + 1] and r.target_id = n.path[g])
|
|
90
|
+
),
|
|
91
|
+
0.5
|
|
92
|
+
) as edge_weight
|
|
93
|
+
from initial i
|
|
94
|
+
cross join lateral expand_memory_neighborhood(i.memory_id, max_depth) n
|
|
95
|
+
join memory_items mi
|
|
96
|
+
on mi.id = n.memory_id
|
|
97
|
+
and mi.is_active = true
|
|
98
|
+
and mi.archived = false
|
|
99
|
+
and mi.superseded_by is null
|
|
100
|
+
where n.depth > 0
|
|
101
|
+
and (project_filter is null or mi.project = project_filter)
|
|
102
|
+
),
|
|
103
|
+
unioned as (
|
|
104
|
+
-- depth-0 seeds get edge_weight = 1.0 (no path traversed).
|
|
105
|
+
select memory_id, content, project, depth, vector_score,
|
|
106
|
+
1.0::float as edge_weight, created_at, path
|
|
107
|
+
from initial
|
|
108
|
+
union all
|
|
109
|
+
select memory_id, content, project, depth, vector_score,
|
|
110
|
+
edge_weight, created_at, path
|
|
111
|
+
from expanded
|
|
112
|
+
),
|
|
113
|
+
scored as (
|
|
114
|
+
-- Same memory may be reached via multiple paths (vector seed AND graph
|
|
115
|
+
-- expansion, or two different seeds). Keep the strongest path: highest
|
|
116
|
+
-- final_score wins, with depth-0 (vector hit) preferred on ties.
|
|
117
|
+
select distinct on (memory_id)
|
|
118
|
+
memory_id,
|
|
119
|
+
content,
|
|
120
|
+
project,
|
|
121
|
+
depth,
|
|
122
|
+
vector_score,
|
|
123
|
+
edge_weight,
|
|
124
|
+
exp(-extract(epoch from (now() - created_at))::float / (30.0 * 86400.0))
|
|
125
|
+
as recency_score,
|
|
126
|
+
vector_score * edge_weight
|
|
127
|
+
* exp(-extract(epoch from (now() - created_at))::float / (30.0 * 86400.0))
|
|
128
|
+
as final_score,
|
|
129
|
+
path
|
|
130
|
+
from unioned
|
|
131
|
+
order by memory_id, final_score desc, depth asc
|
|
132
|
+
)
|
|
133
|
+
select
|
|
134
|
+
memory_id, content, project, depth, vector_score,
|
|
135
|
+
edge_weight, recency_score, final_score, path
|
|
136
|
+
from scored
|
|
137
|
+
order by final_score desc
|
|
138
|
+
limit 50;
|
|
139
|
+
$$;
|
|
140
|
+
|
|
141
|
+
-- Lightweight grant convention follows existing match_memories. Service-role
|
|
142
|
+
-- and authenticated callers should already have execute by inheritance, but
|
|
143
|
+
-- be explicit for the new function so PostgREST surfaces it without a manual
|
|
144
|
+
-- dashboard step.
|
|
145
|
+
|
|
146
|
+
grant execute on function memory_recall_graph(vector, text, int, int)
|
|
147
|
+
to authenticated, service_role, anon;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- Sprint 38 T2 — Graph-inference cron schedule.
|
|
2
|
+
-- Schedules the graph-inference Supabase Edge Function to run daily at
|
|
3
|
+
-- 03:00 UTC (≈ 23:00 ET, after typical work hours). The Edge Function
|
|
4
|
+
-- scans memory_items for pairs above GRAPH_INFERENCE_THRESHOLD (default
|
|
5
|
+
-- 0.85 cosine similarity), inserts edges into memory_relationships with
|
|
6
|
+
-- inferred_by = 'cron-YYYY-MM-DD' for audit trail, and optionally
|
|
7
|
+
-- classifies edge types via Haiku 4.5 (gated by GRAPH_LLM_CLASSIFY=1).
|
|
8
|
+
--
|
|
9
|
+
-- Coexists with the existing rag-system MCP-side ingest classifier:
|
|
10
|
+
-- this cron handles backfill + cross-project + stale-edge refresh; the
|
|
11
|
+
-- ingest classifier handles fresh per-memory edges in the 0.75-0.92
|
|
12
|
+
-- window. See Sprint 38 T2 FINDING for the full coexistence design.
|
|
13
|
+
--
|
|
14
|
+
-- Before applying:
|
|
15
|
+
-- 1. The pg_cron and pg_net extensions must already be enabled
|
|
16
|
+
-- (rumen migration 002 enables them; 003 assumes that prereq).
|
|
17
|
+
-- 2. Add a Vault secret named 'graph_inference_service_role_key'
|
|
18
|
+
-- containing the project's service-role JWT. Separate from
|
|
19
|
+
-- 'rumen_service_role_key' so a key rotation on one cron doesn't
|
|
20
|
+
-- affect the other.
|
|
21
|
+
-- 3. Replace <project-ref> below with the project's Supabase ref
|
|
22
|
+
-- (stack-installer substitutes this at apply-time, same as 002).
|
|
23
|
+
--
|
|
24
|
+
-- Apply with:
|
|
25
|
+
-- psql "$DIRECT_URL" -f migrations/003_graph_inference_schedule.sql
|
|
26
|
+
|
|
27
|
+
-- Idempotent unschedule.
|
|
28
|
+
SELECT cron.unschedule('graph-inference-tick')
|
|
29
|
+
WHERE EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'graph-inference-tick');
|
|
30
|
+
|
|
31
|
+
-- Schedule daily at 03:00 UTC.
|
|
32
|
+
SELECT cron.schedule(
|
|
33
|
+
'graph-inference-tick',
|
|
34
|
+
'0 3 * * *',
|
|
35
|
+
$$
|
|
36
|
+
SELECT net.http_post(
|
|
37
|
+
url := 'https://<project-ref>.supabase.co/functions/v1/graph-inference',
|
|
38
|
+
headers := jsonb_build_object(
|
|
39
|
+
'Content-Type', 'application/json',
|
|
40
|
+
'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'graph_inference_service_role_key')
|
|
41
|
+
),
|
|
42
|
+
body := '{}'::jsonb
|
|
43
|
+
);
|
|
44
|
+
$$
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
-- Verify:
|
|
48
|
+
-- SELECT jobname, schedule, active FROM cron.job WHERE jobname = 'graph-inference-tick';
|
|
49
|
+
-- SELECT * FROM cron.job_run_details WHERE jobname = 'graph-inference-tick' ORDER BY start_time DESC LIMIT 5;
|