@jcyamacho/agent-memory 0.0.13 → 0.0.15
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 +58 -67
- package/dist/index.js +466 -138
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -62,16 +62,34 @@ With a custom database path:
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
With a custom model cache path:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"memory": {
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": [
|
|
73
|
+
"-y",
|
|
74
|
+
"@jcyamacho/agent-memory"
|
|
75
|
+
],
|
|
76
|
+
"env": {
|
|
77
|
+
"AGENT_MEMORY_MODELS_CACHE_PATH": "/absolute/path/to/models"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
65
84
|
Optional LLM instructions to reinforce the MCP's built-in guidance:
|
|
66
85
|
|
|
67
86
|
```text
|
|
68
|
-
Use `recall` at
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
are wrong or no longer relevant. Always pass workspace.
|
|
87
|
+
Use `recall` at conversation start and before design choices, conventions, or
|
|
88
|
+
edge cases. Query with 2-5 short anchor-heavy terms or exact phrases, not
|
|
89
|
+
questions or sentences. `recall` is lexical-first; if it misses, retry once
|
|
90
|
+
with overlapping alternate terms. Use `remember` for one durable fact, then
|
|
91
|
+
use `revise` instead of duplicates and `forget` for wrong or obsolete
|
|
92
|
+
memories. Always pass workspace unless the memory is truly global.
|
|
75
93
|
```
|
|
76
94
|
|
|
77
95
|
## What It Stores
|
|
@@ -101,60 +119,10 @@ The web UI uses the same database as the MCP server.
|
|
|
101
119
|
|
|
102
120
|
## Tools
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Inputs:
|
|
109
|
-
|
|
110
|
-
- `content` -> fact, preference, decision, or context to store
|
|
111
|
-
- `workspace` -> repository or workspace path
|
|
112
|
-
|
|
113
|
-
Output:
|
|
114
|
-
|
|
115
|
-
- `id`
|
|
116
|
-
|
|
117
|
-
### `recall`
|
|
118
|
-
|
|
119
|
-
Retrieve relevant memories for the current task.
|
|
120
|
-
|
|
121
|
-
Inputs:
|
|
122
|
-
|
|
123
|
-
- `terms` -> 2-5 distinctive terms or short phrases that should appear in the
|
|
124
|
-
memory content; avoid full natural-language questions
|
|
125
|
-
- `limit` -> maximum results to return
|
|
126
|
-
- `workspace` -> workspace or repo path; biases ranking toward this workspace
|
|
127
|
-
- `updated_after` -> ISO 8601 lower bound
|
|
128
|
-
- `updated_before` -> ISO 8601 upper bound
|
|
129
|
-
|
|
130
|
-
Output:
|
|
131
|
-
|
|
132
|
-
- `results[]` with `id`, `content`, `score`, `workspace`, and `updated_at`
|
|
133
|
-
|
|
134
|
-
### `revise`
|
|
135
|
-
|
|
136
|
-
Update the content of an existing memory.
|
|
137
|
-
|
|
138
|
-
Inputs:
|
|
139
|
-
|
|
140
|
-
- `id` -> the memory id from a previous recall result
|
|
141
|
-
- `content` -> replacement content for the memory
|
|
142
|
-
|
|
143
|
-
Output:
|
|
144
|
-
|
|
145
|
-
- `id`, `updated_at`
|
|
146
|
-
|
|
147
|
-
### `forget`
|
|
148
|
-
|
|
149
|
-
Permanently delete a memory.
|
|
150
|
-
|
|
151
|
-
Inputs:
|
|
152
|
-
|
|
153
|
-
- `id` -> the memory id from a previous recall result
|
|
154
|
-
|
|
155
|
-
Output:
|
|
156
|
-
|
|
157
|
-
- `id`, `deleted`
|
|
122
|
+
- `remember` saves durable facts, preferences, decisions, and project context.
|
|
123
|
+
- `recall` retrieves the most relevant saved memories.
|
|
124
|
+
- `revise` updates an existing memory when it becomes outdated.
|
|
125
|
+
- `forget` deletes a memory that is no longer relevant.
|
|
158
126
|
|
|
159
127
|
## How Ranking Works
|
|
160
128
|
|
|
@@ -163,18 +131,21 @@ memories:
|
|
|
163
131
|
|
|
164
132
|
1. **Text relevance** is the primary signal -- memories whose content best
|
|
165
133
|
matches your search terms rank highest.
|
|
166
|
-
2. **
|
|
134
|
+
2. **Embedding similarity** is the next strongest signal. Recall builds an
|
|
135
|
+
embedding from your normalized search terms and boosts memories whose stored
|
|
136
|
+
embeddings are most semantically similar.
|
|
137
|
+
3. **Workspace match** is a strong secondary signal. When you pass
|
|
167
138
|
`workspace`, exact matches rank highest, sibling repositories get a small
|
|
168
139
|
boost, and unrelated workspaces rank lowest.
|
|
169
|
-
|
|
140
|
+
4. **Global memories** (saved without a workspace) are treated as relevant
|
|
170
141
|
everywhere. When you pass `workspace`, they rank below exact workspace
|
|
171
142
|
matches and above sibling or unrelated repositories.
|
|
172
|
-
|
|
143
|
+
5. **Recency** is a minor tiebreaker -- newer memories rank slightly above older
|
|
173
144
|
ones when other signals are equal.
|
|
174
145
|
|
|
175
|
-
If you omit `workspace`, recall
|
|
176
|
-
For best results, pass `workspace` whenever you have one. Save
|
|
177
|
-
a workspace only when they apply across all projects.
|
|
146
|
+
If you omit `workspace`, recall still uses text relevance, embedding similarity,
|
|
147
|
+
and recency. For best results, pass `workspace` whenever you have one. Save
|
|
148
|
+
memories without a workspace only when they apply across all projects.
|
|
178
149
|
|
|
179
150
|
## Database location
|
|
180
151
|
|
|
@@ -196,6 +167,26 @@ Set `AGENT_MEMORY_DB_PATH` when you want to:
|
|
|
196
167
|
- share a memory DB across multiple clients
|
|
197
168
|
- store the DB somewhere easier to back up or inspect
|
|
198
169
|
|
|
170
|
+
## Model cache location
|
|
171
|
+
|
|
172
|
+
By default, downloaded embedding model files are cached at:
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
~/.config/agent-memory/models
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Override it with:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
AGENT_MEMORY_MODELS_CACHE_PATH=/absolute/path/to/models
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Set `AGENT_MEMORY_MODELS_CACHE_PATH` when you want to:
|
|
185
|
+
|
|
186
|
+
- keep model artifacts out of `node_modules`
|
|
187
|
+
- share the model cache across reinstalls or multiple clients
|
|
188
|
+
- store model downloads somewhere easier to inspect or manage
|
|
189
|
+
|
|
199
190
|
Beta note: schema changes are not migrated. If you are upgrading from an older
|
|
200
191
|
beta, delete the existing memory DB and let the server create a new one.
|
|
201
192
|
|
package/dist/index.js
CHANGED
|
@@ -2431,9 +2431,9 @@ var require_validate = __commonJS((exports) => {
|
|
|
2431
2431
|
}
|
|
2432
2432
|
}
|
|
2433
2433
|
function returnResults(it) {
|
|
2434
|
-
const { gen, schemaEnv, validateName, ValidationError, opts } = it;
|
|
2434
|
+
const { gen, schemaEnv, validateName, ValidationError: ValidationError2, opts } = it;
|
|
2435
2435
|
if (schemaEnv.$async) {
|
|
2436
|
-
gen.if((0, codegen_1._)`${names_1.default.errors} === 0`, () => gen.return(names_1.default.data), () => gen.throw((0, codegen_1._)`new ${
|
|
2436
|
+
gen.if((0, codegen_1._)`${names_1.default.errors} === 0`, () => gen.return(names_1.default.data), () => gen.throw((0, codegen_1._)`new ${ValidationError2}(${names_1.default.vErrors})`));
|
|
2437
2437
|
} else {
|
|
2438
2438
|
gen.assign((0, codegen_1._)`${validateName}.errors`, names_1.default.vErrors);
|
|
2439
2439
|
if (opts.unevaluated)
|
|
@@ -2783,14 +2783,14 @@ var require_validate = __commonJS((exports) => {
|
|
|
2783
2783
|
var require_validation_error = __commonJS((exports) => {
|
|
2784
2784
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2785
2785
|
|
|
2786
|
-
class
|
|
2786
|
+
class ValidationError2 extends Error {
|
|
2787
2787
|
constructor(errors3) {
|
|
2788
2788
|
super("validation failed");
|
|
2789
2789
|
this.errors = errors3;
|
|
2790
2790
|
this.ajv = this.validation = true;
|
|
2791
2791
|
}
|
|
2792
2792
|
}
|
|
2793
|
-
exports.default =
|
|
2793
|
+
exports.default = ValidationError2;
|
|
2794
2794
|
});
|
|
2795
2795
|
|
|
2796
2796
|
// node_modules/ajv/dist/compile/ref_error.js
|
|
@@ -12464,13 +12464,14 @@ class StdioServerTransport {
|
|
|
12464
12464
|
}
|
|
12465
12465
|
}
|
|
12466
12466
|
// package.json
|
|
12467
|
-
var version2 = "0.0.
|
|
12467
|
+
var version2 = "0.0.15";
|
|
12468
12468
|
|
|
12469
12469
|
// src/config.ts
|
|
12470
12470
|
import { homedir } from "node:os";
|
|
12471
12471
|
import { join } from "node:path";
|
|
12472
12472
|
import { parseArgs } from "node:util";
|
|
12473
12473
|
var AGENT_MEMORY_DB_PATH_ENV = "AGENT_MEMORY_DB_PATH";
|
|
12474
|
+
var AGENT_MEMORY_MODELS_CACHE_PATH_ENV = "AGENT_MEMORY_MODELS_CACHE_PATH";
|
|
12474
12475
|
var DEFAULT_UI_PORT = 6580;
|
|
12475
12476
|
function resolveConfig(environment = process.env, argv = process.argv.slice(2)) {
|
|
12476
12477
|
const { values } = parseArgs({
|
|
@@ -12482,12 +12483,146 @@ function resolveConfig(environment = process.env, argv = process.argv.slice(2))
|
|
|
12482
12483
|
strict: false
|
|
12483
12484
|
});
|
|
12484
12485
|
return {
|
|
12485
|
-
databasePath: environment
|
|
12486
|
+
databasePath: resolveDatabasePath(environment),
|
|
12487
|
+
modelsCachePath: resolveModelsCachePath(environment),
|
|
12486
12488
|
uiMode: Boolean(values.ui),
|
|
12487
12489
|
uiPort: Number(values.port) || DEFAULT_UI_PORT
|
|
12488
12490
|
};
|
|
12489
12491
|
}
|
|
12492
|
+
function resolveDatabasePath(environment = process.env) {
|
|
12493
|
+
return environment[AGENT_MEMORY_DB_PATH_ENV] || join(homedir(), ".config", "agent-memory", "memory.db");
|
|
12494
|
+
}
|
|
12495
|
+
function resolveModelsCachePath(environment = process.env) {
|
|
12496
|
+
return environment[AGENT_MEMORY_MODELS_CACHE_PATH_ENV] || join(homedir(), ".config", "agent-memory", "models");
|
|
12497
|
+
}
|
|
12498
|
+
|
|
12499
|
+
// src/embedding/service.ts
|
|
12500
|
+
import { mkdirSync } from "node:fs";
|
|
12501
|
+
import { pipeline, env as transformersEnv } from "@huggingface/transformers";
|
|
12502
|
+
|
|
12503
|
+
// src/errors.ts
|
|
12504
|
+
class MemoryError extends Error {
|
|
12505
|
+
code;
|
|
12506
|
+
constructor(code, message, options) {
|
|
12507
|
+
super(message, options);
|
|
12508
|
+
this.name = "MemoryError";
|
|
12509
|
+
this.code = code;
|
|
12510
|
+
}
|
|
12511
|
+
}
|
|
12512
|
+
|
|
12513
|
+
class ValidationError extends MemoryError {
|
|
12514
|
+
constructor(message) {
|
|
12515
|
+
super("VALIDATION_ERROR", message);
|
|
12516
|
+
this.name = "ValidationError";
|
|
12517
|
+
}
|
|
12518
|
+
}
|
|
12490
12519
|
|
|
12520
|
+
class NotFoundError extends MemoryError {
|
|
12521
|
+
constructor(message) {
|
|
12522
|
+
super("NOT_FOUND", message);
|
|
12523
|
+
this.name = "NotFoundError";
|
|
12524
|
+
}
|
|
12525
|
+
}
|
|
12526
|
+
|
|
12527
|
+
class PersistenceError extends MemoryError {
|
|
12528
|
+
constructor(message, options) {
|
|
12529
|
+
super("PERSISTENCE_ERROR", message, options);
|
|
12530
|
+
this.name = "PersistenceError";
|
|
12531
|
+
}
|
|
12532
|
+
}
|
|
12533
|
+
|
|
12534
|
+
// src/embedding/service.ts
|
|
12535
|
+
var DEFAULT_EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
12536
|
+
|
|
12537
|
+
class EmbeddingService {
|
|
12538
|
+
options;
|
|
12539
|
+
extractorPromise;
|
|
12540
|
+
modelsCachePath;
|
|
12541
|
+
constructor(options = {}) {
|
|
12542
|
+
this.options = options;
|
|
12543
|
+
this.modelsCachePath = options.modelsCachePath ?? resolveModelsCachePath();
|
|
12544
|
+
}
|
|
12545
|
+
async createVector(text) {
|
|
12546
|
+
const normalizedText = text.trim();
|
|
12547
|
+
if (!normalizedText) {
|
|
12548
|
+
throw new ValidationError("Text is required.");
|
|
12549
|
+
}
|
|
12550
|
+
const extractor = await this.getExtractor();
|
|
12551
|
+
const embedding = await extractor(normalizedText);
|
|
12552
|
+
return normalizeVector(embedding.tolist());
|
|
12553
|
+
}
|
|
12554
|
+
getExtractor() {
|
|
12555
|
+
if (!this.extractorPromise) {
|
|
12556
|
+
configureModelsCache(this.modelsCachePath);
|
|
12557
|
+
this.extractorPromise = (this.options.createExtractor ?? createDefaultExtractor)();
|
|
12558
|
+
}
|
|
12559
|
+
return this.extractorPromise;
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12562
|
+
function configureModelsCache(modelsCachePath) {
|
|
12563
|
+
mkdirSync(modelsCachePath, { recursive: true });
|
|
12564
|
+
transformersEnv.useFSCache = true;
|
|
12565
|
+
transformersEnv.cacheDir = modelsCachePath;
|
|
12566
|
+
}
|
|
12567
|
+
async function createDefaultExtractor() {
|
|
12568
|
+
const extractor = await pipeline("feature-extraction", DEFAULT_EMBEDDING_MODEL);
|
|
12569
|
+
return (text) => extractor(text, {
|
|
12570
|
+
pooling: "mean",
|
|
12571
|
+
normalize: true
|
|
12572
|
+
});
|
|
12573
|
+
}
|
|
12574
|
+
function normalizeVector(value) {
|
|
12575
|
+
if (value.length === 0) {
|
|
12576
|
+
throw new ValidationError("Embedding model returned an empty vector.");
|
|
12577
|
+
}
|
|
12578
|
+
const [firstItem] = value;
|
|
12579
|
+
if (typeof firstItem === "number") {
|
|
12580
|
+
return value.map((item) => {
|
|
12581
|
+
if (typeof item !== "number" || !Number.isFinite(item)) {
|
|
12582
|
+
throw new ValidationError("Embedding model returned a non-numeric vector.");
|
|
12583
|
+
}
|
|
12584
|
+
return item;
|
|
12585
|
+
});
|
|
12586
|
+
}
|
|
12587
|
+
if (Array.isArray(firstItem)) {
|
|
12588
|
+
return normalizeVector(firstItem);
|
|
12589
|
+
}
|
|
12590
|
+
throw new ValidationError("Embedding model returned an unexpected vector shape.");
|
|
12591
|
+
}
|
|
12592
|
+
// src/embedding/similarity.ts
|
|
12593
|
+
function compareVectors(left, right) {
|
|
12594
|
+
validateVector(left, "Left vector");
|
|
12595
|
+
validateVector(right, "Right vector");
|
|
12596
|
+
if (left.length !== right.length) {
|
|
12597
|
+
throw new ValidationError("Vectors must have the same length.");
|
|
12598
|
+
}
|
|
12599
|
+
let dotProduct = 0;
|
|
12600
|
+
let leftMagnitude = 0;
|
|
12601
|
+
let rightMagnitude = 0;
|
|
12602
|
+
for (const [index, leftValue] of left.entries()) {
|
|
12603
|
+
const rightValue = right[index];
|
|
12604
|
+
if (rightValue === undefined) {
|
|
12605
|
+
throw new ValidationError("Vectors must have the same length.");
|
|
12606
|
+
}
|
|
12607
|
+
dotProduct += leftValue * rightValue;
|
|
12608
|
+
leftMagnitude += leftValue * leftValue;
|
|
12609
|
+
rightMagnitude += rightValue * rightValue;
|
|
12610
|
+
}
|
|
12611
|
+
if (leftMagnitude === 0 || rightMagnitude === 0) {
|
|
12612
|
+
throw new ValidationError("Vectors must not have zero magnitude.");
|
|
12613
|
+
}
|
|
12614
|
+
return dotProduct / (Math.sqrt(leftMagnitude) * Math.sqrt(rightMagnitude));
|
|
12615
|
+
}
|
|
12616
|
+
function validateVector(vector, label) {
|
|
12617
|
+
if (vector.length === 0) {
|
|
12618
|
+
throw new ValidationError(`${label} must not be empty.`);
|
|
12619
|
+
}
|
|
12620
|
+
for (const value of vector) {
|
|
12621
|
+
if (!Number.isFinite(value)) {
|
|
12622
|
+
throw new ValidationError(`${label} must contain only finite numbers.`);
|
|
12623
|
+
}
|
|
12624
|
+
}
|
|
12625
|
+
}
|
|
12491
12626
|
// node_modules/zod/v3/helpers/util.js
|
|
12492
12627
|
var util;
|
|
12493
12628
|
(function(util2) {
|
|
@@ -19895,37 +20030,6 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
19895
20030
|
}
|
|
19896
20031
|
};
|
|
19897
20032
|
|
|
19898
|
-
// src/errors.ts
|
|
19899
|
-
class MemoryError extends Error {
|
|
19900
|
-
code;
|
|
19901
|
-
constructor(code, message, options) {
|
|
19902
|
-
super(message, options);
|
|
19903
|
-
this.name = "MemoryError";
|
|
19904
|
-
this.code = code;
|
|
19905
|
-
}
|
|
19906
|
-
}
|
|
19907
|
-
|
|
19908
|
-
class ValidationError extends MemoryError {
|
|
19909
|
-
constructor(message) {
|
|
19910
|
-
super("VALIDATION_ERROR", message);
|
|
19911
|
-
this.name = "ValidationError";
|
|
19912
|
-
}
|
|
19913
|
-
}
|
|
19914
|
-
|
|
19915
|
-
class NotFoundError extends MemoryError {
|
|
19916
|
-
constructor(message) {
|
|
19917
|
-
super("NOT_FOUND", message);
|
|
19918
|
-
this.name = "NotFoundError";
|
|
19919
|
-
}
|
|
19920
|
-
}
|
|
19921
|
-
|
|
19922
|
-
class PersistenceError extends MemoryError {
|
|
19923
|
-
constructor(message, options) {
|
|
19924
|
-
super("PERSISTENCE_ERROR", message, options);
|
|
19925
|
-
this.name = "PersistenceError";
|
|
19926
|
-
}
|
|
19927
|
-
}
|
|
19928
|
-
|
|
19929
20033
|
// src/mcp/tools/shared.ts
|
|
19930
20034
|
function toMcpError(error2) {
|
|
19931
20035
|
if (error2 instanceof McpError) {
|
|
@@ -19954,11 +20058,11 @@ function parseOptionalDate(value, fieldName) {
|
|
|
19954
20058
|
|
|
19955
20059
|
// src/mcp/tools/forget.ts
|
|
19956
20060
|
var forgetInputSchema = {
|
|
19957
|
-
id: string2().describe("The id
|
|
20061
|
+
id: string2().describe("The memory id to delete. Use an id returned by `recall`.")
|
|
19958
20062
|
};
|
|
19959
20063
|
function registerForgetTool(server, memory) {
|
|
19960
20064
|
server.registerTool("forget", {
|
|
19961
|
-
description:
|
|
20065
|
+
description: 'Permanently delete a wrong or obsolete memory. Use `revise` instead when the fact still exists and only needs correction. Returns `<memory id="..." deleted="true" />`.',
|
|
19962
20066
|
inputSchema: forgetInputSchema
|
|
19963
20067
|
}, async ({ id }) => {
|
|
19964
20068
|
try {
|
|
@@ -19977,12 +20081,13 @@ var toNormalizedScore = (value) => value;
|
|
|
19977
20081
|
|
|
19978
20082
|
// src/ranking.ts
|
|
19979
20083
|
var RETRIEVAL_SCORE_WEIGHT = 8;
|
|
20084
|
+
var EMBEDDING_SIMILARITY_WEIGHT = 5;
|
|
19980
20085
|
var WORKSPACE_MATCH_WEIGHT = 4;
|
|
19981
|
-
var RECENCY_WEIGHT =
|
|
19982
|
-
var MAX_COMPOSITE_SCORE = RETRIEVAL_SCORE_WEIGHT + WORKSPACE_MATCH_WEIGHT + RECENCY_WEIGHT;
|
|
20086
|
+
var RECENCY_WEIGHT = 2;
|
|
20087
|
+
var MAX_COMPOSITE_SCORE = RETRIEVAL_SCORE_WEIGHT + EMBEDDING_SIMILARITY_WEIGHT + WORKSPACE_MATCH_WEIGHT + RECENCY_WEIGHT;
|
|
19983
20088
|
var GLOBAL_WORKSPACE_SCORE = 0.5;
|
|
19984
20089
|
var SIBLING_WORKSPACE_SCORE = 0.25;
|
|
19985
|
-
function rerankSearchResults(results, workspace) {
|
|
20090
|
+
function rerankSearchResults(results, workspace, queryEmbedding) {
|
|
19986
20091
|
if (results.length <= 1) {
|
|
19987
20092
|
return results;
|
|
19988
20093
|
}
|
|
@@ -19991,18 +20096,25 @@ function rerankSearchResults(results, workspace) {
|
|
|
19991
20096
|
const minUpdatedAt = Math.min(...updatedAtTimes);
|
|
19992
20097
|
const maxUpdatedAt = Math.max(...updatedAtTimes);
|
|
19993
20098
|
return results.map((result) => {
|
|
20099
|
+
const embeddingSimilarityScore = computeEmbeddingSimilarityScore(result, queryEmbedding);
|
|
19994
20100
|
const workspaceScore = computeWorkspaceScore(result.workspace, normalizedQueryWs);
|
|
19995
20101
|
const recencyScore = maxUpdatedAt === minUpdatedAt ? 0 : (result.updatedAt.getTime() - minUpdatedAt) / (maxUpdatedAt - minUpdatedAt);
|
|
19996
|
-
const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
|
|
20102
|
+
const combinedScore = (result.score * RETRIEVAL_SCORE_WEIGHT + embeddingSimilarityScore * EMBEDDING_SIMILARITY_WEIGHT + workspaceScore * WORKSPACE_MATCH_WEIGHT + recencyScore * RECENCY_WEIGHT) / MAX_COMPOSITE_SCORE;
|
|
19997
20103
|
return {
|
|
19998
20104
|
...result,
|
|
19999
20105
|
score: toNormalizedScore(combinedScore)
|
|
20000
20106
|
};
|
|
20001
20107
|
}).sort((a, b) => b.score - a.score);
|
|
20002
20108
|
}
|
|
20109
|
+
function computeEmbeddingSimilarityScore(result, queryEmbedding) {
|
|
20110
|
+
return normalizeCosineSimilarity(compareVectors(result.embedding, queryEmbedding));
|
|
20111
|
+
}
|
|
20003
20112
|
function normalizeWorkspacePath(value) {
|
|
20004
20113
|
return value.trim().replaceAll("\\", "/").replace(/\/+/g, "/").split("/").filter(Boolean).join("/");
|
|
20005
20114
|
}
|
|
20115
|
+
function normalizeCosineSimilarity(value) {
|
|
20116
|
+
return (value + 1) / 2;
|
|
20117
|
+
}
|
|
20006
20118
|
function computeWorkspaceScore(memoryWs, queryWs) {
|
|
20007
20119
|
if (!queryWs) {
|
|
20008
20120
|
return 0;
|
|
@@ -20033,24 +20145,33 @@ var MAX_LIST_LIMIT = 100;
|
|
|
20033
20145
|
|
|
20034
20146
|
class MemoryService {
|
|
20035
20147
|
repository;
|
|
20036
|
-
|
|
20148
|
+
embeddingService;
|
|
20149
|
+
constructor(repository, embeddingService) {
|
|
20037
20150
|
this.repository = repository;
|
|
20151
|
+
this.embeddingService = embeddingService;
|
|
20038
20152
|
}
|
|
20039
20153
|
async create(input) {
|
|
20040
20154
|
const content = input.content.trim();
|
|
20041
20155
|
if (!content) {
|
|
20042
20156
|
throw new ValidationError("Memory content is required.");
|
|
20043
20157
|
}
|
|
20044
|
-
|
|
20158
|
+
const memory = await this.repository.create({
|
|
20045
20159
|
content,
|
|
20160
|
+
embedding: await this.embeddingService.createVector(content),
|
|
20046
20161
|
workspace: normalizeOptionalString(input.workspace)
|
|
20047
20162
|
});
|
|
20163
|
+
return toPublicMemoryRecord(memory);
|
|
20048
20164
|
}
|
|
20049
20165
|
async update(input) {
|
|
20050
20166
|
const content = input.content.trim();
|
|
20051
20167
|
if (!content)
|
|
20052
20168
|
throw new ValidationError("Memory content is required.");
|
|
20053
|
-
|
|
20169
|
+
const memory = await this.repository.update({
|
|
20170
|
+
id: input.id,
|
|
20171
|
+
content,
|
|
20172
|
+
embedding: await this.embeddingService.createVector(content)
|
|
20173
|
+
});
|
|
20174
|
+
return toPublicMemoryRecord(memory);
|
|
20054
20175
|
}
|
|
20055
20176
|
async delete(input) {
|
|
20056
20177
|
const id = input.id.trim();
|
|
@@ -20059,16 +20180,18 @@ class MemoryService {
|
|
|
20059
20180
|
return this.repository.delete({ id });
|
|
20060
20181
|
}
|
|
20061
20182
|
async get(id) {
|
|
20062
|
-
|
|
20183
|
+
const memory = await this.repository.get(id);
|
|
20184
|
+
return memory ? toPublicMemoryRecord(memory) : undefined;
|
|
20063
20185
|
}
|
|
20064
20186
|
async list(input) {
|
|
20065
20187
|
const workspace = normalizeOptionalString(input.workspace);
|
|
20066
|
-
|
|
20188
|
+
const page = await this.repository.list({
|
|
20067
20189
|
workspace,
|
|
20068
20190
|
workspaceIsNull: workspace ? false : Boolean(input.workspaceIsNull),
|
|
20069
20191
|
offset: normalizeOffset(input.offset),
|
|
20070
20192
|
limit: normalizeListLimit(input.limit)
|
|
20071
20193
|
});
|
|
20194
|
+
return toPublicMemoryPage(page);
|
|
20072
20195
|
}
|
|
20073
20196
|
async listWorkspaces() {
|
|
20074
20197
|
return this.repository.listWorkspaces();
|
|
@@ -20086,10 +20209,38 @@ class MemoryService {
|
|
|
20086
20209
|
updatedAfter: input.updatedAfter,
|
|
20087
20210
|
updatedBefore: input.updatedBefore
|
|
20088
20211
|
};
|
|
20089
|
-
const results = await
|
|
20090
|
-
|
|
20212
|
+
const [results, queryEmbedding] = await Promise.all([
|
|
20213
|
+
this.repository.search(normalizedQuery),
|
|
20214
|
+
this.embeddingService.createVector(terms.join(" "))
|
|
20215
|
+
]);
|
|
20216
|
+
return rerankSearchResults(results, workspace, queryEmbedding).slice(0, requestedLimit).map(toPublicSearchResult);
|
|
20091
20217
|
}
|
|
20092
20218
|
}
|
|
20219
|
+
function toPublicMemoryRecord(memory) {
|
|
20220
|
+
return {
|
|
20221
|
+
id: memory.id,
|
|
20222
|
+
content: memory.content,
|
|
20223
|
+
workspace: memory.workspace,
|
|
20224
|
+
createdAt: memory.createdAt,
|
|
20225
|
+
updatedAt: memory.updatedAt
|
|
20226
|
+
};
|
|
20227
|
+
}
|
|
20228
|
+
function toPublicSearchResult(result) {
|
|
20229
|
+
return {
|
|
20230
|
+
id: result.id,
|
|
20231
|
+
content: result.content,
|
|
20232
|
+
score: result.score,
|
|
20233
|
+
workspace: result.workspace,
|
|
20234
|
+
createdAt: result.createdAt,
|
|
20235
|
+
updatedAt: result.updatedAt
|
|
20236
|
+
};
|
|
20237
|
+
}
|
|
20238
|
+
function toPublicMemoryPage(page) {
|
|
20239
|
+
return {
|
|
20240
|
+
items: page.items.map(toPublicMemoryRecord),
|
|
20241
|
+
hasMore: page.hasMore
|
|
20242
|
+
};
|
|
20243
|
+
}
|
|
20093
20244
|
function normalizeLimit(value) {
|
|
20094
20245
|
if (value === undefined) {
|
|
20095
20246
|
return DEFAULT_RECALL_LIMIT;
|
|
@@ -20119,22 +20270,23 @@ function normalizeTerms(terms) {
|
|
|
20119
20270
|
|
|
20120
20271
|
// src/mcp/tools/recall.ts
|
|
20121
20272
|
var recallInputSchema = {
|
|
20122
|
-
terms: array(string2()).min(1).describe("Search terms
|
|
20123
|
-
limit: number2().int().min(1).max(MAX_RECALL_LIMIT).optional().describe("Maximum
|
|
20124
|
-
workspace: string2().optional().describe("
|
|
20125
|
-
updated_after: string2().optional().describe("Only return memories updated at or after this ISO 8601 timestamp.
|
|
20126
|
-
updated_before: string2().optional().describe("Only return memories updated at or before this ISO 8601 timestamp.
|
|
20273
|
+
terms: array(string2()).min(1).describe("Search terms for lexical memory lookup. Pass 2-5 short anchor-heavy terms or exact phrases as separate entries. Prefer identifiers, commands, file paths, package names, and conventions likely to appear verbatim in the memory. Avoid vague words, full sentences, and repeating the workspace name. If recall misses, retry once with overlapping alternate terms."),
|
|
20274
|
+
limit: number2().int().min(1).max(MAX_RECALL_LIMIT).optional().describe("Maximum matches to return. Keep this small when you only need the strongest hits."),
|
|
20275
|
+
workspace: string2().optional().describe("Pass the current working directory. This strongly boosts memories from the active project while still allowing global and cross-workspace matches."),
|
|
20276
|
+
updated_after: string2().optional().describe("Only return memories updated at or after this ISO 8601 timestamp."),
|
|
20277
|
+
updated_before: string2().optional().describe("Only return memories updated at or before this ISO 8601 timestamp.")
|
|
20127
20278
|
};
|
|
20128
20279
|
function toMemoryXml(r) {
|
|
20129
20280
|
const workspace = r.workspace ? ` workspace="${escapeXml(r.workspace)}"` : "";
|
|
20130
20281
|
const content = escapeXml(r.content);
|
|
20131
|
-
|
|
20282
|
+
const score = Number(r.score.toFixed(3)).toString();
|
|
20283
|
+
return `<memory id="${r.id}" score="${score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
|
|
20132
20284
|
${content}
|
|
20133
20285
|
</memory>`;
|
|
20134
20286
|
}
|
|
20135
20287
|
function registerRecallTool(server, memory) {
|
|
20136
20288
|
server.registerTool("recall", {
|
|
20137
|
-
description: "
|
|
20289
|
+
description: "Retrieve relevant memories for the current task. Use at conversation start and before design choices, conventions, or edge cases. Query with 2-5 short anchor-heavy terms or exact phrases, not questions or full sentences. `recall` is lexical-first; semantic reranking only reorders lexical matches. If it misses, retry once with overlapping alternate terms. Pass workspace. Returns `<memories>...</memories>` or a no-match hint.",
|
|
20138
20290
|
inputSchema: recallInputSchema
|
|
20139
20291
|
}, async ({ terms, limit, workspace, updated_after, updated_before }) => {
|
|
20140
20292
|
try {
|
|
@@ -20145,7 +20297,7 @@ function registerRecallTool(server, memory) {
|
|
|
20145
20297
|
updatedAfter: parseOptionalDate(updated_after, "updated_after"),
|
|
20146
20298
|
updatedBefore: parseOptionalDate(updated_before, "updated_before")
|
|
20147
20299
|
});
|
|
20148
|
-
const text = results.length === 0 ? "No matching memories found." : `<memories>
|
|
20300
|
+
const text = results.length === 0 ? "No matching memories found. Retry once with 1-3 alternate overlapping terms or an exact phrase likely to appear in the memory text. Recall is lexical-first, so semantic reranking cannot rescue a query with no wording overlap." : `<memories>
|
|
20149
20301
|
${results.map(toMemoryXml).join(`
|
|
20150
20302
|
`)}
|
|
20151
20303
|
</memories>`;
|
|
@@ -20160,12 +20312,12 @@ ${results.map(toMemoryXml).join(`
|
|
|
20160
20312
|
|
|
20161
20313
|
// src/mcp/tools/remember.ts
|
|
20162
20314
|
var rememberInputSchema = {
|
|
20163
|
-
content: string2().describe("
|
|
20164
|
-
workspace: string2().optional().describe("
|
|
20315
|
+
content: string2().describe("One durable fact to save. Use a single self-contained sentence or short note with concrete nouns, identifiers, commands, file paths, or exact phrases the agent is likely to reuse."),
|
|
20316
|
+
workspace: string2().optional().describe("Pass the current working directory for project-specific memories. Omit only for truly global memories.")
|
|
20165
20317
|
};
|
|
20166
20318
|
function registerRememberTool(server, memory) {
|
|
20167
20319
|
server.registerTool("remember", {
|
|
20168
|
-
description:
|
|
20320
|
+
description: 'Save one durable memory for later recall. Use when the user states a stable preference, corrects you, or establishes reusable project context not obvious from code or git history. Save one fact per memory. Call `recall` first; use `revise` instead of creating duplicates. Do not store secrets, temporary task state, or codebase facts. Returns `<memory id="..." />`.',
|
|
20169
20321
|
inputSchema: rememberInputSchema
|
|
20170
20322
|
}, async ({ content, workspace }) => {
|
|
20171
20323
|
try {
|
|
@@ -20184,12 +20336,12 @@ function registerRememberTool(server, memory) {
|
|
|
20184
20336
|
|
|
20185
20337
|
// src/mcp/tools/revise.ts
|
|
20186
20338
|
var reviseInputSchema = {
|
|
20187
|
-
id: string2().describe("The id
|
|
20188
|
-
content: string2().describe("The replacement
|
|
20339
|
+
id: string2().describe("The memory id to update. Use an id returned by `recall`."),
|
|
20340
|
+
content: string2().describe("The corrected replacement for that same fact. Keep it to one durable fact.")
|
|
20189
20341
|
};
|
|
20190
20342
|
function registerReviseTool(server, memory) {
|
|
20191
20343
|
server.registerTool("revise", {
|
|
20192
|
-
description:
|
|
20344
|
+
description: 'Replace one existing memory with corrected wording. Use after `recall` when the same fact still applies but details changed. Do not append unrelated facts or merge memories. Returns `<memory id="..." updated_at="..." />`.',
|
|
20193
20345
|
inputSchema: reviseInputSchema
|
|
20194
20346
|
}, async ({ id, content }) => {
|
|
20195
20347
|
try {
|
|
@@ -20210,14 +20362,15 @@ function registerReviseTool(server, memory) {
|
|
|
20210
20362
|
|
|
20211
20363
|
// src/mcp/server.ts
|
|
20212
20364
|
var SERVER_INSTRUCTIONS = [
|
|
20213
|
-
"
|
|
20214
|
-
"Use `recall` at
|
|
20215
|
-
"
|
|
20216
|
-
"
|
|
20217
|
-
"
|
|
20218
|
-
"Use `
|
|
20219
|
-
"
|
|
20220
|
-
"
|
|
20365
|
+
"Use this server for durable memory: user preferences, corrections, decisions, and project context not obvious from code or git history.",
|
|
20366
|
+
"Use `recall` at conversation start and before design choices, conventions, or edge cases.",
|
|
20367
|
+
"Query `recall` with 2-5 short anchor-heavy terms or exact phrases likely to appear verbatim in memory text: identifiers, commands, file paths, and conventions.",
|
|
20368
|
+
"`recall` is lexical-first; semantic reranking only reorders lexical matches.",
|
|
20369
|
+
"If `recall` misses, retry once with overlapping alternate terms.",
|
|
20370
|
+
"Use `remember` for one durable fact when the user states a preference, corrects you, or a reusable project decision becomes clear.",
|
|
20371
|
+
"Call `recall` before `remember`; if the fact already exists, use `revise` instead of creating a duplicate.",
|
|
20372
|
+
"Use `revise` to correct an existing memory and `forget` to remove a wrong or obsolete one.",
|
|
20373
|
+
"Pass workspace for project-scoped calls. Omit it only for truly global memories."
|
|
20221
20374
|
].join(" ");
|
|
20222
20375
|
function createMcpServer(memory, version3) {
|
|
20223
20376
|
const server = new McpServer({
|
|
@@ -20233,72 +20386,242 @@ function createMcpServer(memory, version3) {
|
|
|
20233
20386
|
return server;
|
|
20234
20387
|
}
|
|
20235
20388
|
|
|
20236
|
-
// src/sqlite
|
|
20237
|
-
import { mkdirSync } from "node:fs";
|
|
20389
|
+
// src/sqlite/db.ts
|
|
20390
|
+
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
20238
20391
|
import { dirname } from "node:path";
|
|
20239
20392
|
import Database from "better-sqlite3";
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
|
|
20243
|
-
|
|
20244
|
-
|
|
20245
|
-
|
|
20246
|
-
|
|
20247
|
-
|
|
20248
|
-
|
|
20249
|
-
|
|
20250
|
-
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
|
|
20256
|
-
|
|
20257
|
-
|
|
20258
|
-
|
|
20259
|
-
|
|
20260
|
-
|
|
20261
|
-
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20267
|
-
|
|
20268
|
-
|
|
20269
|
-
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20273
|
-
|
|
20274
|
-
|
|
20393
|
+
|
|
20394
|
+
// src/sqlite/memory-schema.ts
|
|
20395
|
+
function createMemoriesTable(database, options) {
|
|
20396
|
+
const embeddingColumn = getEmbeddingColumnSql(options.embeddingColumn);
|
|
20397
|
+
database.exec(`
|
|
20398
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
20399
|
+
id TEXT PRIMARY KEY,
|
|
20400
|
+
content TEXT NOT NULL,
|
|
20401
|
+
workspace TEXT,
|
|
20402
|
+
${embeddingColumn}
|
|
20403
|
+
created_at INTEGER NOT NULL,
|
|
20404
|
+
updated_at INTEGER NOT NULL
|
|
20405
|
+
);
|
|
20406
|
+
`);
|
|
20407
|
+
}
|
|
20408
|
+
function createMemoryIndexes(database) {
|
|
20409
|
+
database.exec(`
|
|
20410
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);
|
|
20411
|
+
CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace);
|
|
20412
|
+
`);
|
|
20413
|
+
}
|
|
20414
|
+
function createMemorySearchArtifacts(database, rebuild = false) {
|
|
20415
|
+
database.exec(`
|
|
20416
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
20417
|
+
content,
|
|
20418
|
+
content = 'memories',
|
|
20419
|
+
content_rowid = 'rowid',
|
|
20420
|
+
tokenize = 'porter unicode61'
|
|
20421
|
+
);
|
|
20422
|
+
|
|
20423
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
20424
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
20425
|
+
END;
|
|
20426
|
+
|
|
20427
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
20428
|
+
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
20429
|
+
VALUES ('delete', old.rowid, old.content);
|
|
20430
|
+
END;
|
|
20431
|
+
|
|
20432
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
20433
|
+
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
20434
|
+
VALUES ('delete', old.rowid, old.content);
|
|
20435
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
20436
|
+
END;
|
|
20437
|
+
`);
|
|
20438
|
+
if (rebuild) {
|
|
20439
|
+
database.exec("INSERT INTO memories_fts(memories_fts) VALUES ('rebuild')");
|
|
20440
|
+
}
|
|
20441
|
+
}
|
|
20442
|
+
function dropMemorySearchArtifacts(database) {
|
|
20443
|
+
database.exec(`
|
|
20444
|
+
DROP TRIGGER IF EXISTS memories_ai;
|
|
20445
|
+
DROP TRIGGER IF EXISTS memories_ad;
|
|
20446
|
+
DROP TRIGGER IF EXISTS memories_au;
|
|
20447
|
+
DROP TABLE IF EXISTS memories_fts;
|
|
20448
|
+
`);
|
|
20449
|
+
}
|
|
20450
|
+
function getEmbeddingColumnSql(mode) {
|
|
20451
|
+
switch (mode) {
|
|
20452
|
+
case "omit":
|
|
20453
|
+
return "";
|
|
20454
|
+
case "nullable":
|
|
20455
|
+
return `embedding BLOB,
|
|
20456
|
+
`;
|
|
20457
|
+
case "required":
|
|
20458
|
+
return `embedding BLOB NOT NULL,
|
|
20459
|
+
`;
|
|
20460
|
+
}
|
|
20461
|
+
}
|
|
20462
|
+
|
|
20463
|
+
// src/sqlite/migrations/001-create-memory-schema.ts
|
|
20464
|
+
var createMemorySchemaMigration = {
|
|
20465
|
+
version: 1,
|
|
20466
|
+
async up(database) {
|
|
20467
|
+
createMemoriesTable(database, { embeddingColumn: "omit" });
|
|
20468
|
+
createMemoryIndexes(database);
|
|
20469
|
+
createMemorySearchArtifacts(database);
|
|
20470
|
+
}
|
|
20471
|
+
};
|
|
20472
|
+
|
|
20473
|
+
// src/sqlite/embedding-codec.ts
|
|
20474
|
+
var FLOAT32_BYTE_WIDTH = 4;
|
|
20475
|
+
function encodeEmbedding(vector) {
|
|
20476
|
+
const typedArray = Float32Array.from(vector);
|
|
20477
|
+
return new Uint8Array(typedArray.buffer.slice(0));
|
|
20478
|
+
}
|
|
20479
|
+
function decodeEmbedding(value) {
|
|
20480
|
+
const bytes = toUint8Array(value);
|
|
20481
|
+
if (bytes.byteLength === 0) {
|
|
20482
|
+
throw new Error("Embedding blob is empty.");
|
|
20483
|
+
}
|
|
20484
|
+
if (bytes.byteLength % FLOAT32_BYTE_WIDTH !== 0) {
|
|
20485
|
+
throw new Error("Embedding blob length is not a multiple of 4.");
|
|
20486
|
+
}
|
|
20487
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
20488
|
+
const vector = [];
|
|
20489
|
+
for (let offset = 0;offset < bytes.byteLength; offset += FLOAT32_BYTE_WIDTH) {
|
|
20490
|
+
vector.push(view.getFloat32(offset, true));
|
|
20491
|
+
}
|
|
20492
|
+
return vector;
|
|
20493
|
+
}
|
|
20494
|
+
function toUint8Array(value) {
|
|
20495
|
+
if (value instanceof Uint8Array) {
|
|
20496
|
+
return value;
|
|
20497
|
+
}
|
|
20498
|
+
if (value instanceof ArrayBuffer) {
|
|
20499
|
+
return new Uint8Array(value);
|
|
20500
|
+
}
|
|
20501
|
+
throw new Error("Expected embedding blob as Uint8Array or ArrayBuffer.");
|
|
20502
|
+
}
|
|
20503
|
+
|
|
20504
|
+
// src/sqlite/migrations/002-add-memory-embedding.ts
|
|
20505
|
+
function createAddMemoryEmbeddingMigration(embeddingService = new EmbeddingService) {
|
|
20506
|
+
return {
|
|
20507
|
+
version: 2,
|
|
20508
|
+
async up(database) {
|
|
20509
|
+
database.exec("ALTER TABLE memories ADD COLUMN embedding BLOB");
|
|
20510
|
+
const rows = database.prepare("SELECT id, content FROM memories ORDER BY created_at ASC").all();
|
|
20511
|
+
const updateStatement = database.prepare("UPDATE memories SET embedding = ? WHERE id = ?");
|
|
20512
|
+
for (const row of rows) {
|
|
20513
|
+
const embedding = await embeddingService.createVector(row.content);
|
|
20514
|
+
updateStatement.run(encodeEmbedding(embedding), row.id);
|
|
20515
|
+
}
|
|
20516
|
+
const nullRows = database.prepare("SELECT COUNT(*) AS count FROM memories WHERE embedding IS NULL").all();
|
|
20517
|
+
if ((nullRows[0]?.count ?? 0) > 0) {
|
|
20518
|
+
throw new Error("Failed to backfill embeddings for all memories.");
|
|
20519
|
+
}
|
|
20520
|
+
dropMemorySearchArtifacts(database);
|
|
20521
|
+
database.exec("ALTER TABLE memories RENAME TO memories_old");
|
|
20522
|
+
createMemoriesTable(database, { embeddingColumn: "required" });
|
|
20523
|
+
database.exec(`
|
|
20524
|
+
INSERT INTO memories (id, content, workspace, embedding, created_at, updated_at)
|
|
20525
|
+
SELECT id, content, workspace, embedding, created_at, updated_at
|
|
20526
|
+
FROM memories_old
|
|
20527
|
+
`);
|
|
20528
|
+
database.exec("DROP TABLE memories_old");
|
|
20529
|
+
createMemoryIndexes(database);
|
|
20530
|
+
createMemorySearchArtifacts(database, true);
|
|
20531
|
+
}
|
|
20532
|
+
};
|
|
20533
|
+
}
|
|
20534
|
+
var addMemoryEmbeddingMigration = createAddMemoryEmbeddingMigration();
|
|
20535
|
+
|
|
20536
|
+
// src/sqlite/migrations/index.ts
|
|
20537
|
+
function createMemoryMigrations(embeddingService = new EmbeddingService) {
|
|
20538
|
+
return [createMemorySchemaMigration, createAddMemoryEmbeddingMigration(embeddingService)];
|
|
20539
|
+
}
|
|
20540
|
+
var MEMORY_MIGRATIONS = createMemoryMigrations();
|
|
20541
|
+
|
|
20542
|
+
// src/sqlite/db.ts
|
|
20543
|
+
var PRAGMA_STATEMENTS = [
|
|
20544
|
+
"journal_mode = WAL",
|
|
20545
|
+
"synchronous = NORMAL",
|
|
20546
|
+
"foreign_keys = ON",
|
|
20547
|
+
"busy_timeout = 5000"
|
|
20548
|
+
];
|
|
20549
|
+
async function openMemoryDatabase(databasePath, options = {}) {
|
|
20550
|
+
let database;
|
|
20275
20551
|
try {
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
initializeMemoryDatabase(database);
|
|
20552
|
+
mkdirSync2(dirname(databasePath), { recursive: true });
|
|
20553
|
+
database = new Database(databasePath);
|
|
20554
|
+
await initializeMemoryDatabase(database, createMemoryMigrations(options.embeddingService));
|
|
20279
20555
|
return database;
|
|
20556
|
+
} catch (error2) {
|
|
20557
|
+
database?.close();
|
|
20558
|
+
if (error2 instanceof PersistenceError) {
|
|
20559
|
+
throw error2;
|
|
20560
|
+
}
|
|
20561
|
+
throw new PersistenceError("Failed to initialize the SQLite database.", {
|
|
20562
|
+
cause: error2
|
|
20563
|
+
});
|
|
20564
|
+
}
|
|
20565
|
+
}
|
|
20566
|
+
async function initializeMemoryDatabase(database, migrations = MEMORY_MIGRATIONS) {
|
|
20567
|
+
try {
|
|
20568
|
+
applyPragmas(database);
|
|
20569
|
+
await runSqliteMigrations(database, migrations);
|
|
20280
20570
|
} catch (error2) {
|
|
20281
20571
|
throw new PersistenceError("Failed to initialize the SQLite database.", {
|
|
20282
20572
|
cause: error2
|
|
20283
20573
|
});
|
|
20284
20574
|
}
|
|
20285
20575
|
}
|
|
20286
|
-
function
|
|
20576
|
+
async function runSqliteMigrations(database, migrations) {
|
|
20577
|
+
validateMigrations(migrations);
|
|
20578
|
+
let currentVersion = getUserVersion(database);
|
|
20579
|
+
for (const migration of migrations) {
|
|
20580
|
+
if (migration.version <= currentVersion) {
|
|
20581
|
+
continue;
|
|
20582
|
+
}
|
|
20583
|
+
database.exec("BEGIN");
|
|
20584
|
+
try {
|
|
20585
|
+
await migration.up(database);
|
|
20586
|
+
setUserVersion(database, migration.version);
|
|
20587
|
+
database.exec("COMMIT");
|
|
20588
|
+
currentVersion = migration.version;
|
|
20589
|
+
} catch (error2) {
|
|
20590
|
+
try {
|
|
20591
|
+
database.exec("ROLLBACK");
|
|
20592
|
+
} catch {}
|
|
20593
|
+
throw error2;
|
|
20594
|
+
}
|
|
20595
|
+
}
|
|
20596
|
+
}
|
|
20597
|
+
function applyPragmas(database) {
|
|
20287
20598
|
if (database.pragma) {
|
|
20288
|
-
|
|
20289
|
-
|
|
20290
|
-
|
|
20291
|
-
|
|
20292
|
-
}
|
|
20293
|
-
|
|
20294
|
-
database.exec(
|
|
20295
|
-
database.exec("PRAGMA foreign_keys = ON");
|
|
20296
|
-
database.exec("PRAGMA busy_timeout = 5000");
|
|
20599
|
+
for (const statement of PRAGMA_STATEMENTS) {
|
|
20600
|
+
database.pragma(statement);
|
|
20601
|
+
}
|
|
20602
|
+
return;
|
|
20603
|
+
}
|
|
20604
|
+
for (const statement of PRAGMA_STATEMENTS) {
|
|
20605
|
+
database.exec(`PRAGMA ${statement}`);
|
|
20297
20606
|
}
|
|
20298
|
-
database.exec(MEMORY_SCHEMA);
|
|
20299
20607
|
}
|
|
20300
|
-
|
|
20301
|
-
|
|
20608
|
+
function getUserVersion(database) {
|
|
20609
|
+
const rows = database.prepare("PRAGMA user_version").all();
|
|
20610
|
+
return rows[0]?.user_version ?? 0;
|
|
20611
|
+
}
|
|
20612
|
+
function setUserVersion(database, version3) {
|
|
20613
|
+
database.exec(`PRAGMA user_version = ${version3}`);
|
|
20614
|
+
}
|
|
20615
|
+
function validateMigrations(migrations) {
|
|
20616
|
+
let previousVersion = 0;
|
|
20617
|
+
for (const migration of migrations) {
|
|
20618
|
+
if (!Number.isInteger(migration.version) || migration.version <= previousVersion) {
|
|
20619
|
+
throw new Error("SQLite migrations must use strictly increasing versions.");
|
|
20620
|
+
}
|
|
20621
|
+
previousVersion = migration.version;
|
|
20622
|
+
}
|
|
20623
|
+
}
|
|
20624
|
+
// src/sqlite/repository.ts
|
|
20302
20625
|
import { randomUUID } from "node:crypto";
|
|
20303
20626
|
var DEFAULT_SEARCH_LIMIT = 15;
|
|
20304
20627
|
var DEFAULT_LIST_LIMIT2 = 15;
|
|
@@ -20317,6 +20640,7 @@ class SqliteMemoryRepository {
|
|
|
20317
20640
|
id,
|
|
20318
20641
|
content,
|
|
20319
20642
|
workspace,
|
|
20643
|
+
embedding,
|
|
20320
20644
|
created_at,
|
|
20321
20645
|
updated_at
|
|
20322
20646
|
) VALUES (
|
|
@@ -20324,11 +20648,12 @@ class SqliteMemoryRepository {
|
|
|
20324
20648
|
?,
|
|
20325
20649
|
?,
|
|
20326
20650
|
?,
|
|
20651
|
+
?,
|
|
20327
20652
|
?
|
|
20328
20653
|
)
|
|
20329
20654
|
`);
|
|
20330
|
-
this.getStatement = database.prepare("SELECT id, content, workspace, created_at, updated_at FROM memories WHERE id = ?");
|
|
20331
|
-
this.updateStatement = database.prepare("UPDATE memories SET content = ?, updated_at = ? WHERE id = ?");
|
|
20655
|
+
this.getStatement = database.prepare("SELECT id, content, workspace, embedding, created_at, updated_at FROM memories WHERE id = ?");
|
|
20656
|
+
this.updateStatement = database.prepare("UPDATE memories SET content = ?, embedding = ?, updated_at = ? WHERE id = ?");
|
|
20332
20657
|
this.deleteStatement = database.prepare("DELETE FROM memories WHERE id = ?");
|
|
20333
20658
|
this.listWorkspacesStatement = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace");
|
|
20334
20659
|
}
|
|
@@ -20338,11 +20663,12 @@ class SqliteMemoryRepository {
|
|
|
20338
20663
|
const memory = {
|
|
20339
20664
|
id: randomUUID(),
|
|
20340
20665
|
content: input.content,
|
|
20666
|
+
embedding: input.embedding,
|
|
20341
20667
|
workspace: input.workspace,
|
|
20342
20668
|
createdAt: now,
|
|
20343
20669
|
updatedAt: now
|
|
20344
20670
|
};
|
|
20345
|
-
this.insertStatement.run(memory.id, memory.content, memory.workspace, memory.createdAt.getTime(), memory.updatedAt.getTime());
|
|
20671
|
+
this.insertStatement.run(memory.id, memory.content, memory.workspace, encodeEmbedding(memory.embedding), memory.createdAt.getTime(), memory.updatedAt.getTime());
|
|
20346
20672
|
return memory;
|
|
20347
20673
|
} catch (error2) {
|
|
20348
20674
|
throw new PersistenceError("Failed to save memory.", { cause: error2 });
|
|
@@ -20367,6 +20693,7 @@ class SqliteMemoryRepository {
|
|
|
20367
20693
|
m.id,
|
|
20368
20694
|
m.content,
|
|
20369
20695
|
m.workspace,
|
|
20696
|
+
m.embedding,
|
|
20370
20697
|
m.created_at,
|
|
20371
20698
|
m.updated_at,
|
|
20372
20699
|
MAX(0, -bm25(memories_fts)) AS score
|
|
@@ -20379,7 +20706,7 @@ class SqliteMemoryRepository {
|
|
|
20379
20706
|
const rows = statement.all(...params);
|
|
20380
20707
|
const maxScore = Math.max(...rows.map((row) => row.score), 0);
|
|
20381
20708
|
return rows.map((row) => ({
|
|
20382
|
-
...
|
|
20709
|
+
...toMemoryEntity(row),
|
|
20383
20710
|
score: toNormalizedScore(maxScore > 0 ? row.score / maxScore : 0)
|
|
20384
20711
|
}));
|
|
20385
20712
|
} catch (error2) {
|
|
@@ -20392,7 +20719,7 @@ class SqliteMemoryRepository {
|
|
|
20392
20719
|
try {
|
|
20393
20720
|
const rows = this.getStatement.all(id);
|
|
20394
20721
|
const row = rows[0];
|
|
20395
|
-
return row ?
|
|
20722
|
+
return row ? toMemoryEntity(row) : undefined;
|
|
20396
20723
|
} catch (error2) {
|
|
20397
20724
|
throw new PersistenceError("Failed to find memory.", { cause: error2 });
|
|
20398
20725
|
}
|
|
@@ -20413,7 +20740,7 @@ class SqliteMemoryRepository {
|
|
|
20413
20740
|
const queryLimit = limit + 1;
|
|
20414
20741
|
params.push(queryLimit, offset);
|
|
20415
20742
|
const statement = this.database.prepare(`
|
|
20416
|
-
SELECT id, content, workspace, created_at, updated_at
|
|
20743
|
+
SELECT id, content, workspace, embedding, created_at, updated_at
|
|
20417
20744
|
FROM memories
|
|
20418
20745
|
${whereClause}
|
|
20419
20746
|
ORDER BY created_at DESC
|
|
@@ -20421,7 +20748,7 @@ class SqliteMemoryRepository {
|
|
|
20421
20748
|
`);
|
|
20422
20749
|
const rows = statement.all(...params);
|
|
20423
20750
|
const hasMore = rows.length > limit;
|
|
20424
|
-
const items = (hasMore ? rows.slice(0, limit) : rows).map(
|
|
20751
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryEntity);
|
|
20425
20752
|
return { items, hasMore };
|
|
20426
20753
|
} catch (error2) {
|
|
20427
20754
|
throw new PersistenceError("Failed to list memories.", { cause: error2 });
|
|
@@ -20431,7 +20758,7 @@ class SqliteMemoryRepository {
|
|
|
20431
20758
|
let result;
|
|
20432
20759
|
try {
|
|
20433
20760
|
const now = Date.now();
|
|
20434
|
-
result = this.updateStatement.run(input.content, now, input.id);
|
|
20761
|
+
result = this.updateStatement.run(input.content, encodeEmbedding(input.embedding), now, input.id);
|
|
20435
20762
|
} catch (error2) {
|
|
20436
20763
|
throw new PersistenceError("Failed to update memory.", { cause: error2 });
|
|
20437
20764
|
}
|
|
@@ -20464,9 +20791,10 @@ class SqliteMemoryRepository {
|
|
|
20464
20791
|
}
|
|
20465
20792
|
}
|
|
20466
20793
|
}
|
|
20467
|
-
var
|
|
20794
|
+
var toMemoryEntity = (row) => ({
|
|
20468
20795
|
id: row.id,
|
|
20469
20796
|
content: row.content,
|
|
20797
|
+
embedding: decodeEmbedding(row.embedding),
|
|
20470
20798
|
workspace: row.workspace ?? undefined,
|
|
20471
20799
|
createdAt: new Date(row.created_at),
|
|
20472
20800
|
updatedAt: new Date(row.updated_at)
|
|
@@ -20479,7 +20807,6 @@ function toFtsTerm(term) {
|
|
|
20479
20807
|
}
|
|
20480
20808
|
return `"${escaped}"*`;
|
|
20481
20809
|
}
|
|
20482
|
-
|
|
20483
20810
|
// node_modules/@hono/node-server/dist/index.mjs
|
|
20484
20811
|
import { createServer as createServerHTTP } from "http";
|
|
20485
20812
|
import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
|
|
@@ -23870,9 +24197,10 @@ function startWebServer(memory, options) {
|
|
|
23870
24197
|
|
|
23871
24198
|
// src/index.ts
|
|
23872
24199
|
var config2 = resolveConfig();
|
|
23873
|
-
var
|
|
23874
|
-
var
|
|
23875
|
-
var
|
|
24200
|
+
var embeddingService = new EmbeddingService({ modelsCachePath: config2.modelsCachePath });
|
|
24201
|
+
var database = await openMemoryDatabase(config2.databasePath, { embeddingService });
|
|
24202
|
+
var repository2 = new SqliteMemoryRepository(database);
|
|
24203
|
+
var memoryService = new MemoryService(repository2, embeddingService);
|
|
23876
24204
|
if (config2.uiMode) {
|
|
23877
24205
|
let shutdown = function() {
|
|
23878
24206
|
server.close();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jcyamacho/agent-memory",
|
|
3
3
|
"main": "dist/index.js",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.15",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-memory": "dist/index.js"
|
|
7
7
|
},
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"url": "git+https://github.com/jcyamacho/agent-memory.git"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build": "bun build src/index.ts --target=node --external better-sqlite3 --outfile dist/index.js --banner \"#!/usr/bin/env node\"",
|
|
23
|
+
"build": "bun build src/index.ts --target=node --external better-sqlite3 --external @huggingface/transformers --outfile dist/index.js --banner \"#!/usr/bin/env node\"",
|
|
24
24
|
"start": "node dist/index.js",
|
|
25
25
|
"test": "bun test",
|
|
26
26
|
"lint": "biome check --write && tsc --noEmit",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@hono/node-server": "^1.19.11",
|
|
41
|
+
"@huggingface/transformers": "^3.8.1",
|
|
41
42
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
42
43
|
"better-sqlite3": "^12.6.2",
|
|
43
44
|
"hono": "^4.12.8",
|