praana 0.5.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.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/praana.js +17 -0
  4. package/bin/pran.js +17 -0
  5. package/dist/app-banner.d.ts +11 -0
  6. package/dist/app-banner.js +161 -0
  7. package/dist/app-controller.d.ts +44 -0
  8. package/dist/app-controller.js +143 -0
  9. package/dist/app-identity.d.ts +18 -0
  10. package/dist/app-identity.js +52 -0
  11. package/dist/auto-compact.d.ts +16 -0
  12. package/dist/auto-compact.js +101 -0
  13. package/dist/cli-args.d.ts +14 -0
  14. package/dist/cli-args.js +69 -0
  15. package/dist/compile-classic.d.ts +21 -0
  16. package/dist/compile-classic.js +106 -0
  17. package/dist/compiler.d.ts +75 -0
  18. package/dist/compiler.js +406 -0
  19. package/dist/config.d.ts +3 -0
  20. package/dist/config.js +433 -0
  21. package/dist/context-engine/activity-log.d.ts +9 -0
  22. package/dist/context-engine/activity-log.js +109 -0
  23. package/dist/context-engine/artifact-store.d.ts +32 -0
  24. package/dist/context-engine/artifact-store.js +272 -0
  25. package/dist/context-engine/bm25.d.ts +3 -0
  26. package/dist/context-engine/bm25.js +32 -0
  27. package/dist/context-engine/checkpoint.d.ts +34 -0
  28. package/dist/context-engine/checkpoint.js +430 -0
  29. package/dist/context-engine/classify.d.ts +3 -0
  30. package/dist/context-engine/classify.js +60 -0
  31. package/dist/context-engine/db.d.ts +73 -0
  32. package/dist/context-engine/db.js +505 -0
  33. package/dist/context-engine/distiller.d.ts +30 -0
  34. package/dist/context-engine/distiller.js +67 -0
  35. package/dist/context-engine/engine-compiler.d.ts +23 -0
  36. package/dist/context-engine/engine-compiler.js +297 -0
  37. package/dist/context-engine/error-tracker.d.ts +21 -0
  38. package/dist/context-engine/error-tracker.js +74 -0
  39. package/dist/context-engine/event-lineage.d.ts +26 -0
  40. package/dist/context-engine/event-lineage.js +120 -0
  41. package/dist/context-engine/extraction.d.ts +26 -0
  42. package/dist/context-engine/extraction.js +83 -0
  43. package/dist/context-engine/index.d.ts +82 -0
  44. package/dist/context-engine/index.js +238 -0
  45. package/dist/context-engine/scoring.d.ts +13 -0
  46. package/dist/context-engine/scoring.js +47 -0
  47. package/dist/context-engine/state-snapshot.d.ts +8 -0
  48. package/dist/context-engine/state-snapshot.js +50 -0
  49. package/dist/context-engine/summarize.d.ts +6 -0
  50. package/dist/context-engine/summarize.js +32 -0
  51. package/dist/context-engine/telemetry.d.ts +25 -0
  52. package/dist/context-engine/telemetry.js +64 -0
  53. package/dist/context-engine/turn-digest.d.ts +50 -0
  54. package/dist/context-engine/turn-digest.js +250 -0
  55. package/dist/context-engine/turn-ledger.d.ts +18 -0
  56. package/dist/context-engine/turn-ledger.js +184 -0
  57. package/dist/context-engine/turn-recorder.d.ts +24 -0
  58. package/dist/context-engine/turn-recorder.js +88 -0
  59. package/dist/context-engine/types.d.ts +201 -0
  60. package/dist/context-engine/types.js +4 -0
  61. package/dist/context-pressure.d.ts +19 -0
  62. package/dist/context-pressure.js +36 -0
  63. package/dist/distillers/generic.d.ts +14 -0
  64. package/dist/distillers/generic.js +93 -0
  65. package/dist/distillers/git-diff.d.ts +8 -0
  66. package/dist/distillers/git-diff.js +119 -0
  67. package/dist/distillers/index.d.ts +2 -0
  68. package/dist/distillers/index.js +16 -0
  69. package/dist/distillers/npm-test.d.ts +8 -0
  70. package/dist/distillers/npm-test.js +50 -0
  71. package/dist/distillers/rg-results.d.ts +8 -0
  72. package/dist/distillers/rg-results.js +28 -0
  73. package/dist/distillers/tsc-errors.d.ts +8 -0
  74. package/dist/distillers/tsc-errors.js +52 -0
  75. package/dist/event-log.d.ts +56 -0
  76. package/dist/event-log.js +214 -0
  77. package/dist/llm.d.ts +29 -0
  78. package/dist/llm.js +155 -0
  79. package/dist/logger.d.ts +94 -0
  80. package/dist/logger.js +287 -0
  81. package/dist/main.d.ts +1 -0
  82. package/dist/main.js +54 -0
  83. package/dist/memory/confidence.d.ts +7 -0
  84. package/dist/memory/confidence.js +37 -0
  85. package/dist/memory/consolidation.d.ts +26 -0
  86. package/dist/memory/consolidation.js +166 -0
  87. package/dist/memory/db.d.ts +40 -0
  88. package/dist/memory/db.js +283 -0
  89. package/dist/memory/dedup.d.ts +6 -0
  90. package/dist/memory/dedup.js +50 -0
  91. package/dist/memory/embedder-factory.d.ts +3 -0
  92. package/dist/memory/embedder-factory.js +81 -0
  93. package/dist/memory/embeddings.d.ts +15 -0
  94. package/dist/memory/embeddings.js +67 -0
  95. package/dist/memory/index.d.ts +9 -0
  96. package/dist/memory/index.js +11 -0
  97. package/dist/memory/ollama-summarizer.d.ts +19 -0
  98. package/dist/memory/ollama-summarizer.js +72 -0
  99. package/dist/memory/openai-summarizer.d.ts +21 -0
  100. package/dist/memory/openai-summarizer.js +51 -0
  101. package/dist/memory/store.d.ts +61 -0
  102. package/dist/memory/store.js +502 -0
  103. package/dist/memory/summarizer-factory.d.ts +3 -0
  104. package/dist/memory/summarizer-factory.js +69 -0
  105. package/dist/memory/summarizer.d.ts +4 -0
  106. package/dist/memory/summarizer.js +112 -0
  107. package/dist/memory/types.d.ts +87 -0
  108. package/dist/memory/types.js +17 -0
  109. package/dist/model-context.d.ts +15 -0
  110. package/dist/model-context.js +212 -0
  111. package/dist/project-detector.d.ts +37 -0
  112. package/dist/project-detector.js +604 -0
  113. package/dist/render.d.ts +15 -0
  114. package/dist/render.js +46 -0
  115. package/dist/session.d.ts +118 -0
  116. package/dist/session.js +809 -0
  117. package/dist/skills/index.d.ts +69 -0
  118. package/dist/skills/index.js +885 -0
  119. package/dist/skills/types.d.ts +93 -0
  120. package/dist/skills/types.js +8 -0
  121. package/dist/slash-commands.d.ts +14 -0
  122. package/dist/slash-commands.js +301 -0
  123. package/dist/state-graph.d.ts +38 -0
  124. package/dist/state-graph.js +255 -0
  125. package/dist/status-bar.d.ts +54 -0
  126. package/dist/status-bar.js +184 -0
  127. package/dist/thinking-display.d.ts +21 -0
  128. package/dist/thinking-display.js +37 -0
  129. package/dist/tool-summary.d.ts +4 -0
  130. package/dist/tool-summary.js +67 -0
  131. package/dist/tools/index.d.ts +925 -0
  132. package/dist/tools/index.js +86 -0
  133. package/dist/tools/knowledge.d.ts +140 -0
  134. package/dist/tools/knowledge.js +260 -0
  135. package/dist/tools/memory.d.ts +39 -0
  136. package/dist/tools/memory.js +300 -0
  137. package/dist/tools/search-code.d.ts +134 -0
  138. package/dist/tools/search-code.js +390 -0
  139. package/dist/tools/system.d.ts +16 -0
  140. package/dist/tools/system.js +499 -0
  141. package/dist/tools/tool-def.d.ts +6 -0
  142. package/dist/tools/tool-def.js +3 -0
  143. package/dist/turn-control.d.ts +51 -0
  144. package/dist/turn-control.js +210 -0
  145. package/dist/turn.d.ts +20 -0
  146. package/dist/turn.js +624 -0
  147. package/dist/types.d.ts +233 -0
  148. package/dist/types.js +4 -0
  149. package/dist/ui/readline-ui.d.ts +2 -0
  150. package/dist/ui/readline-ui.js +176 -0
  151. package/dist/ui/tui/app.d.ts +13 -0
  152. package/dist/ui/tui/app.js +270 -0
  153. package/dist/ui/tui/busy-indicator.d.ts +2 -0
  154. package/dist/ui/tui/busy-indicator.js +13 -0
  155. package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
  156. package/dist/ui/tui/components/gutter-rule.js +9 -0
  157. package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
  158. package/dist/ui/tui/components/inline-tool-row.js +8 -0
  159. package/dist/ui/tui/components/prompt-input.d.ts +20 -0
  160. package/dist/ui/tui/components/prompt-input.js +120 -0
  161. package/dist/ui/tui/components/system-line.d.ts +5 -0
  162. package/dist/ui/tui/components/system-line.js +6 -0
  163. package/dist/ui/tui/components/thinking-block.d.ts +11 -0
  164. package/dist/ui/tui/components/thinking-block.js +31 -0
  165. package/dist/ui/tui/components/toast-line.d.ts +4 -0
  166. package/dist/ui/tui/components/toast-line.js +8 -0
  167. package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
  168. package/dist/ui/tui/components/tool-result-line.js +6 -0
  169. package/dist/ui/tui/components/turn-footer.d.ts +5 -0
  170. package/dist/ui/tui/components/turn-footer.js +7 -0
  171. package/dist/ui/tui/components/user-block.d.ts +6 -0
  172. package/dist/ui/tui/components/user-block.js +6 -0
  173. package/dist/ui/tui/logo-banner.d.ts +5 -0
  174. package/dist/ui/tui/logo-banner.js +8 -0
  175. package/dist/ui/tui/markdown-render.d.ts +16 -0
  176. package/dist/ui/tui/markdown-render.js +218 -0
  177. package/dist/ui/tui/palette.d.ts +12 -0
  178. package/dist/ui/tui/palette.js +13 -0
  179. package/dist/ui/tui/reasoning-summary.d.ts +12 -0
  180. package/dist/ui/tui/reasoning-summary.js +27 -0
  181. package/dist/ui/tui/reducer.d.ts +92 -0
  182. package/dist/ui/tui/reducer.js +260 -0
  183. package/dist/ui/tui/run.d.ts +3 -0
  184. package/dist/ui/tui/run.js +40 -0
  185. package/dist/ui/tui/sink.d.ts +4 -0
  186. package/dist/ui/tui/sink.js +89 -0
  187. package/dist/ui/tui/status-bar-view.d.ts +5 -0
  188. package/dist/ui/tui/status-bar-view.js +44 -0
  189. package/dist/ui/tui/terminal-height.d.ts +12 -0
  190. package/dist/ui/tui/terminal-height.js +20 -0
  191. package/dist/ui/tui/terminal-width.d.ts +2 -0
  192. package/dist/ui/tui/terminal-width.js +5 -0
  193. package/dist/ui/tui/tool-display.d.ts +23 -0
  194. package/dist/ui/tui/tool-display.js +217 -0
  195. package/dist/ui/tui/transcript-line.d.ts +12 -0
  196. package/dist/ui/tui/transcript-line.js +43 -0
  197. package/dist/ui/tui/transcript-replay.d.ts +12 -0
  198. package/dist/ui/tui/transcript-replay.js +117 -0
  199. package/dist/ui-events.d.ts +39 -0
  200. package/dist/ui-events.js +33 -0
  201. package/dist/ui.d.ts +77 -0
  202. package/dist/ui.js +179 -0
  203. package/package.json +73 -0
  204. package/praana.config.example.toml +231 -0
