@jhizzard/termdeck 0.2.0 → 0.2.2

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.
Files changed (28) hide show
  1. package/README.md +191 -169
  2. package/config/config.example.yaml +10 -10
  3. package/package.json +7 -6
  4. package/packages/cli/src/index.js +44 -1
  5. package/packages/cli/src/init-engram.js +344 -0
  6. package/packages/cli/src/init-rumen.js +425 -0
  7. package/packages/client/public/index.html +10 -10
  8. package/packages/server/src/config.js +8 -8
  9. package/packages/server/src/index.js +10 -10
  10. package/packages/server/src/{engram-bridge → mnestra-bridge}/index.js +18 -18
  11. package/packages/server/src/rag.js +6 -6
  12. package/packages/server/src/setup/dotenv-io.js +116 -0
  13. package/packages/server/src/setup/engram-migrations/001_engram_tables.sql +116 -0
  14. package/packages/server/src/setup/engram-migrations/002_engram_search_function.sql +141 -0
  15. package/packages/server/src/setup/engram-migrations/003_engram_event_webhook.sql +28 -0
  16. package/packages/server/src/setup/engram-migrations/004_engram_match_count_cap_and_explain.sql +176 -0
  17. package/packages/server/src/setup/engram-migrations/005_v0_1_to_v0_2_upgrade.sql +23 -0
  18. package/packages/server/src/setup/engram-migrations/006_memory_status_rpc.sql +58 -0
  19. package/packages/server/src/setup/index.js +14 -0
  20. package/packages/server/src/setup/migrations.js +80 -0
  21. package/packages/server/src/setup/pg-runner.js +113 -0
  22. package/packages/server/src/setup/prompts.js +177 -0
  23. package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +85 -0
  24. package/packages/server/src/setup/rumen/functions/rumen-tick/tsconfig.json +14 -0
  25. package/packages/server/src/setup/rumen/migrations/001_rumen_tables.sql +91 -0
  26. package/packages/server/src/setup/rumen/migrations/002_pg_cron_schedule.sql +40 -0
  27. package/packages/server/src/setup/supabase-url.js +114 -0
  28. package/packages/server/src/setup/yaml-io.js +99 -0
@@ -1,7 +1,7 @@
1
- // Engram bridge — routes TermDeck memory queries through one of three backends:
1
+ // Mnestra bridge — routes TermDeck memory queries through one of three backends:
2
2
  // - direct: talk to Supabase + OpenAI from the server (pre-bridge behavior)
3
- // - webhook: POST to Engram's HTTP webhook server (T3.1) at rag.engramWebhookUrl
4
- // - mcp: spawn the @jhizzard/engram binary and talk JSON-RPC over stdio
3
+ // - webhook: POST to Mnestra's HTTP webhook server (T3.1) at rag.mnestraWebhookUrl
4
+ // - mcp: spawn the @jhizzard/mnestra binary and talk JSON-RPC over stdio
5
5
  //
6
6
  // All three modes return the same shape:
7
7
  // { memories: Array<{ content, source_type, project, similarity, created_at }>, total }
@@ -11,7 +11,7 @@
11
11
  const { spawn } = require('child_process');
12
12
 
13
13
  function createBridge(config) {
14
- const mode = config.rag?.engramMode || 'direct';
14
+ const mode = config.rag?.mnestraMode || 'direct';
15
15
  const state = { mcpChild: null, mcpQueue: [], mcpNextId: 1, mcpBuffer: '' };
16
16
 
17
17
  async function queryDirect({ question, project, searchAll }) {
@@ -40,7 +40,7 @@ function createBridge(config) {
40
40
  });
41
41
  if (!embeddingRes.ok) {
42
42
  const err = await embeddingRes.text();
43
- console.error('[engram-bridge:direct] embedding failed:', err);
43
+ console.error('[mnestra-bridge:direct] embedding failed:', err);
44
44
  throw new Error('Embedding generation failed');
45
45
  }
46
46
  const embeddingData = await embeddingRes.json();
@@ -68,7 +68,7 @@ function createBridge(config) {
68
68
  });
