lynkr 2.0.0 → 3.0.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.
- package/README.md +226 -15
- package/docs/index.md +230 -11
- package/install.sh +260 -0
- package/package.json +4 -3
- package/src/clients/databricks.js +158 -0
- package/src/clients/routing.js +13 -1
- package/src/config/index.js +68 -1
- package/src/db/index.js +118 -0
- package/src/memory/extractor.js +350 -0
- package/src/memory/index.js +55 -0
- package/src/memory/retriever.js +266 -0
- package/src/memory/search.js +239 -0
- package/src/memory/store.js +411 -0
- package/src/memory/surprise.js +306 -0
- package/src/memory/tools.js +348 -0
- package/src/orchestrator/index.js +170 -0
- package/test/llamacpp-integration.test.js +686 -0
- package/test/memory/extractor.test.js +360 -0
- package/test/memory/retriever.test.js +583 -0
- package/test/memory/search.test.js +389 -0
- package/test/memory/store.test.js +312 -0
- package/test/memory/surprise.test.js +300 -0
- package/test/memory-performance.test.js +472 -0
- package/test/openai-integration.test.js +681 -0
|
@@ -424,6 +424,156 @@ async function invokeAzureOpenAI(body) {
|
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
async function invokeOpenAI(body) {
|
|
428
|
+
if (!config.openai?.apiKey) {
|
|
429
|
+
throw new Error("OpenAI API key is not configured.");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const {
|
|
433
|
+
convertAnthropicToolsToOpenRouter,
|
|
434
|
+
convertAnthropicMessagesToOpenRouter
|
|
435
|
+
} = require("./openrouter-utils");
|
|
436
|
+
|
|
437
|
+
const endpoint = config.openai.endpoint || "https://api.openai.com/v1/chat/completions";
|
|
438
|
+
const headers = {
|
|
439
|
+
"Authorization": `Bearer ${config.openai.apiKey}`,
|
|
440
|
+
"Content-Type": "application/json",
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Add organization header if configured
|
|
444
|
+
if (config.openai.organization) {
|
|
445
|
+
headers["OpenAI-Organization"] = config.openai.organization;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Convert messages and handle system message
|
|
449
|
+
const messages = convertAnthropicMessagesToOpenRouter(body.messages || []);
|
|
450
|
+
|
|
451
|
+
// Anthropic uses separate 'system' field, OpenAI needs it as first message
|
|
452
|
+
if (body.system) {
|
|
453
|
+
messages.unshift({
|
|
454
|
+
role: "system",
|
|
455
|
+
content: body.system
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const openAIBody = {
|
|
460
|
+
model: config.openai.model || "gpt-4o",
|
|
461
|
+
messages,
|
|
462
|
+
temperature: body.temperature ?? 0.7,
|
|
463
|
+
max_tokens: body.max_tokens ?? 4096,
|
|
464
|
+
top_p: body.top_p ?? 1.0,
|
|
465
|
+
stream: body.stream ?? false
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// Add tools - inject standard tools if client didn't send any (passthrough mode)
|
|
469
|
+
let toolsToSend = body.tools;
|
|
470
|
+
let toolsInjected = false;
|
|
471
|
+
|
|
472
|
+
if (!Array.isArray(toolsToSend) || toolsToSend.length === 0) {
|
|
473
|
+
// Client didn't send tools (likely passthrough mode) - inject standard Claude Code tools
|
|
474
|
+
toolsToSend = STANDARD_TOOLS;
|
|
475
|
+
toolsInjected = true;
|
|
476
|
+
logger.info({
|
|
477
|
+
injectedToolCount: STANDARD_TOOLS.length,
|
|
478
|
+
injectedToolNames: STANDARD_TOOLS.map(t => t.name),
|
|
479
|
+
reason: "Client did not send tools (passthrough mode)"
|
|
480
|
+
}, "=== INJECTING STANDARD TOOLS (OpenAI) ===");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
|
|
484
|
+
openAIBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
|
|
485
|
+
openAIBody.parallel_tool_calls = true; // Enable parallel tool calling
|
|
486
|
+
openAIBody.tool_choice = "auto"; // Let the model decide when to use tools
|
|
487
|
+
logger.info({
|
|
488
|
+
toolCount: toolsToSend.length,
|
|
489
|
+
toolNames: toolsToSend.map(t => t.name),
|
|
490
|
+
toolsInjected
|
|
491
|
+
}, "=== SENDING TOOLS TO OPENAI ===");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
logger.info({
|
|
495
|
+
endpoint,
|
|
496
|
+
model: openAIBody.model,
|
|
497
|
+
hasTools: !!openAIBody.tools,
|
|
498
|
+
toolCount: openAIBody.tools?.length || 0,
|
|
499
|
+
temperature: openAIBody.temperature,
|
|
500
|
+
max_tokens: openAIBody.max_tokens,
|
|
501
|
+
}, "=== OPENAI REQUEST ===");
|
|
502
|
+
|
|
503
|
+
return performJsonRequest(endpoint, { headers, body: openAIBody }, "OpenAI");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function invokeLlamaCpp(body) {
|
|
507
|
+
if (!config.llamacpp?.endpoint) {
|
|
508
|
+
throw new Error("llama.cpp endpoint is not configured.");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const {
|
|
512
|
+
convertAnthropicToolsToOpenRouter,
|
|
513
|
+
convertAnthropicMessagesToOpenRouter
|
|
514
|
+
} = require("./openrouter-utils");
|
|
515
|
+
|
|
516
|
+
const endpoint = `${config.llamacpp.endpoint}/v1/chat/completions`;
|
|
517
|
+
const headers = {
|
|
518
|
+
"Content-Type": "application/json",
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Add API key if configured (for secured llama.cpp servers)
|
|
522
|
+
if (config.llamacpp.apiKey) {
|
|
523
|
+
headers["Authorization"] = `Bearer ${config.llamacpp.apiKey}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Convert messages to OpenAI format
|
|
527
|
+
const messages = convertAnthropicMessagesToOpenRouter(body.messages || []);
|
|
528
|
+
|
|
529
|
+
// Handle system message
|
|
530
|
+
if (body.system) {
|
|
531
|
+
messages.unshift({ role: "system", content: body.system });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const llamacppBody = {
|
|
535
|
+
messages,
|
|
536
|
+
temperature: body.temperature ?? 0.7,
|
|
537
|
+
max_tokens: body.max_tokens ?? 4096,
|
|
538
|
+
top_p: body.top_p ?? 1.0,
|
|
539
|
+
stream: body.stream ?? false
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// Inject standard tools if client didn't send any
|
|
543
|
+
let toolsToSend = body.tools;
|
|
544
|
+
let toolsInjected = false;
|
|
545
|
+
|
|
546
|
+
if (!Array.isArray(toolsToSend) || toolsToSend.length === 0) {
|
|
547
|
+
toolsToSend = STANDARD_TOOLS;
|
|
548
|
+
toolsInjected = true;
|
|
549
|
+
logger.info({
|
|
550
|
+
injectedToolCount: STANDARD_TOOLS.length,
|
|
551
|
+
injectedToolNames: STANDARD_TOOLS.map(t => t.name),
|
|
552
|
+
reason: "Client did not send tools (passthrough mode)"
|
|
553
|
+
}, "=== INJECTING STANDARD TOOLS (llama.cpp) ===");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
|
|
557
|
+
llamacppBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
|
|
558
|
+
llamacppBody.tool_choice = "auto";
|
|
559
|
+
logger.info({
|
|
560
|
+
toolCount: toolsToSend.length,
|
|
561
|
+
toolNames: toolsToSend.map(t => t.name),
|
|
562
|
+
toolsInjected
|
|
563
|
+
}, "=== SENDING TOOLS TO LLAMA.CPP ===");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
logger.info({
|
|
567
|
+
endpoint,
|
|
568
|
+
hasTools: !!llamacppBody.tools,
|
|
569
|
+
toolCount: llamacppBody.tools?.length || 0,
|
|
570
|
+
temperature: llamacppBody.temperature,
|
|
571
|
+
max_tokens: llamacppBody.max_tokens,
|
|
572
|
+
}, "=== LLAMA.CPP REQUEST ===");
|
|
573
|
+
|
|
574
|
+
return performJsonRequest(endpoint, { headers, body: llamacppBody }, "llama.cpp");
|
|
575
|
+
}
|
|
576
|
+
|
|
427
577
|
async function invokeModel(body, options = {}) {
|
|
428
578
|
const { determineProvider, isFallbackEnabled, getFallbackProvider } = require("./routing");
|
|
429
579
|
const metricsCollector = getMetricsCollector();
|
|
@@ -463,6 +613,10 @@ async function invokeModel(body, options = {}) {
|
|
|
463
613
|
return await invokeOllama(body);
|
|
464
614
|
} else if (initialProvider === "openrouter") {
|
|
465
615
|
return await invokeOpenRouter(body);
|
|
616
|
+
} else if (initialProvider === "openai") {
|
|
617
|
+
return await invokeOpenAI(body);
|
|
618
|
+
} else if (initialProvider === "llamacpp") {
|
|
619
|
+
return await invokeLlamaCpp(body);
|
|
466
620
|
}
|
|
467
621
|
return await invokeDatabricks(body);
|
|
468
622
|
});
|
|
@@ -538,6 +692,10 @@ async function invokeModel(body, options = {}) {
|
|
|
538
692
|
return await invokeAzureAnthropic(body);
|
|
539
693
|
} else if (fallbackProvider === "openrouter") {
|
|
540
694
|
return await invokeOpenRouter(body);
|
|
695
|
+
} else if (fallbackProvider === "openai") {
|
|
696
|
+
return await invokeOpenAI(body);
|
|
697
|
+
} else if (fallbackProvider === "llamacpp") {
|
|
698
|
+
return await invokeLlamaCpp(body);
|
|
541
699
|
}
|
|
542
700
|
return await invokeDatabricks(body);
|
|
543
701
|
});
|
package/src/clients/routing.js
CHANGED
|
@@ -52,7 +52,7 @@ function determineProvider(payload) {
|
|
|
52
52
|
return "ollama";
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Moderate tool count → OpenRouter or Azure OpenAI (if configured and fallback enabled)
|
|
55
|
+
// Moderate tool count → OpenRouter, OpenAI, or Azure OpenAI (if configured and fallback enabled)
|
|
56
56
|
if (toolCount < maxToolsForOpenRouter && isFallbackEnabled()) {
|
|
57
57
|
if (config.openrouter?.apiKey) {
|
|
58
58
|
logger.debug(
|
|
@@ -60,12 +60,24 @@ function determineProvider(payload) {
|
|
|
60
60
|
"Routing to OpenRouter (moderate tools)"
|
|
61
61
|
);
|
|
62
62
|
return "openrouter";
|
|
63
|
+
} else if (config.openai?.apiKey) {
|
|
64
|
+
logger.debug(
|
|
65
|
+
{ toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "openai" },
|
|
66
|
+
"Routing to OpenAI (moderate tools)"
|
|
67
|
+
);
|
|
68
|
+
return "openai";
|
|
63
69
|
} else if (config.azureOpenAI?.apiKey) {
|
|
64
70
|
logger.debug(
|
|
65
71
|
{ toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "azure-openai" },
|
|
66
72
|
"Routing to Azure OpenAI (moderate tools)"
|
|
67
73
|
);
|
|
68
74
|
return "azure-openai";
|
|
75
|
+
} else if (config.llamacpp?.endpoint) {
|
|
76
|
+
logger.debug(
|
|
77
|
+
{ toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "llamacpp" },
|
|
78
|
+
"Routing to llama.cpp (moderate tools)"
|
|
79
|
+
);
|
|
80
|
+
return "llamacpp";
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
|
package/src/config/index.js
CHANGED
|
@@ -62,7 +62,7 @@ function resolveConfigPath(targetPath) {
|
|
|
62
62
|
return path.resolve(normalised);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai"]);
|
|
65
|
+
const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp"]);
|
|
66
66
|
const rawModelProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
|
|
67
67
|
const modelProvider = SUPPORTED_MODEL_PROVIDERS.has(rawModelProvider)
|
|
68
68
|
? rawModelProvider
|
|
@@ -90,6 +90,17 @@ const azureOpenAIApiKey = process.env.AZURE_OPENAI_API_KEY?.trim() || null;
|
|
|
90
90
|
const azureOpenAIDeployment = process.env.AZURE_OPENAI_DEPLOYMENT?.trim() || "gpt-4o";
|
|
91
91
|
const azureOpenAIApiVersion = process.env.AZURE_OPENAI_API_VERSION?.trim() || "2024-08-01-preview";
|
|
92
92
|
|
|
93
|
+
// OpenAI configuration
|
|
94
|
+
const openAIApiKey = process.env.OPENAI_API_KEY?.trim() || null;
|
|
95
|
+
const openAIModel = process.env.OPENAI_MODEL?.trim() || "gpt-4o";
|
|
96
|
+
const openAIEndpoint = process.env.OPENAI_ENDPOINT?.trim() || "https://api.openai.com/v1/chat/completions";
|
|
97
|
+
const openAIOrganization = process.env.OPENAI_ORGANIZATION?.trim() || null;
|
|
98
|
+
|
|
99
|
+
// llama.cpp configuration
|
|
100
|
+
const llamacppEndpoint = process.env.LLAMACPP_ENDPOINT?.trim() || "http://localhost:8080";
|
|
101
|
+
const llamacppModel = process.env.LLAMACPP_MODEL?.trim() || "default";
|
|
102
|
+
const llamacppTimeout = Number.parseInt(process.env.LLAMACPP_TIMEOUT_MS ?? "120000", 10);
|
|
103
|
+
const llamacppApiKey = process.env.LLAMACPP_API_KEY?.trim() || null;
|
|
93
104
|
|
|
94
105
|
// Hybrid routing configuration
|
|
95
106
|
const preferOllama = process.env.PREFER_OLLAMA === "true";
|
|
@@ -112,6 +123,18 @@ if (!["server", "client", "passthrough"].includes(toolExecutionMode)) {
|
|
|
112
123
|
);
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
// Memory system configuration (Titans-inspired long-term memory)
|
|
127
|
+
const memoryEnabled = process.env.MEMORY_ENABLED !== "false"; // default true
|
|
128
|
+
const memoryRetrievalLimit = Number.parseInt(process.env.MEMORY_RETRIEVAL_LIMIT ?? "5", 10);
|
|
129
|
+
const memorySurpriseThreshold = Number.parseFloat(process.env.MEMORY_SURPRISE_THRESHOLD ?? "0.3");
|
|
130
|
+
const memoryMaxAgeDays = Number.parseInt(process.env.MEMORY_MAX_AGE_DAYS ?? "90", 10);
|
|
131
|
+
const memoryMaxCount = Number.parseInt(process.env.MEMORY_MAX_COUNT ?? "10000", 10);
|
|
132
|
+
const memoryIncludeGlobal = process.env.MEMORY_INCLUDE_GLOBAL !== "false"; // default true
|
|
133
|
+
const memoryInjectionFormat = (process.env.MEMORY_INJECTION_FORMAT ?? "system").toLowerCase();
|
|
134
|
+
const memoryExtractionEnabled = process.env.MEMORY_EXTRACTION_ENABLED !== "false"; // default true
|
|
135
|
+
const memoryDecayEnabled = process.env.MEMORY_DECAY_ENABLED !== "false"; // default true
|
|
136
|
+
const memoryDecayHalfLifeDays = Number.parseInt(process.env.MEMORY_DECAY_HALF_LIFE ?? "30", 10);
|
|
137
|
+
|
|
115
138
|
// Only require Databricks credentials if it's the primary provider or used as fallback
|
|
116
139
|
if (modelProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
|
|
117
140
|
throw new Error("Set DATABRICKS_API_BASE and DATABRICKS_API_KEY before starting the proxy.");
|
|
@@ -134,6 +157,12 @@ if (modelProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApi
|
|
|
134
157
|
);
|
|
135
158
|
}
|
|
136
159
|
|
|
160
|
+
if (modelProvider === "openai" && !openAIApiKey) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"Set OPENAI_API_KEY before starting the proxy.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
137
166
|
if (modelProvider === "ollama") {
|
|
138
167
|
try {
|
|
139
168
|
new URL(ollamaEndpoint);
|
|
@@ -142,6 +171,14 @@ if (modelProvider === "ollama") {
|
|
|
142
171
|
}
|
|
143
172
|
}
|
|
144
173
|
|
|
174
|
+
if (modelProvider === "llamacpp") {
|
|
175
|
+
try {
|
|
176
|
+
new URL(llamacppEndpoint);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
throw new Error("LLAMACPP_ENDPOINT must be a valid URL (default: http://localhost:8080)");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
145
182
|
// Validate hybrid routing configuration
|
|
146
183
|
if (preferOllama) {
|
|
147
184
|
if (!ollamaEndpoint) {
|
|
@@ -353,6 +390,18 @@ const config = {
|
|
|
353
390
|
deployment: azureOpenAIDeployment,
|
|
354
391
|
apiVersion: azureOpenAIApiVersion
|
|
355
392
|
},
|
|
393
|
+
openai: {
|
|
394
|
+
apiKey: openAIApiKey,
|
|
395
|
+
model: openAIModel,
|
|
396
|
+
endpoint: openAIEndpoint,
|
|
397
|
+
organization: openAIOrganization,
|
|
398
|
+
},
|
|
399
|
+
llamacpp: {
|
|
400
|
+
endpoint: llamacppEndpoint,
|
|
401
|
+
model: llamacppModel,
|
|
402
|
+
timeout: Number.isNaN(llamacppTimeout) ? 120000 : llamacppTimeout,
|
|
403
|
+
apiKey: llamacppApiKey,
|
|
404
|
+
},
|
|
356
405
|
modelProvider: {
|
|
357
406
|
type: modelProvider,
|
|
358
407
|
defaultModel,
|
|
@@ -472,6 +521,24 @@ const config = {
|
|
|
472
521
|
},
|
|
473
522
|
profiles: Array.isArray(testProfiles) ? testProfiles : null,
|
|
474
523
|
},
|
|
524
|
+
memory: {
|
|
525
|
+
enabled: memoryEnabled,
|
|
526
|
+
retrievalLimit: Number.isNaN(memoryRetrievalLimit) ? 5 : memoryRetrievalLimit,
|
|
527
|
+
surpriseThreshold: Number.isNaN(memorySurpriseThreshold) ? 0.3 : memorySurpriseThreshold,
|
|
528
|
+
maxAgeDays: Number.isNaN(memoryMaxAgeDays) ? 90 : memoryMaxAgeDays,
|
|
529
|
+
maxCount: Number.isNaN(memoryMaxCount) ? 10000 : memoryMaxCount,
|
|
530
|
+
includeGlobalMemories: memoryIncludeGlobal,
|
|
531
|
+
injectionFormat: ["system", "assistant_preamble"].includes(memoryInjectionFormat)
|
|
532
|
+
? memoryInjectionFormat
|
|
533
|
+
: "system",
|
|
534
|
+
extraction: {
|
|
535
|
+
enabled: memoryExtractionEnabled,
|
|
536
|
+
},
|
|
537
|
+
decay: {
|
|
538
|
+
enabled: memoryDecayEnabled,
|
|
539
|
+
halfLifeDays: Number.isNaN(memoryDecayHalfLifeDays) ? 30 : memoryDecayHalfLifeDays,
|
|
540
|
+
},
|
|
541
|
+
},
|
|
475
542
|
};
|
|
476
543
|
|
|
477
544
|
module.exports = config;
|
package/src/db/index.js
CHANGED
|
@@ -218,6 +218,124 @@ db.exec(`
|
|
|
218
218
|
|
|
219
219
|
CREATE INDEX IF NOT EXISTS idx_test_runs_status
|
|
220
220
|
ON test_runs(status);
|
|
221
|
+
|
|
222
|
+
-- Memory system tables (Titans-inspired long-term memory)
|
|
223
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
224
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
225
|
+
session_id TEXT,
|
|
226
|
+
content TEXT NOT NULL,
|
|
227
|
+
type TEXT NOT NULL,
|
|
228
|
+
category TEXT,
|
|
229
|
+
importance REAL DEFAULT 0.5,
|
|
230
|
+
surprise_score REAL DEFAULT 0.0,
|
|
231
|
+
access_count INTEGER DEFAULT 0,
|
|
232
|
+
decay_factor REAL DEFAULT 1.0,
|
|
233
|
+
source_turn_id INTEGER,
|
|
234
|
+
created_at INTEGER NOT NULL,
|
|
235
|
+
updated_at INTEGER NOT NULL,
|
|
236
|
+
last_accessed_at INTEGER,
|
|
237
|
+
metadata TEXT,
|
|
238
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE SET NULL
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
CREATE INDEX IF NOT EXISTS idx_memories_session_id
|
|
242
|
+
ON memories(session_id);
|
|
243
|
+
|
|
244
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type
|
|
245
|
+
ON memories(type);
|
|
246
|
+
|
|
247
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category
|
|
248
|
+
ON memories(category);
|
|
249
|
+
|
|
250
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance
|
|
251
|
+
ON memories(importance DESC);
|
|
252
|
+
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_memories_surprise
|
|
254
|
+
ON memories(surprise_score DESC);
|
|
255
|
+
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created_at
|
|
257
|
+
ON memories(created_at DESC);
|
|
258
|
+
|
|
259
|
+
CREATE INDEX IF NOT EXISTS idx_memories_last_accessed
|
|
260
|
+
ON memories(last_accessed_at DESC);
|
|
261
|
+
|
|
262
|
+
-- Full-text search for memories
|
|
263
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
264
|
+
content,
|
|
265
|
+
type,
|
|
266
|
+
category,
|
|
267
|
+
content=memories,
|
|
268
|
+
content_rowid=id,
|
|
269
|
+
tokenize="porter unicode61"
|
|
270
|
+
);
|
|
271
|
+
`)
|
|
272
|
+
|
|
273
|
+
;
|
|
274
|
+
|
|
275
|
+
// FTS5 triggers to keep search index in sync
|
|
276
|
+
db.exec(`
|
|
277
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
278
|
+
INSERT INTO memories_fts(rowid, content, type, category)
|
|
279
|
+
VALUES (new.id, new.content, new.type, new.category);
|
|
280
|
+
END;
|
|
281
|
+
|
|
282
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
283
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, type, category)
|
|
284
|
+
VALUES ('delete', old.id, old.content, old.type, old.category);
|
|
285
|
+
END;
|
|
286
|
+
|
|
287
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
288
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, type, category)
|
|
289
|
+
VALUES ('delete', old.id, old.content, old.type, old.category);
|
|
290
|
+
INSERT INTO memories_fts(rowid, content, type, category)
|
|
291
|
+
VALUES (new.id, new.content, new.type, new.category);
|
|
292
|
+
END;
|
|
293
|
+
`);
|
|
294
|
+
|
|
295
|
+
db.exec(`
|
|
296
|
+
-- Memory embeddings for optional semantic search
|
|
297
|
+
CREATE TABLE IF NOT EXISTS memory_embeddings (
|
|
298
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
299
|
+
memory_id INTEGER NOT NULL UNIQUE,
|
|
300
|
+
embedding BLOB NOT NULL,
|
|
301
|
+
model TEXT NOT NULL,
|
|
302
|
+
dimensions INTEGER NOT NULL,
|
|
303
|
+
created_at INTEGER NOT NULL,
|
|
304
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
-- Entity tracking for surprise detection
|
|
308
|
+
CREATE TABLE IF NOT EXISTS memory_entities (
|
|
309
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
310
|
+
entity_type TEXT NOT NULL,
|
|
311
|
+
entity_name TEXT NOT NULL,
|
|
312
|
+
first_seen_at INTEGER NOT NULL,
|
|
313
|
+
last_seen_at INTEGER NOT NULL,
|
|
314
|
+
occurrence_count INTEGER DEFAULT 1,
|
|
315
|
+
properties TEXT,
|
|
316
|
+
UNIQUE(entity_type, entity_name)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
CREATE INDEX IF NOT EXISTS idx_memory_entities_type_name
|
|
320
|
+
ON memory_entities(entity_type, entity_name);
|
|
321
|
+
|
|
322
|
+
-- Memory associations (graph relationships)
|
|
323
|
+
CREATE TABLE IF NOT EXISTS memory_associations (
|
|
324
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
325
|
+
from_memory_id INTEGER NOT NULL,
|
|
326
|
+
to_memory_id INTEGER NOT NULL,
|
|
327
|
+
relationship TEXT,
|
|
328
|
+
strength REAL DEFAULT 0.5,
|
|
329
|
+
created_at INTEGER NOT NULL,
|
|
330
|
+
FOREIGN KEY (from_memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
331
|
+
FOREIGN KEY (to_memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
CREATE INDEX IF NOT EXISTS idx_memory_associations_from
|
|
335
|
+
ON memory_associations(from_memory_id);
|
|
336
|
+
|
|
337
|
+
CREATE INDEX IF NOT EXISTS idx_memory_associations_to
|
|
338
|
+
ON memory_associations(to_memory_id);
|
|
221
339
|
`);
|
|
222
340
|
|
|
223
341
|
logger.info({ dbPath }, "SQLite session store initialised");
|