@@ -0,0 +1,283 @@
1
+ // ============================================================
2
+ // ARIA Memory — SQLite Schema
3
+ // ============================================================
4
+ import Database from "better-sqlite3";
5
+ import * as sqliteVec from "sqlite-vec";
6
+ import { EMBEDDING_DIM } from "./embeddings.js";
7
+ const REEMBED_NEEDED_KEY = "reembed_needed";
8
+ const BASE_SCHEMA = `
9
+ CREATE TABLE IF NOT EXISTS entries (
10
+ id TEXT PRIMARY KEY,
11
+ kind TEXT NOT NULL,
12
+ content TEXT NOT NULL,
13
+ confidence REAL NOT NULL DEFAULT 0.5,
14
+ pinned INTEGER NOT NULL DEFAULT 0,
15
+ created_at INTEGER NOT NULL,
16
+ last_seen_at INTEGER NOT NULL,
17
+ session_id TEXT NOT NULL
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS entry_scopes (
21
+ entry_id TEXT NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
22
+ scope TEXT NOT NULL,
23
+ PRIMARY KEY (entry_id, scope)
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS sessions (
27
+ id TEXT PRIMARY KEY,
28
+ agent TEXT NOT NULL,
29
+ user_id TEXT NOT NULL,
30
+ context_id TEXT NOT NULL,
31
+ started_at INTEGER NOT NULL,
32
+ ended_at INTEGER,
33
+ reason TEXT
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS memory_meta (
37
+ key TEXT PRIMARY KEY,
38
+ value TEXT NOT NULL
39
+ );
40
+
41
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts
42
+ USING fts5(content, entry_id UNINDEXED);
43
+
44
+ CREATE TABLE IF NOT EXISTS pending_reinforcements (
45
+ entry_id TEXT NOT NULL,
46
+ session_id TEXT NOT NULL,
47
+ ts INTEGER NOT NULL,
48
+ PRIMARY KEY (entry_id, session_id)
49
+ );
50
+
51
+ CREATE INDEX IF NOT EXISTS idx_entries_kind ON entries(kind);
52
+ CREATE INDEX IF NOT EXISTS idx_entries_pinned ON entries(pinned);
53
+ CREATE INDEX IF NOT EXISTS idx_entries_last_seen ON entries(last_seen_at);
54
+ CREATE INDEX IF NOT EXISTS idx_scopes_scope ON entry_scopes(scope);
55
+ `;
56
+ export function openMemoryDb(path, embeddingDim = EMBEDDING_DIM) {
57
+ const db = new Database(path);
58
+ sqliteVec.load(db);
59
+ db.pragma("journal_mode = WAL");
60
+ db.pragma("foreign_keys = ON");
61
+ db.exec(BASE_SCHEMA);
62
+ ensureLayerColumns(db);
63
+ ensureFtsBackfill(db);
64
+ const needsReembed = ensureVectorTable(db, embeddingDim);
65
+ return { db, needsReembed };
66
+ }
67
+ function ensureLayerColumns(db) {
68
+ const columns = db
69
+ .prepare("PRAGMA table_info(entries)")
70
+ .all();
71
+ const names = new Set(columns.map((c) => c.name));
72
+ if (!names.has("layer")) {
73
+ db.exec("ALTER TABLE entries ADD COLUMN layer INTEGER NOT NULL DEFAULT 1");
74
+ }
75
+ if (!names.has("confirmation_count")) {
76
+ db.exec("ALTER TABLE entries ADD COLUMN confirmation_count INTEGER NOT NULL DEFAULT 0");
77
+ }
78
+ if (!names.has("retracted")) {
79
+ db.exec("ALTER TABLE entries ADD COLUMN retracted INTEGER NOT NULL DEFAULT 0");
80
+ }
81
+ }
82
+ function ensureFtsBackfill(db) {
83
+ db.exec(`
84
+ DELETE FROM entries_fts
85
+ WHERE entry_id NOT IN (SELECT id FROM entries);
86
+
87
+ INSERT INTO entries_fts (content, entry_id)
88
+ SELECT e.content, e.id
89
+ FROM entries e
90
+ WHERE NOT EXISTS (
91
+ SELECT 1 FROM entries_fts f WHERE f.entry_id = e.id
92
+ );
93
+ `);
94
+ }
95
+ function ensureVectorTable(db, dim) {
96
+ const row = db
97
+ .prepare("SELECT value FROM memory_meta WHERE key = 'embedding_dim'")
98
+ .get();
99
+ const storedDim = row ? parseInt(row.value, 10) : EMBEDDING_DIM;
100
+ const reembedFlag = db
101
+ .prepare("SELECT value FROM memory_meta WHERE key = ?")
102
+ .get(REEMBED_NEEDED_KEY);
103
+ const vecExists = db
104
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='entries_vec'")
105
+ .get();
106
+ if (vecExists && storedDim === dim) {
107
+ return reembedFlag?.value === "1";
108
+ }
109
+ if (vecExists) {
110
+ markReembedNeeded(db);
111
+ db.exec("DROP TABLE IF EXISTS entries_vec");
112
+ }
113
+ db.exec(`
114
+ CREATE VIRTUAL TABLE entries_vec USING vec0(
115
+ entry_id TEXT PRIMARY KEY,
116
+ embedding float[${dim}]
117
+ )
118
+ `);
119
+ db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES ('embedding_dim', ?)").run(String(dim));
120
+ return Boolean(vecExists) || reembedFlag?.value === "1";
121
+ }
122
+ export function markReembedNeeded(db) {
123
+ db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, '1')").run(REEMBED_NEEDED_KEY);
124
+ }
125
+ export function clearReembedNeeded(db) {
126
+ db.prepare("DELETE FROM memory_meta WHERE key = ?").run(REEMBED_NEEDED_KEY);
127
+ }
128
+ // ---- Entry CRUD ----
129
+ export function insertEntry(db, e) {
130
+ const stmt = db.prepare(`INSERT INTO entries (id, kind, content, confidence, pinned, layer, confirmation_count, created_at, last_seen_at, session_id)
131
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
132
+ stmt.run(e.id, e.kind, e.content, e.confidence, e.pinned ? 1 : 0, e.layer, e.confirmation_count, e.created_at, e.last_seen_at, e.session_id);
133
+ const scopeStmt = db.prepare(`INSERT OR IGNORE INTO entry_scopes (entry_id, scope) VALUES (?, ?)`);
134
+ for (const scope of e.scopes) {
135
+ scopeStmt.run(e.id, scope);
136
+ }
137
+ db.prepare("INSERT INTO entries_fts (content, entry_id) VALUES (?, ?)").run(e.content, e.id);
138
+ }
139
+ export function touchEntry(db, id, now) {
140
+ db.prepare("UPDATE entries SET last_seen_at = ? WHERE id = ?").run(now, id);
141
+ }
142
+ export function reinforceEntry(db, id, alpha = 0.15) {
143
+ db.prepare(`
144
+ UPDATE entries
145
+ SET confidence = MIN(1.0, confidence + (1.0 - confidence) * ?), last_seen_at = ?
146
+ WHERE id = ?
147
+ `).run(alpha, Date.now(), id);
148
+ }
149
+ // TODO: wire into a scheduled confidence-decay pass (not yet called from production code).
150
+ export function weakenEntry(db, id, beta = 0.3) {
151
+ db.prepare(`UPDATE entries SET confidence = confidence * (1.0 - ?) WHERE id = ?`).run(beta, id);
152
+ }
153
+ export function stampReinforcement(db, entryId, sessionId) {
154
+ db.prepare(`
155
+ INSERT OR IGNORE INTO pending_reinforcements (entry_id, session_id, ts)
156
+ VALUES (?, ?, ?)
157
+ `).run(entryId, sessionId, Date.now());
158
+ }
159
+ export function flushReinforcements(db, sessionId) {
160
+ const rows = db
161
+ .prepare("SELECT entry_id FROM pending_reinforcements WHERE session_id = ?")
162
+ .all(sessionId);
163
+ for (const { entry_id } of rows) {
164
+ reinforceEntry(db, entry_id);
165
+ }
166
+ db.prepare("DELETE FROM pending_reinforcements WHERE session_id = ?").run(sessionId);
167
+ }
168
+ export function getEntryById(db, id) {
169
+ const row = db.prepare("SELECT * FROM entries WHERE id = ?").get(id);
170
+ if (!row)
171
+ return undefined;
172
+ return rowToEntry(db, row);
173
+ }
174
+ export function getAllEntries(db) {
175
+ const rows = db
176
+ .prepare("SELECT * FROM entries ORDER BY created_at DESC")
177
+ .all();
178
+ return rows.map((r) => rowToEntry(db, r));
179
+ }
180
+ export function getEntriesByScope(db, scopes) {
181
+ const placeholders = scopes.map(() => "?").join(",");
182
+ const sql = `
183
+ SELECT e.* FROM entries e
184
+ JOIN entry_scopes es ON e.id = es.entry_id
185
+ WHERE es.scope IN (${placeholders})
186
+ GROUP BY e.id
187
+ HAVING COUNT(DISTINCT es.scope) = ${scopes.length}
188
+ ORDER BY e.last_seen_at DESC
189
+ `;
190
+ const rows = db.prepare(sql).all(...scopes);
191
+ return rows.map((r) => rowToEntry(db, r));
192
+ }
193
+ export function deleteEntry(db, id) {
194
+ db.prepare("DELETE FROM entries WHERE id = ?").run(id);
195
+ db.prepare("DELETE FROM entries_vec WHERE entry_id = ?").run(id);
196
+ db.prepare("DELETE FROM entries_fts WHERE entry_id = ?").run(id);
197
+ }
198
+ export function retractMemory(db, id) {
199
+ db.prepare("UPDATE entries SET retracted = 1 WHERE id = ?").run(id);
200
+ }
201
+ function rowToEntry(db, row) {
202
+ const scopes = db
203
+ .prepare("SELECT scope FROM entry_scopes WHERE entry_id = ?")
204
+ .all(row.id);
205
+ return {
206
+ id: row.id,
207
+ kind: row.kind,
208
+ content: row.content,
209
+ confidence: row.confidence,
210
+ pinned: row.pinned === 1,
211
+ layer: row.layer === 2 ? 2 : 1,
212
+ confirmation_count: row.confirmation_count ?? 0,
213
+ created_at: row.created_at,
214
+ last_seen_at: row.last_seen_at,
215
+ session_id: row.session_id,
216
+ scopes: scopes.map((s) => s.scope),
217
+ retracted: row.retracted === 1,
218
+ };
219
+ }
220
+ // ---- Sessions ----
221
+ export function startSessionRow(db, s) {
222
+ db.prepare(`INSERT INTO sessions (id, agent, user_id, context_id, started_at)
223
+ VALUES (?, ?, ?, ?, ?)`).run(s.id, s.agent, s.user_id, s.context_id, s.started_at);
224
+ }
225
+ export function endSessionRow(db, id, endedAt, reason) {
226
+ db.prepare("UPDATE sessions SET ended_at = ?, reason = ? WHERE id = ?").run(endedAt, reason, id);
227
+ }
228
+ // ---- Embeddings ----
229
+ export function upsertEmbedding(db, entryId, embedding) {
230
+ const buf = Buffer.from(embedding.buffer);
231
+ db.prepare("DELETE FROM entries_vec WHERE entry_id = ?").run(entryId);
232
+ db.prepare("INSERT INTO entries_vec (entry_id, embedding) VALUES (?, ?)").run(entryId, buf);
233
+ }
234
+ export function searchByVector(db, query, k) {
235
+ const buf = Buffer.from(query.buffer);
236
+ return db
237
+ .prepare(`SELECT entry_id, distance FROM entries_vec
238
+ WHERE embedding MATCH ? AND k = ?
239
+ ORDER BY distance`)
240
+ .all(buf, k);
241
+ }
242
+ export function searchByFts(db, query, k, filters = {}) {
243
+ const ftsQuery = buildFtsQuery(query);
244
+ if (!ftsQuery)
245
+ return [];
246
+ const params = [ftsQuery];
247
+ const joins = [];
248
+ const wheres = ["entries_fts MATCH ?"];
249
+ if (filters.kinds && filters.kinds.length > 0) {
250
+ joins.push("JOIN entries e ON e.id = entries_fts.entry_id");
251
+ wheres.push(`e.kind IN (${filters.kinds.map(() => "?").join(",")})`);
252
+ params.push(...filters.kinds);
253
+ }
254
+ if (filters.scopes && filters.scopes.length > 0) {
255
+ wheres.push(`
256
+ entries_fts.entry_id IN (
257
+ SELECT entry_id
258
+ FROM entry_scopes
259
+ WHERE scope IN (${filters.scopes.map(() => "?").join(",")})
260
+ GROUP BY entry_id
261
+ HAVING COUNT(DISTINCT scope) = ${filters.scopes.length}
262
+ )
263
+ `);
264
+ params.push(...filters.scopes);
265
+ }
266
+ params.push(k);
267
+ return db
268
+ .prepare(`SELECT entries_fts.entry_id, bm25(entries_fts) AS rank
269
+ FROM entries_fts
270
+ ${joins.join("\n ")}
271
+ WHERE ${wheres.join(" AND ")}
272
+ ORDER BY rank
273
+ LIMIT ?`)
274
+ .all(...params);
275
+ }
276
+ function buildFtsQuery(query) {
277
+ const terms = query
278
+ .toLowerCase()
279
+ .match(/[a-z0-9_]+/g);
280
+ if (!terms || terms.length === 0)
281
+ return "";
282
+ return terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
283
+ }
@@ -0,0 +1,6 @@
1
+ import type { SummarizerLLM } from "./types.js";
2
+ export declare function heuristicContradiction(existing: string, incoming: string): boolean;
3
+ export declare function isContradiction(existing: string, incoming: string, llm?: SummarizerLLM | null): Promise<boolean>;
4
+ export declare const DUPLICATE_MATCH_THRESHOLD = 0.92;
5
+ export declare const CONTRADICTION_MATCH_THRESHOLD = 0.8;
6
+ export declare function isNearDuplicate(existing: string, incoming: string, score: number): boolean;
@@ -0,0 +1,50 @@
1
+ const NEGATION_PATTERN = /\b(not|never|no|without|isn't|aren't|doesn't|don't|won't|cannot|can't|missing|absent|disabled)\b/i;
2
+ function normalize(text) {
3
+ return text.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
4
+ }
5
+ function terms(text) {
6
+ return normalize(text)
7
+ .split(" ")
8
+ .filter((w) => w.length >= 3);
9
+ }
10
+ export function heuristicContradiction(existing, incoming) {
11
+ const a = normalize(existing);
12
+ const b = normalize(incoming);
13
+ const negA = NEGATION_PATTERN.test(a);
14
+ const negB = NEGATION_PATTERN.test(b);
15
+ if (negA === negB)
16
+ return false;
17
+ const shared = terms(a).filter((t) => terms(b).includes(t));
18
+ return shared.length >= 2;
19
+ }
20
+ export async function isContradiction(existing, incoming, llm) {
21
+ if (heuristicContradiction(existing, incoming))
22
+ return true;
23
+ if (!llm || !(await llm.available()))
24
+ return false;
25
+ try {
26
+ const raw = await llm.complete({
27
+ system: "You classify whether two memory statements contradict each other. " +
28
+ 'Output ONLY JSON: {"contradicts": true|false}.',
29
+ prompt: `Existing: ${existing}\nIncoming: ${incoming}`,
30
+ temperature: 0,
31
+ maxTokens: 32,
32
+ json: true,
33
+ timeoutMs: 10_000,
34
+ });
35
+ const parsed = JSON.parse(raw);
36
+ return parsed.contradicts === true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ export const DUPLICATE_MATCH_THRESHOLD = 0.92;
43
+ export const CONTRADICTION_MATCH_THRESHOLD = 0.80;
44
+ export function isNearDuplicate(existing, incoming, score) {
45
+ if (normalize(existing) === normalize(incoming))
46
+ return true;
47
+ if (heuristicContradiction(existing, incoming))
48
+ return false;
49
+ return score >= DUPLICATE_MATCH_THRESHOLD;
50
+ }
@@ -0,0 +1,3 @@
1
+ import type { MemoryConfig } from "../types.js";
2
+ import type { Embedder } from "./types.js";
3
+ export declare function createEmbedder(config: MemoryConfig): Promise<Embedder>;
@@ -0,0 +1,81 @@
1
+ // ============================================================
2
+ // ARIA Memory — Embedder factory (auto / ollama / opt-in backends)
3
+ // ============================================================
4
+ import { getAppLogger } from "../logger.js";
5
+ import { envOverride } from "../app-identity.js";
6
+ import { HashEmbedder, OllamaEmbedder } from "./embeddings.js";
7
+ export async function createEmbedder(config) {
8
+ const log = getAppLogger().child("memory");
9
+ const strategy = config.embedder ?? "auto";
10
+ const ollamaUrl = config.ollama_url ?? "http://localhost:11434";
11
+ if (strategy === "auto" || strategy === "ollama") {
12
+ const model = config.ollama_model ?? "nomic-embed-text";
13
+ if (await OllamaEmbedder.isAvailable(ollamaUrl, model)) {
14
+ log.notice(`embedder: ${model}`);
15
+ return new OllamaEmbedder(ollamaUrl, model);
16
+ }
17
+ if (strategy === "ollama") {
18
+ log.warn(`Ollama not available or model '${model}' not loaded — falling back to hash embedder. Run: ollama pull ${model}`);
19
+ }
20
+ }
21
+ if (strategy === "transformers") {
22
+ const embedder = await tryTransformersEmbedder();
23
+ if (embedder)
24
+ return embedder;
25
+ log.warn("@huggingface/transformers not available — falling back to hash embedder. Install with: npm install @huggingface/transformers");
26
+ }
27
+ if (strategy === "llama-cpp") {
28
+ const embedder = await tryLlamaCppEmbedder();
29
+ if (embedder)
30
+ return embedder;
31
+ log.warn("node-llama-cpp not available — falling back to hash embedder. Install with: npm install node-llama-cpp");
32
+ }
33
+ if (strategy === "auto") {
34
+ log.warn("Ollama unavailable — using hash embedder (non-semantic recall). Run `ollama pull nomic-embed-text` for semantic embeddings.");
35
+ }
36
+ else if (strategy !== "hash") {
37
+ log.warn('Semantic recall unavailable — using hash embedder (non-semantic recall). Set embedder = "transformers" in config for local semantic embeddings (no daemon required).');
38
+ }
39
+ return new HashEmbedder();
40
+ }
41
+ async function tryTransformersEmbedder() {
42
+ try {
43
+ const spec = "@huggingface/transformers";
44
+ const { pipeline } = await import(/* webpackIgnore: true */ spec);
45
+ const pipe = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
46
+ return {
47
+ dim: 384,
48
+ async embed(text) {
49
+ const out = await pipe(text, { pooling: "mean", normalize: true });
50
+ return out.data;
51
+ },
52
+ };
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ async function tryLlamaCppEmbedder() {
59
+ try {
60
+ const spec = "node-llama-cpp";
61
+ const { getLlama, LlamaEmbeddingContext } = await import(/* webpackIgnore: true */ spec);
62
+ const llama = await getLlama();
63
+ const modelPath = envOverride("PRAANA_EMBED_MODEL_PATH", "ARIA_EMBED_MODEL_PATH") ??
64
+ "models/nomic-embed-text-v1.5.Q8_0.gguf";
65
+ const model = await llama.loadModel({ modelPath });
66
+ const ctx = await LlamaEmbeddingContext.create({ model });
67
+ // Probe actual output dimension rather than assuming 384.
68
+ const probe = await ctx.getEmbeddingFor("probe");
69
+ const dim = probe.vector.length;
70
+ return {
71
+ dim,
72
+ async embed(text) {
73
+ const { vector } = await ctx.getEmbeddingFor(text);
74
+ return vector;
75
+ },
76
+ };
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
@@ -0,0 +1,15 @@
1
+ import type { Embedder } from "./types.js";
2
+ export declare const EMBEDDING_DIM = 384;
3
+ export declare class HashEmbedder implements Embedder {
4
+ readonly dim = 384;
5
+ embed(text: string): Promise<Float32Array>;
6
+ }
7
+ /** Ollama nomic-embed-text — 768-dim semantic embeddings via local daemon. */
8
+ export declare class OllamaEmbedder implements Embedder {
9
+ private url;
10
+ private model;
11
+ readonly dim = 768;
12
+ constructor(url?: string, model?: string);
13
+ static isAvailable(url: string, model?: string): Promise<boolean>;
14
+ embed(text: string): Promise<Float32Array>;
15
+ }
@@ -0,0 +1,67 @@
1
+ // ============================================================
2
+ // ARIA Memory — Embeddings
3
+ // ============================================================
4
+ export const EMBEDDING_DIM = 384;
5
+ // Deterministic hash-based embedding — fast, offline, zero dependencies.
6
+ // Different text → different vector. Same text → same vector.
7
+ // NOT semantic — just a cheap approximate nearest neighbor.
8
+ export class HashEmbedder {
9
+ dim = EMBEDDING_DIM;
10
+ async embed(text) {
11
+ const vec = new Float32Array(this.dim);
12
+ let h = 2166136261;
13
+ for (let i = 0; i < text.length; i++) {
14
+ h ^= text.charCodeAt(i);
15
+ h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
16
+ }
17
+ const seed = Math.abs(h);
18
+ for (let i = 0; i < this.dim; i++) {
19
+ const x = Math.sin(seed * (i + 1) * 12.9898) * 43758.5453;
20
+ vec[i] = x - Math.floor(x);
21
+ }
22
+ let norm = 0;
23
+ for (let i = 0; i < this.dim; i++)
24
+ norm += vec[i] * vec[i];
25
+ norm = Math.sqrt(norm);
26
+ for (let i = 0; i < this.dim; i++)
27
+ vec[i] /= norm;
28
+ return vec;
29
+ }
30
+ }
31
+ /** Ollama nomic-embed-text — 768-dim semantic embeddings via local daemon. */
32
+ export class OllamaEmbedder {
33
+ url;
34
+ model;
35
+ dim = 768;
36
+ constructor(url = "http://localhost:11434", model = "nomic-embed-text") {
37
+ this.url = url;
38
+ this.model = model;
39
+ }
40
+ static async isAvailable(url, model) {
41
+ try {
42
+ const res = await fetch(`${url}/api/tags`, {
43
+ signal: AbortSignal.timeout(2000),
44
+ });
45
+ if (!res.ok)
46
+ return false;
47
+ if (!model)
48
+ return true;
49
+ const data = (await res.json());
50
+ return data.models.some((m) => m.name === model || m.name.startsWith(`${model}:`));
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ async embed(text) {
57
+ const res = await fetch(`${this.url}/api/embeddings`, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({ model: this.model, prompt: text }),
61
+ });
62
+ if (!res.ok)
63
+ throw new Error(`Ollama embed failed: ${res.status}`);
64
+ const { embedding } = (await res.json());
65
+ return new Float32Array(embedding);
66
+ }
67
+ }
@@ -0,0 +1,9 @@
1
+ export { MemoryStore } from "./store.js";
2
+ export { retractMemory } from "./db.js";
3
+ export { HashEmbedder, OllamaEmbedder, EMBEDDING_DIM } from "./embeddings.js";
4
+ export { createEmbedder } from "./embedder-factory.js";
5
+ export { createSummarizer } from "./summarizer-factory.js";
6
+ export { OpenAISummarizer } from "./openai-summarizer.js";
7
+ export { OllamaSummarizer, listOllamaModelNames, pickDefaultChatModel, } from "./ollama-summarizer.js";
8
+ export { extractLearnings, summarizeTurns } from "./summarizer.js";
9
+ export type { MemoryEntry, MemoryKind, Certainty, SessionContext, SessionEvent, Digest, RecallOptions, RecallResult, RememberOptions, ExtractedLearning, SummarizerLLM, Embedder, } from "./types.js";
@@ -0,0 +1,11 @@
1
+ // ============================================================
2
+ // ARIA Memory — Public API
3
+ // ============================================================
4
+ export { MemoryStore } from "./store.js";
5
+ export { retractMemory } from "./db.js";
6
+ export { HashEmbedder, OllamaEmbedder, EMBEDDING_DIM } from "./embeddings.js";
7
+ export { createEmbedder } from "./embedder-factory.js";
8
+ export { createSummarizer } from "./summarizer-factory.js";
9
+ export { OpenAISummarizer } from "./openai-summarizer.js";
10
+ export { OllamaSummarizer, listOllamaModelNames, pickDefaultChatModel, } from "./ollama-summarizer.js";
11
+ export { extractLearnings, summarizeTurns } from "./summarizer.js";
@@ -0,0 +1,19 @@
1
+ import type { SummarizerLLM } from "./types.js";
2
+ export declare function listOllamaModelNames(url: string): Promise<string[]>;
3
+ /** First installed model that does not look like an embedding model. */
4
+ export declare function pickDefaultChatModel(names: string[]): string | null;
5
+ export declare class OllamaSummarizer implements SummarizerLLM {
6
+ private url;
7
+ private model;
8
+ readonly name: string;
9
+ constructor(url: string, model: string);
10
+ available(): Promise<boolean>;
11
+ complete(opts: {
12
+ system?: string;
13
+ prompt: string;
14
+ temperature?: number;
15
+ maxTokens?: number;
16
+ json?: boolean;
17
+ timeoutMs?: number;
18
+ }): Promise<string>;
19
+ }
@@ -0,0 +1,72 @@
1
+ // ============================================================
2
+ // ARIA Memory — Ollama Summarizer Adapter
3
+ // ============================================================
4
+ import { OllamaEmbedder } from "./embeddings.js";
5
+ export async function listOllamaModelNames(url) {
6
+ try {
7
+ const res = await fetch(`${url.replace(/\/$/, "")}/api/tags`, {
8
+ signal: AbortSignal.timeout(2000),
9
+ });
10
+ if (!res.ok)
11
+ return [];
12
+ const data = (await res.json());
13
+ return data.models.map((m) => m.name);
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ }
19
+ /** First installed model that does not look like an embedding model. */
20
+ export function pickDefaultChatModel(names) {
21
+ const chat = names.filter((n) => !/embed/i.test(n));
22
+ return chat[0] ?? null;
23
+ }
24
+ export class OllamaSummarizer {
25
+ url;
26
+ model;
27
+ name;
28
+ constructor(url, model) {
29
+ this.url = url;
30
+ this.model = model;
31
+ this.url = url.replace(/\/$/, "");
32
+ this.name = `ollama:${model}`;
33
+ }
34
+ async available() {
35
+ return OllamaEmbedder.isAvailable(this.url, this.model);
36
+ }
37
+ async complete(opts) {
38
+ const controller = new AbortController();
39
+ const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 60_000);
40
+ try {
41
+ const messages = [];
42
+ if (opts.system)
43
+ messages.push({ role: "system", content: opts.system });
44
+ messages.push({ role: "user", content: opts.prompt });
45
+ const body = {
46
+ model: this.model,
47
+ messages,
48
+ stream: false,
49
+ options: {
50
+ temperature: opts.temperature ?? 0.3,
51
+ num_predict: opts.maxTokens ?? 1500,
52
+ },
53
+ };
54
+ if (opts.json)
55
+ body.format = "json";
56
+ const res = await fetch(`${this.url}/api/chat`, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify(body),
60
+ signal: controller.signal,
61
+ });
62
+ if (!res.ok) {
63
+ throw new Error(`Summarizer ${res.status}: ${await res.text()}`);
64
+ }
65
+ const json = (await res.json());
66
+ return json.message?.content ?? "";
67
+ }
68
+ finally {
69
+ clearTimeout(timeout);
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,21 @@
1
+ import type { SummarizerLLM } from "./types.js";
2
+ export declare class OpenAISummarizer implements SummarizerLLM {
3
+ readonly name: string;
4
+ private readonly baseUrl;
5
+ private readonly apiKey;
6
+ private readonly model;
7
+ constructor(opts: {
8
+ baseUrl: string;
9
+ apiKey: string;
10
+ model: string;
11
+ });
12
+ available(): Promise<boolean>;
13
+ complete(opts: {
14
+ system?: string;
15
+ prompt: string;
16
+ temperature?: number;
17
+ maxTokens?: number;
18
+ json?: boolean;
19
+ timeoutMs?: number;
20
+ }): Promise<string>;
21
+ }