ahok-skill 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.prettierrc +8 -0
  2. package/Dockerfile +59 -0
  3. package/RAW_SKILL.md +219 -0
  4. package/README.md +277 -0
  5. package/SKILL.md +58 -0
  6. package/bin/opm.js +268 -0
  7. package/data/openmemory.sqlite +0 -0
  8. package/data/openmemory.sqlite-shm +0 -0
  9. package/data/openmemory.sqlite-wal +0 -0
  10. package/dist/ai/graph.js +293 -0
  11. package/dist/ai/mcp.js +397 -0
  12. package/dist/cli.js +78 -0
  13. package/dist/core/cfg.js +87 -0
  14. package/dist/core/db.js +636 -0
  15. package/dist/core/memory.js +116 -0
  16. package/dist/core/migrate.js +227 -0
  17. package/dist/core/models.js +105 -0
  18. package/dist/core/telemetry.js +57 -0
  19. package/dist/core/types.js +2 -0
  20. package/dist/core/vector/postgres.js +52 -0
  21. package/dist/core/vector/valkey.js +246 -0
  22. package/dist/core/vector_store.js +2 -0
  23. package/dist/index.js +44 -0
  24. package/dist/memory/decay.js +301 -0
  25. package/dist/memory/embed.js +675 -0
  26. package/dist/memory/hsg.js +959 -0
  27. package/dist/memory/reflect.js +131 -0
  28. package/dist/memory/user_summary.js +99 -0
  29. package/dist/migrate.js +9 -0
  30. package/dist/ops/compress.js +255 -0
  31. package/dist/ops/dynamics.js +189 -0
  32. package/dist/ops/extract.js +333 -0
  33. package/dist/ops/ingest.js +214 -0
  34. package/dist/server/index.js +109 -0
  35. package/dist/server/middleware/auth.js +137 -0
  36. package/dist/server/routes/auth.js +186 -0
  37. package/dist/server/routes/compression.js +108 -0
  38. package/dist/server/routes/dashboard.js +399 -0
  39. package/dist/server/routes/docs.js +241 -0
  40. package/dist/server/routes/dynamics.js +312 -0
  41. package/dist/server/routes/ide.js +280 -0
  42. package/dist/server/routes/index.js +33 -0
  43. package/dist/server/routes/keys.js +132 -0
  44. package/dist/server/routes/langgraph.js +61 -0
  45. package/dist/server/routes/memory.js +213 -0
  46. package/dist/server/routes/sources.js +140 -0
  47. package/dist/server/routes/system.js +63 -0
  48. package/dist/server/routes/temporal.js +293 -0
  49. package/dist/server/routes/users.js +101 -0
  50. package/dist/server/routes/vercel.js +57 -0
  51. package/dist/server/server.js +211 -0
  52. package/dist/server.js +3 -0
  53. package/dist/sources/base.js +223 -0
  54. package/dist/sources/github.js +171 -0
  55. package/dist/sources/google_drive.js +166 -0
  56. package/dist/sources/google_sheets.js +112 -0
  57. package/dist/sources/google_slides.js +139 -0
  58. package/dist/sources/index.js +34 -0
  59. package/dist/sources/notion.js +165 -0
  60. package/dist/sources/onedrive.js +143 -0
  61. package/dist/sources/web_crawler.js +166 -0
  62. package/dist/temporal_graph/index.js +20 -0
  63. package/dist/temporal_graph/query.js +240 -0
  64. package/dist/temporal_graph/store.js +116 -0
  65. package/dist/temporal_graph/timeline.js +241 -0
  66. package/dist/temporal_graph/types.js +2 -0
  67. package/dist/utils/chunking.js +60 -0
  68. package/dist/utils/index.js +31 -0
  69. package/dist/utils/keyword.js +94 -0
  70. package/dist/utils/text.js +120 -0
  71. package/nodemon.json +7 -0
  72. package/package.json +50 -0
  73. package/references/api_reference.md +66 -0
  74. package/references/examples.md +45 -0
  75. package/src/ai/graph.ts +363 -0
  76. package/src/ai/mcp.ts +494 -0
  77. package/src/cli.ts +94 -0
  78. package/src/core/cfg.ts +110 -0
  79. package/src/core/db.ts +1052 -0
  80. package/src/core/memory.ts +99 -0
  81. package/src/core/migrate.ts +302 -0
  82. package/src/core/models.ts +107 -0
  83. package/src/core/telemetry.ts +47 -0
  84. package/src/core/types.ts +130 -0
  85. package/src/core/vector/postgres.ts +61 -0
  86. package/src/core/vector/valkey.ts +261 -0
  87. package/src/core/vector_store.ts +9 -0
  88. package/src/index.ts +5 -0
  89. package/src/memory/decay.ts +427 -0
  90. package/src/memory/embed.ts +707 -0
  91. package/src/memory/hsg.ts +1245 -0
  92. package/src/memory/reflect.ts +158 -0
  93. package/src/memory/user_summary.ts +110 -0
  94. package/src/migrate.ts +8 -0
  95. package/src/ops/compress.ts +296 -0
  96. package/src/ops/dynamics.ts +272 -0
  97. package/src/ops/extract.ts +360 -0
  98. package/src/ops/ingest.ts +286 -0
  99. package/src/server/index.ts +159 -0
  100. package/src/server/middleware/auth.ts +156 -0
  101. package/src/server/routes/auth.ts +223 -0
  102. package/src/server/routes/compression.ts +106 -0
  103. package/src/server/routes/dashboard.ts +420 -0
  104. package/src/server/routes/docs.ts +380 -0
  105. package/src/server/routes/dynamics.ts +516 -0
  106. package/src/server/routes/ide.ts +283 -0
  107. package/src/server/routes/index.ts +32 -0
  108. package/src/server/routes/keys.ts +131 -0
  109. package/src/server/routes/langgraph.ts +71 -0
  110. package/src/server/routes/memory.ts +440 -0
  111. package/src/server/routes/sources.ts +111 -0
  112. package/src/server/routes/system.ts +68 -0
  113. package/src/server/routes/temporal.ts +335 -0
  114. package/src/server/routes/users.ts +111 -0
  115. package/src/server/routes/vercel.ts +55 -0
  116. package/src/server/server.js +215 -0
  117. package/src/server.ts +1 -0
  118. package/src/sources/base.ts +257 -0
  119. package/src/sources/github.ts +156 -0
  120. package/src/sources/google_drive.ts +144 -0
  121. package/src/sources/google_sheets.ts +85 -0
  122. package/src/sources/google_slides.ts +115 -0
  123. package/src/sources/index.ts +19 -0
  124. package/src/sources/notion.ts +148 -0
  125. package/src/sources/onedrive.ts +131 -0
  126. package/src/sources/web_crawler.ts +161 -0
  127. package/src/temporal_graph/index.ts +4 -0
  128. package/src/temporal_graph/query.ts +299 -0
  129. package/src/temporal_graph/store.ts +156 -0
  130. package/src/temporal_graph/timeline.ts +319 -0
  131. package/src/temporal_graph/types.ts +41 -0
  132. package/src/utils/chunking.ts +66 -0
  133. package/src/utils/index.ts +25 -0
  134. package/src/utils/keyword.ts +137 -0
  135. package/src/utils/text.ts +115 -0
  136. package/tests/test_api_workspace_management.ts +413 -0
  137. package/tests/test_bulk_delete.ts +267 -0
  138. package/tests/test_omnibus.ts +166 -0
  139. package/tests/test_workspace_management.ts +278 -0
  140. package/tests/verify.ts +104 -0
  141. package/tsconfig.json +15 -0
