@jcyamacho/agent-memory 0.0.13 → 0.0.17
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 +47 -104
- package/dist/index.js +539 -145
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -27,51 +27,17 @@ Codex CLI:
|
|
|
27
27
|
codex mcp add memory -- npx -y @jcyamacho/agent-memory
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Example MCP server config:
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"mcpServers": {
|
|
35
|
-
"memory": {
|
|
36
|
-
"command": "npx",
|
|
37
|
-
"args": [
|
|
38
|
-
"-y",
|
|
39
|
-
"@jcyamacho/agent-memory"
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
With a custom database path:
|
|
47
|
-
|
|
48
|
-
```json
|
|
49
|
-
{
|
|
50
|
-
"mcpServers": {
|
|
51
|
-
"memory": {
|
|
52
|
-
"command": "npx",
|
|
53
|
-
"args": [
|
|
54
|
-
"-y",
|
|
55
|
-
"@jcyamacho/agent-memory"
|
|
56
|
-
],
|
|
57
|
-
"env": {
|
|
58
|
-
"AGENT_MEMORY_DB_PATH": "/absolute/path/to/memory.db"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
30
|
Optional LLM instructions to reinforce the MCP's built-in guidance:
|
|
66
31
|
|
|
67
32
|
```text
|
|
68
|
-
Use `
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
33
|
+
Use `memory_recall` at conversation start and before design choices,
|
|
34
|
+
conventions, or edge cases. Query with 2-5 short anchor-heavy terms or exact
|
|
35
|
+
phrases, not
|
|
36
|
+
questions or sentences. `memory_recall` is lexical-first; if it misses, retry once
|
|
37
|
+
with overlapping alternate terms. Use `memory_remember` for one durable fact, then
|
|
38
|
+
use `memory_revise` instead of duplicates and `memory_forget` for wrong or obsolete
|
|
39
|
+
memories. Always pass workspace unless the memory is truly global. Git worktree
|
|
40
|
+
paths are canonicalized to the main repo root on save and recall.
|
|
75
41
|
```
|
|
76
42
|
|
|
77
43
|
## What It Stores
|
|
@@ -101,60 +67,10 @@ The web UI uses the same database as the MCP server.
|
|
|
101
67
|
|
|
102
68
|
## Tools
|
|
103
69
|
|
|
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`
|
|
70
|
+
- `remember` saves durable facts, preferences, decisions, and project context.
|
|
71
|
+
- `recall` retrieves the most relevant saved memories.
|
|
72
|
+
- `revise` updates an existing memory when it becomes outdated.
|
|
73
|
+
- `forget` deletes a memory that is no longer relevant.
|
|
158
74
|
|
|
159
75
|
## How Ranking Works
|
|
160
76
|
|
|
@@ -163,18 +79,25 @@ memories:
|
|
|
163
79
|
|
|
164
80
|
1. **Text relevance** is the primary signal -- memories whose content best
|
|
165
81
|
matches your search terms rank highest.
|
|
166
|
-
2. **
|
|
82
|
+
2. **Embedding similarity** is the next strongest signal. Recall builds an
|
|
83
|
+
embedding from your normalized search terms and boosts memories whose stored
|
|
84
|
+
embeddings are most semantically similar.
|
|
85
|
+
3. **Workspace match** is a strong secondary signal. When you pass
|
|
167
86
|
`workspace`, exact matches rank highest, sibling repositories get a small
|
|
168
87
|
boost, and unrelated workspaces rank lowest.
|
|
169
|
-
|
|
88
|
+
4. **Global memories** (saved without a workspace) are treated as relevant
|
|
170
89
|
everywhere. When you pass `workspace`, they rank below exact workspace
|
|
171
90
|
matches and above sibling or unrelated repositories.
|
|
172
|
-
|
|
91
|
+
5. **Recency** is a minor tiebreaker -- newer memories rank slightly above older
|
|
173
92
|
ones when other signals are equal.
|
|
174
93
|
|
|
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.
|
|
94
|
+
If you omit `workspace`, recall still uses text relevance, embedding similarity,
|
|
95
|
+
and recency. For best results, pass `workspace` whenever you have one. Save
|
|
96
|
+
memories without a workspace only when they apply across all projects.
|
|
97
|
+
|
|
98
|
+
When you save a memory from a git worktree, `agent-memory` stores the main repo
|
|
99
|
+
root as the workspace. `recall` applies the same normalization to incoming
|
|
100
|
+
workspace queries so linked worktrees still match repo-scoped memories exactly.
|
|
178
101
|
|
|
179
102
|
## Database location
|
|
180
103
|
|
|
@@ -196,8 +119,28 @@ Set `AGENT_MEMORY_DB_PATH` when you want to:
|
|
|
196
119
|
- share a memory DB across multiple clients
|
|
197
120
|
- store the DB somewhere easier to back up or inspect
|
|
198
121
|
|
|
199
|
-
|
|
200
|
-
|
|
122
|
+
## Model cache location
|
|
123
|
+
|
|
124
|
+
By default, downloaded embedding model files are cached at:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
~/.config/agent-memory/models
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Override it with:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
AGENT_MEMORY_MODELS_CACHE_PATH=/absolute/path/to/models
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Set `AGENT_MEMORY_MODELS_CACHE_PATH` when you want to:
|
|
137
|
+
|
|
138
|
+
- keep model artifacts out of `node_modules`
|
|
139
|
+
- share the model cache across reinstalls or multiple clients
|
|
140
|
+
- store model downloads somewhere easier to inspect or manage
|
|
141
|
+
|
|
142
|
+
Schema changes are migrated automatically, including workspace normalization for
|
|
143
|
+
existing git worktree memories when the original path can still be resolved.
|
|
201
144
|
|
|
202
145
|
## Development
|
|
203
146
|
|
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.17";
|
|
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
|
+
}
|
|
12519
|
+
|
|
12520
|
+
class NotFoundError extends MemoryError {
|
|
12521
|
+
constructor(message) {
|
|
12522
|
+
super("NOT_FOUND", message);
|
|
12523
|
+
this.name = "NotFoundError";
|
|
12524
|
+
}
|
|
12525
|
+
}
|
|
12490
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,36 @@ var MAX_LIST_LIMIT = 100;
|
|
|
20033
20145
|
|
|
20034
20146
|
class MemoryService {
|
|
20035
20147
|
repository;
|
|
20036
|
-
|
|
20148
|
+
embeddingService;
|
|
20149
|
+
workspaceResolver;
|
|
20150
|
+
constructor(repository, embeddingService, workspaceResolver) {
|
|
20037
20151
|
this.repository = repository;
|
|
20152
|
+
this.embeddingService = embeddingService;
|
|
20153
|
+
this.workspaceResolver = workspaceResolver;
|
|
20038
20154
|
}
|
|
20039
20155
|
async create(input) {
|
|
20040
20156
|
const content = input.content.trim();
|
|
20041
20157
|
if (!content) {
|
|
20042
20158
|
throw new ValidationError("Memory content is required.");
|
|
20043
20159
|
}
|
|
20044
|
-
|
|
20160
|
+
const workspace = await this.workspaceResolver.resolve(input.workspace);
|
|
20161
|
+
const memory = await this.repository.create({
|
|
20045
20162
|
content,
|
|
20046
|
-
|
|
20163
|
+
embedding: await this.embeddingService.createVector(content),
|
|
20164
|
+
workspace
|
|
20047
20165
|
});
|
|
20166
|
+
return toPublicMemoryRecord(memory);
|
|
20048
20167
|
}
|
|
20049
20168
|
async update(input) {
|
|
20050
20169
|
const content = input.content.trim();
|
|
20051
20170
|
if (!content)
|
|
20052
20171
|
throw new ValidationError("Memory content is required.");
|
|
20053
|
-
|
|
20172
|
+
const memory = await this.repository.update({
|
|
20173
|
+
id: input.id,
|
|
20174
|
+
content,
|
|
20175
|
+
embedding: await this.embeddingService.createVector(content)
|
|
20176
|
+
});
|
|
20177
|
+
return toPublicMemoryRecord(memory);
|
|
20054
20178
|
}
|
|
20055
20179
|
async delete(input) {
|
|
20056
20180
|
const id = input.id.trim();
|
|
@@ -20059,16 +20183,18 @@ class MemoryService {
|
|
|
20059
20183
|
return this.repository.delete({ id });
|
|
20060
20184
|
}
|
|
20061
20185
|
async get(id) {
|
|
20062
|
-
|
|
20186
|
+
const memory = await this.repository.get(id);
|
|
20187
|
+
return memory ? toPublicMemoryRecord(memory) : undefined;
|
|
20063
20188
|
}
|
|
20064
20189
|
async list(input) {
|
|
20065
|
-
const workspace =
|
|
20066
|
-
|
|
20190
|
+
const workspace = await this.workspaceResolver.resolve(input.workspace);
|
|
20191
|
+
const page = await this.repository.list({
|
|
20067
20192
|
workspace,
|
|
20068
20193
|
workspaceIsNull: workspace ? false : Boolean(input.workspaceIsNull),
|
|
20069
20194
|
offset: normalizeOffset(input.offset),
|
|
20070
20195
|
limit: normalizeListLimit(input.limit)
|
|
20071
20196
|
});
|
|
20197
|
+
return toPublicMemoryPage(page);
|
|
20072
20198
|
}
|
|
20073
20199
|
async listWorkspaces() {
|
|
20074
20200
|
return this.repository.listWorkspaces();
|
|
@@ -20079,17 +20205,45 @@ class MemoryService {
|
|
|
20079
20205
|
throw new ValidationError("At least one search term is required.");
|
|
20080
20206
|
}
|
|
20081
20207
|
const requestedLimit = normalizeLimit(input.limit);
|
|
20082
|
-
const workspace =
|
|
20208
|
+
const workspace = await this.workspaceResolver.resolve(input.workspace);
|
|
20083
20209
|
const normalizedQuery = {
|
|
20084
20210
|
terms,
|
|
20085
20211
|
limit: requestedLimit * RECALL_CANDIDATE_LIMIT_MULTIPLIER,
|
|
20086
20212
|
updatedAfter: input.updatedAfter,
|
|
20087
20213
|
updatedBefore: input.updatedBefore
|
|
20088
20214
|
};
|
|
20089
|
-
const results = await
|
|
20090
|
-
|
|
20215
|
+
const [results, queryEmbedding] = await Promise.all([
|
|
20216
|
+
this.repository.search(normalizedQuery),
|
|
20217
|
+
this.embeddingService.createVector(terms.join(" "))
|
|
20218
|
+
]);
|
|
20219
|
+
return rerankSearchResults(results, workspace, queryEmbedding).slice(0, requestedLimit).map(toPublicSearchResult);
|
|
20091
20220
|
}
|
|
20092
20221
|
}
|
|
20222
|
+
function toPublicMemoryRecord(memory) {
|
|
20223
|
+
return {
|
|
20224
|
+
id: memory.id,
|
|
20225
|
+
content: memory.content,
|
|
20226
|
+
workspace: memory.workspace,
|
|
20227
|
+
createdAt: memory.createdAt,
|
|
20228
|
+
updatedAt: memory.updatedAt
|
|
20229
|
+
};
|
|
20230
|
+
}
|
|
20231
|
+
function toPublicSearchResult(result) {
|
|
20232
|
+
return {
|
|
20233
|
+
id: result.id,
|
|
20234
|
+
content: result.content,
|
|
20235
|
+
score: result.score,
|
|
20236
|
+
workspace: result.workspace,
|
|
20237
|
+
createdAt: result.createdAt,
|
|
20238
|
+
updatedAt: result.updatedAt
|
|
20239
|
+
};
|
|
20240
|
+
}
|
|
20241
|
+
function toPublicMemoryPage(page) {
|
|
20242
|
+
return {
|
|
20243
|
+
items: page.items.map(toPublicMemoryRecord),
|
|
20244
|
+
hasMore: page.hasMore
|
|
20245
|
+
};
|
|
20246
|
+
}
|
|
20093
20247
|
function normalizeLimit(value) {
|
|
20094
20248
|
if (value === undefined) {
|
|
20095
20249
|
return DEFAULT_RECALL_LIMIT;
|
|
@@ -20108,10 +20262,6 @@ function normalizeListLimit(value) {
|
|
|
20108
20262
|
}
|
|
20109
20263
|
return Math.min(Math.max(value, 1), MAX_LIST_LIMIT);
|
|
20110
20264
|
}
|
|
20111
|
-
function normalizeOptionalString(value) {
|
|
20112
|
-
const trimmed = value?.trim();
|
|
20113
|
-
return trimmed ? trimmed : undefined;
|
|
20114
|
-
}
|
|
20115
20265
|
function normalizeTerms(terms) {
|
|
20116
20266
|
const normalizedTerms = terms.map((term) => term.trim()).filter(Boolean);
|
|
20117
20267
|
return [...new Set(normalizedTerms)];
|
|
@@ -20119,22 +20269,23 @@ function normalizeTerms(terms) {
|
|
|
20119
20269
|
|
|
20120
20270
|
// src/mcp/tools/recall.ts
|
|
20121
20271
|
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.
|
|
20272
|
+
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."),
|
|
20273
|
+
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."),
|
|
20274
|
+
workspace: string2().optional().describe("Pass the current working directory. Git worktree paths are normalized to the main repo root for matching. This strongly boosts memories from the active project while still allowing global and cross-workspace matches."),
|
|
20275
|
+
updated_after: string2().optional().describe("Only return memories updated at or after this ISO 8601 timestamp."),
|
|
20276
|
+
updated_before: string2().optional().describe("Only return memories updated at or before this ISO 8601 timestamp.")
|
|
20127
20277
|
};
|
|
20128
20278
|
function toMemoryXml(r) {
|
|
20129
20279
|
const workspace = r.workspace ? ` workspace="${escapeXml(r.workspace)}"` : "";
|
|
20130
20280
|
const content = escapeXml(r.content);
|
|
20131
|
-
|
|
20281
|
+
const score = Number(r.score.toFixed(3)).toString();
|
|
20282
|
+
return `<memory id="${r.id}" score="${score}"${workspace} updated_at="${r.updatedAt.toISOString()}">
|
|
20132
20283
|
${content}
|
|
20133
20284
|
</memory>`;
|
|
20134
20285
|
}
|
|
20135
20286
|
function registerRecallTool(server, memory) {
|
|
20136
20287
|
server.registerTool("recall", {
|
|
20137
|
-
description: "
|
|
20288
|
+
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; git worktree paths are normalized to the main repo root for matching. Returns `<memories>...</memories>` or a no-match hint.",
|
|
20138
20289
|
inputSchema: recallInputSchema
|
|
20139
20290
|
}, async ({ terms, limit, workspace, updated_after, updated_before }) => {
|
|
20140
20291
|
try {
|
|
@@ -20145,7 +20296,7 @@ function registerRecallTool(server, memory) {
|
|
|
20145
20296
|
updatedAfter: parseOptionalDate(updated_after, "updated_after"),
|
|
20146
20297
|
updatedBefore: parseOptionalDate(updated_before, "updated_before")
|
|
20147
20298
|
});
|
|
20148
|
-
const text = results.length === 0 ? "No matching memories found." : `<memories>
|
|
20299
|
+
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
20300
|
${results.map(toMemoryXml).join(`
|
|
20150
20301
|
`)}
|
|
20151
20302
|
</memories>`;
|
|
@@ -20160,12 +20311,12 @@ ${results.map(toMemoryXml).join(`
|
|
|
20160
20311
|
|
|
20161
20312
|
// src/mcp/tools/remember.ts
|
|
20162
20313
|
var rememberInputSchema = {
|
|
20163
|
-
content: string2().describe("
|
|
20164
|
-
workspace: string2().optional().describe("
|
|
20314
|
+
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."),
|
|
20315
|
+
workspace: string2().optional().describe("Pass the current working directory for project-specific memories. Git worktree paths are saved as the main repo root automatically. Omit only for truly global memories.")
|
|
20165
20316
|
};
|
|
20166
20317
|
function registerRememberTool(server, memory) {
|
|
20167
20318
|
server.registerTool("remember", {
|
|
20168
|
-
description:
|
|
20319
|
+
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
20320
|
inputSchema: rememberInputSchema
|
|
20170
20321
|
}, async ({ content, workspace }) => {
|
|
20171
20322
|
try {
|
|
@@ -20184,12 +20335,12 @@ function registerRememberTool(server, memory) {
|
|
|
20184
20335
|
|
|
20185
20336
|
// src/mcp/tools/revise.ts
|
|
20186
20337
|
var reviseInputSchema = {
|
|
20187
|
-
id: string2().describe("The id
|
|
20188
|
-
content: string2().describe("The replacement
|
|
20338
|
+
id: string2().describe("The memory id to update. Use an id returned by `recall`."),
|
|
20339
|
+
content: string2().describe("The corrected replacement for that same fact. Keep it to one durable fact.")
|
|
20189
20340
|
};
|
|
20190
20341
|
function registerReviseTool(server, memory) {
|
|
20191
20342
|
server.registerTool("revise", {
|
|
20192
|
-
description:
|
|
20343
|
+
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
20344
|
inputSchema: reviseInputSchema
|
|
20194
20345
|
}, async ({ id, content }) => {
|
|
20195
20346
|
try {
|
|
@@ -20210,14 +20361,15 @@ function registerReviseTool(server, memory) {
|
|
|
20210
20361
|
|
|
20211
20362
|
// src/mcp/server.ts
|
|
20212
20363
|
var SERVER_INSTRUCTIONS = [
|
|
20213
|
-
"
|
|
20214
|
-
"Use `recall` at
|
|
20215
|
-
"
|
|
20216
|
-
"
|
|
20217
|
-
"
|
|
20218
|
-
"Use `
|
|
20219
|
-
"
|
|
20220
|
-
"
|
|
20364
|
+
"Use this server for durable memory: user preferences, corrections, decisions, and project context not obvious from code or git history.",
|
|
20365
|
+
"Use `recall` at conversation start and before design choices, conventions, or edge cases.",
|
|
20366
|
+
"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.",
|
|
20367
|
+
"`recall` is lexical-first; semantic reranking only reorders lexical matches.",
|
|
20368
|
+
"If `recall` misses, retry once with overlapping alternate terms.",
|
|
20369
|
+
"Use `remember` for one durable fact when the user states a preference, corrects you, or a reusable project decision becomes clear.",
|
|
20370
|
+
"Call `recall` before `remember`; if the fact already exists, use `revise` instead of creating a duplicate.",
|
|
20371
|
+
"Use `revise` to correct an existing memory and `forget` to remove a wrong or obsolete one.",
|
|
20372
|
+
"Pass workspace for project-scoped calls. Git worktree paths are canonicalized to the main repo root on save and recall. Omit workspace only for truly global memories."
|
|
20221
20373
|
].join(" ");
|
|
20222
20374
|
function createMcpServer(memory, version3) {
|
|
20223
20375
|
const server = new McpServer({
|
|
@@ -20233,72 +20385,262 @@ function createMcpServer(memory, version3) {
|
|
|
20233
20385
|
return server;
|
|
20234
20386
|
}
|
|
20235
20387
|
|
|
20236
|
-
// src/sqlite
|
|
20237
|
-
import { mkdirSync } from "node:fs";
|
|
20388
|
+
// src/sqlite/db.ts
|
|
20389
|
+
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
20238
20390
|
import { dirname } from "node:path";
|
|
20239
20391
|
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
|
-
|
|
20392
|
+
|
|
20393
|
+
// src/sqlite/memory-schema.ts
|
|
20394
|
+
function createMemoriesTable(database, options) {
|
|
20395
|
+
const embeddingColumn = getEmbeddingColumnSql(options.embeddingColumn);
|
|
20396
|
+
database.exec(`
|
|
20397
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
20398
|
+
id TEXT PRIMARY KEY,
|
|
20399
|
+
content TEXT NOT NULL,
|
|
20400
|
+
workspace TEXT,
|
|
20401
|
+
${embeddingColumn}
|
|
20402
|
+
created_at INTEGER NOT NULL,
|
|
20403
|
+
updated_at INTEGER NOT NULL
|
|
20404
|
+
);
|
|
20405
|
+
`);
|
|
20406
|
+
}
|
|
20407
|
+
function createMemoryIndexes(database) {
|
|
20408
|
+
database.exec(`
|
|
20409
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);
|
|
20410
|
+
CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace);
|
|
20411
|
+
`);
|
|
20412
|
+
}
|
|
20413
|
+
function createMemorySearchArtifacts(database, rebuild = false) {
|
|
20414
|
+
database.exec(`
|
|
20415
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
20416
|
+
content,
|
|
20417
|
+
content = 'memories',
|
|
20418
|
+
content_rowid = 'rowid',
|
|
20419
|
+
tokenize = 'porter unicode61'
|
|
20420
|
+
);
|
|
20421
|
+
|
|
20422
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
20423
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
20424
|
+
END;
|
|
20425
|
+
|
|
20426
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
20427
|
+
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
20428
|
+
VALUES ('delete', old.rowid, old.content);
|
|
20429
|
+
END;
|
|
20430
|
+
|
|
20431
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
20432
|
+
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
20433
|
+
VALUES ('delete', old.rowid, old.content);
|
|
20434
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
20435
|
+
END;
|
|
20436
|
+
`);
|
|
20437
|
+
if (rebuild) {
|
|
20438
|
+
database.exec("INSERT INTO memories_fts(memories_fts) VALUES ('rebuild')");
|
|
20439
|
+
}
|
|
20440
|
+
}
|
|
20441
|
+
function dropMemorySearchArtifacts(database) {
|
|
20442
|
+
database.exec(`
|
|
20443
|
+
DROP TRIGGER IF EXISTS memories_ai;
|
|
20444
|
+
DROP TRIGGER IF EXISTS memories_ad;
|
|
20445
|
+
DROP TRIGGER IF EXISTS memories_au;
|
|
20446
|
+
DROP TABLE IF EXISTS memories_fts;
|
|
20447
|
+
`);
|
|
20448
|
+
}
|
|
20449
|
+
function getEmbeddingColumnSql(mode) {
|
|
20450
|
+
switch (mode) {
|
|
20451
|
+
case "omit":
|
|
20452
|
+
return "";
|
|
20453
|
+
case "nullable":
|
|
20454
|
+
return `embedding BLOB,
|
|
20455
|
+
`;
|
|
20456
|
+
case "required":
|
|
20457
|
+
return `embedding BLOB NOT NULL,
|
|
20458
|
+
`;
|
|
20459
|
+
}
|
|
20460
|
+
}
|
|
20461
|
+
|
|
20462
|
+
// src/sqlite/migrations/001-create-memory-schema.ts
|
|
20463
|
+
var createMemorySchemaMigration = {
|
|
20464
|
+
version: 1,
|
|
20465
|
+
async up(database) {
|
|
20466
|
+
createMemoriesTable(database, { embeddingColumn: "omit" });
|
|
20467
|
+
createMemoryIndexes(database);
|
|
20468
|
+
createMemorySearchArtifacts(database);
|
|
20469
|
+
}
|
|
20470
|
+
};
|
|
20471
|
+
|
|
20472
|
+
// src/sqlite/embedding-codec.ts
|
|
20473
|
+
var FLOAT32_BYTE_WIDTH = 4;
|
|
20474
|
+
function encodeEmbedding(vector) {
|
|
20475
|
+
const typedArray = Float32Array.from(vector);
|
|
20476
|
+
return new Uint8Array(typedArray.buffer.slice(0));
|
|
20477
|
+
}
|
|
20478
|
+
function decodeEmbedding(value) {
|
|
20479
|
+
const bytes = toUint8Array(value);
|
|
20480
|
+
if (bytes.byteLength === 0) {
|
|
20481
|
+
throw new Error("Embedding blob is empty.");
|
|
20482
|
+
}
|
|
20483
|
+
if (bytes.byteLength % FLOAT32_BYTE_WIDTH !== 0) {
|
|
20484
|
+
throw new Error("Embedding blob length is not a multiple of 4.");
|
|
20485
|
+
}
|
|
20486
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
20487
|
+
const vector = [];
|
|
20488
|
+
for (let offset = 0;offset < bytes.byteLength; offset += FLOAT32_BYTE_WIDTH) {
|
|
20489
|
+
vector.push(view.getFloat32(offset, true));
|
|
20490
|
+
}
|
|
20491
|
+
return vector;
|
|
20492
|
+
}
|
|
20493
|
+
function toUint8Array(value) {
|
|
20494
|
+
if (value instanceof Uint8Array) {
|
|
20495
|
+
return value;
|
|
20496
|
+
}
|
|
20497
|
+
if (value instanceof ArrayBuffer) {
|
|
20498
|
+
return new Uint8Array(value);
|
|
20499
|
+
}
|
|
20500
|
+
throw new Error("Expected embedding blob as Uint8Array or ArrayBuffer.");
|
|
20501
|
+
}
|
|
20502
|
+
|
|
20503
|
+
// src/sqlite/migrations/002-add-memory-embedding.ts
|
|
20504
|
+
function createAddMemoryEmbeddingMigration(embeddingService) {
|
|
20505
|
+
return {
|
|
20506
|
+
version: 2,
|
|
20507
|
+
async up(database) {
|
|
20508
|
+
database.exec("ALTER TABLE memories ADD COLUMN embedding BLOB");
|
|
20509
|
+
const rows = database.prepare("SELECT id, content FROM memories ORDER BY created_at ASC").all();
|
|
20510
|
+
const updateStatement = database.prepare("UPDATE memories SET embedding = ? WHERE id = ?");
|
|
20511
|
+
for (const row of rows) {
|
|
20512
|
+
const embedding = await embeddingService.createVector(row.content);
|
|
20513
|
+
updateStatement.run(encodeEmbedding(embedding), row.id);
|
|
20514
|
+
}
|
|
20515
|
+
const nullRows = database.prepare("SELECT COUNT(*) AS count FROM memories WHERE embedding IS NULL").all();
|
|
20516
|
+
if ((nullRows[0]?.count ?? 0) > 0) {
|
|
20517
|
+
throw new Error("Failed to backfill embeddings for all memories.");
|
|
20518
|
+
}
|
|
20519
|
+
dropMemorySearchArtifacts(database);
|
|
20520
|
+
database.exec("ALTER TABLE memories RENAME TO memories_old");
|
|
20521
|
+
createMemoriesTable(database, { embeddingColumn: "required" });
|
|
20522
|
+
database.exec(`
|
|
20523
|
+
INSERT INTO memories (id, content, workspace, embedding, created_at, updated_at)
|
|
20524
|
+
SELECT id, content, workspace, embedding, created_at, updated_at
|
|
20525
|
+
FROM memories_old
|
|
20526
|
+
`);
|
|
20527
|
+
database.exec("DROP TABLE memories_old");
|
|
20528
|
+
createMemoryIndexes(database);
|
|
20529
|
+
createMemorySearchArtifacts(database, true);
|
|
20530
|
+
}
|
|
20531
|
+
};
|
|
20532
|
+
}
|
|
20533
|
+
|
|
20534
|
+
// src/sqlite/migrations/003-normalize-workspaces.ts
|
|
20535
|
+
function createNormalizeWorkspaceMigration(workspaceResolver) {
|
|
20536
|
+
return {
|
|
20537
|
+
version: 3,
|
|
20538
|
+
async up(database) {
|
|
20539
|
+
const rows = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace").all();
|
|
20540
|
+
const updateStatement = database.prepare("UPDATE memories SET workspace = ? WHERE workspace = ?");
|
|
20541
|
+
for (const row of rows) {
|
|
20542
|
+
const normalizedWorkspace = await workspaceResolver.resolve(row.workspace);
|
|
20543
|
+
if (!normalizedWorkspace || normalizedWorkspace === row.workspace) {
|
|
20544
|
+
continue;
|
|
20545
|
+
}
|
|
20546
|
+
updateStatement.run(normalizedWorkspace, row.workspace);
|
|
20547
|
+
}
|
|
20548
|
+
}
|
|
20549
|
+
};
|
|
20550
|
+
}
|
|
20551
|
+
|
|
20552
|
+
// src/sqlite/migrations/index.ts
|
|
20553
|
+
function createMemoryMigrations(options) {
|
|
20554
|
+
return [
|
|
20555
|
+
createMemorySchemaMigration,
|
|
20556
|
+
createAddMemoryEmbeddingMigration(options.embeddingService),
|
|
20557
|
+
createNormalizeWorkspaceMigration(options.workspaceResolver)
|
|
20558
|
+
];
|
|
20559
|
+
}
|
|
20560
|
+
|
|
20561
|
+
// src/sqlite/db.ts
|
|
20562
|
+
var PRAGMA_STATEMENTS = [
|
|
20563
|
+
"journal_mode = WAL",
|
|
20564
|
+
"synchronous = NORMAL",
|
|
20565
|
+
"foreign_keys = ON",
|
|
20566
|
+
"busy_timeout = 5000"
|
|
20567
|
+
];
|
|
20568
|
+
async function openMemoryDatabase(databasePath, options) {
|
|
20569
|
+
let database;
|
|
20275
20570
|
try {
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
initializeMemoryDatabase(database);
|
|
20571
|
+
mkdirSync2(dirname(databasePath), { recursive: true });
|
|
20572
|
+
database = new Database(databasePath);
|
|
20573
|
+
await initializeMemoryDatabase(database, createMemoryMigrations(options));
|
|
20279
20574
|
return database;
|
|
20280
20575
|
} catch (error2) {
|
|
20576
|
+
database?.close();
|
|
20577
|
+
if (error2 instanceof PersistenceError) {
|
|
20578
|
+
throw error2;
|
|
20579
|
+
}
|
|
20281
20580
|
throw new PersistenceError("Failed to initialize the SQLite database.", {
|
|
20282
20581
|
cause: error2
|
|
20283
20582
|
});
|
|
20284
20583
|
}
|
|
20285
20584
|
}
|
|
20286
|
-
function initializeMemoryDatabase(database) {
|
|
20585
|
+
async function initializeMemoryDatabase(database, migrations) {
|
|
20586
|
+
try {
|
|
20587
|
+
applyPragmas(database);
|
|
20588
|
+
await runSqliteMigrations(database, migrations);
|
|
20589
|
+
} catch (error2) {
|
|
20590
|
+
throw new PersistenceError("Failed to initialize the SQLite database.", {
|
|
20591
|
+
cause: error2
|
|
20592
|
+
});
|
|
20593
|
+
}
|
|
20594
|
+
}
|
|
20595
|
+
async function runSqliteMigrations(database, migrations) {
|
|
20596
|
+
validateMigrations(migrations);
|
|
20597
|
+
let currentVersion = getUserVersion(database);
|
|
20598
|
+
for (const migration of migrations) {
|
|
20599
|
+
if (migration.version <= currentVersion) {
|
|
20600
|
+
continue;
|
|
20601
|
+
}
|
|
20602
|
+
database.exec("BEGIN");
|
|
20603
|
+
try {
|
|
20604
|
+
await migration.up(database);
|
|
20605
|
+
setUserVersion(database, migration.version);
|
|
20606
|
+
database.exec("COMMIT");
|
|
20607
|
+
currentVersion = migration.version;
|
|
20608
|
+
} catch (error2) {
|
|
20609
|
+
try {
|
|
20610
|
+
database.exec("ROLLBACK");
|
|
20611
|
+
} catch {}
|
|
20612
|
+
throw error2;
|
|
20613
|
+
}
|
|
20614
|
+
}
|
|
20615
|
+
}
|
|
20616
|
+
function applyPragmas(database) {
|
|
20287
20617
|
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");
|
|
20618
|
+
for (const statement of PRAGMA_STATEMENTS) {
|
|
20619
|
+
database.pragma(statement);
|
|
20620
|
+
}
|
|
20621
|
+
return;
|
|
20622
|
+
}
|
|
20623
|
+
for (const statement of PRAGMA_STATEMENTS) {
|
|
20624
|
+
database.exec(`PRAGMA ${statement}`);
|
|
20297
20625
|
}
|
|
20298
|
-
database.exec(MEMORY_SCHEMA);
|
|
20299
20626
|
}
|
|
20300
|
-
|
|
20301
|
-
|
|
20627
|
+
function getUserVersion(database) {
|
|
20628
|
+
const rows = database.prepare("PRAGMA user_version").all();
|
|
20629
|
+
return rows[0]?.user_version ?? 0;
|
|
20630
|
+
}
|
|
20631
|
+
function setUserVersion(database, version3) {
|
|
20632
|
+
database.exec(`PRAGMA user_version = ${version3}`);
|
|
20633
|
+
}
|
|
20634
|
+
function validateMigrations(migrations) {
|
|
20635
|
+
let previousVersion = 0;
|
|
20636
|
+
for (const migration of migrations) {
|
|
20637
|
+
if (!Number.isInteger(migration.version) || migration.version <= previousVersion) {
|
|
20638
|
+
throw new Error("SQLite migrations must use strictly increasing versions.");
|
|
20639
|
+
}
|
|
20640
|
+
previousVersion = migration.version;
|
|
20641
|
+
}
|
|
20642
|
+
}
|
|
20643
|
+
// src/sqlite/repository.ts
|
|
20302
20644
|
import { randomUUID } from "node:crypto";
|
|
20303
20645
|
var DEFAULT_SEARCH_LIMIT = 15;
|
|
20304
20646
|
var DEFAULT_LIST_LIMIT2 = 15;
|
|
@@ -20317,6 +20659,7 @@ class SqliteMemoryRepository {
|
|
|
20317
20659
|
id,
|
|
20318
20660
|
content,
|
|
20319
20661
|
workspace,
|
|
20662
|
+
embedding,
|
|
20320
20663
|
created_at,
|
|
20321
20664
|
updated_at
|
|
20322
20665
|
) VALUES (
|
|
@@ -20324,11 +20667,12 @@ class SqliteMemoryRepository {
|
|
|
20324
20667
|
?,
|
|
20325
20668
|
?,
|
|
20326
20669
|
?,
|
|
20670
|
+
?,
|
|
20327
20671
|
?
|
|
20328
20672
|
)
|
|
20329
20673
|
`);
|
|
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 = ?");
|
|
20674
|
+
this.getStatement = database.prepare("SELECT id, content, workspace, embedding, created_at, updated_at FROM memories WHERE id = ?");
|
|
20675
|
+
this.updateStatement = database.prepare("UPDATE memories SET content = ?, embedding = ?, updated_at = ? WHERE id = ?");
|
|
20332
20676
|
this.deleteStatement = database.prepare("DELETE FROM memories WHERE id = ?");
|
|
20333
20677
|
this.listWorkspacesStatement = database.prepare("SELECT DISTINCT workspace FROM memories WHERE workspace IS NOT NULL ORDER BY workspace");
|
|
20334
20678
|
}
|
|
@@ -20338,11 +20682,12 @@ class SqliteMemoryRepository {
|
|
|
20338
20682
|
const memory = {
|
|
20339
20683
|
id: randomUUID(),
|
|
20340
20684
|
content: input.content,
|
|
20685
|
+
embedding: input.embedding,
|
|
20341
20686
|
workspace: input.workspace,
|
|
20342
20687
|
createdAt: now,
|
|
20343
20688
|
updatedAt: now
|
|
20344
20689
|
};
|
|
20345
|
-
this.insertStatement.run(memory.id, memory.content, memory.workspace, memory.createdAt.getTime(), memory.updatedAt.getTime());
|
|
20690
|
+
this.insertStatement.run(memory.id, memory.content, memory.workspace, encodeEmbedding(memory.embedding), memory.createdAt.getTime(), memory.updatedAt.getTime());
|
|
20346
20691
|
return memory;
|
|
20347
20692
|
} catch (error2) {
|
|
20348
20693
|
throw new PersistenceError("Failed to save memory.", { cause: error2 });
|
|
@@ -20367,6 +20712,7 @@ class SqliteMemoryRepository {
|
|
|
20367
20712
|
m.id,
|
|
20368
20713
|
m.content,
|
|
20369
20714
|
m.workspace,
|
|
20715
|
+
m.embedding,
|
|
20370
20716
|
m.created_at,
|
|
20371
20717
|
m.updated_at,
|
|
20372
20718
|
MAX(0, -bm25(memories_fts)) AS score
|
|
@@ -20379,7 +20725,7 @@ class SqliteMemoryRepository {
|
|
|
20379
20725
|
const rows = statement.all(...params);
|
|
20380
20726
|
const maxScore = Math.max(...rows.map((row) => row.score), 0);
|
|
20381
20727
|
return rows.map((row) => ({
|
|
20382
|
-
...
|
|
20728
|
+
...toMemoryEntity(row),
|
|
20383
20729
|
score: toNormalizedScore(maxScore > 0 ? row.score / maxScore : 0)
|
|
20384
20730
|
}));
|
|
20385
20731
|
} catch (error2) {
|
|
@@ -20392,7 +20738,7 @@ class SqliteMemoryRepository {
|
|
|
20392
20738
|
try {
|
|
20393
20739
|
const rows = this.getStatement.all(id);
|
|
20394
20740
|
const row = rows[0];
|
|
20395
|
-
return row ?
|
|
20741
|
+
return row ? toMemoryEntity(row) : undefined;
|
|
20396
20742
|
} catch (error2) {
|
|
20397
20743
|
throw new PersistenceError("Failed to find memory.", { cause: error2 });
|
|
20398
20744
|
}
|
|
@@ -20413,7 +20759,7 @@ class SqliteMemoryRepository {
|
|
|
20413
20759
|
const queryLimit = limit + 1;
|
|
20414
20760
|
params.push(queryLimit, offset);
|
|
20415
20761
|
const statement = this.database.prepare(`
|
|
20416
|
-
SELECT id, content, workspace, created_at, updated_at
|
|
20762
|
+
SELECT id, content, workspace, embedding, created_at, updated_at
|
|
20417
20763
|
FROM memories
|
|
20418
20764
|
${whereClause}
|
|
20419
20765
|
ORDER BY created_at DESC
|
|
@@ -20421,7 +20767,7 @@ class SqliteMemoryRepository {
|
|
|
20421
20767
|
`);
|
|
20422
20768
|
const rows = statement.all(...params);
|
|
20423
20769
|
const hasMore = rows.length > limit;
|
|
20424
|
-
const items = (hasMore ? rows.slice(0, limit) : rows).map(
|
|
20770
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(toMemoryEntity);
|
|
20425
20771
|
return { items, hasMore };
|
|
20426
20772
|
} catch (error2) {
|
|
20427
20773
|
throw new PersistenceError("Failed to list memories.", { cause: error2 });
|
|
@@ -20431,7 +20777,7 @@ class SqliteMemoryRepository {
|
|
|
20431
20777
|
let result;
|
|
20432
20778
|
try {
|
|
20433
20779
|
const now = Date.now();
|
|
20434
|
-
result = this.updateStatement.run(input.content, now, input.id);
|
|
20780
|
+
result = this.updateStatement.run(input.content, encodeEmbedding(input.embedding), now, input.id);
|
|
20435
20781
|
} catch (error2) {
|
|
20436
20782
|
throw new PersistenceError("Failed to update memory.", { cause: error2 });
|
|
20437
20783
|
}
|
|
@@ -20464,9 +20810,10 @@ class SqliteMemoryRepository {
|
|
|
20464
20810
|
}
|
|
20465
20811
|
}
|
|
20466
20812
|
}
|
|
20467
|
-
var
|
|
20813
|
+
var toMemoryEntity = (row) => ({
|
|
20468
20814
|
id: row.id,
|
|
20469
20815
|
content: row.content,
|
|
20816
|
+
embedding: decodeEmbedding(row.embedding),
|
|
20470
20817
|
workspace: row.workspace ?? undefined,
|
|
20471
20818
|
createdAt: new Date(row.created_at),
|
|
20472
20819
|
updatedAt: new Date(row.updated_at)
|
|
@@ -20479,7 +20826,6 @@ function toFtsTerm(term) {
|
|
|
20479
20826
|
}
|
|
20480
20827
|
return `"${escaped}"*`;
|
|
20481
20828
|
}
|
|
20482
|
-
|
|
20483
20829
|
// node_modules/@hono/node-server/dist/index.mjs
|
|
20484
20830
|
import { createServer as createServerHTTP } from "http";
|
|
20485
20831
|
import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
|
|
@@ -23868,11 +24214,59 @@ function startWebServer(memory, options) {
|
|
|
23868
24214
|
return serve({ fetch: app.fetch, port: options.port });
|
|
23869
24215
|
}
|
|
23870
24216
|
|
|
24217
|
+
// src/workspace-resolver.ts
|
|
24218
|
+
import { execFile } from "node:child_process";
|
|
24219
|
+
import { basename, dirname as dirname2 } from "node:path";
|
|
24220
|
+
import { promisify } from "node:util";
|
|
24221
|
+
var execFileAsync = promisify(execFile);
|
|
24222
|
+
function createGitWorkspaceResolver(options = {}) {
|
|
24223
|
+
const getGitCommonDir = options.getGitCommonDir ?? defaultGetGitCommonDir;
|
|
24224
|
+
const cache = new Map;
|
|
24225
|
+
return {
|
|
24226
|
+
async resolve(workspace) {
|
|
24227
|
+
const trimmed = normalizeOptionalString(workspace);
|
|
24228
|
+
if (!trimmed) {
|
|
24229
|
+
return;
|
|
24230
|
+
}
|
|
24231
|
+
const cached2 = cache.get(trimmed);
|
|
24232
|
+
if (cached2) {
|
|
24233
|
+
return cached2;
|
|
24234
|
+
}
|
|
24235
|
+
const pending = resolveWorkspace(trimmed, getGitCommonDir);
|
|
24236
|
+
cache.set(trimmed, pending);
|
|
24237
|
+
return pending;
|
|
24238
|
+
}
|
|
24239
|
+
};
|
|
24240
|
+
}
|
|
24241
|
+
async function resolveWorkspace(workspace, getGitCommonDir) {
|
|
24242
|
+
try {
|
|
24243
|
+
const gitCommonDir = (await getGitCommonDir(workspace)).trim();
|
|
24244
|
+
if (basename(gitCommonDir) !== ".git") {
|
|
24245
|
+
return workspace;
|
|
24246
|
+
}
|
|
24247
|
+
return dirname2(gitCommonDir);
|
|
24248
|
+
} catch {
|
|
24249
|
+
return workspace;
|
|
24250
|
+
}
|
|
24251
|
+
}
|
|
24252
|
+
async function defaultGetGitCommonDir(cwd) {
|
|
24253
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
24254
|
+
cwd
|
|
24255
|
+
});
|
|
24256
|
+
return stdout.trim();
|
|
24257
|
+
}
|
|
24258
|
+
function normalizeOptionalString(value) {
|
|
24259
|
+
const trimmed = value?.trim();
|
|
24260
|
+
return trimmed ? trimmed : undefined;
|
|
24261
|
+
}
|
|
24262
|
+
|
|
23871
24263
|
// src/index.ts
|
|
23872
24264
|
var config2 = resolveConfig();
|
|
23873
|
-
var
|
|
23874
|
-
var
|
|
23875
|
-
var
|
|
24265
|
+
var embeddingService = new EmbeddingService({ modelsCachePath: config2.modelsCachePath });
|
|
24266
|
+
var workspaceResolver = createGitWorkspaceResolver();
|
|
24267
|
+
var database = await openMemoryDatabase(config2.databasePath, { embeddingService, workspaceResolver });
|
|
24268
|
+
var repository2 = new SqliteMemoryRepository(database);
|
|
24269
|
+
var memoryService = new MemoryService(repository2, embeddingService, workspaceResolver);
|
|
23876
24270
|
if (config2.uiMode) {
|
|
23877
24271
|
let shutdown = function() {
|
|
23878
24272
|
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.17",
|
|
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",
|