@theokit/sdk-memory 0.1.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 (99) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +201 -0
  3. package/README.md +86 -0
  4. package/dist/in-memory-provider.d.ts +38 -0
  5. package/dist/in-memory-provider.d.ts.map +1 -0
  6. package/dist/index.cjs +2585 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +40 -0
  9. package/dist/index.d.ts +40 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +2500 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/internal/active-memory-cache.d.ts +49 -0
  14. package/dist/internal/active-memory-cache.d.ts.map +1 -0
  15. package/dist/internal/active-memory-types.d.ts +30 -0
  16. package/dist/internal/active-memory-types.d.ts.map +1 -0
  17. package/dist/internal/active-memory.d.ts +108 -0
  18. package/dist/internal/active-memory.d.ts.map +1 -0
  19. package/dist/internal/adapter-catalog.d.ts +82 -0
  20. package/dist/internal/adapter-catalog.d.ts.map +1 -0
  21. package/dist/internal/adapter-http-error.d.ts +63 -0
  22. package/dist/internal/adapter-http-error.d.ts.map +1 -0
  23. package/dist/internal/batch-encoder.d.ts +30 -0
  24. package/dist/internal/batch-encoder.d.ts.map +1 -0
  25. package/dist/internal/chunk-markdown.d.ts +66 -0
  26. package/dist/internal/chunk-markdown.d.ts.map +1 -0
  27. package/dist/internal/circuit-breaker.d.ts +36 -0
  28. package/dist/internal/circuit-breaker.d.ts.map +1 -0
  29. package/dist/internal/composite-scorer.d.ts +19 -0
  30. package/dist/internal/composite-scorer.d.ts.map +1 -0
  31. package/dist/internal/deepinfra-embedding.d.ts +25 -0
  32. package/dist/internal/deepinfra-embedding.d.ts.map +1 -0
  33. package/dist/internal/dreaming-diary.d.ts +38 -0
  34. package/dist/internal/dreaming-diary.d.ts.map +1 -0
  35. package/dist/internal/dreaming-phases.d.ts +43 -0
  36. package/dist/internal/dreaming-phases.d.ts.map +1 -0
  37. package/dist/internal/dreaming-run.d.ts +49 -0
  38. package/dist/internal/dreaming-run.d.ts.map +1 -0
  39. package/dist/internal/embedding-adapter.d.ts +57 -0
  40. package/dist/internal/embedding-adapter.d.ts.map +1 -0
  41. package/dist/internal/embedding-cache.d.ts +16 -0
  42. package/dist/internal/embedding-cache.d.ts.map +1 -0
  43. package/dist/internal/index-db.d.ts +50 -0
  44. package/dist/internal/index-db.d.ts.map +1 -0
  45. package/dist/internal/index-manager-contract.d.ts +70 -0
  46. package/dist/internal/index-manager-contract.d.ts.map +1 -0
  47. package/dist/internal/index-manager-dispatch.d.ts +36 -0
  48. package/dist/internal/index-manager-dispatch.d.ts.map +1 -0
  49. package/dist/internal/index-manager.d.ts +94 -0
  50. package/dist/internal/index-manager.d.ts.map +1 -0
  51. package/dist/internal/index-schema.d.ts +35 -0
  52. package/dist/internal/index-schema.d.ts.map +1 -0
  53. package/dist/internal/lance-index.d.ts +70 -0
  54. package/dist/internal/lance-index.d.ts.map +1 -0
  55. package/dist/internal/lance-memory-adapter.d.ts +102 -0
  56. package/dist/internal/lance-memory-adapter.d.ts.map +1 -0
  57. package/dist/internal/markdown-store.d.ts +17 -0
  58. package/dist/internal/markdown-store.d.ts.map +1 -0
  59. package/dist/internal/memory-index.d.ts +68 -0
  60. package/dist/internal/memory-index.d.ts.map +1 -0
  61. package/dist/internal/memory-scope.d.ts +21 -0
  62. package/dist/internal/memory-scope.d.ts.map +1 -0
  63. package/dist/internal/memory-types.d.ts +95 -0
  64. package/dist/internal/memory-types.d.ts.map +1 -0
  65. package/dist/internal/migrate-sqlite-to-lance.d.ts +49 -0
  66. package/dist/internal/migrate-sqlite-to-lance.d.ts.map +1 -0
  67. package/dist/internal/migration.d.ts +10 -0
  68. package/dist/internal/migration.d.ts.map +1 -0
  69. package/dist/internal/mistral-embedding.d.ts +23 -0
  70. package/dist/internal/mistral-embedding.d.ts.map +1 -0
  71. package/dist/internal/ollama-embedding.d.ts +48 -0
  72. package/dist/internal/ollama-embedding.d.ts.map +1 -0
  73. package/dist/internal/openai-compatible.d.ts +24 -0
  74. package/dist/internal/openai-compatible.d.ts.map +1 -0
  75. package/dist/internal/openai-embedding.d.ts +34 -0
  76. package/dist/internal/openai-embedding.d.ts.map +1 -0
  77. package/dist/internal/openrouter-embedding.d.ts +25 -0
  78. package/dist/internal/openrouter-embedding.d.ts.map +1 -0
  79. package/dist/internal/query-analyzer.d.ts +16 -0
  80. package/dist/internal/query-analyzer.d.ts.map +1 -0
  81. package/dist/internal/reader.d.ts +64 -0
  82. package/dist/internal/reader.d.ts.map +1 -0
  83. package/dist/internal/session-loader.d.ts +25 -0
  84. package/dist/internal/session-loader.d.ts.map +1 -0
  85. package/dist/internal/session-summary-writer.d.ts +43 -0
  86. package/dist/internal/session-summary-writer.d.ts.map +1 -0
  87. package/dist/internal/sqlite-vec-loader.d.ts +62 -0
  88. package/dist/internal/sqlite-vec-loader.d.ts.map +1 -0
  89. package/dist/internal/tools.d.ts +44 -0
  90. package/dist/internal/tools.d.ts.map +1 -0
  91. package/dist/internal/transcript-store.d.ts +36 -0
  92. package/dist/internal/transcript-store.d.ts.map +1 -0
  93. package/dist/internal/vec-index.d.ts +81 -0
  94. package/dist/internal/vec-index.d.ts.map +1 -0
  95. package/dist/internal/voyage-embedding.d.ts +23 -0
  96. package/dist/internal/voyage-embedding.d.ts.map +1 -0
  97. package/dist/internal/wiki-loader.d.ts +24 -0
  98. package/dist/internal/wiki-loader.d.ts.map +1 -0
  99. package/package.json +74 -0