@@ -0,0 +1,1245 @@
1
+ import crypto from "node:crypto";
2
+ import { canonical_token_set } from "../utils/text";
3
+ import { inc_q, dec_q, on_query_hit } from "./decay";
4
+ import { env, tier } from "../core/cfg";
5
+ import { cos_sim, buf_to_vec, vec_to_buf } from "../utils/index";
6
+ export interface sector_cfg {
7
+ model: string;
8
+ decay_lambda: number;
9
+ weight: number;
10
+ patterns: RegExp[];
11
+ }
12
+ export interface sector_class {
13
+ primary: string;
14
+ additional: string[];
15
+ confidence: number;
16
+ }
17
+ export interface hsg_mem {
18
+ id: string;
19
+ content: string;
20
+ primary_sector: string;
21
+ sectors: string[];
22
+ tags?: string;
23
+ meta?: any;
24
+ created_at: number;
25
+ updated_at: number;
26
+ last_seen_at: number;
27
+ salience: number;
28
+ decay_lambda: number;
29
+ version: number;
30
+ }
31
+ export interface waypoint {
32
+ src_id: string;
33
+ dst_id: string;
34
+ weight: number;
35
+ created_at: number;
36
+ updated_at: number;
37
+ }
38
+ export interface hsg_q_result {
39
+ id: string;
40
+ content: string;
41
+ score: number;
42
+ sectors: string[];
43
+ primary_sector: string;
44
+ path: string[];
45
+ salience: number;
46
+ last_seen_at: number;
47
+ tags?: string[];
48
+ meta?: any;
49
+ }
50
+ export const sector_configs: Record<string, sector_cfg> = {
51
+ episodic: {
52
+ model: "episodic-optimized",
53
+ decay_lambda: 0.015,
54
+ weight: 1.2,
55
+ patterns: [
56
+ /\b(today|yesterday|tomorrow|last\s+(week|month|year)|next\s+(week|month|year))\b/i,
57
+ /\b(remember\s+when|recall|that\s+time|when\s+I|I\s+was|we\s+were)\b/i,
58
+ /\b(went|saw|met|felt|heard|visited|attended|participated)\b/i,
59
+ /\b(at\s+\d{1,2}:\d{2}|on\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday))\b/i,
60
+ /\b(event|moment|experience|incident|occurrence|happened)\b/i,
61
+ /\bI\s+'?m\s+going\s+to\b/i,
62
+ ],
63
+ },
64
+ semantic: {
65
+ model: "semantic-optimized",
66
+ decay_lambda: 0.005,
67
+ weight: 1.0,
68
+ patterns: [
69
+ /\b(is\s+a|represents|means|stands\s+for|defined\s+as)\b/i,
70
+ /\b(concept|theory|principle|law|hypothesis|theorem|axiom)\b/i,
71
+ /\b(fact|statistic|data|evidence|proof|research|study|report)\b/i,
72
+ /\b(capital|population|distance|weight|height|width|depth)\b/i,
73
+ /\b(history|science|geography|math|physics|biology|chemistry)\b/i,
74
+ /\b(know|understand|learn|read|write|speak)\b/i,
75
+ ],
76
+ },
77
+ procedural: {
78
+ model: "procedural-optimized",
79
+ decay_lambda: 0.008,
80
+ weight: 1.1,
81
+ patterns: [
82
+ /\b(how\s+to|step\s+by\s+step|guide|tutorial|manual|instructions)\b/i,
83
+ /\b(first|second|then|next|finally|afterwards|lastly)\b/i,
84
+ /\b(install|run|execute|compile|build|deploy|configure|setup)\b/i,
85
+ /\b(click|press|type|enter|select|drag|drop|scroll)\b/i,
86
+ /\b(method|function|class|algorithm|routine|recipie)\b/i,
87
+ /\b(to\s+do|to\s+make|to\s+build|to\s+create)\b/i,
88
+ ],
89
+ },
90
+ emotional: {
91
+ model: "emotional-optimized",
92
+ decay_lambda: 0.02,
93
+ weight: 1.3,
94
+ patterns: [
95
+ /\b(feel|feeling|felt|emotions?|mood|vibe)\b/i,
96
+ /\b(happy|sad|angry|mad|excited|scared|anxious|nervous|depressed)\b/i,
97
+ /\b(love|hate|like|dislike|adore|detest|enjoy|loathe)\b/i,
98
+ /\b(amazing|terrible|awesome|awful|wonderful|horrible|great|bad)\b/i,
99
+ /\b(frustrated|confused|overwhelmed|stressed|relaxed|calm)\b/i,
100
+ /\b(wow|omg|yay|nooo|ugh|sigh)\b/i,
101
+ /[!]{2,}/,
102
+ ],
103
+ },
104
+ reflective: {
105
+ model: "reflective-optimized",
106
+ decay_lambda: 0.001,
107
+ weight: 0.8,
108
+ patterns: [
109
+ /\b(realize|realized|realization|insight|epiphany)\b/i,
110
+ /\b(think|thought|thinking|ponder|contemplate|reflect)\b/i,
111
+ /\b(understand|understood|understanding|grasp|comprehend)\b/i,
112
+ /\b(pattern|trend|connection|link|relationship|correlation)\b/i,
113
+ /\b(lesson|moral|takeaway|conclusion|summary|implication)\b/i,
114
+ /\b(feedback|review|analysis|evaluation|assessment)\b/i,
115
+ /\b(improve|grow|change|adapt|evolve)\b/i,
116
+ ],
117
+ },
118
+ };
119
+ export const sectors = Object.keys(sector_configs);
120
+ export const scoring_weights = {
121
+ similarity: 0.35,
122
+ overlap: 0.20,
123
+ waypoint: 0.15,
124
+ recency: 0.10,
125
+ tag_match: 0.20,
126
+ };
127
+ export const hybrid_params = {
128
+ tau: 3,
129
+ beta: 2,
130
+ eta: 0.1,
131
+ gamma: 0.2,
132
+ alpha_reinforce: 0.08,
133
+ t_days: 7,
134
+ t_max_days: 60,
135
+ tau_hours: 1,
136
+ epsilon: 1e-8,
137
+ };
138
+ export const reinforcement = {
139
+ salience_boost: 0.1,
140
+ waypoint_boost: 0.05,
141
+ max_salience: 1.0,
142
+ max_waypoint_weight: 1.0,
143
+ prune_threshold: 0.05,
144
+ };
145
+
146
+ // Sector relationship matrix for cross-sector retrieval
147
+ // Higher values = stronger relationship = less penalty
148
+ export const sector_relationships: Record<string, Record<string, number>> = {
149
+ semantic: { procedural: 0.8, episodic: 0.6, reflective: 0.7, emotional: 0.4 },
150
+ procedural: { semantic: 0.8, episodic: 0.6, reflective: 0.6, emotional: 0.3 },
151
+ episodic: { reflective: 0.8, semantic: 0.6, procedural: 0.6, emotional: 0.7 },
152
+ reflective: { episodic: 0.8, semantic: 0.7, procedural: 0.6, emotional: 0.6 },
153
+ emotional: { episodic: 0.7, reflective: 0.6, semantic: 0.4, procedural: 0.3 },
154
+ };
155
+
156
+ // Detect temporal markers in query for full-sector search
157
+ function has_temporal_markers(text: string): boolean {
158
+ const temporal_patterns = [
159
+ /\b(today|yesterday|tomorrow|this\s+week|last\s+week|this\s+morning)\b/i,
160
+ /\b\d{4}-\d{2}-\d{2}\b/, // ISO date format like 2025-11-20
161
+ /\b20\d{2}[/-]?(0[1-9]|1[0-2])[/-]?(0[1-9]|[12]\d|3[01])\b/, // Date patterns
162
+ /\b(january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{1,2}/i,
163
+ /\bwhat\s+(did|have)\s+(i|we)\s+(do|done)\b/i, // "what did I do" patterns
164
+ ];
165
+ return temporal_patterns.some(p => p.test(text));
166
+ }
167
+
168
+ // Calculate tag match score between query tokens and memory tags
169
+ async function compute_tag_match_score(memory_id: string, query_tokens: Set<string>): Promise<number> {
170
+ const mem = await q.get_mem.get(memory_id);
171
+ if (!mem?.tags) return 0;
172
+
173
+ try {
174
+ const tags = JSON.parse(mem.tags);
175
+ if (!Array.isArray(tags)) return 0;
176
+
177
+ let matches = 0;
178
+ for (const tag of tags) {
179
+ const tag_lower = String(tag).toLowerCase();
180
+ // Check exact match
181
+ if (query_tokens.has(tag_lower)) {
182
+ matches += 2; // Exact match bonus
183
+ } else {
184
+ // Check partial match
185
+ for (const token of query_tokens) {
186
+ if (tag_lower.includes(token) || token.includes(tag_lower)) {
187
+ matches += 1;
188
+ }
189
+ }
190
+ }
191
+ }
192
+ return Math.min(1.0, matches / Math.max(1, tags.length * 2));
193
+ } catch {
194
+ return 0;
195
+ }
196
+ }
197
+
198
+ const compress_vec_for_storage = (
199
+ vec: number[],
200
+ target_dim: number,
201
+ ): number[] => {
202
+ if (vec.length <= target_dim) return vec;
203
+ const compressed = new Float32Array(target_dim);
204
+ const bucket_sz = vec.length / target_dim;
205
+ for (let i = 0; i < target_dim; i++) {
206
+ const start = Math.floor(i * bucket_sz);
207
+ const end = Math.floor((i + 1) * bucket_sz);
208
+ let sum = 0,
209
+ count = 0;
210
+ for (let j = start; j < end && j < vec.length; j++) {
211
+ sum += vec[j];
212
+ count++;
213
+ }
214
+ compressed[i] = count > 0 ? sum / count : 0;
215
+ }
216
+ let norm = 0;
217
+ for (let i = 0; i < target_dim; i++) norm += compressed[i] * compressed[i];
218
+ norm = Math.sqrt(norm);
219
+ if (norm > 0) for (let i = 0; i < target_dim; i++) compressed[i] /= norm;
220
+ return Array.from(compressed);
221
+ };
222
+
223
+ export function classify_content(
224
+ content: string,
225
+ metadata?: any,
226
+ ): sector_class {
227
+ if (metadata?.sector && sectors.includes(metadata.sector)) {
228
+ return {
229
+ primary: metadata.sector,
230
+ additional: [],
231
+ confidence: 1.0,
232
+ };
233
+ }
234
+ const scores: Record<string, number> = {};
235
+ for (const [sector, config] of Object.entries(sector_configs)) {
236
+ let score = 0;
237
+ for (const pattern of config.patterns) {
238
+ const matches = content.match(pattern);
239
+ if (matches) {
240
+ score += matches.length * config.weight;
241
+ }
242
+ }
243
+ scores[sector] = score;
244
+ }
245
+ const sortedScores = Object.entries(scores).sort(([, a], [, b]) => b - a);
246
+ const primary = sortedScores[0][0];
247
+ const primaryScore = sortedScores[0][1];
248
+ const threshold = Math.max(1, primaryScore * 0.3);
249
+ const additional = sortedScores
250
+ .slice(1)
251
+ .filter(([, score]) => score > 0 && score >= threshold)
252
+ .map(([sector]) => sector);
253
+ const confidence =
254
+ primaryScore > 0
255
+ ? Math.min(
256
+ 1.0,
257
+ primaryScore /
258
+ (primaryScore + (sortedScores[1]?.[1] || 0) + 1),
259
+ )
260
+ : 0.2;
261
+ return {
262
+ primary: primaryScore > 0 ? primary : "semantic",
263
+ additional,
264
+ confidence,
265
+ };
266
+ }
267
+ export function calc_decay(
268
+ sec: string,
269
+ init_sal: number,
270
+ days_since: number,
271
+ seg_idx?: number,
272
+ max_seg?: number,
273
+ ): number {
274
+ const cfg = sector_configs[sec];
275
+ if (!cfg) return init_sal;
276
+ let lambda = cfg.decay_lambda;
277
+ if (seg_idx !== undefined && max_seg !== undefined && max_seg > 0) {
278
+ const seg_ratio = Math.sqrt(seg_idx / max_seg);
279
+ lambda = lambda * (1 - seg_ratio);
280
+ }
281
+ const decayed = init_sal * Math.exp(-lambda * days_since);
282
+ const reinf =
283
+ hybrid_params.alpha_reinforce * (1 - Math.exp(-lambda * days_since));
284
+ return Math.max(0, Math.min(1, decayed + reinf));
285
+ }
286
+ export function calc_recency_score(last_seen: number): number {
287
+ const now = Date.now();
288
+ const days_since = (now - last_seen) / (1000 * 60 * 60 * 24);
289
+ const t = hybrid_params.t_days;
290
+ const tmax = hybrid_params.t_max_days;
291
+ return Math.exp(-days_since / t) * (1 - days_since / tmax);
292
+ }
293
+ export function boosted_sim(s: number): number {
294
+ return 1 - Math.exp(-hybrid_params.tau * s);
295
+ }
296
+ export function compute_simhash(text: string): string {
297
+ const tokens = canonical_token_set(text);
298
+ const hashes = Array.from(tokens).map((t) => {
299
+ let h = 0;
300
+ for (let i = 0; i < t.length; i++) {
301
+ h = (h << 5) - h + t.charCodeAt(i);
302
+ h = h & h;
303
+ }
304
+ return h;
305
+ });
306
+ const vec = new Array(64).fill(0);
307
+ for (const h of hashes) {
308
+ for (let i = 0; i < 64; i++) {
309
+ if (h & (1 << i)) vec[i]++;
310
+ else vec[i]--;
311
+ }
312
+ }
313
+ let hash = "";
314
+ for (let i = 0; i < 64; i += 4) {
315
+ const nibble =
316
+ (vec[i] > 0 ? 8 : 0) +
317
+ (vec[i + 1] > 0 ? 4 : 0) +
318
+ (vec[i + 2] > 0 ? 2 : 0) +
319
+ (vec[i + 3] > 0 ? 1 : 0);
320
+ hash += nibble.toString(16);
321
+ }
322
+ return hash;
323
+ }
324
+ export function hamming_dist(hash1: string, hash2: string): number {
325
+ let dist = 0;
326
+ for (let i = 0; i < hash1.length; i++) {
327
+ const xor = parseInt(hash1[i], 16) ^ parseInt(hash2[i], 16);
328
+ dist +=
329
+ (xor & 8 ? 1 : 0) +
330
+ (xor & 4 ? 1 : 0) +
331
+ (xor & 2 ? 1 : 0) +
332
+ (xor & 1 ? 1 : 0);
333
+ }
334
+ return dist;
335
+ }
336
+ export function sigmoid(x: number): number {
337
+ return 1 / (1 + Math.exp(-x));
338
+ }
339
+ export function extract_essence(
340
+ raw: string,
341
+ sec: string,
342
+ max_len: number,
343
+ ): string {
344
+ if (!env.use_summary_only || raw.length <= max_len) return raw;
345
+ // Split on sentence boundaries (punctuation followed by whitespace) to avoid breaking filenames
346
+ const sents = raw
347
+ .split(/(?<=[.!?])\s+/)
348
+ .map((s) => s.trim())
349
+ .filter((s) => s.length > 10);
350
+ if (sents.length === 0) return raw.slice(0, max_len);
351
+ const score_sent = (s: string, idx: number): number => {
352
+ let sc = 0;
353
+ // First sentence bonus - titles/headers are essential for retrieval
354
+ if (idx === 0) sc += 10;
355
+ // Second sentence often contains key context
356
+ if (idx === 1) sc += 5;
357
+ // Header/section markers (markdown or label-style)
358
+ if (/^#+\s/.test(s) || /^[A-Z][A-Z\s]+:/.test(s)) sc += 8;
359
+ // Colon-prefixed labels like "PROBLEM:", "SOLUTION:", "CONTEXT:"
360
+ if (/^[A-Z][a-z]+:/i.test(s)) sc += 6;
361
+ // Date patterns (ISO format)
362
+ if (/\d{4}-\d{2}-\d{2}/.test(s)) sc += 7;
363
+ if (
364
+ /\b(january|february|march|april|may|june|july|august|september|october|november|december)\s+\d+/i.test(
365
+ s,
366
+ )
367
+ )
368
+ sc += 5;
369
+ if (/\$\d+|\d+\s*(miles|dollars|years|months|km)/.test(s)) sc += 4;
370
+ if (/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+/.test(s)) sc += 3;
371
+ if (
372
+ /\b(bought|purchased|serviced|visited|went|got|received|paid|earned|learned|discovered|found|saw|met|completed|finished|fixed|implemented|created|updated|added|removed|resolved)\b/i.test(
373
+ s,
374
+ )
375
+ )
376
+ sc += 4;
377
+ if (/\b(who|what|when|where|why|how)\b/i.test(s)) sc += 2;
378
+ if (s.length < 80) sc += 2;
379
+ if (/\b(I|my|me)\b/.test(s)) sc += 1;
380
+ return sc;
381
+ };
382
+ const scored = sents.map((s, idx) => ({ text: s, score: score_sent(s, idx), idx }));
383
+ // Sort by score to pick the best sentences
384
+ scored.sort((a, b) => b.score - a.score);
385
+
386
+ // Select top sentences until we hit max_len
387
+ const selected: typeof scored = [];
388
+ let current_len = 0;
389
+
390
+ // Always include the first sentence if it fits
391
+ const firstSent = scored.find(s => s.idx === 0);
392
+ if (firstSent && firstSent.text.length < max_len) {
393
+ selected.push(firstSent);
394
+ current_len += firstSent.text.length;
395
+ }
396
+
397
+ for (const item of scored) {
398
+ if (item.idx === 0) continue; // Already handled
399
+ if (current_len + item.text.length + 2 <= max_len) {
400
+ selected.push(item);
401
+ current_len += item.text.length + 2; // +2 for ". "
402
+ }
403
+ }
404
+
405
+ // Sort selected sentences by their original index to restore context flow
406
+ selected.sort((a, b) => a.idx - b.idx);
407
+
408
+ return selected.map(s => s.text).join(" ");
409
+ }
410
+ export function compute_token_overlap(
411
+ q_toks: Set<string>,
412
+ mem_toks: Set<string>,
413
+ ): number {
414
+ if (q_toks.size === 0) return 0;
415
+ let ov = 0;
416
+ for (const t of q_toks) {
417
+ if (mem_toks.has(t)) ov++;
418
+ }
419
+ return ov / q_toks.size;
420
+ }
421
+ export function compute_hybrid_score(
422
+ sim: number,
423
+ tok_ov: number,
424
+ wp_wt: number,
425
+ rec_sc: number,
426
+ keyword_score: number = 0,
427
+ tag_match: number = 0,
428
+ ): number {
429
+ const s_p = boosted_sim(sim);
430
+ const raw =
431
+ scoring_weights.similarity * s_p +
432
+ scoring_weights.overlap * tok_ov +
433
+ scoring_weights.waypoint * wp_wt +
434
+ scoring_weights.recency * rec_sc +
435
+ scoring_weights.tag_match * tag_match +
436
+ keyword_score;
437
+ return sigmoid(raw);
438
+ }
439
+ import {
440
+ q,
441
+ vector_store,
442
+ get_async,
443
+ all_async,
444
+ run_async,
445
+ transaction,
446
+ log_maint_op,
447
+ } from "../core/db";
448
+ export async function create_cross_sector_waypoints(
449
+ prim_id: string,
450
+ prim_sec: string,
451
+ add_secs: string[],
452
+ user_id?: string | null,
453
+ ): Promise<void> {
454
+ const now = Date.now();
455
+ const wt = 0.5;
456
+ for (const sec of add_secs) {
457
+ await q.ins_waypoint.run(
458
+ prim_id,
459
+ `${prim_id}:${sec}`,
460
+ user_id || "anonymous",
461
+ wt,
462
+ now,
463
+ now,
464
+ );
465
+ await q.ins_waypoint.run(
466
+ `${prim_id}:${sec}`,
467
+ prim_id,
468
+ user_id || "anonymous",
469
+ wt,
470
+ now,
471
+ now,
472
+ );
473
+ }
474
+ }
475
+ export function calc_mean_vec(
476
+ emb_res: EmbeddingResult[],
477
+ secs: string[],
478
+ ): number[] {
479
+ const dim = emb_res[0].vector.length;
480
+ const wsum = new Array(dim).fill(0);
481
+ const sec_scores = emb_res.map((r) => ({
482
+ vector: r.vector,
483
+ confidence: sector_configs[r.sector]?.weight || 1.0,
484
+ }));
485
+ const beta = hybrid_params.beta;
486
+ const exp_sum = sec_scores.reduce(
487
+ (sum, s) => sum + Math.exp(beta * s.confidence),
488
+ 0,
489
+ );
490
+ for (const result of emb_res) {
491
+ const sec_wt = sector_configs[result.sector]?.weight || 1.0;
492
+ const sm_wt = Math.exp(beta * sec_wt) / exp_sum;
493
+ for (let i = 0; i < dim; i++) {
494
+ wsum[i] += result.vector[i] * sm_wt;
495
+ }
496
+ }
497
+ const norm =
498
+ Math.sqrt(wsum.reduce((sum, v) => sum + v * v, 0)) +
499
+ hybrid_params.epsilon;
500
+ return wsum.map((v) => v / norm);
501
+ }
502
+ export async function create_single_waypoint(
503
+ new_id: string,
504
+ new_mean: number[],
505
+ ts: number,
506
+ user_id?: string | null,
507
+ ): Promise<void> {
508
+ const thresh = 0.75;
509
+ const mems = user_id
510
+ ? await q.all_mem_by_user.all(user_id, 1000, 0)
511
+ : await q.all_mem.all(1000, 0);
512
+ let best: { id: string; similarity: number } | null = null;
513
+ for (const mem of mems) {
514
+ if (mem.id === new_id || !mem.mean_vec) continue;
515
+ const ex_mean = buf_to_vec(mem.mean_vec);
516
+ const sim = cos_sim(new Float32Array(new_mean), ex_mean);
517
+ if (!best || sim > best.similarity) {
518
+ best = { id: mem.id, similarity: sim };
519
+ }
520
+ }
521
+ if (best) {
522
+ await q.ins_waypoint.run(
523
+ new_id,
524
+ best.id,
525
+ user_id || "anonymous",
526
+ best.similarity,
527
+ ts,
528
+ ts,
529
+ );
530
+ } else {
531
+ await q.ins_waypoint.run(new_id, new_id, user_id || "anonymous", 1.0, ts, ts);
532
+ }
533
+ }
534
+ export async function create_inter_mem_waypoints(
535
+ new_id: string,
536
+ prim_sec: string,
537
+ new_vec: number[],
538
+ ts: number,
539
+ user_id?: string | null,
540
+ ): Promise<void> {
541
+ const thresh = 0.75;
542
+ const wt = 0.5;
543
+ const vecs = await vector_store.getVectorsBySector(prim_sec);
544
+ for (const vr of vecs) {
545
+ if (vr.id === new_id) continue;
546
+ const ex_vec = vr.vector;
547
+ const sim = cos_sim(new Float32Array(new_vec), new Float32Array(ex_vec));
548
+ if (sim >= thresh) {
549
+ await q.ins_waypoint.run(
550
+ new_id,
551
+ vr.id,
552
+ user_id || "anonymous",
553
+ wt,
554
+ ts,
555
+ ts,
556
+ );
557
+ await q.ins_waypoint.run(
558
+ vr.id,
559
+ new_id,
560
+ user_id || "anonymous",
561
+ wt,
562
+ ts,
563
+ ts,
564
+ );
565
+ }
566
+ }
567
+ }
568
+ export async function create_contextual_waypoints(
569
+ mem_id: string,
570
+ rel_ids: string[],
571
+ base_wt: number = 0.3,
572
+ user_id?: string | null,
573
+ ): Promise<void> {
574
+ const now = Date.now();
575
+ for (const rel_id of rel_ids) {
576
+ if (mem_id === rel_id) continue;
577
+ const existing = await q.get_waypoint.get(mem_id, rel_id);
578
+ if (existing) {
579
+ const new_wt = Math.min(1.0, existing.weight + 0.1);
580
+ await q.upd_waypoint.run(mem_id, new_wt, now, rel_id);
581
+ } else {
582
+ await q.ins_waypoint.run(
583
+ mem_id,
584
+ rel_id,
585
+ user_id || "anonymous",
586
+ base_wt,
587
+ now,
588
+ now,
589
+ );
590
+ }
591
+ }
592
+ }
593
+ export async function expand_via_waypoints(
594
+ init_res: string[],
595
+ max_exp: number = 10,
596
+ ): Promise<Array<{ id: string; weight: number; path: string[] }>> {
597
+ const exp: Array<{ id: string; weight: number; path: string[] }> = [];
598
+ const vis = new Set<string>();
599
+ for (const id of init_res) {
600
+ exp.push({ id, weight: 1.0, path: [id] });
601
+ vis.add(id);
602
+ }
603
+ const q_arr = [...exp];
604
+ let exp_cnt = 0;
605
+ while (q_arr.length > 0 && exp_cnt < max_exp) {
606
+ const cur = q_arr.shift()!;
607
+ const neighs = await q.get_neighbors.all(cur.id);
608
+ for (const neigh of neighs) {
609
+ if (vis.has(neigh.dst_id)) continue;
610
+ // Clamp neighbor weight to valid range - protect against corrupted data
611
+ const neigh_wt = Math.min(1.0, Math.max(0, neigh.weight || 0));
612
+ const exp_wt = cur.weight * neigh_wt * 0.8;
613
+ if (exp_wt < 0.1) continue;
614
+ const exp_item = {
615
+ id: neigh.dst_id,
616
+ weight: exp_wt,
617
+ path: [...cur.path, neigh.dst_id],
618
+ };
619
+ exp.push(exp_item);
620
+ vis.add(neigh.dst_id);
621
+ q_arr.push(exp_item);
622
+ exp_cnt++;
623
+ }
624
+ }
625
+ return exp;
626
+ }
627
+ export async function reinforce_waypoints(trav_path: string[]): Promise<void> {
628
+ const now = Date.now();
629
+ for (let i = 0; i < trav_path.length - 1; i++) {
630
+ const src_id = trav_path[i];
631
+ const dst_id = trav_path[i + 1];
632
+ const wp = await q.get_waypoint.get(src_id, dst_id);
633
+ if (wp) {
634
+ const new_wt = Math.min(
635
+ reinforcement.max_waypoint_weight,
636
+ wp.weight + reinforcement.waypoint_boost,
637
+ );
638
+ await q.upd_waypoint.run(src_id, new_wt, now, dst_id);
639
+ }
640
+ }
641
+ }
642
+ export async function prune_weak_waypoints(): Promise<number> {
643
+ await q.prune_waypoints.run(reinforcement.prune_threshold);
644
+ return 0;
645
+ }
646
+ import {
647
+ embedForSector,
648
+ embedQueryForAllSectors,
649
+ embedMultiSector,
650
+ cosineSimilarity,
651
+ bufferToVector,
652
+ vectorToBuffer,
653
+ EmbeddingResult,
654
+ } from "./embed";
655
+ import { chunk_text } from "../utils/chunking";
656
+ import { j } from "../utils";
657
+ import { keyword_filter_memories, extract_keywords } from "../utils/keyword";
658
+ import {
659
+ calculateCrossSectorResonanceScore,
660
+ applyRetrievalTraceReinforcementToMemory,
661
+ propagateAssociativeReinforcementToLinkedNodes,
662
+ ALPHA_LEARNING_RATE_FOR_RECALL_REINFORCEMENT,
663
+ BETA_LEARNING_RATE_FOR_EMOTIONAL_FREQUENCY,
664
+ } from "../ops/dynamics";
665
+ export interface multi_vec_fusion_weights {
666
+ semantic_dimension_weight: number;
667
+ emotional_dimension_weight: number;
668
+ procedural_dimension_weight: number;
669
+ temporal_dimension_weight: number;
670
+ reflective_dimension_weight: number;
671
+ }
672
+ export async function calc_multi_vec_fusion_score(
673
+ mid: string,
674
+ qe: Record<string, number[]>,
675
+ w: multi_vec_fusion_weights,
676
+ ): Promise<number> {
677
+ const vecs = await vector_store.getVectorsById(mid);
678
+ let sum = 0,
679
+ tot = 0;
680
+ const wm: Record<string, number> = {
681
+ semantic: w.semantic_dimension_weight,
682
+ emotional: w.emotional_dimension_weight,
683
+ procedural: w.procedural_dimension_weight,
684
+ episodic: w.temporal_dimension_weight,
685
+ reflective: w.reflective_dimension_weight,
686
+ };
687
+ for (const v of vecs) {
688
+ const qv = qe[v.sector];
689
+ if (!qv) continue;
690
+ const mv = v.vector;
691
+ const sim = cosineSimilarity(qv, mv);
692
+ const wgt = wm[v.sector] || 0.5;
693
+ sum += sim * wgt;
694
+ tot += wgt;
695
+ }
696
+ return tot > 0 ? sum / tot : 0;
697
+ }
698
+ const cache = new Map<string, { r: hsg_q_result[]; t: number }>();
699
+ const sal_cache = new Map<string, { s: number; t: number }>();
700
+ // vec_cache removed
701
+ const seg_cache = new Map<number, any[]>();
702
+ const coact_buf: Array<[string, string]> = [];
703
+ const TTL = 60000;
704
+ const VEC_CACHE_MAX = 1000;
705
+ let active_queries = 0;
706
+ // get_vec removed
707
+ const get_segment = async (seg: number): Promise<any[]> => {
708
+ if (seg_cache.has(seg)) return seg_cache.get(seg)!;
709
+ const rows = await q.get_mem_by_segment.all(seg);
710
+ seg_cache.set(seg, rows);
711
+ if (seg_cache.size > env.cache_segments) {
712
+ const first = seg_cache.keys().next().value;
713
+ if (first !== undefined) seg_cache.delete(first);
714
+ }
715
+ return rows;
716
+ };
717
+ setInterval(async () => {
718
+ if (!coact_buf.length) return;
719
+ const pairs = coact_buf.splice(0, 50);
720
+ const now = Date.now();
721
+ const tau_ms = hybrid_params.tau_hours * 3600000;
722
+ for (const [a, b] of pairs) {
723
+ try {
724
+ const [memA, memB] = await Promise.all([
725
+ q.get_mem.get(a),
726
+ q.get_mem.get(b),
727
+ ]);
728
+ if (!memA || !memB) continue;
729
+ const time_diff = Math.abs(memA.last_seen_at - memB.last_seen_at);
730
+ const temp_fact = Math.exp(-time_diff / tau_ms);
731
+ const wp = await q.get_waypoint.get(a, b);
732
+ const cur_wt = wp?.weight || 0;
733
+ const new_wt = Math.min(
734
+ 1,
735
+ cur_wt + hybrid_params.eta * (1 - cur_wt) * temp_fact,
736
+ );
737
+ const user_id = wp?.user_id || memA?.user_id || memB?.user_id || "anonymous";
738
+ await q.ins_waypoint.run(a, b, user_id, new_wt, wp?.created_at || now, now);
739
+ } catch (e) { }
740
+ }
741
+ }, 1000);
742
+ const get_sal = async (id: string, def_sal: number): Promise<number> => {
743
+ const c = sal_cache.get(id);
744
+ if (c && Date.now() - c.t < TTL) return c.s;
745
+ const m = await q.get_mem.get(id);
746
+ const s = m?.salience ?? def_sal;
747
+ sal_cache.set(id, { s, t: Date.now() });
748
+ return s;
749
+ };
750
+ export async function hsg_query(
751
+ qt: string,
752
+ k = 10,
753
+ f?: { sectors?: string[]; minSalience?: number; user_id?: string; startTime?: number; endTime?: number },
754
+ ): Promise<hsg_q_result[]> {
755
+ // ... (omitted lines to keep context correct, targeting start of function signature change)
756
+ // Actually I'll target the signature and the logic inside the loop.
757
+ // Split into two edits or use multi_replace.
758
+ // Let's use multi_replace.
759
+ if (active_queries >= env.max_active) {
760
+ throw new Error(
761
+ `Rate limit: ${active_queries} active queries (max ${env.max_active})`,
762
+ );
763
+ }
764
+ active_queries++;
765
+ inc_q();
766
+ try {
767
+ const h = `${qt}:${k}:${JSON.stringify(f || {})}`;
768
+ const cached = cache.get(h);
769
+ if (cached && Date.now() - cached.t < TTL) return cached.r;
770
+ const qc = classify_content(qt);
771
+ const is_temporal = has_temporal_markers(qt);
772
+ const qtk = canonical_token_set(qt);
773
+ // Store primary sectors for scoring purposes
774
+ const primary_sectors = [qc.primary, ...qc.additional];
775
+ // Determine which sectors to search
776
+ let ss: string[];
777
+ if (f?.sectors?.length) {
778
+ // User explicitly requested specific sectors
779
+ ss = f.sectors;
780
+ } else {
781
+ // IMPORTANT: Search ALL sectors to enable cross-sector retrieval
782
+ // The sector relationship penalty will down-weight less relevant sectors
783
+ ss = [...sectors];
784
+ }
785
+ if (!ss.length) ss.push("semantic");
786
+ // Batch embed all sectors in one API call for faster queries
787
+ const qe = await embedQueryForAllSectors(qt, ss);
788
+ const w: multi_vec_fusion_weights = {
789
+ semantic_dimension_weight: qc.primary === "semantic" ? 1.2 : 0.8,
790
+ emotional_dimension_weight: qc.primary === "emotional" ? 1.5 : 0.6,
791
+ procedural_dimension_weight:
792
+ qc.primary === "procedural" ? 1.3 : 0.7,
793
+ temporal_dimension_weight: qc.primary === "episodic" ? 1.4 : 0.7,
794
+ reflective_dimension_weight:
795
+ qc.primary === "reflective" ? 1.1 : 0.5,
796
+ };
797
+ const sr: Record<
798
+ string,
799
+ Array<{ id: string; similarity: number }>
800
+ > = {};
801
+ for (const s of ss) {
802
+ const qv = qe[s];
803
+ const results = await vector_store.searchSimilar(s, qv, k * 3);
804
+ sr[s] = results.map(r => ({ id: r.id, similarity: r.score }));
805
+ }
806
+ const all_sims = Object.values(sr).flatMap((r) =>
807
+ r.slice(0, 8).map((x) => x.similarity),
808
+ );
809
+ const avg_top = all_sims.length
810
+ ? all_sims.reduce((a, b) => a + b, 0) / all_sims.length
811
+ : 0;
812
+ const adapt_exp = Math.ceil(0.3 * k * (1 - avg_top));
813
+ const eff_k = k + adapt_exp;
814
+ const high_conf = avg_top >= 0.55;
815
+ const ids = new Set<string>();
816
+ for (const r of Object.values(sr)) for (const x of r) ids.add(x.id);
817
+ const exp = high_conf
818
+ ? []
819
+ : await expand_via_waypoints(Array.from(ids), k * 2);
820
+ for (const e of exp) ids.add(e.id);
821
+
822
+ let keyword_scores = new Map<string, number>();
823
+ if (tier === "hybrid") {
824
+ const all_mems = await Promise.all(
825
+ Array.from(ids).map(async (id) => {
826
+ const m = await q.get_mem.get(id);
827
+ return m ? { id, content: m.content } : null;
828
+ }),
829
+ );
830
+ const valid_mems = all_mems.filter((m) => m !== null) as Array<{
831
+ id: string;
832
+ content: string;
833
+ }>;
834
+ keyword_scores = await keyword_filter_memories(
835
+ qt,
836
+ valid_mems,
837
+ 0.05,
838
+ );
839
+ }
840
+
841
+ const res: hsg_q_result[] = [];
842
+ for (const mid of Array.from(ids)) {
843
+ const m = await q.get_mem.get(mid);
844
+ if (!m || (f?.minSalience && m.salience < f.minSalience)) continue;
845
+ if (f?.user_id && m.user_id !== f.user_id) continue;
846
+ if (f?.startTime && m.created_at < f.startTime) continue;
847
+ if (f?.endTime && m.created_at > f.endTime) continue;
848
+ const mvf = await calc_multi_vec_fusion_score(mid, qe, w);
849
+ const csr = await calculateCrossSectorResonanceScore(
850
+ m.primary_sector,
851
+ qc.primary,
852
+ mvf,
853
+ );
854
+ let bs = csr,
855
+ bsec = m.primary_sector;
856
+ for (const [sec, rr] of Object.entries(sr)) {
857
+ const mat = rr.find((r) => r.id === mid);
858
+ if (mat && mat.similarity > bs) {
859
+ bs = mat.similarity;
860
+ bsec = sec;
861
+ }
862
+ }
863
+
864
+ // Apply sector relationship penalty for cross-sector results
865
+ const mem_sector = m.primary_sector;
866
+ const query_sector = qc.primary;
867
+ let sector_penalty = 1.0;
868
+ if (mem_sector !== query_sector && !primary_sectors.includes(mem_sector)) {
869
+ // Apply penalty based on sector relationship strength
870
+ sector_penalty = sector_relationships[query_sector]?.[mem_sector] || 0.3;
871
+ }
872
+ const adjusted_sim = bs * sector_penalty;
873
+
874
+ const em = exp.find((e: { id: string }) => e.id === mid);
875
+ // Clamp waypoint weight to valid range [0, 1] - protect against corrupted data
876
+ const ww = Math.min(1.0, Math.max(0, em?.weight || 0));
877
+ const ds = (Date.now() - m.last_seen_at) / 86400000;
878
+ const sal = calc_decay(m.primary_sector, m.salience, ds);
879
+ const mtk = canonical_token_set(m.content);
880
+ const tok_ov = compute_token_overlap(qtk, mtk);
881
+ const rec_sc = calc_recency_score(m.last_seen_at);
882
+
883
+ // Calculate tag match score
884
+ const tag_match = await compute_tag_match_score(mid, qtk);
885
+
886
+ const keyword_boost =
887
+ tier === "hybrid"
888
+ ? (keyword_scores.get(mid) || 0) * env.keyword_boost
889
+ : 0;
890
+ const fs = compute_hybrid_score(
891
+ adjusted_sim,
892
+ tok_ov,
893
+ ww,
894
+ rec_sc,
895
+ keyword_boost,
896
+ tag_match,
897
+ );
898
+ const msec = await vector_store.getVectorsById(mid);
899
+ const sl = msec.map((v) => v.sector);
900
+ res.push({
901
+ id: mid,
902
+ content: m.content,
903
+ score: fs,
904
+ sectors: sl,
905
+ primary_sector: m.primary_sector,
906
+ path: em?.path || [mid],
907
+ salience: sal,
908
+ last_seen_at: m.last_seen_at,
909
+ tags: typeof m.tags === 'string' ? JSON.parse(m.tags) : (m.tags || []),
910
+ meta: typeof m.meta === 'string' ? JSON.parse(m.meta) : (m.meta || {}),
911
+ });
912
+ }
913
+ res.sort((a, b) => b.score - a.score);
914
+ const top_cands = res.slice(0, eff_k);
915
+ if (top_cands.length > 0) {
916
+ const scores = top_cands.map((r) => r.score);
917
+ const mean = scores.reduce((a, b) => a + b, 0) / scores.length;
918
+ const variance =
919
+ scores.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) /
920
+ scores.length;
921
+ const stdDev = Math.sqrt(variance);
922
+ for (const r of top_cands) {
923
+ r.score = (r.score - mean) / (stdDev + hybrid_params.epsilon);
924
+ }
925
+ top_cands.sort((a, b) => b.score - a.score);
926
+ }
927
+ const top = top_cands.slice(0, k);
928
+ const tids = top.map((r) => r.id);
929
+
930
+ // Update feedback scores for returned memories (simple learning)
931
+ for (const r of top) {
932
+ const cur_fb = (await q.get_mem.get(r.id))?.feedback_score || 0;
933
+ const new_fb = cur_fb * 0.9 + r.score * 0.1; // Exponential moving average
934
+ await q.upd_feedback.run(r.id, new_fb);
935
+ }
936
+
937
+ for (let i = 0; i < tids.length; i++) {
938
+ for (let j = i + 1; j < tids.length; j++) {
939
+ const [a, b] = [tids[i], tids[j]].sort();
940
+ coact_buf.push([a, b]);
941
+ }
942
+ }
943
+ for (const r of top) {
944
+ const rsal = await applyRetrievalTraceReinforcementToMemory(
945
+ r.id,
946
+ r.salience,
947
+ );
948
+ await q.upd_seen.run(r.id, Date.now(), rsal, Date.now());
949
+ if (r.path.length > 1) {
950
+ await reinforce_waypoints(r.path);
951
+ const wps = await q.get_waypoints_by_src.all(r.id);
952
+ const lns = wps.map((wp: any) => ({
953
+ target_id: wp.dst_id,
954
+ weight: wp.weight,
955
+ }));
956
+ const pru =
957
+ await propagateAssociativeReinforcementToLinkedNodes(
958
+ r.id,
959
+ rsal,
960
+ lns,
961
+ );
962
+ for (const u of pru) {
963
+ const linked_mem = await q.get_mem.get(u.node_id);
964
+ if (linked_mem) {
965
+ const time_diff =
966
+ (Date.now() - linked_mem.last_seen_at) / 86400000;
967
+ const decay_fact = Math.exp(-0.02 * time_diff);
968
+ const ctx_boost =
969
+ hybrid_params.gamma *
970
+ (rsal - linked_mem.salience) *
971
+ decay_fact;
972
+ const new_sal = Math.max(
973
+ 0,
974
+ Math.min(1, linked_mem.salience + ctx_boost),
975
+ );
976
+ await q.upd_seen.run(
977
+ u.node_id,
978
+ Date.now(),
979
+ new_sal,
980
+ Date.now(),
981
+ );
982
+ }
983
+ }
984
+ }
985
+ }
986
+
987
+ for (const r of top) {
988
+ on_query_hit(r.id, r.primary_sector, (text) =>
989
+ embedForSector(text, r.primary_sector),
990
+ ).catch(() => { });
991
+ }
992
+
993
+ cache.set(h, { r: top, t: Date.now() });
994
+ return top;
995
+ } finally {
996
+ active_queries--;
997
+ dec_q();
998
+ }
999
+ }
1000
+ export async function run_decay_process(): Promise<{
1001
+ processed: number;
1002
+ decayed: number;
1003
+ }> {
1004
+ const mems = await q.all_mem.all(10000, 0);
1005
+ let p = 0,
1006
+ d = 0;
1007
+ for (const m of mems) {
1008
+ const ds = (Date.now() - m.last_seen_at) / 86400000;
1009
+ const ns = calc_decay(m.primary_sector, m.salience, ds);
1010
+ if (ns !== m.salience) {
1011
+ await q.upd_seen.run(m.id, m.last_seen_at, ns, Date.now());
1012
+ d++;
1013
+ }
1014
+ p++;
1015
+ }
1016
+ if (d > 0) await log_maint_op("decay", d);
1017
+ return { processed: p, decayed: d };
1018
+ }
1019
+
1020
+ // Helper to ensure user exists
1021
+ async function ensure_user_exists(user_id: string): Promise<void> {
1022
+ try {
1023
+ const existing = await q.get_user.get(user_id);
1024
+ if (!existing) {
1025
+ await q.ins_user.run(
1026
+ user_id,
1027
+ null, // clerk_id
1028
+ null, // api_key
1029
+ null, // stripe_customer_id
1030
+ null, // stripe_subscription_id
1031
+ 1000, // capacity
1032
+ 0, // usage
1033
+ "User profile initializing...", // Initial summary
1034
+ 0, // Reflection count
1035
+ Date.now(),
1036
+ Date.now()
1037
+ );
1038
+ }
1039
+ } catch (error) {
1040
+ console.error(`[HSG] Failed to ensure user ${user_id} exists:`, error);
1041
+ // Don't throw, proceed with memory creation (legacy behavior)
1042
+ }
1043
+ }
1044
+
1045
+ export async function add_hsg_memory(
1046
+ content: string,
1047
+ tags?: string,
1048
+ metadata?: any,
1049
+ user_id?: string,
1050
+ key_id?: string,
1051
+ ): Promise<{
1052
+ id: string;
1053
+ primary_sector: string;
1054
+ sectors: string[];
1055
+ chunks: number;
1056
+ deduplicated?: boolean;
1057
+ }> {
1058
+ const simhash = compute_simhash(content);
1059
+ const existing = await q.get_mem_by_simhash.get(simhash);
1060
+ if (existing && hamming_dist(simhash, existing.simhash) <= 3) {
1061
+ const now = Date.now();
1062
+ const boosted_sal = Math.min(1, existing.salience + 0.15);
1063
+ await q.upd_seen.run(existing.id, now, boosted_sal, now);
1064
+ return {
1065
+ id: existing.id,
1066
+ primary_sector: existing.primary_sector,
1067
+ sectors: [existing.primary_sector],
1068
+ chunks: 1,
1069
+ deduplicated: true,
1070
+ };
1071
+ }
1072
+ const id = crypto.randomUUID();
1073
+ const now = Date.now();
1074
+
1075
+ // Ensure user exists in the users table
1076
+ if (user_id) {
1077
+ await ensure_user_exists(user_id);
1078
+ }
1079
+
1080
+ const chunks = chunk_text(content);
1081
+ const use_chunking = chunks.length > 1;
1082
+ const classification = classify_content(content, metadata);
1083
+ const all_sectors = [classification.primary, ...classification.additional];
1084
+ await transaction.begin();
1085
+ try {
1086
+ const max_seg_res = await q.get_max_segment.get();
1087
+ let cur_seg = max_seg_res?.max_seg ?? 0;
1088
+ const seg_cnt_res = await q.get_segment_count.get(cur_seg);
1089
+ const seg_cnt = seg_cnt_res?.c ?? 0;
1090
+ if (seg_cnt >= env.seg_size) {
1091
+ cur_seg++;
1092
+ // Use stderr for debug output to avoid breaking MCP JSON-RPC protocol
1093
+ console.error(
1094
+ `[HSG] Rotated to segment ${cur_seg} (previous segment full: ${seg_cnt} memories)`,
1095
+ );
1096
+ }
1097
+ const stored_content = extract_essence(
1098
+ content,
1099
+ classification.primary,
1100
+ env.summary_max_length,
1101
+ );
1102
+ const sec_cfg = sector_configs[classification.primary];
1103
+ const init_sal = Math.max(
1104
+ 0,
1105
+ Math.min(1, 0.4 + 0.1 * classification.additional.length),
1106
+ );
1107
+ await q.ins_mem.run(
1108
+ id,
1109
+ user_id || "anonymous",
1110
+ cur_seg,
1111
+ stored_content,
1112
+ simhash,
1113
+ classification.primary,
1114
+ tags || null,
1115
+ JSON.stringify(metadata || {}),
1116
+ now,
1117
+ now,
1118
+ now,
1119
+ init_sal,
1120
+ sec_cfg.decay_lambda,
1121
+ 1,
1122
+ null,
1123
+ null,
1124
+ null, // compressed_vec
1125
+ 0, // feedback_score
1126
+ key_id || null, // memory_key_id
1127
+ );
1128
+ const emb_res = await embedMultiSector(
1129
+ id,
1130
+ content,
1131
+ all_sectors,
1132
+ use_chunking ? chunks : undefined,
1133
+ );
1134
+ for (const result of emb_res) {
1135
+ await vector_store.storeVector(
1136
+ id,
1137
+ result.sector,
1138
+ result.vector,
1139
+ result.dim,
1140
+ user_id || "anonymous",
1141
+ );
1142
+ }
1143
+ const mean_vec = calc_mean_vec(emb_res, all_sectors);
1144
+ const mean_vec_buf = vectorToBuffer(mean_vec);
1145
+ await q.upd_mean_vec.run(id, mean_vec.length, mean_vec_buf);
1146
+
1147
+ // Store compressed vector for smart tier (for future query optimization)
1148
+ if (tier === "smart" && mean_vec.length > 128) {
1149
+ const comp = compress_vec_for_storage(mean_vec, 128);
1150
+ const comp_buf = vectorToBuffer(comp);
1151
+ await q.upd_compressed_vec.run(comp_buf, id);
1152
+ }
1153
+
1154
+ await create_single_waypoint(id, mean_vec, now, user_id);
1155
+
1156
+ if (user_id && user_id !== "anonymous") {
1157
+ await q.inc_user_usage.run(user_id, 1);
1158
+ }
1159
+
1160
+ await transaction.commit();
1161
+ return {
1162
+ id,
1163
+ primary_sector: classification.primary,
1164
+ sectors: all_sectors,
1165
+ chunks: chunks.length,
1166
+ };
1167
+ } catch (error) {
1168
+ await transaction.rollback();
1169
+ throw error;
1170
+ }
1171
+ }
1172
+ export async function reinforce_memory(
1173
+ id: string,
1174
+ boost: number = 0.1,
1175
+ ): Promise<void> {
1176
+ const mem = await q.get_mem.get(id);
1177
+ if (!mem) throw new Error(`Memory ${id} not found`);
1178
+ const new_sal = Math.min(reinforcement.max_salience, mem.salience + boost);
1179
+ await q.upd_seen.run(id, Date.now(), new_sal, Date.now());
1180
+ if (new_sal > 0.8) await log_maint_op("consolidate", 1);
1181
+ }
1182
+ export async function update_memory(
1183
+ id: string,
1184
+ content?: string,
1185
+ tags?: string[],
1186
+ metadata?: any,
1187
+ ): Promise<{ id: string; updated: boolean }> {
1188
+ const mem = await q.get_mem.get(id);
1189
+ if (!mem) throw new Error(`Memory ${id} not found`);
1190
+ const new_content = content !== undefined ? content : mem.content;
1191
+ const new_tags = tags !== undefined ? j(tags) : mem.tags || "[]";
1192
+ const new_meta = metadata !== undefined ? j(metadata) : mem.meta || "{}";
1193
+ await transaction.begin();
1194
+ try {
1195
+ if (content !== undefined && content !== mem.content) {
1196
+ const chunks = chunk_text(new_content);
1197
+ const use_chunking = chunks.length > 1;
1198
+ const classification = classify_content(new_content, metadata);
1199
+ const all_sectors = [
1200
+ classification.primary,
1201
+ ...classification.additional,
1202
+ ];
1203
+ await vector_store.deleteVectors(id);
1204
+ const emb_res = await embedMultiSector(
1205
+ id,
1206
+ new_content,
1207
+ all_sectors,
1208
+ use_chunking ? chunks : undefined,
1209
+ );
1210
+ for (const result of emb_res) {
1211
+ await vector_store.storeVector(
1212
+ id,
1213
+ result.sector,
1214
+ result.vector,
1215
+ result.dim,
1216
+ mem.user_id || "anonymous",
1217
+ );
1218
+ }
1219
+ const mean_vec = calc_mean_vec(emb_res, all_sectors);
1220
+ const mean_vec_buf = vectorToBuffer(mean_vec);
1221
+ await q.upd_mean_vec.run(id, mean_vec.length, mean_vec_buf);
1222
+ await q.upd_mem_with_sector.run(
1223
+ new_content,
1224
+ classification.primary,
1225
+ new_tags,
1226
+ new_meta,
1227
+ Date.now(),
1228
+ id,
1229
+ );
1230
+ } else {
1231
+ await q.upd_mem.run(
1232
+ new_content,
1233
+ new_tags,
1234
+ new_meta,
1235
+ Date.now(),
1236
+ id,
1237
+ );
1238
+ }
1239
+ await transaction.commit();
1240
+ return { id, updated: true };
1241
+ } catch (error) {
1242
+ await transaction.rollback();
1243
+ throw error;
1244
+ }
1245
+ }