69
69
  if (!searchRes.ok) {
70
70
  const err = await searchRes.text();
71
- console.error('[engram-bridge:direct] supabase search failed:', err);
71
+ console.error('[mnestra-bridge:direct] supabase search failed:', err);
72
72
  throw new Error('Memory search failed');
73
73
  }
74
74
  const rows = await searchRes.json();
@@ -85,7 +85,7 @@ function createBridge(config) {
85
85
  }
86
86
 
87
87
  async function queryWebhook({ question, project, searchAll }) {
88
- const url = config.rag?.engramWebhookUrl || 'http://localhost:37778/engram';
88
+ const url = config.rag?.mnestraWebhookUrl || 'http://localhost:37778/mnestra';
89
89
  const res = await fetch(url, {
90
90
  method: 'POST',
91
91
  headers: { 'Content-Type': 'application/json' },
@@ -98,8 +98,8 @@ function createBridge(config) {
98
98
  });
99
99
  if (!res.ok) {
100
100
  const err = await res.text();
101
- console.error('[engram-bridge:webhook] request failed:', err);
102
- throw new Error(`Engram webhook returned ${res.status}`);
101
+ console.error('[mnestra-bridge:webhook] request failed:', err);
102
+ throw new Error(`Mnestra webhook returned ${res.status}`);
103
103
  }
104
104
  const data = await res.json();
105
105
  const rows = data.memories || [];
@@ -118,7 +118,7 @@ function createBridge(config) {
118
118
  function ensureMcpChild() {
119
119
  if (state.mcpChild && !state.mcpChild.killed) return state.mcpChild;
120
120
 
121
- const bin = config.rag?.engramBinary || 'engram';
121
+ const bin = config.rag?.mnestraBinary || 'mnestra';
122
122
  const child = spawn(bin, ['serve', '--stdio'], { stdio: ['pipe', 'pipe', 'pipe'] });
123
123
  state.mcpChild = child;
124
124
  state.mcpBuffer = '';
@@ -135,24 +135,24 @@ function createBridge(config) {
135
135
  const pending = state.mcpQueue.find((p) => p.id === msg.id);
136
136
  if (pending) {
137
137
  state.mcpQueue = state.mcpQueue.filter((p) => p !== pending);
138
- if (msg.error) pending.reject(new Error(msg.error.message || 'Engram MCP error'));
138
+ if (msg.error) pending.reject(new Error(msg.error.message || 'Mnestra MCP error'));
139
139
  else pending.resolve(msg.result);
140
140
  }
141
141
  } catch (err) {
142
- console.error('[engram-bridge:mcp] parse error:', err.message, line);
142
+ console.error('[mnestra-bridge:mcp] parse error:', err.message, line);
143
143
  }
144
144
  }
145
145
  });
146
146
 
147
147
  child.stderr.on('data', (chunk) => {
148
- console.error('[engram-bridge:mcp]', chunk.toString('utf-8').trim());
148
+ console.error('[mnestra-bridge:mcp]', chunk.toString('utf-8').trim());
149
149
  });
150
150
 
151
151
  child.on('exit', (code, signal) => {
152
- console.warn(`[engram-bridge:mcp] child exited (code=${code}, signal=${signal}); will respawn on next call`);
152
+ console.warn(`[mnestra-bridge:mcp] child exited (code=${code}, signal=${signal}); will respawn on next call`);
153
153
  state.mcpChild = null;
154
154
  for (const pending of state.mcpQueue) {
155
- pending.reject(new Error('Engram MCP child exited'));
155
+ pending.reject(new Error('Mnestra MCP child exited'));
156
156
  }
157
157
  state.mcpQueue = [];
158
158
  });
@@ -177,7 +177,7 @@ function createBridge(config) {
177
177
  const pending = state.mcpQueue.find((p) => p.id === id);
178
178
  if (pending) {
179
179
  state.mcpQueue = state.mcpQueue.filter((p) => p !== pending);
180
- pending.reject(new Error('Engram MCP call timed out'));
180
+ pending.reject(new Error('Mnestra MCP call timed out'));
181
181
  }
182
182
  }, 15000);
