@gamaze/hicortex 0.2.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.
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ /**
3
+ * Retrieval layer with composite scoring, RRF fusion, and graph traversal.
4
+ * Ported from hicortex/retrieval.py — same scoring model and weights.
5
+ *
6
+ * Scoring model:
7
+ * score = similarity * 0.4 + effective_strength * 0.3 + connection_score * 0.2 + recency * 0.1
8
+ *
9
+ * Decay model (B+E+D):
10
+ * base_decay = 0.0005 (~60-day half-life at importance 0.5)
11
+ * decay_rate = 1 - base_decay * (1 - importance)
12
+ * decay_rate = 1 - (1 - decay_rate) * 0.7^access_count
13
+ * decay_rate = 1 - (1 - decay_rate) * 0.7^link_count
14
+ * floor = base_strength * importance * 0.1
15
+ * effective = floor + (base - floor) * decay_rate^hours
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.effectiveStrength = effectiveStrength;
52
+ exports.retrieve = retrieve;
53
+ exports.searchContext = searchContext;
54
+ const storage = __importStar(require("./storage.js"));
55
+ const BASE_DECAY = 0.0005;
56
+ const DEFAULT_GRAPH_DISTANCE = 0.5;
57
+ const RRF_K = 60;
58
+ // ---------------------------------------------------------------------------
59
+ // Timestamp parsing
60
+ // ---------------------------------------------------------------------------
61
+ function parseTimestamp(ts) {
62
+ if (!ts)
63
+ return new Date();
64
+ try {
65
+ const dt = new Date(ts);
66
+ if (isNaN(dt.getTime()))
67
+ return new Date();
68
+ return dt;
69
+ }
70
+ catch {
71
+ return new Date();
72
+ }
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Scoring helpers
76
+ // ---------------------------------------------------------------------------
77
+ /**
78
+ * Compute decayed strength with adaptive decay (B+E+D model).
79
+ * Exported for use by consolidation decay/prune stage.
80
+ */
81
+ function effectiveStrength(baseStrength, lastAccessed, now, options) {
82
+ const importance = options?.importance ?? baseStrength;
83
+ const accessCount = options?.accessCount ?? 0;
84
+ const linkCount = options?.linkCount ?? 0;
85
+ const hours = Math.max((now.getTime() - parseTimestamp(lastAccessed).getTime()) / 3_600_000, 0);
86
+ // B: Importance slows decay
87
+ let decayRate = 1.0 - BASE_DECAY * (1.0 - importance);
88
+ // E: Access hardening
89
+ const hardening = 0.7;
90
+ decayRate = 1.0 - (1.0 - decayRate) * Math.pow(hardening, accessCount);
91
+ // E: Connectivity hardening
92
+ decayRate = 1.0 - (1.0 - decayRate) * Math.pow(hardening, linkCount);
93
+ // D: Asymptotic floor
94
+ const floor = baseStrength * importance * 0.1;
95
+ return floor + (baseStrength - floor) * Math.pow(decayRate, hours);
96
+ }
97
+ /**
98
+ * Return a composite relevance score in [0, 1] for a candidate memory.
99
+ */
100
+ function computeScore(memory, distance, connectionCount, maxConnections, now) {
101
+ const similarity = Math.max(0, 1.0 - distance);
102
+ const effStrength = effectiveStrength(memory.base_strength ?? 0.5, memory.last_accessed, now, {
103
+ accessCount: memory.access_count ?? 0,
104
+ linkCount: connectionCount,
105
+ });
106
+ const connScore = maxConnections > 0 ? connectionCount / maxConnections : 0;
107
+ const hoursSinceCreated = Math.max((now.getTime() - parseTimestamp(memory.created_at).getTime()) / 3_600_000, 0);
108
+ const recency = Math.pow(0.9995, hoursSinceCreated);
109
+ return similarity * 0.4 + effStrength * 0.3 + connScore * 0.2 + recency * 0.1;
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Graph traversal
113
+ // ---------------------------------------------------------------------------
114
+ function collectLinks(db, seedIds, maxHops = 2) {
115
+ const visited = new Set(seedIds);
116
+ const connectionCounts = new Map();
117
+ let frontier = new Set(seedIds);
118
+ for (let hop = 0; hop < maxHops; hop++) {
119
+ const nextFrontier = new Set();
120
+ for (const mid of frontier) {
121
+ const links = storage.getLinks(db, mid, "both");
122
+ const count = links.length;
123
+ connectionCounts.set(mid, (connectionCounts.get(mid) ?? 0) + count);
124
+ for (const link of links) {
125
+ const linkedId = link.source_id === mid ? link.target_id : link.source_id;
126
+ if (linkedId && !visited.has(linkedId)) {
127
+ visited.add(linkedId);
128
+ nextFrontier.add(linkedId);
129
+ }
130
+ }
131
+ }
132
+ frontier = nextFrontier;
133
+ if (frontier.size === 0)
134
+ break;
135
+ }
136
+ // Ensure newly discovered nodes also have a connection count
137
+ for (const mid of visited) {
138
+ if (!connectionCounts.has(mid)) {
139
+ const links = storage.getLinks(db, mid, "both");
140
+ connectionCounts.set(mid, links.length);
141
+ }
142
+ }
143
+ return connectionCounts;
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Formatting
147
+ // ---------------------------------------------------------------------------
148
+ function formatResult(memory, score, effStr, connections) {
149
+ return {
150
+ id: memory.id,
151
+ content: memory.content ?? "",
152
+ score: Math.round(score * 1e6) / 1e6,
153
+ effective_strength: Math.round(effStr * 1e6) / 1e6,
154
+ access_count: memory.access_count ?? 0,
155
+ memory_type: memory.memory_type ?? "episode",
156
+ project: memory.project ?? null,
157
+ created_at: memory.created_at ?? "",
158
+ connections,
159
+ };
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Strengthening
163
+ // ---------------------------------------------------------------------------
164
+ function strengthen(db, memories, now) {
165
+ const nowIso = now.toISOString();
166
+ for (const mem of memories) {
167
+ if (!mem.id)
168
+ continue;
169
+ try {
170
+ storage.strengthenMemory(db, mem.id, nowIso);
171
+ }
172
+ catch {
173
+ // Non-fatal — log would be ideal but we keep going
174
+ }
175
+ }
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // Reciprocal Rank Fusion
179
+ // ---------------------------------------------------------------------------
180
+ function reciprocalRankFusion(rankedLists, k = RRF_K) {
181
+ const scores = new Map();
182
+ for (const ranked of rankedLists) {
183
+ for (let rank = 0; rank < ranked.length; rank++) {
184
+ const mid = ranked[rank];
185
+ scores.set(mid, (scores.get(mid) ?? 0) + 1.0 / (k + rank + 1));
186
+ }
187
+ }
188
+ return scores;
189
+ }
190
+ /**
191
+ * Main retrieval: BM25 + vector search with RRF fusion, graph traversal,
192
+ * and composite scoring. Strengthens accessed memories.
193
+ */
194
+ async function retrieve(db, embedFn, query, options) {
195
+ const limit = options?.limit ?? 5;
196
+ const project = options?.project;
197
+ const privacy = options?.privacy;
198
+ const sourceAgent = options?.sourceAgent;
199
+ const now = new Date();
200
+ // 1. Embed
201
+ const queryEmbedding = await embedFn(query);
202
+ // 2. Dual retrieval — vector + BM25
203
+ const fetchLimit = limit * 3;
204
+ let vecCandidates = storage.vectorSearch(db, queryEmbedding, fetchLimit, []);
205
+ let ftsCandidates = [];
206
+ try {
207
+ ftsCandidates = storage.searchFts(db, query, fetchLimit, privacy, sourceAgent);
208
+ }
209
+ catch {
210
+ // FTS5 search can fail on special characters; fall back to vector-only
211
+ }
212
+ if (vecCandidates.length === 0 && ftsCandidates.length === 0) {
213
+ return [];
214
+ }
215
+ // Post-filter vector candidates (sqlite-vec can't filter)
216
+ if (project) {
217
+ vecCandidates = vecCandidates.filter((c) => c.project === project);
218
+ ftsCandidates = ftsCandidates.filter((c) => c.project === project);
219
+ }
220
+ if (privacy) {
221
+ vecCandidates = vecCandidates.filter((c) => privacy.includes(c.privacy));
222
+ }
223
+ if (sourceAgent) {
224
+ vecCandidates = vecCandidates.filter((c) => c.source_agent === sourceAgent);
225
+ }
226
+ // 3. RRF fusion
227
+ const vecRanked = vecCandidates.map((c) => c.id);
228
+ const ftsRanked = ftsCandidates.map((c) => c.id);
229
+ const rrfScores = reciprocalRankFusion([vecRanked, ftsRanked]);
230
+ // Build unified candidate map
231
+ const candidateMap = new Map();
232
+ for (const c of vecCandidates) {
233
+ candidateMap.set(c.id, { mem: c, distance: c.distance });
234
+ }
235
+ for (const c of ftsCandidates) {
236
+ if (!candidateMap.has(c.id)) {
237
+ candidateMap.set(c.id, { mem: c, distance: DEFAULT_GRAPH_DISTANCE });
238
+ }
239
+ }
240
+ // 4. Graph traversal
241
+ const seedIds = [...candidateMap.keys()];
242
+ const connectionCounts = collectLinks(db, seedIds, 2);
243
+ // Pull in graph-discovered memories not in the candidate set
244
+ const graphIds = [...connectionCounts.keys()].filter((mid) => !candidateMap.has(mid));
245
+ for (const gid of graphIds) {
246
+ const mem = storage.getMemory(db, gid);
247
+ if (!mem)
248
+ continue;
249
+ if (project && mem.project !== project)
250
+ continue;
251
+ if (privacy && !privacy.includes(mem.privacy))
252
+ continue;
253
+ if (sourceAgent && mem.source_agent !== sourceAgent)
254
+ continue;
255
+ candidateMap.set(gid, { mem, distance: DEFAULT_GRAPH_DISTANCE });
256
+ }
257
+ // 5. Compute composite scores
258
+ const maxConnections = Math.max(...([...connectionCounts.values()].length > 0
259
+ ? [...connectionCounts.values()]
260
+ : [0]));
261
+ const scored = [];
262
+ const maxRrf = Math.max(...([...rrfScores.values()].length > 0 ? [...rrfScores.values()] : [1]));
263
+ for (const [mid, { mem, distance }] of candidateMap) {
264
+ const connCount = connectionCounts.get(mid) ?? 0;
265
+ const composite = computeScore(mem, distance, connCount, maxConnections, now);
266
+ const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
267
+ accessCount: mem.access_count ?? 0,
268
+ linkCount: connectionCounts.get(mem.id) ?? 0,
269
+ });
270
+ const rrf = rrfScores.get(mid) ?? 0;
271
+ const normalizedRrf = maxRrf > 0 ? rrf / maxRrf : 0;
272
+ const finalScore = composite * 0.8 + normalizedRrf * 0.2;
273
+ scored.push({ mem, finalScore, effStr, connCount });
274
+ }
275
+ // 6. Sort and take top N
276
+ scored.sort((a, b) => b.finalScore - a.finalScore);
277
+ const top = scored.slice(0, limit);
278
+ const results = top.map((t) => formatResult(t.mem, t.finalScore, t.effStr, t.connCount));
279
+ // 7. Strengthen
280
+ strengthen(db, top.map((t) => t.mem), now);
281
+ return results;
282
+ }
283
+ /**
284
+ * Get recent context, optionally filtered by project and privacy.
285
+ */
286
+ function searchContext(db, options) {
287
+ const limit = options?.limit ?? 10;
288
+ const project = options?.project;
289
+ const privacy = options?.privacy;
290
+ const now = new Date();
291
+ let candidates = storage.getRecentMemories(db, 30, limit * 3);
292
+ if (project) {
293
+ candidates = candidates.filter((c) => c.project === project);
294
+ }
295
+ if (privacy) {
296
+ candidates = candidates.filter((c) => privacy.includes(c.privacy));
297
+ }
298
+ if (candidates.length === 0)
299
+ return [];
300
+ const allIds = candidates.map((c) => c.id);
301
+ const connectionCounts = collectLinks(db, allIds, 1);
302
+ const maxConnections = Math.max(...([...connectionCounts.values()].length > 0
303
+ ? [...connectionCounts.values()]
304
+ : [0]));
305
+ const scored = [];
306
+ for (const mem of candidates) {
307
+ const connCount = connectionCounts.get(mem.id) ?? 0;
308
+ const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now);
309
+ const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
310
+ accessCount: mem.access_count ?? 0,
311
+ linkCount: connectionCounts.get(mem.id) ?? 0,
312
+ });
313
+ scored.push({ mem, score, effStr, connCount });
314
+ }
315
+ scored.sort((a, b) => b.score - a.score);
316
+ const top = scored.slice(0, limit);
317
+ const results = top.map((t) => formatResult(t.mem, t.score, t.effStr, t.connCount));
318
+ strengthen(db, top.map((t) => t.mem), now);
319
+ return results;
320
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Storage layer — CRUD operations for the SQLite + sqlite-vec database.
3
+ * Ported from hicortex/storage.py. All functions are synchronous (better-sqlite3).
4
+ */
5
+ import type Database from "better-sqlite3";
6
+ import type { Memory, MemoryLink, InsertMemoryOptions } from "./types.js";
7
+ /**
8
+ * Serialize a Float32Array embedding to a Buffer for sqlite-vec.
9
+ */
10
+ export declare function embedToBlob(embedding: Float32Array): Buffer;
11
+ /**
12
+ * Insert a memory and its vector embedding. Returns the generated UUID.
13
+ */
14
+ export declare function insertMemory(db: Database.Database, content: string, embedding: Float32Array, opts?: InsertMemoryOptions): string;
15
+ /**
16
+ * Get a single memory by ID. Returns null if not found.
17
+ */
18
+ export declare function getMemory(db: Database.Database, memoryId: string): Memory | null;
19
+ /**
20
+ * Update specific fields on a memory.
21
+ */
22
+ export declare function updateMemory(db: Database.Database, memoryId: string, fields: Record<string, unknown>): void;
23
+ /**
24
+ * Atomically increment access_count and reset last_accessed.
25
+ */
26
+ export declare function strengthenMemory(db: Database.Database, memoryId: string, nowIsoStr: string): void;
27
+ /**
28
+ * Delete a memory, its vector, and all its links.
29
+ */
30
+ export declare function deleteMemory(db: Database.Database, memoryId: string): void;
31
+ /**
32
+ * Find similar memories by vector distance. Returns memories with distance field.
33
+ */
34
+ export declare function vectorSearch(db: Database.Database, queryEmbedding: Float32Array, limit?: number, excludeIds?: string[]): Array<Memory & {
35
+ distance: number;
36
+ }>;
37
+ /**
38
+ * Full-text search using FTS5 BM25 ranking.
39
+ * Returns memories with a rank field (lower is better).
40
+ */
41
+ export declare function searchFts(db: Database.Database, query: string, limit?: number, privacy?: string[], sourceAgent?: string): Array<Memory & {
42
+ rank: number;
43
+ }>;
44
+ /**
45
+ * Create a link between two memories.
46
+ */
47
+ export declare function addLink(db: Database.Database, sourceId: string, targetId: string, relationship: string, strength?: number): void;
48
+ /**
49
+ * Get links for a memory. direction: 'outgoing', 'incoming', or 'both'.
50
+ */
51
+ export declare function getLinks(db: Database.Database, memoryId: string, direction?: "outgoing" | "incoming" | "both"): MemoryLink[];
52
+ /**
53
+ * Delete all links involving a memory.
54
+ */
55
+ export declare function deleteLinks(db: Database.Database, memoryId: string): void;
56
+ /**
57
+ * Batch insert memories. Returns count inserted.
58
+ */
59
+ export declare function insertMemoriesBatch(db: Database.Database, memories: Array<{
60
+ content: string;
61
+ embedding: Float32Array;
62
+ sourceAgent?: string;
63
+ sourceSession?: string | null;
64
+ project?: string | null;
65
+ privacy?: string;
66
+ memoryType?: string;
67
+ baseStrength?: number;
68
+ }>): number;
69
+ /**
70
+ * Return total memory count.
71
+ */
72
+ export declare function countMemories(db: Database.Database): number;
73
+ /**
74
+ * Get memories created in the last N days, newest first.
75
+ */
76
+ export declare function getRecentMemories(db: Database.Database, days?: number, limit?: number): Memory[];
77
+ /**
78
+ * Get all memories ingested after a timestamp.
79
+ * Uses ingested_at (when the memory entered the DB) for consolidation correctness.
80
+ */
81
+ export declare function getMemoriesSince(db: Database.Database, sinceIso: string): Memory[];
82
+ /**
83
+ * Get lesson-type memories from the last N days.
84
+ */
85
+ export declare function getLessons(db: Database.Database, days?: number, project?: string | null): Memory[];
86
+ /**
87
+ * Get memories older than cutoff with zero access (prune candidates).
88
+ */
89
+ export declare function getPruneCandidates(db: Database.Database, cutoffIso: string): Memory[];
90
+ /**
91
+ * Get link counts for all memories in a single query.
92
+ * Returns a map of memory_id -> total link count (both directions).
93
+ */
94
+ export declare function getAllLinkCounts(db: Database.Database): Map<string, number>;
95
+ /**
96
+ * Get all memories with default base_strength (never scored).
97
+ */
98
+ export declare function getUnscoredMemories(db: Database.Database): Memory[];