package/dist/index.js ADDED
@@ -0,0 +1,2500 @@
1
+ import { mkdir, readFile, readdir, rename, stat, access, unlink } from 'fs/promises';
2
+ import { join, resolve, dirname, relative } from 'path';
3
+ import { Security } from '@theokit/sdk';
4
+ import { replaceFileAtomic, withCwdMutex, atomicWriteJson, applyWalWithFallback, sanitizeFts5Query } from '@theokit/sdk/internal/persistence';
5
+ import { sanitizeIdentifier, safePathJoin } from '@theokit/sdk/path-safety';
6
+ import { createHash } from 'crypto';
7
+ import { AuthenticationError, RateLimitError, ConfigurationError, NetworkError, UnknownAgentError } from '@theokit/sdk/errors';
8
+ import { mkdirSync, existsSync, rmSync, renameSync } from 'fs';
9
+ import { createRequire } from 'module';
10
+
11
+ // src/in-memory-provider.ts
12
+ var ADAPTER_ID = "in-memory-md";
13
+ var INTERNAL_STATE = /* @__PURE__ */ Symbol("sdk-memory.in-memory.state");
14
+ function buildCapabilities() {
15
+ return {
16
+ history: false,
17
+ sessions: false,
18
+ tenancy: false,
19
+ reasoning: false,
20
+ toolSchemas: false,
21
+ prefetch: false
22
+ };
23
+ }
24
+ function buildAdapter(state) {
25
+ return {
26
+ id: ADAPTER_ID,
27
+ capabilities: buildCapabilities(),
28
+ isAvailable() {
29
+ return true;
30
+ },
31
+ async write(content, _ctx) {
32
+ const text = typeof content === "string" ? content : content.map((m) => m.content).join("\n");
33
+ const safeText = Security.redact(text);
34
+ const id = `${ADAPTER_ID}:${state.counter.next++}`;
35
+ state.facts.set(id, {
36
+ id,
37
+ content: safeText,
38
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
39
+ });
40
+ return id;
41
+ },
42
+ async recall(_query, _ctx, k) {
43
+ const all = Array.from(state.facts.values());
44
+ const limit = k !== void 0 && k >= 0 ? k : all.length;
45
+ return all.slice(0, limit);
46
+ },
47
+ async delete(id) {
48
+ if (!String(id).startsWith(`${ADAPTER_ID}:`)) {
49
+ throw new Error(
50
+ `MemoryAdapter.delete: id "${String(id)}" was not minted by adapter "${ADAPTER_ID}"`
51
+ );
52
+ }
53
+ state.facts.delete(id);
54
+ }
55
+ };
56
+ }
57
+ var MAX_SUMMARY_TURN_CHARS = 2e3;
58
+ function truncate(text) {
59
+ if (text.length <= MAX_SUMMARY_TURN_CHARS) return text;
60
+ return `${text.slice(0, MAX_SUMMARY_TURN_CHARS)}\u2026`;
61
+ }
62
+ function sanitizeRunId(runId) {
63
+ return runId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128);
64
+ }
65
+ function renderSessionSummaryMarkdown(args) {
66
+ const iso = new Date(args.at).toISOString();
67
+ const safeUser = Security.redact(truncate(args.userText));
68
+ const safeAssistant = Security.redact(truncate(args.assistantText));
69
+ return [
70
+ "---",
71
+ `runId: ${args.runId}`,
72
+ `agentId: ${args.agentId}`,
73
+ `at: ${iso}`,
74
+ `status: ${args.status}`,
75
+ "---",
76
+ "",
77
+ "## User",
78
+ "",
79
+ safeUser,
80
+ "",
81
+ "## Assistant",
82
+ "",
83
+ safeAssistant,
84
+ ""
85
+ ].join("\n");
86
+ }
87
+ function extractAgentIdFromFrontmatter(content) {
88
+ const match = content.match(/^agentId:\s*(.+)$/m);
89
+ if (match === null || match[1] === void 0) return void 0;
90
+ return match[1].trim();
91
+ }
92
+ async function recallSessionSummaries(cwd, userMessage, agentIdFilter) {
93
+ const RECALL_CAP = 5;
94
+ const sessionsDir2 = join(cwd, ".theokit", "memory", "sessions");
95
+ let files;
96
+ try {
97
+ files = await readdir(sessionsDir2);
98
+ } catch {
99
+ return [];
100
+ }
101
+ const query = userMessage.toLowerCase().trim();
102
+ if (query.length === 0) return [];
103
+ const hits = [];
104
+ for (const file of files) {
105
+ if (!file.endsWith(".md")) continue;
106
+ if (hits.length >= RECALL_CAP) break;
107
+ const filePath = join(sessionsDir2, file);
108
+ let content;
109
+ try {
110
+ content = await readFile(filePath, "utf-8");
111
+ } catch {
112
+ continue;
113
+ }
114
+ if (agentIdFilter !== void 0) {
115
+ const fileAgentId = extractAgentIdFromFrontmatter(content);
116
+ if (fileAgentId !== agentIdFilter) continue;
117
+ }
118
+ if (content.toLowerCase().includes(query)) {
119
+ const userIdx = content.indexOf("## User");
120
+ const snippet = userIdx >= 0 ? content.slice(userIdx, userIdx + 200).replace(/\s+/g, " ") : file;
121
+ const id = `${ADAPTER_ID}:session:${file}`;
122
+ hits.push({
123
+ id,
124
+ content: snippet,
125
+ score: 0.5
126
+ // substring-match has no real score; placeholder
127
+ });
128
+ }
129
+ }
130
+ return hits;
131
+ }
132
+ function buildMemorySearchTool(state) {
133
+ return {
134
+ name: "memory_search",
135
+ description: "Search past session summaries for content related to a query. Use when the user asks about something they mentioned in a previous conversation. Returns up to 5 short snippets.",
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: {
139
+ query: {
140
+ type: "string",
141
+ description: "What to search for (a topic, fact, or keyword)."
142
+ }
143
+ },
144
+ required: ["query"],
145
+ additionalProperties: false
146
+ },
147
+ handler: async (input) => {
148
+ const query = typeof input.query === "string" ? input.query : "";
149
+ if (query.length === 0) {
150
+ return JSON.stringify({ ok: false, results: [], error: "empty query" });
151
+ }
152
+ const hits = await recallSessionSummaries(state.cwd, query);
153
+ return JSON.stringify({
154
+ ok: true,
155
+ count: hits.length,
156
+ results: hits.map((h) => ({ id: String(h.id), snippet: h.content }))
157
+ });
158
+ }
159
+ };
160
+ }
161
+ function buildMemoryRememberTool(state) {
162
+ return {
163
+ name: "memory_remember",
164
+ description: "Persist a short fact about the user/conversation to memory so it can be recalled in future turns. Pass a single concise sentence as `content`.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ content: { type: "string", description: "The fact to remember (one sentence)." }
169
+ },
170
+ required: ["content"],
171
+ additionalProperties: false
172
+ },
173
+ handler: async (input) => {
174
+ const content = typeof input.content === "string" ? input.content : "";
175
+ if (content.length === 0) {
176
+ return JSON.stringify({ ok: false, error: "empty content" });
177
+ }
178
+ const safeContent = Security.redact(content);
179
+ const id = `${ADAPTER_ID}:${state.counter.next++}`;
180
+ state.facts.set(id, {
181
+ id,
182
+ content: safeContent,
183
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
184
+ });
185
+ return JSON.stringify({ ok: true, id });
186
+ }
187
+ };
188
+ }
189
+ function createInMemoryMarkdownProvider() {
190
+ return {
191
+ async init(opts) {
192
+ const state = {
193
+ facts: /* @__PURE__ */ new Map(),
194
+ counter: { next: 0 },
195
+ cwd: opts.cwd
196
+ };
197
+ const adapter = buildAdapter(state);
198
+ return {
199
+ adapter,
200
+ [INTERNAL_STATE]: state
201
+ };
202
+ },
203
+ buildTools(handle, _agent) {
204
+ const state = handle[INTERNAL_STATE];
205
+ if (state === void 0) return [];
206
+ return [buildMemoryRememberTool(state), buildMemorySearchTool(state)];
207
+ },
208
+ async runActivePass(handle, args) {
209
+ const state = handle[INTERNAL_STATE];
210
+ if (state === void 0) return { facts: [] };
211
+ const inProcessFacts = Array.from(state.facts.values());
212
+ const persistedHits = await recallSessionSummaries(state.cwd, args.userMessage, args.agentId);
213
+ const allFacts = [...inProcessFacts, ...persistedHits];
214
+ if (allFacts.length === 0) return { facts: [] };
215
+ const systemPromptAdditions = [
216
+ "Known facts about the user/session (recall):",
217
+ ...allFacts.map((f) => `- ${f.content}`)
218
+ ].join("\n");
219
+ return { facts: allFacts, systemPromptAdditions };
220
+ },
221
+ sync(_handle) {
222
+ return;
223
+ },
224
+ async recordSessionSummary(args) {
225
+ if (args.status !== "finished") return;
226
+ const sessionsPath = join(args.cwd, ".theokit", "memory", "sessions");
227
+ const filePath = join(sessionsPath, `${sanitizeRunId(args.runId)}.md`);
228
+ await mkdir(sessionsPath, { recursive: true });
229
+ const body = renderSessionSummaryMarkdown(args);
230
+ await replaceFileAtomic(filePath, body);
231
+ },
232
+ dispose(handle) {
233
+ const state = handle[INTERNAL_STATE];
234
+ if (state === void 0) return;
235
+ state.facts.clear();
236
+ }
237
+ };
238
+ }
239
+ function redactSecrets(text, opts) {
240
+ return Security.redact(text, opts);
241
+ }
242
+ function legacyMemoryJsonPath(cwd, config) {
243
+ if (config.storePath !== void 0) {
244
+ return resolve(cwd, config.storePath);
245
+ }
246
+ const namespace = sanitizeIdentifier(config.namespace ?? "default");
247
+ const scope = sanitizeIdentifier(config.scope ?? "agent", { maxLen: 16 });
248
+ const userId = sanitizeIdentifier(config.userId ?? "default");
249
+ return safePathJoin(cwd, ".theokit", "memory", namespace, `${scope}-${userId}.json`);
250
+ }
251
+
252
+ // src/internal/markdown-store.ts
253
+ var MEMORY_MD_HEADER = "# Memory\n\n> Auto-managed by @theokit/sdk. Edit freely \u2014 the SDK reads from here.\n";
254
+ var FACTS_HEADING = "## Facts";
255
+ function memoryDir(cwd) {
256
+ return join(cwd, ".theokit", "memory");
257
+ }
258
+ function memoryMdPath(cwd) {
259
+ return join(memoryDir(cwd), "MEMORY.md");
260
+ }
261
+ function notesDir(cwd) {
262
+ return join(memoryDir(cwd), "notes");
263
+ }
264
+ async function readFactsFromMarkdown(cwd) {
265
+ let raw;
266
+ try {
267
+ raw = await readFile(memoryMdPath(cwd), "utf8");
268
+ } catch {
269
+ return [];
270
+ }
271
+ return parseFactsSection(raw);
272
+ }
273
+ function appendFactToMarkdown(cwd, fact) {
274
+ return withCwdMutex(memoryDir(cwd), async () => {
275
+ const path = memoryMdPath(cwd);
276
+ let raw = "";
277
+ try {
278
+ raw = await readFile(path, "utf8");
279
+ } catch {
280
+ raw = "";
281
+ }
282
+ const sanitized = redactSecrets(fact.text);
283
+ const next = insertFactBullet(raw, sanitized);
284
+ await mkdir(memoryDir(cwd), { recursive: true });
285
+ await replaceFileAtomic(path, next);
286
+ });
287
+ }
288
+ async function listNotes(cwd) {
289
+ let entries = [];
290
+ try {
291
+ entries = await readdir(notesDir(cwd));
292
+ } catch {
293
+ return [];
294
+ }
295
+ return entries.filter((name) => name.endsWith(".md")).map((name) => ({ slug: name.replace(/\.md$/, ""), path: join(notesDir(cwd), name) }));
296
+ }
297
+ function parseFactsSection(raw) {
298
+ const idx = raw.indexOf(FACTS_HEADING);
299
+ if (idx === -1) return [];
300
+ const tail = raw.slice(idx + FACTS_HEADING.length);
301
+ const nextHeading = tail.search(/\n#{1,2}\s/);
302
+ const block = nextHeading === -1 ? tail : tail.slice(0, nextHeading);
303
+ return block.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => ({ text: line.slice(2).trim() }));
304
+ }
305
+ function insertFactBullet(raw, fact) {
306
+ const bullet = `- ${fact}`;
307
+ if (raw.length === 0) {
308
+ return `${MEMORY_MD_HEADER}
309
+ ${FACTS_HEADING}
310
+
311
+ ${bullet}
312
+ `;
313
+ }
314
+ const idx = raw.indexOf(FACTS_HEADING);
315
+ if (idx === -1) {
316
+ const sep = raw.endsWith("\n") ? "" : "\n";
317
+ return `${raw}${sep}
318
+ ${FACTS_HEADING}
319
+
320
+ ${bullet}
321
+ `;
322
+ }
323
+ const after = idx + FACTS_HEADING.length;
324
+ const nextHeading = raw.slice(after).search(/\n#{1,2}\s/);
325
+ if (nextHeading === -1) {
326
+ const trailing = raw.endsWith("\n") ? "" : "\n";
327
+ return `${raw}${trailing}${bullet}
328
+ `;
329
+ }
330
+ const insertAt = after + nextHeading;
331
+ return `${raw.slice(0, insertAt)}
332
+ ${bullet}${raw.slice(insertAt)}`;
333
+ }
334
+ async function readFacts(cwd, config) {
335
+ if (!config.enabled) return [];
336
+ return readFactsFromMarkdown(cwd);
337
+ }
338
+ async function appendFact(cwd, config, fact) {
339
+ if (!config.enabled) return;
340
+ await appendFactToMarkdown(cwd, fact);
341
+ }
342
+
343
+ // src/internal/transcript-store.ts
344
+ async function persistActiveMemoryTranscript(cwd, transcript) {
345
+ try {
346
+ const dir = join(memoryDir(cwd), "transcripts", "active-memory");
347
+ const file = join(dir, `${transcript.runId}.json`);
348
+ await atomicWriteJson(file, transcript);
349
+ } catch (cause) {
350
+ const message = cause instanceof Error ? cause.message : String(cause);
351
+ process.stderr.write(`[theokit-sdk] active-memory transcript persist failed: ${message}
352
+ `);
353
+ }
354
+ }
355
+
356
+ // src/internal/active-memory.ts
357
+ var SPAN_MEMORY_RECALL = "memory.recall";
358
+ var HISTOGRAM_MEMORY_RECALL_DURATION_MS = "theokit_memory_recall_duration_ms";
359
+ var DEFAULTS = {
360
+ queryMode: "recent",
361
+ timeoutMs: 15e3,
362
+ maxSummaryChars: 220,
363
+ recentUserTurns: 2
364
+ };
365
+ async function runActiveMemory(args) {
366
+ const started = Date.now();
367
+ const span = startMemoryRecallSpan(args);
368
+ try {
369
+ if (args.options.enabled !== true || args.index === void 0) {
370
+ return endRecallSpan(span, args, {
371
+ summary: void 0,
372
+ durationMs: 0,
373
+ status: "skipped",
374
+ hits: []
375
+ });
376
+ }
377
+ const cfg = { ...DEFAULTS, ...args.options };
378
+ const breakerKey = args.agentKey ?? "default";
379
+ if (args.breaker?.shouldSkip(breakerKey) === true) {
380
+ return endRecallSpan(span, args, {
381
+ summary: void 0,
382
+ durationMs: 0,
383
+ status: "skipped",
384
+ hits: []
385
+ });
386
+ }
387
+ const cached = args.cache?.get(args.userText, cfg.queryMode);
388
+ if (cached !== void 0) return endRecallSpan(span, args, cached);
389
+ const query = buildQuery(args.userText, args.priorMessages, cfg.queryMode, cfg.recentUserTurns);
390
+ if (query.trim().length === 0) {
391
+ const finalized2 = await finalize(args, cfg.queryMode, {
392
+ summary: void 0,
393
+ durationMs: Date.now() - started,
394
+ status: "no-recall",
395
+ hits: []
396
+ });
397
+ return endRecallSpan(span, args, finalized2);
398
+ }
399
+ const result = await searchOnce(args, cfg, query, started);
400
+ notifyBreaker(args.breaker, breakerKey, result.status);
401
+ const finalized = await finalize(args, cfg.queryMode, result);
402
+ return endRecallSpan(span, args, finalized);
403
+ } catch (cause) {
404
+ span.recordException(cause);
405
+ span.setStatus({ code: 2, message: cause instanceof Error ? cause.message : String(cause) });
406
+ span.end();
407
+ throw cause;
408
+ }
409
+ }
410
+ function startMemoryRecallSpan(args) {
411
+ const telemetry = args.telemetry;
412
+ if (telemetry === void 0) {
413
+ return {
414
+ setAttribute: () => {
415
+ },
416
+ setAttributes: () => {
417
+ },
418
+ addEvent: () => {
419
+ },
420
+ setStatus: () => {
421
+ },
422
+ recordException: () => {
423
+ },
424
+ end: () => {
425
+ },
426
+ spanContext: () => ({ traceId: "0".repeat(32), spanId: "0".repeat(16) }),
427
+ isRecording: () => false
428
+ };
429
+ }
430
+ return telemetry.startSpan(SPAN_MEMORY_RECALL, {
431
+ queryMode: args.options.queryMode ?? "recent",
432
+ ...args.userId !== void 0 ? { userId: args.userId } : {},
433
+ ...args.namespace !== void 0 ? { namespace: args.namespace } : {},
434
+ ...args.scope !== void 0 ? { scope: args.scope } : {}
435
+ });
436
+ }
437
+ function endRecallSpan(span, args, result) {
438
+ span.setAttributes({
439
+ hits: result.hits.length,
440
+ status: result.status,
441
+ durationMs: result.durationMs
442
+ });
443
+ span.end();
444
+ args.telemetry?.recordHistogram(HISTOGRAM_MEMORY_RECALL_DURATION_MS, result.durationMs, {
445
+ status: result.status,
446
+ ...args.userId !== void 0 ? { userId: args.userId } : {},
447
+ ...args.namespace !== void 0 ? { namespace: args.namespace } : {},
448
+ ...args.scope !== void 0 ? { scope: args.scope } : {}
449
+ });
450
+ return result;
451
+ }
452
+ async function searchOnce(args, cfg, query, started) {
453
+ if (args.index === void 0) {
454
+ return { summary: void 0, durationMs: 0, status: "skipped", hits: [] };
455
+ }
456
+ try {
457
+ const hits = await withTimeout(args.index.search(query, { maxResults: 5 }), cfg.timeoutMs);
458
+ if (hits === "timeout") {
459
+ return { summary: void 0, durationMs: Date.now() - started, status: "timeout", hits: [] };
460
+ }
461
+ if (hits.length === 0) {
462
+ return {
463
+ summary: void 0,
464
+ durationMs: Date.now() - started,
465
+ status: "no-recall",
466
+ hits: []
467
+ };
468
+ }
469
+ const summary = formatSummary(hits, cfg.maxSummaryChars);
470
+ return { summary, durationMs: Date.now() - started, status: "ok", hits };
471
+ } catch (cause) {
472
+ const message = cause instanceof Error ? cause.message : String(cause);
473
+ process.stderr.write(`[theokit-sdk] active-memory recall failed: ${message}
474
+ `);
475
+ return { summary: void 0, durationMs: Date.now() - started, status: "error", hits: [] };
476
+ }
477
+ }
478
+ function notifyBreaker(breaker, key, status) {
479
+ if (breaker === void 0) return;
480
+ if (status === "timeout") breaker.recordTimeout(key);
481
+ else if (status === "ok" || status === "no-recall") breaker.recordSuccess(key);
482
+ }
483
+ async function finalize(args, queryMode, result) {
484
+ args.cache?.set(args.userText, queryMode, result);
485
+ if (args.persistTranscripts === true && args.cwd !== void 0) {
486
+ await persistActiveMemoryTranscript(args.cwd, {
487
+ runId: args.runId ?? `run-${Date.now()}`,
488
+ startedAtMs: Date.now() - result.durationMs,
489
+ userText: args.userText,
490
+ queryMode,
491
+ status: result.status,
492
+ durationMs: result.durationMs,
493
+ summary: result.summary,
494
+ hits: result.hits.map((h) => ({
495
+ path: h.path,
496
+ startLine: h.startLine,
497
+ endLine: h.endLine,
498
+ score: h.score,
499
+ snippet: h.snippet
500
+ }))
501
+ });
502
+ }
503
+ return result;
504
+ }
505
+ function buildQuery(userText, priorMessages, queryMode, recentUserTurns) {
506
+ if (queryMode === "message") return userText;
507
+ if (queryMode === "full") {
508
+ const allUserTurns = priorMessages.filter((m) => m.role === "user").map((m) => m.text);
509
+ return [...allUserTurns, userText].join("\n");
510
+ }
511
+ const recent = priorMessages.filter((m) => m.role === "user").slice(-recentUserTurns).map((m) => m.text);
512
+ return [...recent, userText].join("\n");
513
+ }
514
+ function formatSummary(hits, maxChars) {
515
+ const lines = hits.map((h) => `- ${h.citation}: ${h.snippet}`);
516
+ let summary = lines.join("\n");
517
+ if (summary.length > maxChars) summary = `${summary.slice(0, maxChars - 1)}\u2026`;
518
+ return summary;
519
+ }
520
+ async function withTimeout(promise, timeoutMs) {
521
+ let timer;
522
+ const timeout = new Promise((resolve) => {
523
+ timer = setTimeout(() => resolve("timeout"), timeoutMs);
524
+ });
525
+ try {
526
+ const winner = await Promise.race([promise, timeout]);
527
+ return winner;
528
+ } finally {
529
+ if (timer !== void 0) clearTimeout(timer);
530
+ }
531
+ }
532
+
533
+ // src/internal/circuit-breaker.ts
534
+ var DEFAULT_MAX_TIMEOUTS = 3;
535
+ var DEFAULT_COOLDOWN_MS = 6e4;
536
+ var CircuitBreaker = class {
537
+ constructor(opts = {}) {
538
+ this.opts = opts;
539
+ }
540
+ opts;
541
+ states = /* @__PURE__ */ new Map();
542
+ /** @returns true when the breaker is open and the call should be skipped. */
543
+ shouldSkip(key) {
544
+ const state = this.states.get(key);
545
+ if (state === void 0) return false;
546
+ if (state.cooldownUntilMs === 0) return false;
547
+ if (this.now() < state.cooldownUntilMs) return true;
548
+ state.cooldownUntilMs = 0;
549
+ state.consecutiveTimeouts = 0;
550
+ return false;
551
+ }
552
+ recordSuccess(key) {
553
+ const state = this.states.get(key);
554
+ if (state === void 0) return;
555
+ state.consecutiveTimeouts = 0;
556
+ state.cooldownUntilMs = 0;
557
+ }
558
+ recordTimeout(key) {
559
+ const state = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
560
+ state.consecutiveTimeouts += 1;
561
+ if (state.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
562
+ state.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS);
563
+ }
564
+ this.states.set(key, state);
565
+ }
566
+ /** @internal — tests inspect counter state. */
567
+ inspect(key) {
568
+ return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
569
+ }
570
+ now() {
571
+ return this.opts.now?.() ?? Date.now();
572
+ }
573
+ };
574
+ var DEFAULT_TTL_MS = 15e3;
575
+ var DEFAULT_CAPACITY = 1e3;
576
+ var ActiveMemoryCache = class {
577
+ constructor(opts = {}) {
578
+ this.opts = opts;
579
+ }
580
+ opts;
581
+ map = /* @__PURE__ */ new Map();
582
+ get(userText, queryMode, tenantCtx) {
583
+ const key = cacheKey(userText, queryMode, tenantCtx);
584
+ const entry = this.map.get(key);
585
+ if (entry === void 0) return void 0;
586
+ if (this.now() >= entry.expiresAtMs) {
587
+ this.map.delete(key);
588
+ return void 0;
589
+ }
590
+ this.map.delete(key);
591
+ this.map.set(key, entry);
592
+ return entry.result;
593
+ }
594
+ set(userText, queryMode, result, tenantCtx) {
595
+ const key = cacheKey(userText, queryMode, tenantCtx);
596
+ if (this.map.size >= (this.opts.capacity ?? DEFAULT_CAPACITY)) {
597
+ const oldest = this.map.keys().next().value;
598
+ if (oldest !== void 0) this.map.delete(oldest);
599
+ }
600
+ this.map.set(key, {
601
+ expiresAtMs: this.now() + (this.opts.ttlMs ?? DEFAULT_TTL_MS),
602
+ result
603
+ });
604
+ }
605
+ size() {
606
+ return this.map.size;
607
+ }
608
+ now() {
609
+ return this.opts.now?.() ?? Date.now();
610
+ }
611
+ };
612
+ function cacheKey(userText, queryMode, ctx) {
613
+ const parts = [queryMode, ctx?.namespace ?? "", ctx?.userId ?? "", ctx?.scope ?? "", userText];
614
+ return createHash("sha256").update(parts.join("\0")).digest("hex");
615
+ }
616
+ var RAW_MAX_BYTES = 2048;
617
+ function parseRetryAfter(headers) {
618
+ if (headers === void 0) return void 0;
619
+ const raw = headers.get("retry-after");
620
+ if (raw === null) return void 0;
621
+ const n = Number(raw);
622
+ if (Number.isFinite(n) && n >= 0) return Math.ceil(n);
623
+ return void 0;
624
+ }
625
+ function truncateRaw(body) {
626
+ if (body === null || body === void 0) return void 0;
627
+ const s = typeof body === "string" ? body : JSON.stringify(body);
628
+ const truncated = s.length <= RAW_MAX_BYTES ? s : `${s.slice(0, RAW_MAX_BYTES)}\u2026`;
629
+ return redactSecrets(truncated);
630
+ }
631
+ function buildErrorMetadata(args) {
632
+ const retryAfter = parseRetryAfter(args.headers);
633
+ const raw = truncateRaw(args.body);
634
+ return {
635
+ provider: args.provider,
636
+ endpoint: args.endpoint,
637
+ code: args.code,
638
+ statusCode: args.status,
639
+ ...retryAfter !== void 0 ? { retryAfter } : {},
640
+ ...raw !== void 0 ? { raw } : {}
641
+ };
642
+ }
643
+ function mapOpenAICompatibleError(args) {
644
+ const { providerId, status, body, headers, endpoint } = args;
645
+ const code = mapOpenAiStatusToCode(status, body);
646
+ const message = formatMessage(providerId, status, code);
647
+ const metadata = buildErrorMetadata({
648
+ provider: providerId,
649
+ endpoint,
650
+ code,
651
+ status,
652
+ headers,
653
+ body
654
+ });
655
+ if (status === 401 || status === 403) {
656
+ return new AuthenticationError(message, { code: `${providerId}_auth_failed`, metadata });
657
+ }
658
+ if (status === 429) {
659
+ return new RateLimitError(message, { code: `${providerId}_rate_limit`, metadata });
660
+ }
661
+ if (status === 400) {
662
+ return new ConfigurationError(message, { code: `${providerId}_${code}`, metadata });
663
+ }
664
+ if (status === 408) {
665
+ return new NetworkError(message, { code: `${providerId}_timeout`, metadata });
666
+ }
667
+ if (status >= 500 && status < 600) {
668
+ return new NetworkError(message, { code: `${providerId}_server_error`, metadata });
669
+ }
670
+ return new UnknownAgentError(message, { code: `${providerId}_unknown`, metadata });
671
+ }
672
+ function extractOpenAiErrorCode(body) {
673
+ if (body === null || typeof body !== "object") return void 0;
674
+ const err = body.error;
675
+ if (err === void 0 || err === null) return void 0;
676
+ if (typeof err.code === "string") return err.code;
677
+ if (typeof err.type === "string") return err.type;
678
+ return void 0;
679
+ }
680
+ function mapOpenAiStatusToCode(status, body) {
681
+ const rawCode = extractOpenAiErrorCode(body)?.toLowerCase() ?? "";
682
+ if (rawCode.includes("context_length") || rawCode.includes("context_window") || rawCode.includes("too_many_tokens") || rawCode.includes("max_tokens")) {
683
+ return "context_too_long";
684
+ }
685
+ if (rawCode.includes("content_filter") || rawCode.includes("content_policy") || rawCode.includes("safety")) {
686
+ return "content_filtered";
687
+ }
688
+ if (rawCode.includes("model_not_found") || rawCode.includes("model_unavailable") || rawCode.includes("invalid_model")) {
689
+ return "model_unavailable";
690
+ }
691
+ if (status === 402 || rawCode.includes("insufficient_quota") || rawCode.includes("quota_exceeded")) {
692
+ return "quota_exceeded";
693
+ }
694
+ if (status === 401 || status === 403) return "auth_failed";
695
+ if (status === 429) return "rate_limit";
696
+ if (status === 408) return "timeout";
697
+ if (status === 400) return "invalid_request";
698
+ if (status >= 500 && status < 600) return "server_error";
699
+ return "unknown";
700
+ }
701
+ function formatMessage(providerId, status, code) {
702
+ return `${providerId} API error: ${code} (HTTP ${status})`;
703
+ }
704
+
705
+ // src/internal/embedding-cache.ts
706
+ var LruEmbeddingCache = class {
707
+ constructor(capacity = 5e3) {
708
+ this.capacity = capacity;
709
+ }
710
+ capacity;
711
+ map = /* @__PURE__ */ new Map();
712
+ get(key) {
713
+ const value = this.map.get(key);
714
+ if (value === void 0) return void 0;
715
+ this.map.delete(key);
716
+ this.map.set(key, value);
717
+ return value;
718
+ }
719
+ set(key, value) {
720
+ if (this.map.has(key)) this.map.delete(key);
721
+ else if (this.map.size >= this.capacity) {
722
+ const oldest = this.map.keys().next().value;
723
+ if (oldest !== void 0) this.map.delete(oldest);
724
+ }
725
+ this.map.set(key, value);
726
+ }
727
+ size() {
728
+ return this.map.size;
729
+ }
730
+ };
731
+
732
+ // src/internal/openai-compatible.ts
733
+ var MAX_BATCH = 100;
734
+ var MAX_RETRIES = 2;
735
+ async function createOpenAiCompatibleRuntime(cfg, options) {
736
+ const model = options.model ?? cfg.defaultModel;
737
+ const apiKey = options.apiKey ?? process.env[cfg.apiKeyEnv];
738
+ if (apiKey === void 0 || apiKey.length === 0) {
739
+ throw new AuthenticationError(`${cfg.apiKeyEnv} missing`, {
740
+ code: "embedding_missing_api_key"
741
+ });
742
+ }
743
+ const envBaseUrl = cfg.baseUrlEnv !== void 0 ? process.env[cfg.baseUrlEnv] : void 0;
744
+ const baseUrl = options.baseUrl ?? envBaseUrl ?? cfg.defaultBaseUrl;
745
+ const fetchImpl = options.fetch ?? fetch;
746
+ const cache = options.cache ?? new LruEmbeddingCache();
747
+ const dimension = cfg.dimensionByModel[model];
748
+ if (dimension === void 0) {
749
+ throw new ConfigurationError(
750
+ `${cfg.id} adapter: model "${model}" is not in the dimension table. Use one of: ${Object.keys(cfg.dimensionByModel).join(", ")}. To add it, update dimensionByModel in the adapter source.`,
751
+ { code: "embedding_unknown_model" }
752
+ );
753
+ }
754
+ const embeddingsPath = cfg.embeddingsPath ?? "/v1/embeddings";
755
+ const stats = {
756
+ cacheHits: 0,
757
+ cacheMisses: 0,
758
+ httpCalls: 0,
759
+ retries: 0
760
+ };
761
+ return {
762
+ id: cfg.id,
763
+ model,
764
+ dimension,
765
+ stats: () => ({ ...stats }),
766
+ embed: (texts) => embedTexts({
767
+ texts,
768
+ cache,
769
+ stats,
770
+ model,
771
+ dimension,
772
+ apiKey,
773
+ baseUrl,
774
+ embeddingsPath,
775
+ fetchImpl,
776
+ providerId: cfg.id
777
+ })
778
+ };
779
+ }
780
+ async function embedTexts(input) {
781
+ const { texts, cache, stats, model, dimension } = input;
782
+ const results = new Array(texts.length);
783
+ const pending = [];
784
+ for (let i = 0; i < texts.length; i++) {
785
+ classifyEntry({
786
+ index: i,
787
+ text: texts[i] ?? "",
788
+ model,
789
+ dimension,
790
+ cache,
791
+ stats,
792
+ results,
793
+ pending
794
+ });
795
+ }
796
+ await runBatches(input, pending, results);
797
+ return results.map((v) => v ?? new Array(dimension).fill(0));
798
+ }
799
+ function classifyEntry(args) {
800
+ if (args.text.trim().length === 0) {
801
+ args.results[args.index] = new Array(args.dimension).fill(0);
802
+ return;
803
+ }
804
+ const key = hashKey(args.model, args.text);
805
+ const cached = args.cache.get(key);
806
+ if (cached !== void 0) {
807
+ args.stats.cacheHits += 1;
808
+ args.results[args.index] = cached;
809
+ return;
810
+ }
811
+ args.stats.cacheMisses += 1;
812
+ args.pending.push({ index: args.index, text: args.text, key });
813
+ }
814
+ async function runBatches(input, pending, results) {
815
+ for (let offset = 0; offset < pending.length; offset += MAX_BATCH) {
816
+ const batch = pending.slice(offset, offset + MAX_BATCH);
817
+ const vectors = await embedBatch({
818
+ apiKey: input.apiKey,
819
+ baseUrl: input.baseUrl,
820
+ embeddingsPath: input.embeddingsPath,
821
+ model: input.model,
822
+ inputs: batch.map((b) => b.text),
823
+ fetchImpl: input.fetchImpl,
824
+ stats: input.stats,
825
+ providerId: input.providerId
826
+ });
827
+ for (let j = 0; j < batch.length; j++) {
828
+ const slot = batch[j];
829
+ const vector = vectors[j];
830
+ if (slot === void 0 || vector === void 0) continue;
831
+ results[slot.index] = vector;
832
+ input.cache.set(slot.key, vector);
833
+ }
834
+ }
835
+ }
836
+ async function embedBatch(opts) {
837
+ const url = `${opts.baseUrl.replace(/\/$/, "")}${opts.embeddingsPath}`;
838
+ let attempt = 0;
839
+ while (true) {
840
+ opts.stats.httpCalls += 1;
841
+ const response = await postEmbedRequest(opts, url);
842
+ if (response.ok)
843
+ return await parseEmbedResponse(response, opts.providerId, opts.embeddingsPath);
844
+ if (isRetryable(response.status) && attempt < MAX_RETRIES) {
845
+ attempt += 1;
846
+ opts.stats.retries += 1;
847
+ await sleep(linearBackoffMs(attempt));
848
+ continue;
849
+ }
850
+ const text = await response.text().catch(() => "");
851
+ let body = text;
852
+ try {
853
+ body = JSON.parse(text);
854
+ } catch {
855
+ }
856
+ throw mapOpenAICompatibleError({
857
+ providerId: opts.providerId,
858
+ status: response.status,
859
+ body,
860
+ headers: response.headers,
861
+ endpoint: opts.embeddingsPath
862
+ });
863
+ }
864
+ }
865
+ async function postEmbedRequest(opts, url) {
866
+ return opts.fetchImpl(url, {
867
+ method: "POST",
868
+ headers: {
869
+ "content-type": "application/json",
870
+ authorization: `Bearer ${opts.apiKey}`
871
+ },
872
+ body: JSON.stringify({ model: opts.model, input: opts.inputs })
873
+ });
874
+ }
875
+ async function parseEmbedResponse(response, providerId, endpoint) {
876
+ const json = await response.json();
877
+ if (!Array.isArray(json.data)) {
878
+ throw new NetworkError(`${providerId} ${endpoint} returned no data`, {
879
+ code: "embedding_invalid_response",
880
+ metadata: {
881
+ provider: providerId,
882
+ endpoint,
883
+ code: "invalid_request",
884
+ raw: json
885
+ }
886
+ });
887
+ }
888
+ return json.data.map((d) => d.embedding);
889
+ }
890
+ function isRetryable(status) {
891
+ return status === 429 || status >= 500 && status < 600;
892
+ }
893
+ function hashKey(model, text) {
894
+ return createHash("sha256").update(`${model} ${text}`).digest("hex");
895
+ }
896
+ function linearBackoffMs(attempt) {
897
+ return 50 * attempt;
898
+ }
899
+ function sleep(ms) {
900
+ return new Promise((resolve) => setTimeout(resolve, ms));
901
+ }
902
+
903
+ // src/internal/deepinfra-embedding.ts
904
+ var DEFAULT_DEEPINFRA_EMBEDDING_MODEL = "BAAI/bge-large-en-v1.5";
905
+ var DIMENSION_BY_MODEL = {
906
+ "BAAI/bge-large-en-v1.5": 1024,
907
+ "BAAI/bge-base-en-v1.5": 768,
908
+ "BAAI/bge-small-en-v1.5": 384,
909
+ "BAAI/bge-m3": 1024,
910
+ "intfloat/e5-large-v2": 1024,
911
+ "intfloat/e5-base-v2": 768,
912
+ "intfloat/multilingual-e5-large": 1024,
913
+ "sentence-transformers/all-MiniLM-L6-v2": 384,
914
+ "thenlper/gte-large": 1024,
915
+ "thenlper/gte-base": 768
916
+ };
917
+ var deepinfraMemoryEmbeddingProviderAdapter = {
918
+ id: "deepinfra",
919
+ defaultModel: DEFAULT_DEEPINFRA_EMBEDDING_MODEL,
920
+ transport: "remote",
921
+ authProviderId: "deepinfra",
922
+ autoSelectPriority: 13,
923
+ create: (options) => createOpenAiCompatibleRuntime(
924
+ {
925
+ id: "deepinfra",
926
+ defaultBaseUrl: "https://api.deepinfra.com",
927
+ apiKeyEnv: "DEEPINFRA_API_KEY",
928
+ baseUrlEnv: "DEEPINFRA_API_BASE_URL",
929
+ defaultModel: DEFAULT_DEEPINFRA_EMBEDDING_MODEL,
930
+ dimensionByModel: DIMENSION_BY_MODEL,
931
+ embeddingsPath: "/v1/openai/embeddings"
932
+ },
933
+ options
934
+ )
935
+ };
936
+
937
+ // src/internal/mistral-embedding.ts
938
+ var DEFAULT_MISTRAL_EMBEDDING_MODEL = "mistral-embed";
939
+ var DIMENSION_BY_MODEL2 = {
940
+ "mistral-embed": 1024
941
+ };
942
+ var mistralMemoryEmbeddingProviderAdapter = {
943
+ id: "mistral",
944
+ defaultModel: DEFAULT_MISTRAL_EMBEDDING_MODEL,
945
+ transport: "remote",
946
+ authProviderId: "mistral",
947
+ autoSelectPriority: 18,
948
+ create: (options) => createOpenAiCompatibleRuntime(
949
+ {
950
+ id: "mistral",
951
+ defaultBaseUrl: "https://api.mistral.ai",
952
+ apiKeyEnv: "MISTRAL_API_KEY",
953
+ baseUrlEnv: "MISTRAL_API_BASE_URL",
954
+ defaultModel: DEFAULT_MISTRAL_EMBEDDING_MODEL,
955
+ dimensionByModel: DIMENSION_BY_MODEL2
956
+ },
957
+ options
958
+ )
959
+ };
960
+
961
+ // src/internal/ollama-embedding.ts
962
+ var DEFAULT_OLLAMA_EMBEDDING_MODEL = "nomic-embed-text";
963
+ var OLLAMA_LOCAL_SENTINEL_KEY = "ollama-local";
964
+ var DIMENSION_BY_MODEL3 = {
965
+ // Nomic — local default, recommended quality/size balance.
966
+ "nomic-embed-text": 768,
967
+ "nomic-embed-text:latest": 768,
968
+ // Sentence-transformers MiniLM — smaller, faster, lower quality.
969
+ "all-minilm": 384,
970
+ "all-minilm:latest": 384,
971
+ // BGE family.
972
+ "bge-large": 1024,
973
+ "bge-large:latest": 1024,
974
+ "bge-m3": 1024,
975
+ "bge-m3:latest": 1024,
976
+ // mxbai-embed-large.
977
+ "mxbai-embed-large": 1024,
978
+ "mxbai-embed-large:latest": 1024
979
+ };
980
+ var ollamaMemoryEmbeddingProviderAdapter = {
981
+ id: "ollama",
982
+ defaultModel: DEFAULT_OLLAMA_EMBEDDING_MODEL,
983
+ transport: "local",
984
+ authProviderId: "ollama",
985
+ // Lower than remote providers — chosen only when explicitly requested
986
+ // OR when no remote provider key is configured (local-first developer).
987
+ autoSelectPriority: 10,
988
+ create: (options) => {
989
+ const apiKey = options.apiKey ?? process.env.OLLAMA_API_KEY ?? OLLAMA_LOCAL_SENTINEL_KEY;
990
+ return createOpenAiCompatibleRuntime(
991
+ {
992
+ id: "ollama",
993
+ defaultBaseUrl: "http://localhost:11434",
994
+ apiKeyEnv: "OLLAMA_API_KEY",
995
+ baseUrlEnv: "OLLAMA_HOST",
996
+ defaultModel: DEFAULT_OLLAMA_EMBEDDING_MODEL,
997
+ dimensionByModel: DIMENSION_BY_MODEL3
998
+ },
999
+ { ...options, apiKey }
1000
+ );
1001
+ }
1002
+ };
1003
+
1004
+ // src/internal/openai-embedding.ts
1005
+ var DEFAULT_OPENAI_EMBEDDING_MODEL = "text-embedding-3-small";
1006
+ var DIMENSION_BY_MODEL4 = {
1007
+ "text-embedding-3-small": 1536,
1008
+ "text-embedding-3-large": 3072,
1009
+ "text-embedding-ada-002": 1536
1010
+ };
1011
+ var openAiMemoryEmbeddingProviderAdapter = {
1012
+ id: "openai",
1013
+ defaultModel: DEFAULT_OPENAI_EMBEDDING_MODEL,
1014
+ transport: "remote",
1015
+ authProviderId: "openai",
1016
+ autoSelectPriority: 20,
1017
+ create: (options) => createOpenAiCompatibleRuntime(
1018
+ {
1019
+ id: "openai",
1020
+ defaultBaseUrl: "https://api.openai.com",
1021
+ apiKeyEnv: "OPENAI_API_KEY",
1022
+ baseUrlEnv: "OPENAI_API_BASE_URL",
1023
+ defaultModel: DEFAULT_OPENAI_EMBEDDING_MODEL,
1024
+ dimensionByModel: DIMENSION_BY_MODEL4
1025
+ },
1026
+ options
1027
+ )
1028
+ };
1029
+
1030
+ // src/internal/openrouter-embedding.ts
1031
+ var DEFAULT_OPENROUTER_EMBEDDING_MODEL = "openai/text-embedding-3-small";
1032
+ var DIMENSION_BY_MODEL5 = {
1033
+ "openai/text-embedding-3-small": 1536,
1034
+ "openai/text-embedding-3-large": 3072,
1035
+ "openai/text-embedding-ada-002": 1536,
1036
+ "mistralai/mistral-embed": 1024
1037
+ };
1038
+ var openRouterMemoryEmbeddingProviderAdapter = {
1039
+ id: "openrouter",
1040
+ defaultModel: DEFAULT_OPENROUTER_EMBEDDING_MODEL,
1041
+ transport: "remote",
1042
+ authProviderId: "openrouter",
1043
+ autoSelectPriority: 15,
1044
+ create: (options) => createOpenAiCompatibleRuntime(
1045
+ {
1046
+ id: "openrouter",
1047
+ defaultBaseUrl: "https://openrouter.ai/api",
1048
+ apiKeyEnv: "OPENROUTER_API_KEY",
1049
+ baseUrlEnv: "OPENROUTER_API_BASE_URL",
1050
+ defaultModel: DEFAULT_OPENROUTER_EMBEDDING_MODEL,
1051
+ dimensionByModel: DIMENSION_BY_MODEL5
1052
+ },
1053
+ options
1054
+ )
1055
+ };
1056
+
1057
+ // src/internal/voyage-embedding.ts
1058
+ var DEFAULT_VOYAGE_EMBEDDING_MODEL = "voyage-3-lite";
1059
+ var DIMENSION_BY_MODEL6 = {
1060
+ "voyage-3-lite": 512,
1061
+ "voyage-3": 1024,
1062
+ "voyage-3-large": 1024,
1063
+ "voyage-code-3": 1024,
1064
+ "voyage-multilingual-2": 1024
1065
+ };
1066
+ var voyageMemoryEmbeddingProviderAdapter = {
1067
+ id: "voyage",
1068
+ defaultModel: DEFAULT_VOYAGE_EMBEDDING_MODEL,
1069
+ transport: "remote",
1070
+ authProviderId: "voyage",
1071
+ autoSelectPriority: 14,
1072
+ create: (options) => createOpenAiCompatibleRuntime(
1073
+ {
1074
+ id: "voyage",
1075
+ defaultBaseUrl: "https://api.voyageai.com",
1076
+ apiKeyEnv: "VOYAGE_API_KEY",
1077
+ baseUrlEnv: "VOYAGE_API_BASE_URL",
1078
+ defaultModel: DEFAULT_VOYAGE_EMBEDDING_MODEL,
1079
+ dimensionByModel: DIMENSION_BY_MODEL6
1080
+ },
1081
+ options
1082
+ )
1083
+ };
1084
+
1085
+ // src/internal/adapter-catalog.ts
1086
+ var MEMORY_EMBEDDING_ADAPTERS = {
1087
+ openai: openAiMemoryEmbeddingProviderAdapter,
1088
+ mistral: mistralMemoryEmbeddingProviderAdapter,
1089
+ openrouter: openRouterMemoryEmbeddingProviderAdapter,
1090
+ voyage: voyageMemoryEmbeddingProviderAdapter,
1091
+ deepinfra: deepinfraMemoryEmbeddingProviderAdapter,
1092
+ ollama: ollamaMemoryEmbeddingProviderAdapter
1093
+ };
1094
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1095
+ function chunkMarkdown(text, options = {}) {
1096
+ const maxChars = options.maxChars ?? 800;
1097
+ if (text.length === 0) return [];
1098
+ const lines = text.split("\n");
1099
+ const acc = { chunks: [], lines: [], start: 1, heading: void 0 };
1100
+ for (let i = 0; i < lines.length; i++) {
1101
+ const line = lines[i] ?? "";
1102
+ const lineNumber = i + 1;
1103
+ processLine(acc, line, lineNumber, maxChars);
1104
+ }
1105
+ flushAccumulator(acc, lines.length, maxChars);
1106
+ return acc.chunks;
1107
+ }
1108
+ function processLine(acc, line, lineNumber, maxChars) {
1109
+ const headingMatch = HEADING_RE.exec(line);
1110
+ const isBlank = line.trim().length === 0;
1111
+ if (headingMatch !== null && acc.lines.length > 0) {
1112
+ flushAccumulator(acc, lineNumber - 1, maxChars);
1113
+ }
1114
+ if (headingMatch !== null) acc.heading = headingMatch[2];
1115
+ acc.lines.push(line);
1116
+ if (isBlank && acc.lines.length > 1) {
1117
+ const joined = acc.lines.join("\n");
1118
+ if (joined.trim().length > 0) flushAccumulator(acc, lineNumber, maxChars);
1119
+ }
1120
+ }
1121
+ function flushAccumulator(acc, endLineExclusive, maxChars) {
1122
+ if (acc.lines.length === 0) return;
1123
+ const chunkText = acc.lines.join("\n");
1124
+ if (chunkText.trim().length === 0) {
1125
+ acc.lines = [];
1126
+ acc.start = endLineExclusive + 1;
1127
+ return;
1128
+ }
1129
+ if (chunkText.length > maxChars) {
1130
+ pushOversizedSlices(acc.chunks, chunkText, acc.start, acc.heading, maxChars);
1131
+ } else {
1132
+ acc.chunks.push(buildChunk(chunkText, acc.start, endLineExclusive, acc.heading));
1133
+ }
1134
+ acc.lines = [];
1135
+ acc.start = endLineExclusive + 1;
1136
+ }
1137
+ function buildChunk(text, startLine, endLine, heading) {
1138
+ return {
1139
+ startLine,
1140
+ endLine,
1141
+ text,
1142
+ hash: createHash("sha256").update(text).digest("hex"),
1143
+ ...heading !== void 0 ? { heading } : {}
1144
+ };
1145
+ }
1146
+ function pushOversizedSlices(chunks, text, startLine, heading, maxChars) {
1147
+ let remaining = text;
1148
+ let lineCursor = startLine;
1149
+ while (remaining.length > maxChars) {
1150
+ const splitAt = findWordBoundarySplit(remaining, maxChars);
1151
+ const slice = remaining.slice(0, splitAt);
1152
+ const sliceLines = slice.split("\n").length;
1153
+ chunks.push(buildChunk(slice, lineCursor, lineCursor + sliceLines - 1, heading));
1154
+ remaining = remaining.slice(splitAt).replace(/^\s+/, "");
1155
+ lineCursor += sliceLines - 1;
1156
+ }
1157
+ if (remaining.length > 0) {
1158
+ const sliceLines = remaining.split("\n").length;
1159
+ chunks.push(buildChunk(remaining, lineCursor, lineCursor + sliceLines - 1, heading));
1160
+ }
1161
+ }
1162
+ function findWordBoundarySplit(text, maxChars) {
1163
+ if (text.length <= maxChars) return text.length;
1164
+ for (let i = maxChars; i > Math.max(0, maxChars - 200); i--) {
1165
+ if (/\s/.test(text[i] ?? "x")) return i;
1166
+ }
1167
+ return maxChars;
1168
+ }
1169
+ function diaryPath(cwd) {
1170
+ return join(memoryDir(cwd), "dream-diary.md");
1171
+ }
1172
+ function renderDiaryEntry(entry) {
1173
+ const stamp = new Date(entry.timestampMs).toISOString();
1174
+ const hash = entryHash(entry).slice(0, 8);
1175
+ return [
1176
+ `## ${stamp}`,
1177
+ "",
1178
+ `- entry-hash: ${hash}`,
1179
+ `- facts before: ${entry.factsBefore}`,
1180
+ `- facts after: ${entry.factsAfter}`,
1181
+ `- duplicates removed: ${entry.duplicatesRemoved}`,
1182
+ `- clusters created: ${entry.clustersCreated}`,
1183
+ `- notes written: ${entry.notesWritten}`,
1184
+ ""
1185
+ ].join("\n");
1186
+ }
1187
+ async function appendDiaryEntry(cwd, entry) {
1188
+ const path = diaryPath(cwd);
1189
+ let raw = "";
1190
+ try {
1191
+ raw = await readFile(path, "utf8");
1192
+ } catch {
1193
+ raw = "# Dream Diary\n\n";
1194
+ }
1195
+ const next = `${raw.endsWith("\n") ? raw : `${raw}
1196
+ `}${renderDiaryEntry(entry)}`;
1197
+ await replaceFileAtomic(path, next);
1198
+ }
1199
+ function entryHash(entry) {
1200
+ return createHash("sha256").update(
1201
+ [
1202
+ entry.factsBefore,
1203
+ entry.factsAfter,
1204
+ entry.duplicatesRemoved,
1205
+ entry.clustersCreated,
1206
+ entry.notesWritten
1207
+ ].join("|")
1208
+ ).digest("hex");
1209
+ }
1210
+
1211
+ // src/internal/dreaming-phases.ts
1212
+ var DEFAULT_DEDUP_THRESHOLD = 0.95;
1213
+ var DEFAULT_CLUSTER_THRESHOLD = 0.75;
1214
+ async function lightPhase(facts, embedding, threshold = DEFAULT_DEDUP_THRESHOLD) {
1215
+ if (facts.length <= 1) return { kept: [...facts], duplicatesRemoved: 0 };
1216
+ const vectors = await embedding.embed(facts.map((f) => f.text));
1217
+ const keptIdx = [];
1218
+ const keptVecs = [];
1219
+ for (let i = 0; i < facts.length; i++) {
1220
+ const vec = vectors[i] ?? [];
1221
+ const isDup = keptVecs.some((kept2) => cosineSimilarity(vec, kept2) >= threshold);
1222
+ if (isDup) continue;
1223
+ keptIdx.push(i);
1224
+ keptVecs.push(vec);
1225
+ }
1226
+ const kept = keptIdx.map((i) => facts[i]);
1227
+ return { kept, duplicatesRemoved: facts.length - kept.length };
1228
+ }
1229
+ var DEFAULT_MAX_FACTS_PER_SWEEP = 500;
1230
+ async function remPhase(facts, embedding, threshold = DEFAULT_CLUSTER_THRESHOLD, maxFactsPerSweep = DEFAULT_MAX_FACTS_PER_SWEEP) {
1231
+ if (facts.length === 0) return { clusters: [] };
1232
+ const capped = facts.length > maxFactsPerSweep ? facts.slice(0, maxFactsPerSweep) : facts;
1233
+ const vectors = await embedding.embed(capped.map((f) => f.text));
1234
+ const clusterOfIdx = unionFindByPairs(vectors, threshold);
1235
+ const groups = bucketFactsByClusterRoot(capped, clusterOfIdx);
1236
+ return { clusters: [...groups.values()].map(buildClusterFromMembers) };
1237
+ }
1238
+ function unionFindByPairs(vectors, threshold) {
1239
+ const clusterOfIdx = vectors.map((_, i) => i);
1240
+ for (let i = 0; i < vectors.length; i++) {
1241
+ for (let j = i + 1; j < vectors.length; j++) {
1242
+ if (cosineSimilarity(vectors[i] ?? [], vectors[j] ?? []) >= threshold) {
1243
+ unifyClusters(clusterOfIdx, i, j);
1244
+ }
1245
+ }
1246
+ }
1247
+ return clusterOfIdx;
1248
+ }
1249
+ function bucketFactsByClusterRoot(facts, clusterOfIdx) {
1250
+ const groups = /* @__PURE__ */ new Map();
1251
+ for (let i = 0; i < facts.length; i++) {
1252
+ const root = findRoot(clusterOfIdx, i);
1253
+ const list = groups.get(root) ?? [];
1254
+ list.push(facts[i]);
1255
+ groups.set(root, list);
1256
+ }
1257
+ return groups;
1258
+ }
1259
+ function buildClusterFromMembers(members) {
1260
+ const sorted = [...members].sort((a, b) => b.text.length - a.text.length);
1261
+ return { representativeText: sorted[0]?.text ?? "", members };
1262
+ }
1263
+ function deepPhase(clusters, timestampMs) {
1264
+ if (clusters.length === 0) return "";
1265
+ const isoStamp = new Date(timestampMs).toISOString();
1266
+ const lines = [`# Dreamed ${isoStamp}`, ""];
1267
+ for (let i = 0; i < clusters.length; i++) {
1268
+ const c = clusters[i];
1269
+ if (c === void 0) continue;
1270
+ lines.push(`## Cluster ${i + 1}: ${c.representativeText}`);
1271
+ lines.push("");
1272
+ for (const member of c.members) lines.push(`- ${member.text}`);
1273
+ lines.push("");
1274
+ }
1275
+ return `${lines.join("\n")}
1276
+ `;
1277
+ }
1278
+ function cosineSimilarity(a, b) {
1279
+ if (a.length === 0 || b.length === 0 || a.length !== b.length) return 0;
1280
+ let dot = 0;
1281
+ let aNorm = 0;
1282
+ let bNorm = 0;
1283
+ for (let i = 0; i < a.length; i++) {
1284
+ const ai = a[i] ?? 0;
1285
+ const bi = b[i] ?? 0;
1286
+ dot += ai * bi;
1287
+ aNorm += ai * ai;
1288
+ bNorm += bi * bi;
1289
+ }
1290
+ const denom = Math.sqrt(aNorm) * Math.sqrt(bNorm);
1291
+ return denom === 0 ? 0 : dot / denom;
1292
+ }
1293
+ function findRoot(parents, i) {
1294
+ let root = i;
1295
+ while (parents[root] !== root) {
1296
+ const next = parents[root] ?? root;
1297
+ if (next === root) break;
1298
+ root = next;
1299
+ }
1300
+ parents[i] = root;
1301
+ return root;
1302
+ }
1303
+ function unifyClusters(parents, a, b) {
1304
+ const rootA = findRoot(parents, a);
1305
+ const rootB = findRoot(parents, b);
1306
+ if (rootA !== rootB) parents[rootB] = rootA;
1307
+ }
1308
+ function runDreamingSweep(options) {
1309
+ return withCwdMutex(`dream:${options.cwd}`, () => runInner(options));
1310
+ }
1311
+ async function runInner(options) {
1312
+ const now = options.now ?? Date.now;
1313
+ const timestampMs = now();
1314
+ try {
1315
+ const facts = await readFactsFromMarkdown(options.cwd);
1316
+ if (facts.length === 0) {
1317
+ return emptyResult("skipped");
1318
+ }
1319
+ const dedup = await lightPhase(facts, options.embedding, options.dedupThreshold);
1320
+ const rem = await remPhase(dedup.kept, options.embedding, options.clusterThreshold);
1321
+ const notesWritten = await writeConsolidatedNotes(options.cwd, rem.clusters, timestampMs);
1322
+ const result = {
1323
+ status: "ok",
1324
+ factsBefore: facts.length,
1325
+ factsAfter: dedup.kept.length,
1326
+ duplicatesRemoved: dedup.duplicatesRemoved,
1327
+ clustersCreated: rem.clusters.length,
1328
+ notesWritten,
1329
+ diaryEntryHash: void 0
1330
+ };
1331
+ await appendDiaryEntry(options.cwd, {
1332
+ timestampMs,
1333
+ factsBefore: result.factsBefore,
1334
+ factsAfter: result.factsAfter,
1335
+ duplicatesRemoved: result.duplicatesRemoved,
1336
+ clustersCreated: result.clustersCreated,
1337
+ notesWritten: result.notesWritten
1338
+ });
1339
+ return result;
1340
+ } catch (cause) {
1341
+ const message = cause instanceof Error ? cause.message : String(cause);
1342
+ process.stderr.write(`[theokit-sdk] dreaming sweep failed: ${message}
1343
+ `);
1344
+ return emptyResult("error");
1345
+ }
1346
+ }
1347
+ async function writeConsolidatedNotes(cwd, clusters, timestampMs) {
1348
+ if (clusters.length === 0) return 0;
1349
+ const notesDir2 = join(memoryDir(cwd), "notes");
1350
+ await mkdir(notesDir2, { recursive: true });
1351
+ const isoSlug = new Date(timestampMs).toISOString().replace(/[^\dT]/g, "-");
1352
+ const file = join(notesDir2, `dreamed-${isoSlug}.md`);
1353
+ const body = deepPhase(clusters, timestampMs);
1354
+ await replaceFileAtomic(file, body);
1355
+ return 1;
1356
+ }
1357
+ function emptyResult(status) {
1358
+ return {
1359
+ status,
1360
+ factsBefore: 0,
1361
+ factsAfter: 0,
1362
+ duplicatesRemoved: 0,
1363
+ clustersCreated: 0,
1364
+ notesWritten: 0,
1365
+ diaryEntryHash: void 0
1366
+ };
1367
+ }
1368
+
1369
+ // src/internal/index-schema.ts
1370
+ var SCHEMA_STATEMENTS = [
1371
+ `CREATE TABLE IF NOT EXISTS files (
1372
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1373
+ path TEXT UNIQUE NOT NULL,
1374
+ rel_path TEXT NOT NULL,
1375
+ mtime INTEGER NOT NULL,
1376
+ hash TEXT NOT NULL,
1377
+ source TEXT NOT NULL DEFAULT 'memory'
1378
+ )`,
1379
+ `CREATE TABLE IF NOT EXISTS chunks (
1380
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1381
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
1382
+ start_line INTEGER NOT NULL,
1383
+ end_line INTEGER NOT NULL,
1384
+ text TEXT NOT NULL,
1385
+ hash TEXT NOT NULL,
1386
+ session_key TEXT
1387
+ )`,
1388
+ `CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
1389
+ text,
1390
+ content='chunks',
1391
+ content_rowid='id'
1392
+ )`,
1393
+ `CREATE TRIGGER IF NOT EXISTS chunks_fts_insert AFTER INSERT ON chunks BEGIN
1394
+ INSERT INTO chunks_fts(rowid, text) VALUES (new.id, new.text);
1395
+ END`,
1396
+ `CREATE TRIGGER IF NOT EXISTS chunks_fts_delete AFTER DELETE ON chunks BEGIN
1397
+ INSERT INTO chunks_fts(chunks_fts, rowid, text) VALUES ('delete', old.id, old.text);
1398
+ END`,
1399
+ `CREATE TABLE IF NOT EXISTS meta (
1400
+ key TEXT PRIMARY KEY,
1401
+ value TEXT
1402
+ )`,
1403
+ `CREATE INDEX IF NOT EXISTS idx_chunks_file_id ON chunks(file_id)`
1404
+ ];
1405
+ var MIGRATION_V2_STATEMENTS = [
1406
+ "ALTER TABLE chunks ADD COLUMN created_at INTEGER",
1407
+ "ALTER TABLE chunks ADD COLUMN importance REAL DEFAULT 0.5",
1408
+ "ALTER TABLE chunks ADD COLUMN scope TEXT DEFAULT '/'"
1409
+ ];
1410
+ var PRAGMA_STATEMENTS = [
1411
+ "PRAGMA synchronous=NORMAL",
1412
+ "PRAGMA foreign_keys=ON"
1413
+ ];
1414
+
1415
+ // src/internal/index-db.ts
1416
+ async function openMemoryDb(opts) {
1417
+ await mkdir(dirname(opts.filePath), { recursive: true });
1418
+ try {
1419
+ return await openConcrete(opts.filePath);
1420
+ } catch (cause) {
1421
+ if (opts.recoverCorrupt !== false && isCorruptionError(cause)) {
1422
+ await renameAside(opts.filePath);
1423
+ return await openConcrete(opts.filePath);
1424
+ }
1425
+ throw cause;
1426
+ }
1427
+ }
1428
+ async function openConcrete(filePath) {
1429
+ const db = await loadDriver(filePath);
1430
+ applyWalWithFallback(db, "memory-index");
1431
+ for (const pragma of PRAGMA_STATEMENTS) db.exec(pragma);
1432
+ for (const stmt of SCHEMA_STATEMENTS) db.exec(stmt);
1433
+ return db;
1434
+ }
1435
+ async function loadDriver(filePath) {
1436
+ try {
1437
+ const mod = await import('better-sqlite3');
1438
+ const Ctor = mod.default ?? mod;
1439
+ const db = new Ctor(filePath);
1440
+ return db;
1441
+ } catch (cause) {
1442
+ const message = cause instanceof Error ? cause.message : String(cause);
1443
+ throw new ConfigurationError(
1444
+ `Failed to load SQLite driver. Install \`better-sqlite3\` or run on Node 22.5+ for built-in \`node:sqlite\`. Cause: ${message}`,
1445
+ { code: "sqlite_driver_unavailable", cause }
1446
+ );
1447
+ }
1448
+ }
1449
+ function isCorruptionError(cause) {
1450
+ if (!(cause instanceof Error)) return false;
1451
+ const msg = cause.message.toLowerCase();
1452
+ return msg.includes("malformed") || msg.includes("not a database") || msg.includes("encrypted") || msg.includes("disk image is malformed");
1453
+ }
1454
+ async function renameAside(filePath) {
1455
+ const asidePath = `${filePath}.corrupt-${Date.now()}`;
1456
+ await rename(filePath, asidePath).catch(() => void 0);
1457
+ await rename(`${filePath}-wal`, `${asidePath}-wal`).catch(() => void 0);
1458
+ await rename(`${filePath}-shm`, `${asidePath}-shm`).catch(() => void 0);
1459
+ process.stderr.write(
1460
+ `[theokit-sdk] memory index corrupt; renamed aside to ${asidePath} and rebuilt schema
1461
+ `
1462
+ );
1463
+ }
1464
+ function defaultIndexPath(cwd) {
1465
+ return join(cwd, ".theokit", "memory", ".index", "memory.sqlite");
1466
+ }
1467
+ function requireLance() {
1468
+ try {
1469
+ const r = createRequire(import.meta.url);
1470
+ return r("@lancedb/lancedb");
1471
+ } catch (cause) {
1472
+ throw new ConfigurationError(
1473
+ 'Lance backend selected but `@lancedb/lancedb` is not installed. Install with: `pnpm add @lancedb/lancedb`. SQLite remains available as the default (omit `backend: "lance"`).',
1474
+ { code: "lance_backend_unavailable", cause }
1475
+ );
1476
+ }
1477
+ }
1478
+ var LanceIndex = class _LanceIndex {
1479
+ constructor(table, embedding, embeddingDim) {
1480
+ this.table = table;
1481
+ this.embedding = embedding;
1482
+ this.embeddingDim = embeddingDim;
1483
+ }
1484
+ table;
1485
+ embedding;
1486
+ embeddingDim;
1487
+ static async open(opts) {
1488
+ const lance = requireLance();
1489
+ const storagePath = opts.storagePath ?? join(opts.cwd, ".theokit", "memory", "lance");
1490
+ mkdirSync(storagePath, { recursive: true });
1491
+ const conn = await lance.connect(storagePath);
1492
+ const dim = opts.embedding.dimension;
1493
+ const tableName = "facts";
1494
+ const existing = await conn.tableNames();
1495
+ let table;
1496
+ if (existing.includes(tableName)) {
1497
+ table = await conn.openTable(tableName);
1498
+ const schema = await table.schema();
1499
+ const embField = schema.fields.find((f) => f.name === "embedding");
1500
+ const existingDim = embField?.type?.listSize ?? embField?.type?.fixedSize;
1501
+ if (typeof existingDim === "number" && existingDim !== dim) {
1502
+ throw new ConfigurationError(
1503
+ `Embedding dimension mismatch in Lance index: storage has ${existingDim}-dim vectors, current provider yields ${dim}-dim. Run \`theokit-migrate-memory\` after switching providers, or use a different storagePath.`,
1504
+ { code: "embedding_dimension_mismatch" }
1505
+ );
1506
+ }
1507
+ } else {
1508
+ const bootstrap = {
1509
+ id: "__bootstrap__",
1510
+ text: "",
1511
+ source: "memory",
1512
+ embedding: new Array(dim).fill(0),
1513
+ namespace: "__bootstrap__",
1514
+ scope: "__bootstrap__",
1515
+ user_id: "__bootstrap__",
1516
+ timestamp: 0
1517
+ };
1518
+ table = await conn.createTable(tableName, [bootstrap]);
1519
+ await table.delete("namespace = '__bootstrap__'");
1520
+ }
1521
+ return new _LanceIndex(table, opts.embedding, dim);
1522
+ }
1523
+ async addFacts(facts) {
1524
+ if (facts.length === 0) return;
1525
+ const texts = facts.map((f) => f.text);
1526
+ const embeddings = await this.embedding.embed(texts);
1527
+ const rows = facts.map((f, i) => ({
1528
+ ...f,
1529
+ embedding: embeddings[i] ?? new Array(this.embeddingDim).fill(0)
1530
+ }));
1531
+ await this.table.add(rows);
1532
+ }
1533
+ /**
1534
+ * Search facts by semantic similarity. EC-1: filters use Lance's
1535
+ * SQL string predicate with `'` → `''` escape — bind parameters are
1536
+ * not supported by Lance 0.30.
1537
+ */
1538
+ async search(query, opts) {
1539
+ const [embedding] = await this.embedding.embed([query]);
1540
+ if (embedding === void 0) return [];
1541
+ const predicates = [`namespace = '${escapeSqlValue(opts.namespace)}'`];
1542
+ if (opts.scope !== void 0) {
1543
+ predicates.push(`scope = '${escapeSqlValue(opts.scope)}'`);
1544
+ }
1545
+ let q = this.table.search(embedding).where(predicates.join(" AND "));
1546
+ if (opts.limit !== void 0) q = q.limit(opts.limit);
1547
+ const results = await q.toArray();
1548
+ return results.filter((r) => opts.sources === void 0 || opts.sources.includes(r.source)).map((r) => ({
1549
+ id: r.id,
1550
+ text: r.text,
1551
+ source: r.source,
1552
+ namespace: r.namespace,
1553
+ scope: r.scope,
1554
+ userId: r.user_id,
1555
+ // Lower _distance = better match; normalize to 0..1 (1=best).
1556
+ score: 1 / (1 + (r._distance ?? 1))
1557
+ }));
1558
+ }
1559
+ async countFacts(namespace) {
1560
+ return this.table.countRows();
1561
+ }
1562
+ async removeFacts(ids) {
1563
+ if (ids.length === 0) return;
1564
+ const escaped = ids.map((id) => `'${id.replace(/'/g, "''")}'`).join(", ");
1565
+ await this.table.delete(`id IN (${escaped})`);
1566
+ }
1567
+ async close() {
1568
+ }
1569
+ };
1570
+ function escapeSqlValue(value) {
1571
+ for (let i = 0; i < value.length; i++) {
1572
+ const code = value.charCodeAt(i);
1573
+ if (code === 0 || code >= 1 && code <= 31 || code === 127) {
1574
+ throw new ConfigurationError(
1575
+ `SQL filter value contains control character at position ${i} (0x${code.toString(16)}). This may indicate an injection attempt.`,
1576
+ { code: "sql_injection_blocked" }
1577
+ );
1578
+ }
1579
+ }
1580
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "''");
1581
+ }
1582
+ function isLanceAvailable() {
1583
+ try {
1584
+ const r = createRequire(import.meta.url);
1585
+ r("@lancedb/lancedb");
1586
+ return true;
1587
+ } catch {
1588
+ return false;
1589
+ }
1590
+ }
1591
+ function lanceStoragePath(cwd) {
1592
+ return join(cwd, ".theokit", "memory", "lance");
1593
+ }
1594
+
1595
+ // src/internal/memory-index.ts
1596
+ function parseSearchOptions(options = {}) {
1597
+ return {
1598
+ maxResults: Math.max(1, options.maxResults ?? 10),
1599
+ minScore: options.minScore ?? 0
1600
+ };
1601
+ }
1602
+
1603
+ // src/internal/lance-memory-adapter.ts
1604
+ var DEFAULT_NAMESPACE = "default";
1605
+ var EMPTY_SYNC_RESULT = Object.freeze({
1606
+ filesScanned: 0,
1607
+ filesUpdated: 0,
1608
+ chunksWritten: 0,
1609
+ chunksEmbedded: 0
1610
+ });
1611
+ var LanceMemoryAdapter = class {
1612
+ constructor(inner) {
1613
+ this.inner = inner;
1614
+ }
1615
+ inner;
1616
+ /**
1617
+ * No-op for Lance — see file header. Returns zero counts so callers'
1618
+ * existing logging (`filesScanned: X`) does not break.
1619
+ */
1620
+ async sync() {
1621
+ return EMPTY_SYNC_RESULT;
1622
+ }
1623
+ // jscpd:ignore-start — search signature + early-out is idiomatic
1624
+ // overlap with IndexManager.search; further factoring would hurt
1625
+ // readability (the bodies diverge sharply right after).
1626
+ async search(query, options = {}) {
1627
+ if (query.trim().length === 0) return [];
1628
+ const { maxResults, minScore } = parseSearchOptions(options);
1629
+ const lanceHits = await this.inner.search(query, {
1630
+ namespace: DEFAULT_NAMESPACE,
1631
+ limit: maxResults * 2,
1632
+ ...options.sources !== void 0 ? { sources: options.sources } : {}
1633
+ });
1634
+ return lanceHits.filter((h) => h.score >= minScore).slice(0, maxResults).map((h) => translateLanceHit(h));
1635
+ }
1636
+ status() {
1637
+ return {
1638
+ backend: "hybrid",
1639
+ filesIndexed: 0,
1640
+ chunksIndexed: 0
1641
+ };
1642
+ }
1643
+ async close() {
1644
+ await this.inner.close();
1645
+ }
1646
+ /**
1647
+ * Escape hatch for advanced callers (migration tool, benchmark script)
1648
+ * that need direct access to addFacts/countFacts/removeFacts.
1649
+ */
1650
+ unwrap() {
1651
+ return this.inner;
1652
+ }
1653
+ };
1654
+ function translateLanceHit(hit) {
1655
+ return {
1656
+ path: hit.id,
1657
+ startLine: 0,
1658
+ endLine: 0,
1659
+ score: hit.score,
1660
+ textScore: 0,
1661
+ vectorScore: hit.score,
1662
+ snippet: hit.text.slice(0, 200),
1663
+ source: hit.source,
1664
+ citation: hit.id
1665
+ };
1666
+ }
1667
+
1668
+ // src/internal/index-manager-dispatch.ts
1669
+ var VALID_BACKENDS = ["sqlite-vec", "lance"];
1670
+ function assertValidBackend(backend) {
1671
+ if (!VALID_BACKENDS.includes(backend)) {
1672
+ throw new ConfigurationError(
1673
+ `Invalid memory backend "${backend}". Valid values: ${VALID_BACKENDS.join(", ")}.`,
1674
+ { code: "invalid_memory_backend" }
1675
+ );
1676
+ }
1677
+ }
1678
+ async function openLanceIndex(opts) {
1679
+ if (opts.embedding === void 0) {
1680
+ throw new ConfigurationError(
1681
+ "Lance backend requires an embedding runtime. Pass `embedding: { provider, model, apiKey }` in Memory.create options (or omit `backend` to use SQLite default).",
1682
+ { code: "lance_requires_embedding" }
1683
+ );
1684
+ }
1685
+ const lance = await LanceIndex.open({
1686
+ cwd: opts.cwd,
1687
+ embedding: opts.embedding,
1688
+ ...opts.filePath !== void 0 ? { storagePath: opts.filePath } : {}
1689
+ });
1690
+ return new LanceMemoryAdapter(lance);
1691
+ }
1692
+ var MAX_TURN_CHARS = 2e3;
1693
+ function sessionsDir(cwd) {
1694
+ return join(memoryDir(cwd), "sessions");
1695
+ }
1696
+ function sessionSummaryPath(cwd, runId) {
1697
+ return join(sessionsDir(cwd), `${sanitizeRunId2(runId)}.md`);
1698
+ }
1699
+ function sanitizeRunId2(runId) {
1700
+ return runId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128);
1701
+ }
1702
+ function truncate2(text) {
1703
+ if (text.length <= MAX_TURN_CHARS) return text;
1704
+ return `${text.slice(0, MAX_TURN_CHARS)}\u2026`;
1705
+ }
1706
+ async function writeSessionSummary(input) {
1707
+ if (input.status !== "finished") return;
1708
+ const path = sessionSummaryPath(input.cwd, input.runId);
1709
+ await mkdir(sessionsDir(input.cwd), { recursive: true });
1710
+ const safeUser = redactSecrets(truncate2(input.userText));
1711
+ const safeAssistant = redactSecrets(truncate2(input.assistantText));
1712
+ const iso = new Date(input.at).toISOString();
1713
+ const body = [
1714
+ "---",
1715
+ `runId: ${input.runId}`,
1716
+ `agentId: ${input.agentId}`,
1717
+ `at: ${iso}`,
1718
+ `status: ${input.status}`,
1719
+ "---",
1720
+ "",
1721
+ "## User",
1722
+ "",
1723
+ safeUser,
1724
+ "",
1725
+ "## Assistant",
1726
+ "",
1727
+ safeAssistant,
1728
+ ""
1729
+ ].join("\n");
1730
+ await replaceFileAtomic(path, body);
1731
+ }
1732
+
1733
+ // src/internal/session-loader.ts
1734
+ async function discoverSessionFiles(cwd) {
1735
+ let entries;
1736
+ try {
1737
+ entries = await readdir(sessionsDir(cwd));
1738
+ } catch {
1739
+ return [];
1740
+ }
1741
+ const root = memoryDir(cwd);
1742
+ return entries.filter((entry) => entry.endsWith(".md")).map((entry) => {
1743
+ const absolutePath = join(sessionsDir(cwd), entry);
1744
+ return {
1745
+ absolutePath,
1746
+ relPath: relativeToRoot(root, absolutePath)
1747
+ };
1748
+ });
1749
+ }
1750
+ function relativeToRoot(root, absolutePath) {
1751
+ if (absolutePath.startsWith(`${root}/`)) return absolutePath.slice(root.length + 1);
1752
+ return absolutePath;
1753
+ }
1754
+ async function loadSqliteVecExtension(db) {
1755
+ try {
1756
+ const mod = await import('sqlite-vec');
1757
+ const loadFn = mod.load;
1758
+ if (typeof loadFn !== "function") {
1759
+ throw new Error("sqlite-vec module exposes no `load` export");
1760
+ }
1761
+ loadFn(db);
1762
+ } catch (cause) {
1763
+ const message = cause instanceof Error ? cause.message : String(cause);
1764
+ throw new ConfigurationError(
1765
+ `sqlite-vec extension unavailable. Install \`sqlite-vec\` and ensure the native binary matches your Node + OS. Cause: ${message}`,
1766
+ { code: "sqlite_vec_unavailable", cause }
1767
+ );
1768
+ }
1769
+ }
1770
+ function isSqliteVecLoaded(db) {
1771
+ try {
1772
+ const row = db.prepare("SELECT vec_version() as v").get();
1773
+ return row !== void 0 && row.v !== void 0;
1774
+ } catch {
1775
+ return false;
1776
+ }
1777
+ }
1778
+
1779
+ // src/internal/vec-index.ts
1780
+ var META_KEY_PROVIDER_ID = "embedding.providerId";
1781
+ var META_KEY_MODEL = "embedding.model";
1782
+ var META_KEY_DIMENSION = "embedding.dimension";
1783
+ function readEmbeddingIdentity(db) {
1784
+ const get = (key) => {
1785
+ const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
1786
+ return row !== void 0 ? String(row.value) : void 0;
1787
+ };
1788
+ const providerId = get(META_KEY_PROVIDER_ID);
1789
+ const model = get(META_KEY_MODEL);
1790
+ const dimRaw = get(META_KEY_DIMENSION);
1791
+ if (providerId === void 0 || model === void 0 || dimRaw === void 0) return void 0;
1792
+ const dimension = Number(dimRaw);
1793
+ if (!Number.isFinite(dimension) || dimension <= 0) return void 0;
1794
+ return { providerId, model, dimension };
1795
+ }
1796
+ function writeEmbeddingIdentity(db, identity) {
1797
+ const stmt = db.prepare(
1798
+ "INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
1799
+ );
1800
+ stmt.run(META_KEY_PROVIDER_ID, identity.providerId);
1801
+ stmt.run(META_KEY_MODEL, identity.model);
1802
+ stmt.run(META_KEY_DIMENSION, String(identity.dimension));
1803
+ }
1804
+ function identityMatches(a, b) {
1805
+ return a.providerId === b.providerId && a.model === b.model && a.dimension === b.dimension;
1806
+ }
1807
+ function dropVectorIndex(db) {
1808
+ db.exec("DROP TABLE IF EXISTS embeddings");
1809
+ }
1810
+ function createVectorIndex(db, dimension) {
1811
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS embeddings USING vec0(
1812
+ chunk_id INTEGER PRIMARY KEY,
1813
+ vec FLOAT[${dimension}]
1814
+ )`);
1815
+ }
1816
+ function packVector(vec) {
1817
+ const f32 = new Float32Array(vec.length);
1818
+ for (let i = 0; i < vec.length; i++) f32[i] = vec[i] ?? 0;
1819
+ return Buffer.from(f32.buffer);
1820
+ }
1821
+ function upsertEmbedding(db, chunkId, vec) {
1822
+ const id = BigInt(chunkId);
1823
+ db.prepare("DELETE FROM embeddings WHERE chunk_id = ?").run(id);
1824
+ db.prepare("INSERT INTO embeddings (chunk_id, vec) VALUES (?, ?)").run(id, packVector(vec));
1825
+ }
1826
+ function vectorSearch(db, query, k) {
1827
+ const rows = db.prepare(
1828
+ `SELECT chunk_id, distance FROM embeddings
1829
+ WHERE vec MATCH ? AND k = ?
1830
+ ORDER BY distance`
1831
+ ).all(packVector(query), BigInt(k));
1832
+ return rows.map((row) => ({ chunk_id: Number(row.chunk_id), distance: Number(row.distance) }));
1833
+ }
1834
+ async function embedMissingChunks(args) {
1835
+ const rows = args.db.prepare(
1836
+ `SELECT chunks.id as id, chunks.text as text
1837
+ FROM chunks
1838
+ LEFT JOIN embeddings ON embeddings.chunk_id = chunks.id
1839
+ WHERE embeddings.chunk_id IS NULL`
1840
+ ).all();
1841
+ if (rows.length === 0) return 0;
1842
+ const normalized = rows.map((row) => ({ id: Number(row.id), text: String(row.text ?? "") }));
1843
+ const vectors = await args.runtime.embed(normalized.map((r) => r.text));
1844
+ for (let i = 0; i < normalized.length; i++) {
1845
+ const row = normalized[i];
1846
+ const vector = vectors[i];
1847
+ if (row === void 0 || vector === void 0) continue;
1848
+ upsertEmbedding(args.db, row.id, vector);
1849
+ }
1850
+ return normalized.length;
1851
+ }
1852
+ function wikiDir(cwd) {
1853
+ return join(memoryDir(cwd), "wiki");
1854
+ }
1855
+ async function discoverWikiFiles(cwd) {
1856
+ let entries;
1857
+ try {
1858
+ entries = await readdir(wikiDir(cwd));
1859
+ } catch {
1860
+ return [];
1861
+ }
1862
+ const root = memoryDir(cwd);
1863
+ return entries.filter((entry) => entry.endsWith(".md")).map((entry) => ({
1864
+ absolutePath: join(wikiDir(cwd), entry),
1865
+ relPath: join("wiki", entry)
1866
+ })).map((file) => ({
1867
+ absolutePath: file.absolutePath,
1868
+ relPath: relativeToRoot2(root, file.absolutePath)
1869
+ }));
1870
+ }
1871
+ function relativeToRoot2(root, absolutePath) {
1872
+ if (absolutePath.startsWith(`${root}/`)) return absolutePath.slice(root.length + 1);
1873
+ return absolutePath;
1874
+ }
1875
+
1876
+ // src/internal/index-manager.ts
1877
+ var IndexManager = class _IndexManager {
1878
+ constructor(cwd, db, embedding) {
1879
+ this.cwd = cwd;
1880
+ this.db = db;
1881
+ this.embedding = embedding;
1882
+ }
1883
+ cwd;
1884
+ db;
1885
+ embedding;
1886
+ lastSyncMs;
1887
+ vectorReady = false;
1888
+ static async open(opts) {
1889
+ const backend = opts.backend ?? "sqlite-vec";
1890
+ assertValidBackend(backend);
1891
+ if (backend === "lance") return await openLanceIndex(opts);
1892
+ return await _IndexManager.openSqliteInternal(opts);
1893
+ }
1894
+ /** Internal SQLite-path open. Renamed from previous public `open`. */
1895
+ static async openSqliteInternal(opts) {
1896
+ const filePath = opts.filePath ?? defaultIndexPath(opts.cwd);
1897
+ const db = await openMemoryDb({ filePath });
1898
+ const manager = new _IndexManager(opts.cwd, db, opts.embedding);
1899
+ if (opts.embedding !== void 0) await manager.initVectorBackend(opts.embedding);
1900
+ return manager;
1901
+ }
1902
+ async initVectorBackend(runtime) {
1903
+ await loadSqliteVecExtension(this.db);
1904
+ const currentIdentity = {
1905
+ providerId: runtime.id,
1906
+ model: runtime.model,
1907
+ dimension: runtime.dimension
1908
+ };
1909
+ const persisted = readEmbeddingIdentity(this.db);
1910
+ if (persisted !== void 0 && !identityMatches(persisted, currentIdentity)) {
1911
+ dropVectorIndex(this.db);
1912
+ }
1913
+ createVectorIndex(this.db, runtime.dimension);
1914
+ writeEmbeddingIdentity(this.db, currentIdentity);
1915
+ this.vectorReady = true;
1916
+ }
1917
+ /** Walk the memory corpus + (re)index changed files. */
1918
+ async sync() {
1919
+ const files = await collectMarkdownFiles(this.cwd);
1920
+ let filesUpdated = 0;
1921
+ let chunksWritten = 0;
1922
+ const existingByPath = this.loadFilesIndex();
1923
+ for (const entry of files) {
1924
+ const raw = await readFile(entry.absolutePath, "utf8");
1925
+ const hash = sha256(raw);
1926
+ const existing = existingByPath.get(entry.absolutePath);
1927
+ if (existing !== void 0 && existing.hash === hash) continue;
1928
+ const stats = await stat(entry.absolutePath);
1929
+ const fileId = this.upsertFile(
1930
+ entry.absolutePath,
1931
+ entry.relPath,
1932
+ hash,
1933
+ stats.mtimeMs,
1934
+ entry.source
1935
+ );
1936
+ this.deleteChunksForFile(fileId);
1937
+ const chunks = chunkMarkdown(raw);
1938
+ for (const chunk of chunks) {
1939
+ this.insertChunk(fileId, chunk.startLine, chunk.endLine, chunk.text, chunk.hash);
1940
+ }
1941
+ filesUpdated += 1;
1942
+ chunksWritten += chunks.length;
1943
+ }
1944
+ let chunksEmbedded = 0;
1945
+ if (this.vectorReady && this.embedding !== void 0) {
1946
+ chunksEmbedded = await embedMissingChunks({ db: this.db, runtime: this.embedding });
1947
+ }
1948
+ this.lastSyncMs = Date.now();
1949
+ return { filesScanned: files.length, filesUpdated, chunksWritten, chunksEmbedded };
1950
+ }
1951
+ async search(query, options = {}) {
1952
+ if (query.trim().length === 0) return [];
1953
+ const { maxResults, minScore } = parseSearchOptions(options);
1954
+ const textHits = this.ftsSearch(query, maxResults * 2);
1955
+ const vectorHitsById = await this.vectorSearchById(query, maxResults * 2);
1956
+ const combined = this.combineHybridScores(textHits, vectorHitsById, options);
1957
+ return combined.filter((h) => h.score >= minScore).filter((h) => options.sources === void 0 || options.sources.includes(h.source)).slice(0, maxResults);
1958
+ }
1959
+ status() {
1960
+ const files = this.db.prepare("SELECT COUNT(*) as n FROM files").get() ?? { n: 0 };
1961
+ const chunks = this.db.prepare("SELECT COUNT(*) as n FROM chunks").get() ?? { n: 0 };
1962
+ const status = {
1963
+ backend: this.vectorReady ? "hybrid" : "fts-only",
1964
+ filesIndexed: Number(files.n ?? 0),
1965
+ chunksIndexed: Number(chunks.n ?? 0)
1966
+ };
1967
+ if (this.lastSyncMs !== void 0) status.lastSyncMs = this.lastSyncMs;
1968
+ return status;
1969
+ }
1970
+ // ───── search internals ──────────────────────────────────────────────
1971
+ ftsSearch(query, limit) {
1972
+ const sanitized = sanitizeFts5Query(query);
1973
+ if (sanitized.length === 0) return [];
1974
+ const stmt = this.db.prepare(
1975
+ `SELECT chunks.id as id, files.rel_path as rel_path, files.source as source,
1976
+ chunks.start_line as start_line, chunks.end_line as end_line,
1977
+ chunks.text as text, bm25(chunks_fts) as bm25_score
1978
+ FROM chunks_fts
1979
+ JOIN chunks ON chunks_fts.rowid = chunks.id
1980
+ JOIN files ON chunks.file_id = files.id
1981
+ WHERE chunks_fts MATCH ?
1982
+ ORDER BY bm25_score
1983
+ LIMIT ?`
1984
+ );
1985
+ let rows = [];
1986
+ try {
1987
+ rows = stmt.all(sanitized, limit);
1988
+ } catch {
1989
+ return [];
1990
+ }
1991
+ return rows.map((row) => {
1992
+ const bm25 = Number(row.bm25_score ?? 0);
1993
+ const textScore = bm25 < 0 ? 1 / (1 + Math.abs(bm25)) : 1 / (1 + bm25);
1994
+ const startLine = Number(row.start_line ?? 0);
1995
+ const endLine = Number(row.end_line ?? 0);
1996
+ const path = String(row.rel_path);
1997
+ return {
1998
+ path,
1999
+ startLine,
2000
+ endLine,
2001
+ score: textScore,
2002
+ textScore,
2003
+ snippet: truncateSnippet(String(row.text ?? "")),
2004
+ source: String(row.source),
2005
+ citation: `${path}:${startLine}-${endLine}`,
2006
+ // sneak the chunk id through for the hybrid join below
2007
+ // (cast away later)
2008
+ chunkId: Number(row.id)
2009
+ };
2010
+ });
2011
+ }
2012
+ async vectorSearchById(query, limit) {
2013
+ if (!this.vectorReady || this.embedding === void 0) return /* @__PURE__ */ new Map();
2014
+ const [queryVec] = await this.embedding.embed([query]);
2015
+ if (queryVec === void 0) return /* @__PURE__ */ new Map();
2016
+ const rows = vectorSearch(this.db, queryVec, limit);
2017
+ const out = /* @__PURE__ */ new Map();
2018
+ for (const row of rows) {
2019
+ const score = 1 / (1 + Math.max(0, row.distance));
2020
+ out.set(row.chunk_id, { vectorScore: score });
2021
+ }
2022
+ return out;
2023
+ }
2024
+ combineHybridScores(textHits, vectorHitsById, options) {
2025
+ const weights = resolveWeights(options);
2026
+ const merged = this.mergeHits(textHits, vectorHitsById);
2027
+ const combined = [...merged.values()].map(
2028
+ (hit) => blendScores(hit, vectorHitsById.get(hit.chunkId)?.vectorScore ?? 0, weights)
2029
+ );
2030
+ return combined.sort((a, b) => b.score - a.score);
2031
+ }
2032
+ mergeHits(textHits, vectorHitsById) {
2033
+ const merged = /* @__PURE__ */ new Map();
2034
+ for (const hit of textHits) merged.set(hit.chunkId, hit);
2035
+ const missingIds = [...vectorHitsById.keys()].filter((id) => !merged.has(id));
2036
+ if (missingIds.length > 0) {
2037
+ for (const hit of this.fetchChunksByIds(missingIds)) {
2038
+ merged.set(hit.chunkId, { ...hit, score: 0, textScore: 0 });
2039
+ }
2040
+ }
2041
+ return merged;
2042
+ }
2043
+ fetchChunksByIds(ids) {
2044
+ if (ids.length === 0) return [];
2045
+ const placeholders = ids.map(() => "?").join(",");
2046
+ const rows = this.db.prepare(
2047
+ `SELECT chunks.id as id, files.rel_path as rel_path, files.source as source,
2048
+ chunks.start_line as start_line, chunks.end_line as end_line,
2049
+ chunks.text as text
2050
+ FROM chunks JOIN files ON chunks.file_id = files.id
2051
+ WHERE chunks.id IN (${placeholders})`
2052
+ ).all(...ids);
2053
+ return rows.map((row) => {
2054
+ const startLine = Number(row.start_line ?? 0);
2055
+ const endLine = Number(row.end_line ?? 0);
2056
+ const path = String(row.rel_path);
2057
+ return {
2058
+ chunkId: Number(row.id),
2059
+ path,
2060
+ startLine,
2061
+ endLine,
2062
+ score: 0,
2063
+ textScore: 0,
2064
+ snippet: truncateSnippet(String(row.text ?? "")),
2065
+ source: String(row.source),
2066
+ citation: `${path}:${startLine}-${endLine}`
2067
+ };
2068
+ });
2069
+ }
2070
+ // ───── persistence helpers ─────────────────────────────────────────
2071
+ loadFilesIndex() {
2072
+ const rows = this.db.prepare("SELECT id, path, hash FROM files").all();
2073
+ return new Map(rows.map((row) => [row.path, { id: row.id, hash: row.hash }]));
2074
+ }
2075
+ upsertFile(absPath, relPath, hash, mtimeMs, source = "memory") {
2076
+ const stmt = this.db.prepare(
2077
+ `INSERT INTO files (path, rel_path, mtime, hash, source) VALUES (?, ?, ?, ?, ?)
2078
+ ON CONFLICT(path) DO UPDATE SET hash = excluded.hash, mtime = excluded.mtime, source = excluded.source
2079
+ RETURNING id`
2080
+ );
2081
+ const row = stmt.get(absPath, relPath, Math.floor(mtimeMs), hash, source);
2082
+ return row.id;
2083
+ }
2084
+ deleteChunksForFile(fileId) {
2085
+ this.db.prepare("DELETE FROM chunks WHERE file_id = ?").run(fileId);
2086
+ }
2087
+ insertChunk(fileId, startLine, endLine, text, hash) {
2088
+ this.db.prepare(
2089
+ "INSERT INTO chunks (file_id, start_line, end_line, text, hash) VALUES (?, ?, ?, ?, ?)"
2090
+ ).run(fileId, startLine, endLine, text, hash);
2091
+ }
2092
+ close() {
2093
+ this.db.close();
2094
+ }
2095
+ };
2096
+ function resolveWeights(options) {
2097
+ const vectorWeight = options.vectorWeight ?? 0.6;
2098
+ const textWeight = options.textWeight ?? 0.4;
2099
+ const total = vectorWeight + textWeight || 1;
2100
+ return { vectorWeight, textWeight, total };
2101
+ }
2102
+ function blendScores(hit, vectorScore, weights) {
2103
+ const score = (vectorScore * weights.vectorWeight + hit.textScore * weights.textWeight) / weights.total;
2104
+ return {
2105
+ path: hit.path,
2106
+ startLine: hit.startLine,
2107
+ endLine: hit.endLine,
2108
+ score,
2109
+ textScore: hit.textScore,
2110
+ snippet: hit.snippet,
2111
+ source: hit.source,
2112
+ citation: hit.citation,
2113
+ ...vectorScore > 0 ? { vectorScore } : {}
2114
+ };
2115
+ }
2116
+ async function collectMarkdownFiles(cwd) {
2117
+ const root = memoryDir(cwd);
2118
+ const results = [];
2119
+ try {
2120
+ await stat(memoryMdPath(cwd));
2121
+ results.push({
2122
+ absolutePath: memoryMdPath(cwd),
2123
+ relPath: relative(root, memoryMdPath(cwd)),
2124
+ source: "memory"
2125
+ });
2126
+ } catch {
2127
+ }
2128
+ try {
2129
+ const entries = await readdir(notesDir(cwd));
2130
+ for (const entry of entries) {
2131
+ if (!entry.endsWith(".md")) continue;
2132
+ const abs = join(notesDir(cwd), entry);
2133
+ results.push({ absolutePath: abs, relPath: relative(root, abs), source: "memory" });
2134
+ }
2135
+ } catch {
2136
+ }
2137
+ const wikiFiles = await discoverWikiFiles(cwd);
2138
+ for (const wiki of wikiFiles) {
2139
+ results.push({
2140
+ absolutePath: wiki.absolutePath,
2141
+ relPath: wiki.relPath,
2142
+ source: "wiki"
2143
+ });
2144
+ }
2145
+ const sessionFiles = await discoverSessionFiles(cwd);
2146
+ for (const session of sessionFiles) {
2147
+ results.push({
2148
+ absolutePath: session.absolutePath,
2149
+ relPath: session.relPath,
2150
+ source: "sessions"
2151
+ });
2152
+ }
2153
+ return results;
2154
+ }
2155
+ function sha256(text) {
2156
+ return createHash("sha256").update(text).digest("hex");
2157
+ }
2158
+ function truncateSnippet(text) {
2159
+ const max = 500;
2160
+ return text.length <= max ? text : `${text.slice(0, max)}\u2026`;
2161
+ }
2162
+ async function readAllSqliteFacts(cwd) {
2163
+ const dbPath = defaultIndexPath(cwd);
2164
+ if (!existsSync(dbPath)) return [];
2165
+ const db = await openMemoryDb({ filePath: dbPath });
2166
+ try {
2167
+ const stmt = db.prepare("SELECT id, path, source, start_line, end_line, text FROM chunks");
2168
+ const rows = stmt.all();
2169
+ return rows.map((r) => ({
2170
+ ...r,
2171
+ namespace: "default",
2172
+ scope: "agent",
2173
+ user_id: "default"
2174
+ }));
2175
+ } finally {
2176
+ db.close();
2177
+ }
2178
+ }
2179
+ function nfcEqual(a, b) {
2180
+ return a.normalize("NFC") === b.normalize("NFC");
2181
+ }
2182
+ async function migrateSqliteToLance(opts) {
2183
+ const cwd = opts.cwd;
2184
+ const finalPath = lanceStoragePath(cwd);
2185
+ const newPath = join(cwd, ".theokit", "memory", "lance-new");
2186
+ const rawLog = opts.logger ?? ((m) => console.log(m));
2187
+ const log = (m) => rawLog(redactSecrets(m));
2188
+ if (existsSync(finalPath)) {
2189
+ throw new ConfigurationError(
2190
+ `Destination already exists: ${finalPath}. Remove it manually (\`rm -rf ${finalPath}\`) and re-run.`,
2191
+ { code: "migration_destination_exists" }
2192
+ );
2193
+ }
2194
+ if (existsSync(newPath)) {
2195
+ log(`Removing stale ${newPath} from a previous run...`);
2196
+ rmSync(newPath, { recursive: true, force: true });
2197
+ }
2198
+ log(`Reading SQLite facts from ${cwd}/.theokit/memory/index.sqlite ...`);
2199
+ const sqliteFacts = await readAllSqliteFacts(cwd);
2200
+ log(`SQLite has ${sqliteFacts.length} facts.`);
2201
+ if (sqliteFacts.length === 0) {
2202
+ return {
2203
+ countSqlite: 0,
2204
+ countLance: 0,
2205
+ validated: true,
2206
+ sampleComparisons: [],
2207
+ lancePath: finalPath,
2208
+ committed: false
2209
+ };
2210
+ }
2211
+ const placeholderEmbedding = {
2212
+ id: "migration-placeholder",
2213
+ model: "deterministic-hash",
2214
+ dimension: 8,
2215
+ embed: async (texts) => {
2216
+ return texts.map((t) => {
2217
+ const arr = new Array(8).fill(0);
2218
+ for (let i = 0; i < t.length; i += 1) {
2219
+ arr[i % 8] += t.charCodeAt(i);
2220
+ }
2221
+ return arr.map((x) => x / 1e3);
2222
+ });
2223
+ },
2224
+ stats: () => ({ cacheHits: 0, cacheMisses: 0, httpCalls: 0, retries: 0 })
2225
+ };
2226
+ mkdirSync(newPath, { recursive: true });
2227
+ log(`Writing Lance index to ${newPath} ...`);
2228
+ const lance = await LanceIndex.open({
2229
+ cwd,
2230
+ embedding: placeholderEmbedding,
2231
+ storagePath: newPath
2232
+ });
2233
+ const batchSize = opts.batchSize ?? 100;
2234
+ for (let i = 0; i < sqliteFacts.length; i += batchSize) {
2235
+ const batch = sqliteFacts.slice(i, i + batchSize).map((f) => ({
2236
+ id: f.id,
2237
+ text: f.text,
2238
+ source: f.source,
2239
+ namespace: f.namespace ?? "default",
2240
+ scope: f.scope ?? "agent",
2241
+ user_id: f.user_id ?? "default",
2242
+ timestamp: 0
2243
+ }));
2244
+ await lance.addFacts(batch);
2245
+ log(` Migrated ${Math.min(i + batchSize, sqliteFacts.length)}/${sqliteFacts.length}`);
2246
+ }
2247
+ const lanceCount = await lance.countFacts("default");
2248
+ log(`Lance has ${lanceCount} facts (expected ${sqliteFacts.length}).`);
2249
+ const sampleSize = Math.min(10, sqliteFacts.length);
2250
+ const sampleComparisons = [];
2251
+ for (let i = 0; i < sampleSize; i += 1) {
2252
+ const sqliteFact = sqliteFacts[Math.floor(i * sqliteFacts.length / sampleSize)];
2253
+ if (sqliteFact === void 0) continue;
2254
+ const hits = await lance.search(sqliteFact.text, {
2255
+ namespace: sqliteFact.namespace ?? "default",
2256
+ limit: 1
2257
+ });
2258
+ const hit = hits[0];
2259
+ const match = hit !== void 0 && nfcEqual(hit.text, sqliteFact.text);
2260
+ sampleComparisons.push({ id: sqliteFact.id, match });
2261
+ }
2262
+ const allMatch = sampleComparisons.every((c) => c.match);
2263
+ const countMatch = lanceCount === sqliteFacts.length;
2264
+ const validated = allMatch && countMatch;
2265
+ await lance.close();
2266
+ if (opts.dryRun === true) {
2267
+ log("Dry-run mode \u2014 discarding Lance staging dir.");
2268
+ rmSync(newPath, { recursive: true, force: true });
2269
+ return {
2270
+ countSqlite: sqliteFacts.length,
2271
+ countLance: lanceCount,
2272
+ validated,
2273
+ sampleComparisons,
2274
+ lancePath: finalPath,
2275
+ committed: false
2276
+ };
2277
+ }
2278
+ if (!validated) {
2279
+ log("Validation FAILED \u2014 leaving SQLite intact, removing Lance staging dir.");
2280
+ rmSync(newPath, { recursive: true, force: true });
2281
+ return {
2282
+ countSqlite: sqliteFacts.length,
2283
+ countLance: lanceCount,
2284
+ validated: false,
2285
+ sampleComparisons,
2286
+ lancePath: finalPath,
2287
+ committed: false
2288
+ };
2289
+ }
2290
+ renameSync(newPath, finalPath);
2291
+ log(`Migration committed to ${finalPath}.`);
2292
+ return {
2293
+ countSqlite: sqliteFacts.length,
2294
+ countLance: lanceCount,
2295
+ validated: true,
2296
+ sampleComparisons,
2297
+ lancePath: finalPath,
2298
+ committed: true
2299
+ };
2300
+ }
2301
+ async function fileExists(path) {
2302
+ try {
2303
+ await access(path);
2304
+ return true;
2305
+ } catch {
2306
+ return false;
2307
+ }
2308
+ }
2309
+ async function readLegacyFacts(jsonPath) {
2310
+ try {
2311
+ const raw = await readFile(jsonPath, "utf8");
2312
+ const parsed = JSON.parse(raw);
2313
+ return Array.isArray(parsed.facts) ? parsed.facts : [];
2314
+ } catch {
2315
+ return void 0;
2316
+ }
2317
+ }
2318
+ async function writeMigratedFacts(cwd, jsonPath, facts) {
2319
+ try {
2320
+ for (const fact of facts) await appendFactToMarkdown(cwd, fact);
2321
+ await unlink(jsonPath).catch(() => void 0);
2322
+ process.stderr.write(
2323
+ `[theokit-sdk] migrated ${facts.length} fact(s) from ${jsonPath} to MEMORY.md
2324
+ `
2325
+ );
2326
+ return { migrated: true, factCount: facts.length };
2327
+ } catch (cause) {
2328
+ const message = cause instanceof Error ? cause.message : String(cause);
2329
+ process.stderr.write(`[theokit-sdk] memory migration failed (readonly fs?): ${message}
2330
+ `);
2331
+ return { migrated: false, factCount: 0, reason: "readonly-fs" };
2332
+ }
2333
+ }
2334
+ var migrationRun = /* @__PURE__ */ new Set();
2335
+ async function migrateLegacyJson(cwd, config) {
2336
+ const key = `${cwd}::${config.namespace ?? "default"}::${config.scope ?? "agent"}::${config.userId ?? "default"}`;
2337
+ if (migrationRun.has(key)) return { migrated: false, factCount: 0, reason: "already-migrated" };
2338
+ migrationRun.add(key);
2339
+ const jsonPath = legacyMemoryJsonPath(cwd, config);
2340
+ if (!await fileExists(jsonPath)) {
2341
+ return { migrated: false, factCount: 0, reason: "no-legacy-json" };
2342
+ }
2343
+ if (await fileExists(memoryMdPath(cwd))) {
2344
+ process.stderr.write(
2345
+ `[theokit-sdk] memory migration skipped: both MEMORY.md and legacy JSON exist at ${jsonPath}; leaving both intact
2346
+ `
2347
+ );
2348
+ return { migrated: false, factCount: 0, reason: "markdown-exists" };
2349
+ }
2350
+ const facts = await readLegacyFacts(jsonPath);
2351
+ if (facts === void 0) return { migrated: false, factCount: 0, reason: "no-legacy-json" };
2352
+ return writeMigratedFacts(cwd, jsonPath, facts);
2353
+ }
2354
+ function resetMigrationStateForTests() {
2355
+ migrationRun.clear();
2356
+ }
2357
+ var DEFAULT_MEMORY_READ_LINES = 200;
2358
+ async function readMemoryFileBounded(opts) {
2359
+ const absolutePath = resolve(opts.cwd, opts.relPath);
2360
+ const raw = await readFile(absolutePath, "utf8");
2361
+ const allLines = raw.split("\n");
2362
+ const trimmedLines = allLines.length > 0 && allLines[allLines.length - 1] === "" ? allLines.slice(0, -1) : allLines;
2363
+ const totalLines = trimmedLines.length;
2364
+ const requestedFrom = Math.max(1, opts.from ?? 1);
2365
+ const requestedLines = Math.max(1, opts.lines ?? DEFAULT_MEMORY_READ_LINES);
2366
+ const startIdx = requestedFrom - 1;
2367
+ const endIdx = Math.min(totalLines, startIdx + requestedLines);
2368
+ const slice = trimmedLines.slice(startIdx, endIdx);
2369
+ const linesReturned = slice.length;
2370
+ const remainingLines = Math.max(0, totalLines - endIdx);
2371
+ const truncated = remainingLines > 0;
2372
+ return {
2373
+ path: opts.relPath,
2374
+ from: requestedFrom,
2375
+ linesReturned,
2376
+ totalLines,
2377
+ truncated,
2378
+ remainingLines,
2379
+ text: slice.join("\n")
2380
+ };
2381
+ }
2382
+ var SEARCH_DESCRIPTION = "Mandatory recall step: semantically search MEMORY.md + memory/notes/*.md (and optional wiki supplements) before answering questions about prior work, decisions, dates, people, preferences, or todos. `corpus=memory` (default) restricts to indexed memory files; `corpus=wiki` restricts to read-only wiki supplements; `corpus=all` returns both. Returns ranked `{ path, startLine, endLine, score, snippet, citation }` hits.";
2383
+ var GET_DESCRIPTION = "Safe exact excerpt read from MEMORY.md or memory/notes/*.md. Defaults to a bounded excerpt (200 lines). Returns `{ path, from, linesReturned, totalLines, truncated, remainingLines, text }`. Paths are resolved against the memory root \u2014 attempts to read outside `.theokit/memory/` are rejected.";
2384
+ var MEMORY_SEARCH_SCHEMA = {
2385
+ type: "object",
2386
+ required: ["query"],
2387
+ properties: {
2388
+ query: { type: "string", description: "Natural-language query." },
2389
+ maxResults: { type: "integer", minimum: 1, maximum: 50 },
2390
+ minScore: { type: "number", minimum: 0, maximum: 1 },
2391
+ corpus: { type: "string", enum: ["memory", "sessions", "wiki", "all"] }
2392
+ },
2393
+ additionalProperties: false
2394
+ };
2395
+ var MEMORY_GET_SCHEMA = {
2396
+ type: "object",
2397
+ required: ["path"],
2398
+ properties: {
2399
+ path: { type: "string", description: "Path relative to .theokit/memory/" },
2400
+ from: { type: "integer", minimum: 1 },
2401
+ lines: { type: "integer", minimum: 1, maximum: 2e3 }
2402
+ },
2403
+ additionalProperties: false
2404
+ };
2405
+ var DEFAULT_MAX_TOTAL_CHARS = 16384;
2406
+ function createMemorySearchTool(opts) {
2407
+ const cap = opts.maxTotalChars ?? DEFAULT_MAX_TOTAL_CHARS;
2408
+ return {
2409
+ name: "memory_search",
2410
+ description: SEARCH_DESCRIPTION,
2411
+ inputSchema: MEMORY_SEARCH_SCHEMA,
2412
+ async execute(input) {
2413
+ const query = readString(input.query, "query is required");
2414
+ const maxResults = readNumber(input.maxResults) ?? 10;
2415
+ const minScore = readNumber(input.minScore);
2416
+ const corpus = readString(input.corpus, void 0, true);
2417
+ const sources = mapCorpusToSources(corpus);
2418
+ const searchOptions = {
2419
+ maxResults,
2420
+ ...minScore !== void 0 ? { minScore } : {},
2421
+ ...sources !== void 0 ? { sources } : {}
2422
+ };
2423
+ const hits = await opts.index.search(query, searchOptions);
2424
+ return JSON.stringify(capByTotalChars(hits, cap));
2425
+ }
2426
+ };
2427
+ }
2428
+ function createMemoryGetTool(opts) {
2429
+ const memoryRoot = resolve(memoryDir(opts.cwd));
2430
+ return {
2431
+ name: "memory_get",
2432
+ description: GET_DESCRIPTION,
2433
+ inputSchema: MEMORY_GET_SCHEMA,
2434
+ async execute(input) {
2435
+ const relPath = readString(input.path, "path is required");
2436
+ const from = readNumber(input.from);
2437
+ const lines = readNumber(input.lines);
2438
+ const resolved = resolve(memoryRoot, relPath);
2439
+ if (!isPathInside(memoryRoot, resolved)) {
2440
+ throw new ConfigurationError(
2441
+ `memory_get rejected path that escapes memory root: ${relPath}`,
2442
+ { code: "memory_path_escapes_root" }
2443
+ );
2444
+ }
2445
+ const readArgs = {
2446
+ cwd: memoryRoot,
2447
+ relPath
2448
+ };
2449
+ if (from !== void 0) readArgs.from = from;
2450
+ if (lines !== void 0) readArgs.lines = lines;
2451
+ const result = await readMemoryFileBounded(readArgs);
2452
+ return JSON.stringify(result);
2453
+ }
2454
+ };
2455
+ }
2456
+ function readString(value, errorIfMissing, allowUndefined = false) {
2457
+ if (typeof value === "string") return value;
2458
+ if (allowUndefined && (value === void 0 || value === null)) return "";
2459
+ if (errorIfMissing !== void 0) {
2460
+ throw new ConfigurationError(errorIfMissing, { code: "memory_tool_bad_args" });
2461
+ }
2462
+ return "";
2463
+ }
2464
+ function readNumber(value) {
2465
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2466
+ if (typeof value === "string" && value.trim().length > 0) {
2467
+ const n = Number(value);
2468
+ if (Number.isFinite(n)) return n;
2469
+ }
2470
+ return void 0;
2471
+ }
2472
+ function mapCorpusToSources(corpus) {
2473
+ if (corpus === "" || corpus === "all") return void 0;
2474
+ if (corpus === "memory") return ["memory"];
2475
+ if (corpus === "wiki") return ["wiki"];
2476
+ if (corpus === "sessions") return ["sessions"];
2477
+ return void 0;
2478
+ }
2479
+ function capByTotalChars(hits, maxTotalChars) {
2480
+ if (hits.length === 0) return { hits: [], truncated: false };
2481
+ const kept = [];
2482
+ let runningChars = 0;
2483
+ for (const hit of hits) {
2484
+ const snippetLen = hit.snippet.length + (hit.citation?.length ?? 0) + 32;
2485
+ if (kept.length > 0 && runningChars + snippetLen > maxTotalChars) {
2486
+ return { hits: kept, truncated: true };
2487
+ }
2488
+ kept.push(hit);
2489
+ runningChars += snippetLen;
2490
+ }
2491
+ return { hits: kept, truncated: false };
2492
+ }
2493
+ function isPathInside(root, candidate) {
2494
+ const normalizedRoot = root.endsWith("/") ? root : `${root}/`;
2495
+ return candidate === root || candidate.startsWith(normalizedRoot);
2496
+ }
2497
+
2498
+ export { ActiveMemoryCache, CircuitBreaker, DEFAULT_DEEPINFRA_EMBEDDING_MODEL, DEFAULT_MEMORY_READ_LINES, DEFAULT_MISTRAL_EMBEDDING_MODEL, DEFAULT_OLLAMA_EMBEDDING_MODEL, DEFAULT_OPENAI_EMBEDDING_MODEL, DEFAULT_OPENROUTER_EMBEDDING_MODEL, DEFAULT_VOYAGE_EMBEDDING_MODEL, IndexManager, LanceIndex, LanceMemoryAdapter, MEMORY_EMBEDDING_ADAPTERS, META_KEY_DIMENSION, META_KEY_MODEL, META_KEY_PROVIDER_ID, MIGRATION_V2_STATEMENTS, PRAGMA_STATEMENTS, SCHEMA_STATEMENTS, VALID_BACKENDS, appendDiaryEntry, appendFact, appendFactToMarkdown, assertValidBackend, buildErrorMetadata, chunkMarkdown, createInMemoryMarkdownProvider, createMemoryGetTool, createMemorySearchTool, createOpenAiCompatibleRuntime, createVectorIndex, deepPhase, deepinfraMemoryEmbeddingProviderAdapter, defaultIndexPath, diaryPath, discoverSessionFiles, discoverWikiFiles, dropVectorIndex, embedMissingChunks, entryHash, identityMatches, isLanceAvailable, isSqliteVecLoaded, lanceStoragePath, legacyMemoryJsonPath, lightPhase, listNotes, loadSqliteVecExtension, mapOpenAICompatibleError, memoryDir, memoryMdPath, migrateLegacyJson, migrateSqliteToLance, mistralMemoryEmbeddingProviderAdapter, notesDir, ollamaMemoryEmbeddingProviderAdapter, openAiMemoryEmbeddingProviderAdapter, openLanceIndex, openMemoryDb, openRouterMemoryEmbeddingProviderAdapter, packVector, parseRetryAfter, parseSearchOptions, persistActiveMemoryTranscript, readEmbeddingIdentity, readFacts, readFactsFromMarkdown, readMemoryFileBounded, redactSecrets, remPhase, renderDiaryEntry, resetMigrationStateForTests, runActiveMemory, runDreamingSweep, sessionSummaryPath, sessionsDir, truncateRaw, upsertEmbedding, vectorSearch, voyageMemoryEmbeddingProviderAdapter, wikiDir, writeEmbeddingIdentity, writeSessionSummary };
2499
+ //# sourceMappingURL=index.js.map
2500
+ //# sourceMappingURL=index.js.map