183
183
  });
@@ -214,7 +214,7 @@ function createBridge(config) {
214
214
  }
215
215
  }
216
216
 
217
- async function queryEngram({ question, project, searchAll }) {
217
+ async function queryMnestra({ question, project, searchAll }) {
218
218
  switch (mode) {
219
219
  case 'webhook':
220
220
  return queryWebhook({ question, project, searchAll });
@@ -226,7 +226,7 @@ function createBridge(config) {
226
226
  }
227
227
  }
228
228
 
229
- return { mode, queryEngram };
229
+ return { mode, queryMnestra };
230
230
  }
231
231
 
232
232
  module.exports = { createBridge };
@@ -16,10 +16,10 @@ class RAGIntegration {
16
16
 
17
17
  // Table configuration matching Josh's multi-layer schema
18
18
  this.tables = {
19
- sessionMemory: config.rag?.tables?.session || 'engram_session_memory',
20
- projectMemory: config.rag?.tables?.project || 'engram_project_memory',
21
- developerMemory: config.rag?.tables?.developer || 'engram_developer_memory',
22
- commandLog: config.rag?.tables?.commands || 'engram_commands'
19
+ sessionMemory: config.rag?.tables?.session || 'mnestra_session_memory',
20
+ projectMemory: config.rag?.tables?.project || 'mnestra_project_memory',
21
+ developerMemory: config.rag?.tables?.developer || 'mnestra_developer_memory',
22
+ commandLog: config.rag?.tables?.commands || 'mnestra_commands'
23
23
  };
24
24
 
25
25
  if (this.enabled) {
@@ -120,7 +120,7 @@ class RAGIntegration {
120
120
  }
121
121
  } catch (err) {
122
122
  // Will be retried by sync loop
123
- console.error('[engram] Push failed:', err.message);
123
+ console.error('[mnestra] Push failed:', err.message);
124
124
  }
125
125
  }
126
126
 
@@ -163,7 +163,7 @@ class RAGIntegration {
163
163
  markRagEventsSynced(this.db, synced);
164
164
  }
165
165
  } catch (err) {
166
- console.error('[engram] Sync cycle error:', err.message);
166
+ console.error('[mnestra] Sync cycle error:', err.message);
167
167
  }
168
168
  }, this.syncInterval);
169
169
  }
@@ -0,0 +1,116 @@
1
+ // Merge-aware reader/writer for ~/.termdeck/secrets.env.
2
+ //
3
+ // The existing config.js module already parses the file at load time, but for
4
+ // the `init` wizards we need to UPDATE the file without clobbering values the
5
+ // user has already set. This helper preserves unknown keys, preserves order
6
+ // of existing keys, and appends new keys at the bottom.
7
+ //
8
+ // File format (same subset as config.js parseDotenv):
9
+ // KEY=value
10
+ // KEY="quoted"
11
+ // KEY='single'
12
+ // # comments are preserved
13
+ // blank lines are preserved
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+
19
+ const SECRETS_PATH = path.join(os.homedir(), '.termdeck', 'secrets.env');
20
+
21
+ function readSecretsRaw(filepath = SECRETS_PATH) {
22
+ if (!fs.existsSync(filepath)) return { exists: false, lines: [], keys: {} };
23
+ const raw = fs.readFileSync(filepath, 'utf-8');
24
+ const lines = raw.split(/\r?\n/);
25
+ const keys = {};
26
+ lines.forEach((line, idx) => {
27
+ const trimmed = line.trim();
28
+ if (!trimmed || trimmed.startsWith('#')) return;
29
+ const eq = trimmed.indexOf('=');
30
+ if (eq === -1) return;
31
+ const key = trimmed.slice(0, eq).trim();
32
+ let val = trimmed.slice(eq + 1).trim();
33
+ if ((val.startsWith('"') && val.endsWith('"')) ||
34
+ (val.startsWith("'") && val.endsWith("'"))) {
35
+ val = val.slice(1, -1);
36
+ }
37
+ keys[key] = { value: val, lineIndex: idx };
38
+ });
39
+ return { exists: true, lines, keys };
40
+ }
41
+
42
+ // Escape a value for safe re-serialization. Wraps in double quotes if the
43
+ // value contains whitespace, `#`, or `"`. Always safe to wrap — we wrap when
44
+ // in doubt to avoid ambiguity with the dotenv parser.
45
+ function formatValue(value) {
46
+ if (value == null) return '';
47
+ const str = String(value);
48
+ if (str === '') return '';
49
+ const needsQuoting = /[\s#"'=]/.test(str);
50
+ if (!needsQuoting) return str;
51
+ const escaped = str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
52
+ return `"${escaped}"`;
53
+ }
54
+
55
+ // Write a merged secrets.env. `updates` is an object of key→value pairs. Pass
56
+ // null/undefined value to delete a key. Existing lines are preserved for keys
57
+ // not listed in `updates`. New keys get appended to the bottom with a header.
58
+ function writeSecrets(updates, filepath = SECRETS_PATH) {
59
+ const dir = path.dirname(filepath);
60
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
61
+
62
+ const { exists, lines, keys } = readSecretsRaw(filepath);
63
+ const originalLines = exists ? lines.slice() : [
64
+ '# TermDeck secrets — loaded on server startup.',
65
+ '# Never commit this file.',
66
+ ''
67
+ ];
68
+ const workingLines = originalLines.slice();
69
+ const toAppend = [];
70
+
71
+ for (const [key, rawVal] of Object.entries(updates)) {
72
+ if (rawVal == null || rawVal === '') {
73
+ // Delete existing line (by overwriting with blank). Safer to mark for
74
+ // deletion than splice (splice shifts indices of subsequent keys).
75
+ if (keys[key]) workingLines[keys[key].lineIndex] = '';
76
+ continue;
77
+ }
78
+ const formatted = `${key}=${formatValue(rawVal)}`;
79
+ if (keys[key]) {
80
+ workingLines[keys[key].lineIndex] = formatted;
81
+ } else {
82
+ toAppend.push(formatted);
83
+ }
84
+ }
85
+
86
+ let out = workingLines.join('\n');
87
+ if (toAppend.length > 0) {
88
+ if (out.length > 0 && !out.endsWith('\n')) out += '\n';
89
+ // Add a header for the first append-pass on an empty file.
90
+ if (!exists) out = originalLines.join('\n') + toAppend.join('\n') + '\n';
91
+ else out = out.replace(/\n+$/, '') + '\n' + toAppend.join('\n') + '\n';
92
+ } else if (!out.endsWith('\n')) {
93
+ out += '\n';
94
+ }
95
+
96
+ // chmod 600 for secrets — owner read/write only.
97
+ fs.writeFileSync(filepath, out, { encoding: 'utf-8', mode: 0o600 });
98
+ try { fs.chmodSync(filepath, 0o600); } catch (_err) { /* best-effort */ }
99
+
100
+ return { path: filepath, wrote: Object.keys(updates).length, appended: toAppend.length };
101
+ }
102
+
103
+ // Convenience read — returns `{ SUPABASE_URL, ... }` object, no metadata.
104
+ function readSecrets(filepath = SECRETS_PATH) {
105
+ const { keys } = readSecretsRaw(filepath);
106
+ const out = {};
107
+ for (const [k, v] of Object.entries(keys)) out[k] = v.value;
108
+ return out;
109
+ }
110
+
111
+ module.exports = {
112
+ SECRETS_PATH,
113
+ readSecrets,
114
+ writeSecrets,
115
+ _formatValue: formatValue
116
+ };
@@ -0,0 +1,116 @@
1
+ -- Engram v0.1 — core tables
2
+ --
3
+ -- Run against a Postgres 15+ database that has pgvector installed
4
+ -- (Supabase already ships with it). Apply in order:
5
+ -- 001_engram_tables.sql
6
+ -- 002_engram_search_function.sql
7
+ -- 003_engram_event_webhook.sql
8
+
9
+ create extension if not exists "vector";
10
+ create extension if not exists "pg_trgm";
11
+ create extension if not exists "pgcrypto";
12
+
13
+ -- ── memory_items ──────────────────────────────────────────────────────────
14
+
15
+ create table if not exists memory_items (
16
+ id uuid primary key default gen_random_uuid(),
17
+ content text not null,
18
+ embedding vector(1536),
19
+ source_type text not null default 'fact',
20
+ category text,
21
+ project text not null default 'global',
22
+ metadata jsonb not null default '{}'::jsonb,
23
+ is_active boolean not null default true,
24
+ archived boolean not null default false,
25
+ superseded_by uuid references memory_items(id) on delete set null,
26
+ created_at timestamptz not null default now(),
27
+ updated_at timestamptz not null default now()
28
+ );
29
+
30
+ create index if not exists memory_items_project_idx
31
+ on memory_items(project) where is_active = true and archived = false;
32
+
33
+ create index if not exists memory_items_source_type_idx
34
+ on memory_items(source_type) where is_active = true and archived = false;
35
+
36
+ create index if not exists memory_items_created_at_idx
37
+ on memory_items(created_at desc);
38
+
39
+ create index if not exists memory_items_content_trgm_idx
40
+ on memory_items using gin (content gin_trgm_ops);
41
+
42
+ -- HNSW vector index. If your Postgres/pgvector is too old for HNSW,
43
+ -- swap to ivfflat:
44
+ -- create index memory_items_embedding_idx on memory_items
45
+ -- using ivfflat (embedding vector_cosine_ops) with (lists = 100);
46
+ create index if not exists memory_items_embedding_hnsw_idx
47
+ on memory_items using hnsw (embedding vector_cosine_ops)
48
+ with (m = 16, ef_construction = 64);
49
+
50
+ -- ── memory_sessions ───────────────────────────────────────────────────────
51
+
52
+ create table if not exists memory_sessions (
53
+ id uuid primary key default gen_random_uuid(),
54
+ project text not null default 'global',
55
+ summary text,
56
+ metadata jsonb not null default '{}'::jsonb,
57
+ created_at timestamptz not null default now()
58
+ );
59
+
60
+ create index if not exists memory_sessions_project_idx on memory_sessions(project);
61
+ create index if not exists memory_sessions_created_at_idx on memory_sessions(created_at desc);
62
+
63
+ -- ── memory_relationships ──────────────────────────────────────────────────
64
+
65
+ create table if not exists memory_relationships (
66
+ id uuid primary key default gen_random_uuid(),
67
+ source_id uuid not null references memory_items(id) on delete cascade,
68
+ target_id uuid not null references memory_items(id) on delete cascade,
69
+ relationship_type text not null,
70
+ created_at timestamptz not null default now(),
71
+ unique (source_id, target_id, relationship_type),
72
+ check (source_id <> target_id),
73
+ check (relationship_type in ('supersedes','relates_to','contradicts','elaborates','caused_by'))
74
+ );
75
+
76
+ create index if not exists memory_relationships_source_idx on memory_relationships(source_id);
77
+ create index if not exists memory_relationships_target_idx on memory_relationships(target_id);
78
+
79
+ -- ── match_memories helper RPC ─────────────────────────────────────────────
80
+ -- Used by remember.ts (dedup) and consolidate.ts (cluster seeding).
81
+
82
+ create or replace function match_memories (
83
+ query_embedding vector(1536),
84
+ match_threshold float,
85
+ match_count int,
86
+ filter_project text default null
87
+ )
88
+ returns table (
89
+ id uuid,
90
+ content text,
91
+ source_type text,
92
+ category text,
93
+ project text,
94
+ metadata jsonb,
95
+ similarity float
96
+ )
97
+ language sql stable
98
+ as $$
99
+ select
100
+ m.id,
101
+ m.content,
102
+ m.source_type,
103
+ m.category,
104
+ m.project,
105
+ m.metadata,
106
+ 1 - (m.embedding <=> query_embedding) as similarity
107
+ from memory_items m
108
+ where m.is_active = true
109
+ and m.archived = false
110
+ and m.superseded_by is null
111
+ and m.embedding is not null
112
+ and (filter_project is null or m.project = filter_project)
113
+ and 1 - (m.embedding <=> query_embedding) >= match_threshold
114
+ order by m.embedding <=> query_embedding asc
115
+ limit match_count;
116
+ $$;
@@ -0,0 +1,141 @@
1
+ -- Engram v0.1 — memory_hybrid_search
2
+ --
3
+ -- Reciprocal rank fusion over full-text and semantic search, with the
4
+ -- three SQL-side fixes from RAG-MEMORY-IMPROVEMENTS-AND-TERMDECK-STRATEGY.md:
5
+ --
6
+ -- Fix 1 — Tiered recency decay by source_type. Architectural decisions
7
+ -- decay on a one-year half-life; bug fixes on a 30-day half-life;
8
+ -- session summaries and document chunks on a 14-day half-life.
9
+ --
10
+ -- Fix 3 — Source_type weighting. Decisions and architecture outrank
11
+ -- raw document chunks in the final fused score.
12
+ --
13
+ -- Fix 5 — Project affinity scoring. Exact project match multiplies the
14
+ -- score by 1.5x; mismatches are penalised 0.7x.
15
+
16
+ create or replace function memory_hybrid_search (
17
+ query_text text,
18
+ query_embedding vector(1536),
19
+ match_count int default 20,
20
+ full_text_weight float default 1.0,
21
+ semantic_weight float default 1.0,
22
+ rrf_k int default 60,
23
+ filter_project text default null,
24
+ filter_source_type text default null
25
+ )
26
+ returns table (
27
+ id uuid,
28
+ content text,
29
+ source_type text,
30
+ category text,
31
+ project text,
32
+ metadata jsonb,
33
+ score float,
34
+ created_at timestamptz
35
+ )
36
+ language sql stable
37
+ as $$
38
+ with candidates as (
39
+ select
40
+ m.id,
41
+ m.content,
42
+ m.source_type,
43
+ m.category,
44
+ m.project,
45
+ m.metadata,
46
+ m.created_at,
47
+ m.embedding,
48
+ ts_rank_cd(to_tsvector('english', m.content), plainto_tsquery('english', query_text))
49
+ as ft_rank,
50
+ 1 - (m.embedding <=> query_embedding) as sem_rank,
51
+ extract(epoch from (now() - m.created_at))::float as age_seconds
52
+ from memory_items m
53
+ where m.is_active = true
54
+ and m.archived = false
55
+ and m.superseded_by is null
56
+ and m.embedding is not null
57
+ and (filter_project is null or m.project = filter_project)
58
+ and (filter_source_type is null or m.source_type = filter_source_type)
59
+ ),
60
+ ft_ranked as (
61
+ select id, row_number() over (order by ft_rank desc nulls last) as rank
62
+ from candidates where ft_rank > 0
63
+ ),
64
+ sem_ranked as (
65
+ select id, row_number() over (order by sem_rank desc nulls last) as rank
66
+ from candidates
67
+ ),
68
+ fused as (
69
+ select
70
+ c.id,
71
+ c.content,
72
+ c.source_type,
73
+ c.category,
74
+ c.project,
75
+ c.metadata,
76
+ c.created_at,
77
+ c.age_seconds,
78
+ -- RRF base score
79
+ coalesce(full_text_weight / (rrf_k + ft.rank), 0.0) +
80
+ coalesce(semantic_weight / (rrf_k + sr.rank), 0.0) as base_score
81
+ from candidates c
82
+ left join ft_ranked ft on ft.id = c.id
83
+ left join sem_ranked sr on sr.id = c.id
84
+ ),
85
+ scored as (
86
+ select
87
+ f.id,
88
+ f.content,
89
+ f.source_type,
90
+ f.category,
91
+ f.project,
92
+ f.metadata,
93
+ f.created_at,
94
+ f.base_score
95
+ -- Fix 1: tiered recency decay by source_type
96
+ * case f.source_type
97
+ when 'decision' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
98
+ when 'architecture' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
99
+ when 'preference' then 1.0 / (1.0 + f.age_seconds / (365.0 * 86400.0))
100
+ when 'fact' then 1.0 / (1.0 + f.age_seconds / ( 90.0 * 86400.0))
101
+ when 'convention' then 1.0 / (1.0 + f.age_seconds / ( 90.0 * 86400.0))
102
+ when 'bug_fix' then 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
103
+ when 'debugging' then 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
104
+ when 'session_summary' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
105
+ when 'document_chunk' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
106
+ when 'code_context' then 1.0 / (1.0 + f.age_seconds / ( 14.0 * 86400.0))
107
+ else 1.0 / (1.0 + f.age_seconds / ( 30.0 * 86400.0))
108
+ end
109
+ -- Fix 3: source_type weighting
110
+ * case f.source_type
111
+ when 'decision' then 1.5
112
+ when 'architecture' then 1.4
113
+ when 'bug_fix' then 1.3
114
+ when 'preference' then 1.2
115
+ when 'fact' then 1.0
116
+ when 'document_chunk' then 0.6
117
+ else 1.0
118
+ end
119
+ -- Fix 5: project affinity scoring
120
+ * case
121
+ when filter_project is null then 1.0
122
+ when f.project = filter_project then 1.5
123
+ when f.project = 'global' then 1.0
124
+ else 0.7
125
+ end
126
+ as score
127
+ from fused f
128
+ )
129
+ select
130
+ s.id,
131
+ s.content,
132
+ s.source_type,
133
+ s.category,
134
+ s.project,
135
+ s.metadata,
136
+ s.score,
137
+ s.created_at
138
+ from scored s
139
+ order by s.score desc
140
+ limit match_count;
141
+ $$;
@@ -0,0 +1,28 @@
1
+ -- Engram v0.1 — real-time event webhook (Fix 6)
2
+ --
3
+ -- Fix 6 from RAG-MEMORY-IMPROVEMENTS-AND-TERMDECK-STRATEGY.md is a
4
+ -- real-time event intake path so TermDeck (or any other client) can
5
+ -- POST terminal events — "server started on :8080", "tests failing",
6
+ -- "error detected" — and have them land in memory immediately.
7
+ --
8
+ -- That intake is implemented as an HTTP endpoint inside the Engram MCP
9
+ -- server process, not as a SQL trigger. This file exists as a placeholder
10
+ -- so the migration history is explicit and future database-side changes
11
+ -- (e.g. an events queue table for async ingestion) have a home.
12
+ --
13
+ -- If you want to add a durable event queue later, add it below this line.
14
+
15
+ -- Example future shape (commented out — not applied):
16
+ --
17
+ -- create table if not exists memory_events (
18
+ -- id uuid primary key default gen_random_uuid(),
19
+ -- project text not null,
20
+ -- source text not null,
21
+ -- event_type text not null,
22
+ -- payload jsonb not null default '{}'::jsonb,
23
+ -- processed boolean not null default false,
24
+ -- created_at timestamptz not null default now()
25
+ -- );
26
+ --
27
+ -- create index if not exists memory_events_unprocessed_idx
28
+ -- on memory_events(created_at) where processed = false;