hippocamp 0.2.12 → 0.2.14
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/AGENTS.md +9 -1
- package/README.md +10 -2
- package/docs/development.md +21 -0
- package/docs/how-it-works.md +1 -1
- package/docs/results.md +56 -0
- package/package.json +2 -1
- package/scripts/hippocamp-memory.cjs +53 -8
- package/scripts/install-claude.cjs +8 -4
- package/scripts/eval-lagoon.cjs +0 -135
- package/scripts/eval-qa.cjs +0 -371
- package/scripts/eval-retrieval.cjs +0 -323
- package/test/dream-runner.test.cjs +0 -86
- package/test/dream.test.cjs +0 -190
- package/test/install-common.test.cjs +0 -224
- package/test/mcp-params.test.cjs +0 -80
- package/test/memory-efficiency.test.cjs +0 -718
- package/test/package-paths.test.cjs +0 -42
- package/test/sync-memory.test.cjs +0 -119
|
@@ -1,718 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const fs = require("node:fs/promises");
|
|
3
|
-
const os = require("node:os");
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
const test = require("node:test");
|
|
6
|
-
|
|
7
|
-
const dream = require("../scripts/hippocamp-dream.cjs");
|
|
8
|
-
const memory = require("../scripts/hippocamp-memory.cjs");
|
|
9
|
-
|
|
10
|
-
function modelResponse(output) {
|
|
11
|
-
return {
|
|
12
|
-
ok: true,
|
|
13
|
-
status: 200,
|
|
14
|
-
async text() {
|
|
15
|
-
return JSON.stringify({ output_text: JSON.stringify(output) });
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
test("Dream compacts an oversized result again without cropping", async (t) => {
|
|
21
|
-
const originalFetch = global.fetch;
|
|
22
|
-
const outputs = [
|
|
23
|
-
{
|
|
24
|
-
current_state_md: `# Current State\n\n- ${"a".repeat(140)}`,
|
|
25
|
-
open_threads_md: "# Open Threads\n\n- Keep the active task.",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
current_state_md: "# Current State\n\n- Active.",
|
|
29
|
-
open_threads_md: "# Open Threads\n\n- Continue.",
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
const requests = [];
|
|
33
|
-
|
|
34
|
-
global.fetch = async (_url, options) => {
|
|
35
|
-
requests.push(JSON.parse(options.body));
|
|
36
|
-
return modelResponse(outputs.shift());
|
|
37
|
-
};
|
|
38
|
-
t.after(() => {
|
|
39
|
-
global.fetch = originalFetch;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const result = await dream.compactDreamOutput({
|
|
43
|
-
apiKey: "test-key",
|
|
44
|
-
baseUrl: "https://manifest.test",
|
|
45
|
-
beforeBytes: 500,
|
|
46
|
-
model: "test-model",
|
|
47
|
-
report: {
|
|
48
|
-
slug: "test-project",
|
|
49
|
-
wakeUpChars: 500,
|
|
50
|
-
files: {
|
|
51
|
-
"current_state.md": { content: `# Current State\n\n- ${"z".repeat(300)}` },
|
|
52
|
-
"open_threads.md": { content: "# Open Threads\n\n- Keep the active task." },
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
targetChars: 100,
|
|
56
|
-
threadEvidence: {
|
|
57
|
-
evidenceBudgetChars: 100,
|
|
58
|
-
evidenceChars: 0,
|
|
59
|
-
openThreadCount: 1,
|
|
60
|
-
items: [],
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
assert.equal(result.passes, 2);
|
|
65
|
-
assert.equal(requests.length, 2);
|
|
66
|
-
assert.ok(result.afterBytes <= 102);
|
|
67
|
-
assert.match(requests[1].input, /previous compaction pass was still too large/i);
|
|
68
|
-
assert.equal(result.output.currentState, "# Current State\n\n- Active.");
|
|
69
|
-
assert.equal(result.output.openThreads, "# Open Threads\n\n- Continue.");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("search uses indexed events and returns coherent evidence", async (t) => {
|
|
73
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
74
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-search-"));
|
|
75
|
-
const lagoonRoot = path.join(tempRoot, "lagoon");
|
|
76
|
-
const projectRoot = path.join(tempRoot, "search-project");
|
|
77
|
-
|
|
78
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = lagoonRoot;
|
|
79
|
-
t.after(async () => {
|
|
80
|
-
if (originalRoot === undefined) {
|
|
81
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
82
|
-
} else {
|
|
83
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
84
|
-
}
|
|
85
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
await memory.appendEvent({
|
|
89
|
-
content: "Decision:\nUse indexed search only.",
|
|
90
|
-
keywords: ["strict-index"],
|
|
91
|
-
date: "2026-09-21",
|
|
92
|
-
projectRoot,
|
|
93
|
-
scope: "project",
|
|
94
|
-
sync: false,
|
|
95
|
-
timestamp: "2026-09-21T10:00:00.000Z",
|
|
96
|
-
title: "Strict indexed recall",
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const indexed = await memory.searchMemory({
|
|
100
|
-
projectRoot,
|
|
101
|
-
query: "strict index",
|
|
102
|
-
scope: "project",
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
assert.equal(indexed.results.length, 1);
|
|
106
|
-
assert.match(indexed.results[0].snippet, /Use indexed search only\./);
|
|
107
|
-
assert.doesNotMatch(indexed.results[0].snippet, /\.\.\./);
|
|
108
|
-
|
|
109
|
-
await memory.appendEvent({
|
|
110
|
-
content: `Decision:\n${"x".repeat(1300)}`,
|
|
111
|
-
keywords: ["oversized-metadata"],
|
|
112
|
-
date: "2026-09-21",
|
|
113
|
-
projectRoot,
|
|
114
|
-
scope: "project",
|
|
115
|
-
sync: false,
|
|
116
|
-
timestamp: "2026-09-21T11:00:00.000Z",
|
|
117
|
-
title: "Large metadata-only match",
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const oversized = await memory.searchMemory({
|
|
121
|
-
projectRoot,
|
|
122
|
-
query: "oversized metadata",
|
|
123
|
-
scope: "project",
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// A keyword-only match still returns whole paragraphs; the oversized run is left out, never cropped.
|
|
127
|
-
assert.equal(oversized.results[0]?.heading, "Large metadata-only match");
|
|
128
|
-
assert.equal(oversized.results[0].snippet, "Decision:");
|
|
129
|
-
|
|
130
|
-
const projectMemoryRoot = path.join(lagoonRoot, "projects", "search-project");
|
|
131
|
-
await fs.writeFile(
|
|
132
|
-
path.join(projectMemoryRoot, "current_state.md"),
|
|
133
|
-
"# Current State\n\n- Durable zebra setting is active.\n",
|
|
134
|
-
"utf8",
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const curated = await memory.searchMemory({
|
|
138
|
-
projectRoot,
|
|
139
|
-
query: "durable zebra",
|
|
140
|
-
scope: "project",
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
assert.equal(curated.results[0].path, "current_state.md");
|
|
144
|
-
assert.match(curated.results[0].snippet, /Durable zebra setting is active\./);
|
|
145
|
-
|
|
146
|
-
const eventsRoot = path.join(projectMemoryRoot, "events");
|
|
147
|
-
await fs.writeFile(
|
|
148
|
-
path.join(eventsRoot, "2026-09-20.md"),
|
|
149
|
-
"# Events: 2026-09-20\n\n## 09:00 — Hidden fallback\n\nKeywords:\n- quasar-velvet\n\nShould not be scanned.\n",
|
|
150
|
-
"utf8",
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const unindexed = await memory.searchMemory({
|
|
154
|
-
projectRoot,
|
|
155
|
-
query: "quasar velvet",
|
|
156
|
-
scope: "project",
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
assert.deepEqual(unindexed.results, []);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("search ranks indexed event bodies with typo tolerance", async (t) => {
|
|
163
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
164
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-body-search-"));
|
|
165
|
-
const projectRoot = path.join(tempRoot, "body-project");
|
|
166
|
-
|
|
167
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
168
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
169
|
-
t.after(async () => {
|
|
170
|
-
if (originalRoot === undefined) {
|
|
171
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
172
|
-
} else {
|
|
173
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
174
|
-
}
|
|
175
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
for (const [title, keywords, content] of [
|
|
179
|
-
["Deploy note", ["deploy"], "The zanzibar migration needs a rollback plan."],
|
|
180
|
-
["Unrelated note", ["billing"], "Invoices are generated monthly."],
|
|
181
|
-
]) {
|
|
182
|
-
await memory.appendEvent({ content, keywords, projectRoot, scope: "project", sync: false, title });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const search = await memory.searchMemory({ projectRoot, query: "zanzibr rollback", scope: "project" });
|
|
186
|
-
|
|
187
|
-
assert.equal(search.results.length, 1);
|
|
188
|
-
assert.equal(search.results[0].heading, "Deploy note");
|
|
189
|
-
assert.equal(search.results[0].match, "body");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("search still matches events written with legacy Cues sections and indexes", async (t) => {
|
|
193
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
194
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-legacy-cues-"));
|
|
195
|
-
const projectRoot = path.join(tempRoot, "legacy-project");
|
|
196
|
-
const eventsRoot = path.join(tempRoot, "lagoon", "projects", "legacy-project", "events");
|
|
197
|
-
|
|
198
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
199
|
-
await fs.mkdir(eventsRoot, { recursive: true });
|
|
200
|
-
t.after(async () => {
|
|
201
|
-
if (originalRoot === undefined) {
|
|
202
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
203
|
-
} else {
|
|
204
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
205
|
-
}
|
|
206
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const id = "2026-09-01T09:00:00.000Z";
|
|
210
|
-
await fs.writeFile(
|
|
211
|
-
path.join(eventsRoot, "2026-09-01.md"),
|
|
212
|
-
`# Events — 2026-09-01\n\n## ${id} — Old note\n\nCues:\n- quasar-velvet\n\nOld body.\n`,
|
|
213
|
-
"utf8",
|
|
214
|
-
);
|
|
215
|
-
await fs.writeFile(
|
|
216
|
-
path.join(eventsRoot, "2026-09-01.index.json"),
|
|
217
|
-
JSON.stringify({ version: 1, path: "2026-09-01.md", events: [{ id, heading: "Old note", cues: ["quasar-velvet"] }] }),
|
|
218
|
-
"utf8",
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const search = await memory.searchMemory({ projectRoot, query: "quasar velvet", scope: "project" });
|
|
222
|
-
|
|
223
|
-
assert.equal(search.results.length, 1);
|
|
224
|
-
assert.deepEqual(search.results[0].keywords, ["quasar-velvet"]);
|
|
225
|
-
assert.equal(search.results[0].match, "keywords");
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("search returns whole sentences from an oversized matching paragraph", async (t) => {
|
|
229
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
230
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-long-paragraph-"));
|
|
231
|
-
const projectRoot = path.join(tempRoot, "long-project");
|
|
232
|
-
|
|
233
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
234
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
235
|
-
t.after(async () => {
|
|
236
|
-
if (originalRoot === undefined) {
|
|
237
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
238
|
-
} else {
|
|
239
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
240
|
-
}
|
|
241
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const filler = "The weekly sync covered routine updates. ".repeat(40);
|
|
245
|
-
await memory.appendEvent({
|
|
246
|
-
content: `${filler}The kestrel database moved to eu-west-3. ${filler}`,
|
|
247
|
-
keywords: ["infra"],
|
|
248
|
-
projectRoot,
|
|
249
|
-
scope: "project",
|
|
250
|
-
sync: false,
|
|
251
|
-
title: "Weekly sync",
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const search = await memory.searchMemory({ projectRoot, query: "kestrel database", scope: "project" });
|
|
255
|
-
|
|
256
|
-
assert.equal(search.results.length, 1);
|
|
257
|
-
assert.match(search.results[0].snippet, /The kestrel database moved to eu-west-3\./);
|
|
258
|
-
assert.ok(search.results[0].snippet.length <= 1200);
|
|
259
|
-
assert.match(search.results[0].snippet, /^The weekly sync|^The kestrel/);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("search returns every matching paragraph that fits the excerpt budget", async (t) => {
|
|
263
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
264
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-multi-paragraph-"));
|
|
265
|
-
const projectRoot = path.join(tempRoot, "multi-project");
|
|
266
|
-
|
|
267
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
268
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
269
|
-
t.after(async () => {
|
|
270
|
-
if (originalRoot === undefined) {
|
|
271
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
272
|
-
} else {
|
|
273
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
274
|
-
}
|
|
275
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const filler = "The weekly sync covered routine updates.";
|
|
279
|
-
const paragraphs = [
|
|
280
|
-
"The kestrel move started on Monday.",
|
|
281
|
-
...Array(30).fill(filler),
|
|
282
|
-
"The kestrel database move finished in eu-west-3 on Friday.",
|
|
283
|
-
];
|
|
284
|
-
await memory.appendEvent({
|
|
285
|
-
content: paragraphs.join("\n\n"),
|
|
286
|
-
keywords: ["infra"],
|
|
287
|
-
projectRoot,
|
|
288
|
-
scope: "project",
|
|
289
|
-
sync: false,
|
|
290
|
-
title: "Weekly sync",
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
const search = await memory.searchMemory({ projectRoot, query: "kestrel database move", scope: "project" });
|
|
294
|
-
const snippet = search.results[0].snippet;
|
|
295
|
-
|
|
296
|
-
// The last paragraph matches more query words, so only the file-order sort puts it second.
|
|
297
|
-
assert.ok(snippet.indexOf("started on Monday") < snippet.indexOf("finished in eu-west-3"));
|
|
298
|
-
assert.match(snippet, /The kestrel move started on Monday\./);
|
|
299
|
-
assert.match(snippet, /The kestrel database move finished in eu-west-3/);
|
|
300
|
-
assert.ok(snippet.length <= 1200);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test("events appended in the same millisecond keep distinct ids and bodies", async (t) => {
|
|
304
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
305
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-same-ms-"));
|
|
306
|
-
const projectRoot = path.join(tempRoot, "same-ms-project");
|
|
307
|
-
|
|
308
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
309
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
310
|
-
t.after(async () => {
|
|
311
|
-
if (originalRoot === undefined) {
|
|
312
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
313
|
-
} else {
|
|
314
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
315
|
-
}
|
|
316
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
const timestamp = "2026-10-01T09:00:00.000Z";
|
|
320
|
-
const first = await memory.appendEvent({
|
|
321
|
-
content: "The zanzibar migration needs a rollback plan.",
|
|
322
|
-
keywords: ["deploy"],
|
|
323
|
-
projectRoot,
|
|
324
|
-
scope: "project",
|
|
325
|
-
sync: false,
|
|
326
|
-
timestamp,
|
|
327
|
-
title: "Deploy note",
|
|
328
|
-
});
|
|
329
|
-
const second = await memory.appendEvent({
|
|
330
|
-
content: "Invoices are generated monthly.",
|
|
331
|
-
keywords: ["billing"],
|
|
332
|
-
projectRoot,
|
|
333
|
-
scope: "project",
|
|
334
|
-
sync: false,
|
|
335
|
-
timestamp,
|
|
336
|
-
title: "Billing note",
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
assert.equal(first.timestamp, timestamp);
|
|
340
|
-
assert.equal(second.timestamp, "2026-10-01T09:00:00.001Z");
|
|
341
|
-
|
|
342
|
-
const search = await memory.searchMemory({ projectRoot, query: "invoices monthly", scope: "project" });
|
|
343
|
-
|
|
344
|
-
assert.equal(search.results.length, 1);
|
|
345
|
-
assert.equal(search.results[0].heading, "Billing note");
|
|
346
|
-
assert.match(search.results[0].snippet, /Invoices are generated monthly\./);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
test("parallel appends keep every event", async (t) => {
|
|
350
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
351
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-parallel-append-"));
|
|
352
|
-
const projectRoot = path.join(tempRoot, "parallel-project");
|
|
353
|
-
|
|
354
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
355
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
356
|
-
t.after(async () => {
|
|
357
|
-
if (originalRoot === undefined) {
|
|
358
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
359
|
-
} else {
|
|
360
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
361
|
-
}
|
|
362
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const results = await Promise.all(
|
|
366
|
-
Array.from({ length: 20 }, (_, index) =>
|
|
367
|
-
memory.appendEvent({
|
|
368
|
-
content: `Parallel event ${index}.`,
|
|
369
|
-
keywords: ["parallel"],
|
|
370
|
-
date: "2026-10-01",
|
|
371
|
-
projectRoot,
|
|
372
|
-
scope: "project",
|
|
373
|
-
sync: false,
|
|
374
|
-
timestamp: "2026-10-01T09:00:00.000Z",
|
|
375
|
-
title: `Event ${index}`,
|
|
376
|
-
}),
|
|
377
|
-
),
|
|
378
|
-
);
|
|
379
|
-
const content = await fs.readFile(results[0].root + "/events/2026-10-01.md", "utf8");
|
|
380
|
-
const ids = [...content.matchAll(/^## (\S+)/gm)].map((match) => match[1]);
|
|
381
|
-
|
|
382
|
-
assert.equal(ids.length, 20);
|
|
383
|
-
assert.equal(new Set(ids).size, 20);
|
|
384
|
-
assert.equal(new Set(results.map((result) => result.timestamp)).size, 20);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
test("search favors events from a month named in the query", async (t) => {
|
|
388
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
389
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-month-search-"));
|
|
390
|
-
const projectRoot = path.join(tempRoot, "month-project");
|
|
391
|
-
|
|
392
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
393
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
394
|
-
t.after(async () => {
|
|
395
|
-
if (originalRoot === undefined) {
|
|
396
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
397
|
-
} else {
|
|
398
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
399
|
-
}
|
|
400
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
for (const timestamp of ["2026-05-02T08:00:00.000Z", "2026-08-05T10:00:00.000Z", "2026-09-01T09:00:00.000Z"]) {
|
|
404
|
-
await memory.appendEvent({
|
|
405
|
-
content: "Published the Android build to Google Play production.",
|
|
406
|
-
keywords: ["android-release"],
|
|
407
|
-
date: timestamp.slice(0, 10),
|
|
408
|
-
projectRoot,
|
|
409
|
-
scope: "project",
|
|
410
|
-
sync: false,
|
|
411
|
-
timestamp,
|
|
412
|
-
title: "Android release",
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const byMonth = await memory.searchMemory({ projectRoot, query: "android build in august", scope: "project" });
|
|
417
|
-
const byDay = await memory.searchMemory({ projectRoot, query: "android build 2026-05-02", scope: "project" });
|
|
418
|
-
|
|
419
|
-
assert.equal(byMonth.results[0].id, "2026-08-05T10:00:00.000Z");
|
|
420
|
-
assert.equal(byDay.results[0].id, "2026-05-02T08:00:00.000Z");
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test("search favors events from a relative date named in the query", async (t) => {
|
|
424
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
425
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-relative-date-"));
|
|
426
|
-
const projectRoot = path.join(tempRoot, "relative-project");
|
|
427
|
-
|
|
428
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
429
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
430
|
-
t.after(async () => {
|
|
431
|
-
if (originalRoot === undefined) {
|
|
432
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
433
|
-
} else {
|
|
434
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
435
|
-
}
|
|
436
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// Seven days ago is always in last calendar week; one day ago is yesterday.
|
|
440
|
-
const daysAgo = (days) => new Date(Date.now() - days * 86_400_000).toISOString();
|
|
441
|
-
const timestamps = { yesterday: daysAgo(1), lastWeek: daysAgo(7), older: daysAgo(30) };
|
|
442
|
-
|
|
443
|
-
for (const [key, timestamp] of Object.entries(timestamps)) {
|
|
444
|
-
await memory.appendEvent({
|
|
445
|
-
// The old event matches the words better, so only the date words can rank the others first.
|
|
446
|
-
content: key === "older" ? "Deploy key rotation: rotated the deploy key again." : "Rotated the key for staging.",
|
|
447
|
-
keywords: ["deploy-key"],
|
|
448
|
-
date: timestamp.slice(0, 10),
|
|
449
|
-
projectRoot,
|
|
450
|
-
scope: "project",
|
|
451
|
-
sync: false,
|
|
452
|
-
timestamp,
|
|
453
|
-
title: "Deploy key rotation",
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const ids = async (phrase) =>
|
|
458
|
-
(await memory.searchMemory({ projectRoot, query: `deploy key rotation ${phrase}`, scope: "project" })).results.map(
|
|
459
|
-
(result) => result.id,
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
assert.deepEqual(await ids("yesterday"), [timestamps.yesterday]);
|
|
463
|
-
// On a Monday, yesterday is also in last week.
|
|
464
|
-
const lastWeek = await ids("last week");
|
|
465
|
-
assert.ok(lastWeek.includes(timestamps.lastWeek));
|
|
466
|
-
assert.ok(!lastWeek.includes(timestamps.older));
|
|
467
|
-
assert.deepEqual(await ids("today"), []);
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
test("search returns a snippet for a long event matched only by its date", async (t) => {
|
|
471
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
472
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-date-only-"));
|
|
473
|
-
const projectRoot = path.join(tempRoot, "date-only-project");
|
|
474
|
-
|
|
475
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
476
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
477
|
-
t.after(async () => {
|
|
478
|
-
if (originalRoot === undefined) {
|
|
479
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
480
|
-
} else {
|
|
481
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
482
|
-
}
|
|
483
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
await memory.appendEvent({
|
|
487
|
-
content: Array(30).fill("The weekly sync covered routine updates.").join("\n\n"),
|
|
488
|
-
keywords: ["sync"],
|
|
489
|
-
date: "2026-08-05",
|
|
490
|
-
projectRoot,
|
|
491
|
-
scope: "project",
|
|
492
|
-
sync: false,
|
|
493
|
-
timestamp: "2026-08-05T10:00:00.000Z",
|
|
494
|
-
title: "Weekly sync",
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
const search = await memory.searchMemory({ projectRoot, query: "2026-08-05", scope: "project" });
|
|
498
|
-
|
|
499
|
-
assert.equal(search.results.length, 1);
|
|
500
|
-
assert.match(search.results[0].snippet, /^The weekly sync covered routine updates\./);
|
|
501
|
-
assert.ok(search.results[0].snippet.length <= 1200);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
test("search returns nothing when no memory is about the question", async (t) => {
|
|
505
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
506
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-unrelated-"));
|
|
507
|
-
const projectRoot = path.join(tempRoot, "unrelated-project");
|
|
508
|
-
|
|
509
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
510
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
511
|
-
t.after(async () => {
|
|
512
|
-
if (originalRoot === undefined) {
|
|
513
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
514
|
-
} else {
|
|
515
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
516
|
-
}
|
|
517
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
for (const [title, content] of [
|
|
521
|
-
["Backend deploy", "The backend deploys to Cloud Run after the build passes."],
|
|
522
|
-
["Deploy rollback", "A failed backend deploy rolls back to the previous revision."],
|
|
523
|
-
["Billing note", "Invoices are generated monthly."],
|
|
524
|
-
]) {
|
|
525
|
-
await memory.appendEvent({ content, keywords: ["ops"], projectRoot, scope: "project", sync: false, title });
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const unrelated = await memory.searchMemory({ projectRoot, query: "which kubernetes namespace does the backend deploy to", scope: "project" });
|
|
529
|
-
const related = await memory.searchMemory({ projectRoot, query: "where does the backend deploy", scope: "project" });
|
|
530
|
-
|
|
531
|
-
const monthOnly = await memory.searchMemory({ projectRoot, query: "deploy in august", scope: "project" });
|
|
532
|
-
|
|
533
|
-
assert.deepEqual(unrelated.results, []);
|
|
534
|
-
assert.equal(related.results[0].heading, "Backend deploy");
|
|
535
|
-
// No event is from August; the month name must not count as a missing topic word.
|
|
536
|
-
assert.ok(monthOnly.results.length > 0);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
test("search keeps an event matched only by a keyword its long body never repeats", async (t) => {
|
|
540
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
541
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-keyword-only-"));
|
|
542
|
-
const projectRoot = path.join(tempRoot, "keyword-only-project");
|
|
543
|
-
|
|
544
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
545
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
546
|
-
t.after(async () => {
|
|
547
|
-
if (originalRoot === undefined) {
|
|
548
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
549
|
-
} else {
|
|
550
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
551
|
-
}
|
|
552
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
const content = [
|
|
556
|
-
"Opened three hardening PRs after the security audit.",
|
|
557
|
-
"The backend PR rotates the cron secret and checks request signatures on every scheduled job. ".repeat(8),
|
|
558
|
-
"The website PR updates the content studio dependencies and removes an unused preview route. ".repeat(8),
|
|
559
|
-
].join("\n\n");
|
|
560
|
-
|
|
561
|
-
assert.ok(content.length > 1200);
|
|
562
|
-
await memory.appendEvent({
|
|
563
|
-
content,
|
|
564
|
-
keywords: ["security-audit", "strava-oauth"],
|
|
565
|
-
projectRoot,
|
|
566
|
-
scope: "project",
|
|
567
|
-
sync: false,
|
|
568
|
-
title: "Security audit opened hardening PRs",
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
const search = await memory.searchMemory({ projectRoot, query: "Strava bug", scope: "project" });
|
|
572
|
-
|
|
573
|
-
assert.equal(search.results[0]?.heading, "Security audit opened hardening PRs");
|
|
574
|
-
assert.match(search.results[0].snippet, /^Opened three hardening PRs/);
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
test("search returns nothing when a rare word only resembles memory words", async (t) => {
|
|
578
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
579
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-lookalike-"));
|
|
580
|
-
const projectRoot = path.join(tempRoot, "lookalike-project");
|
|
581
|
-
|
|
582
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
583
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
584
|
-
t.after(async () => {
|
|
585
|
-
if (originalRoot === undefined) {
|
|
586
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
587
|
-
} else {
|
|
588
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
589
|
-
}
|
|
590
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
await memory.appendEvent({
|
|
594
|
-
content: "Each worker instance reads the instruct prompt and its content before a sync.",
|
|
595
|
-
keywords: ["health-sync"],
|
|
596
|
-
projectRoot,
|
|
597
|
-
scope: "project",
|
|
598
|
-
sync: false,
|
|
599
|
-
title: "Apple Health sync banner hardening",
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// "instinct" is one typo away from "instance" and "instruct" but means something else.
|
|
603
|
-
const lookalike = await memory.searchMemory({ projectRoot, query: "Instinct", scope: "project" });
|
|
604
|
-
const shared = await memory.searchMemory({ projectRoot, query: "context", scope: "project" });
|
|
605
|
-
const plural = await memory.searchMemory({ projectRoot, query: "worker instances", scope: "project" });
|
|
606
|
-
|
|
607
|
-
assert.deepEqual(lookalike.results, []);
|
|
608
|
-
assert.deepEqual(shared.results, []);
|
|
609
|
-
assert.equal(plural.results[0]?.heading, "Apple Health sync banner hardening");
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
test("an empty folder has no project and uses global memory", async (t) => {
|
|
613
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
614
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-empty-folder-"));
|
|
615
|
-
const lagoonRoot = path.join(tempRoot, "lagoon");
|
|
616
|
-
const emptyRoot = path.join(tempRoot, "billowy-glove");
|
|
617
|
-
const codeRoot = path.join(tempRoot, "job-search");
|
|
618
|
-
|
|
619
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = lagoonRoot;
|
|
620
|
-
// Only .git, like a fresh session folder some agents create.
|
|
621
|
-
await fs.mkdir(path.join(emptyRoot, ".git"), { recursive: true });
|
|
622
|
-
await fs.mkdir(codeRoot, { recursive: true });
|
|
623
|
-
await fs.writeFile(path.join(codeRoot, "notes.md"), "Job search notes.\n", "utf8");
|
|
624
|
-
t.after(async () => {
|
|
625
|
-
if (originalRoot === undefined) {
|
|
626
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
627
|
-
} else {
|
|
628
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
629
|
-
}
|
|
630
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
const wake = await memory.wakeUp({ projectRoot: emptyRoot });
|
|
634
|
-
const event = await memory.appendEvent({
|
|
635
|
-
content: "Created the applying-to-jobs skill.",
|
|
636
|
-
keywords: ["applying-to-jobs"],
|
|
637
|
-
projectRoot: emptyRoot,
|
|
638
|
-
scope: "project",
|
|
639
|
-
sync: false,
|
|
640
|
-
title: "Job application skill created",
|
|
641
|
-
});
|
|
642
|
-
await memory.appendEvent({
|
|
643
|
-
content: "Created the applying-to-jobs tracker in Notion.",
|
|
644
|
-
keywords: ["applying-to-jobs-tracker"],
|
|
645
|
-
projectRoot: codeRoot,
|
|
646
|
-
scope: "project",
|
|
647
|
-
sync: false,
|
|
648
|
-
title: "Job tracker created",
|
|
649
|
-
});
|
|
650
|
-
const fromEmpty = await memory.searchMemory({ projectRoot: emptyRoot, query: "applying to jobs skill" });
|
|
651
|
-
const projectScope = await memory.searchMemory({ projectRoot: emptyRoot, query: "applying to jobs", scope: "project" });
|
|
652
|
-
const fromProject = await memory.searchMemory({ projectRoot: codeRoot, query: "applying to jobs skill" });
|
|
653
|
-
|
|
654
|
-
assert.equal(wake.projectSlug, null);
|
|
655
|
-
assert.equal(wake.projectMemoryRoot, lagoonRoot);
|
|
656
|
-
assert.equal(path.dirname(path.dirname(event.indexPath)), lagoonRoot);
|
|
657
|
-
// Global memory is searched once, and other projects stay out of it.
|
|
658
|
-
assert.deepEqual(fromEmpty.results.map((result) => result.heading), ["Job application skill created"]);
|
|
659
|
-
assert.deepEqual(projectScope.results.map((result) => result.heading), ["Job application skill created"]);
|
|
660
|
-
assert.equal(fromProject.results[0]?.heading, "Job application skill created");
|
|
661
|
-
assert.equal(memory.getProjectSlug(codeRoot), "job-search");
|
|
662
|
-
// A folder that does not exist keeps its name, as Dream's synthetic roots do.
|
|
663
|
-
assert.equal(memory.getProjectSlug(path.join(tempRoot, "missing")), "missing");
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
test("Project slug is the same for NFC and NFD folder names", () => {
|
|
667
|
-
const name = "Génération IA";
|
|
668
|
-
assert.equal(memory.getProjectSlug(`/tmp/${name.normalize("NFC")}`), "g-n-ration-ia");
|
|
669
|
-
assert.equal(memory.getProjectSlug(`/tmp/${name.normalize("NFD")}`), "g-n-ration-ia");
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
test("A deleted working directory asks for projectRoot", async (t) => {
|
|
673
|
-
const originalCwd = process.cwd();
|
|
674
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-deleted-cwd-"));
|
|
675
|
-
t.after(() => process.chdir(originalCwd));
|
|
676
|
-
process.chdir(tempRoot);
|
|
677
|
-
await fs.rm(tempRoot, { recursive: true });
|
|
678
|
-
|
|
679
|
-
assert.throws(() => memory.getProjectRoot(), /Pass projectRoot/);
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
test("event keywords are written as wiki links and still indexed as plain keywords", async (t) => {
|
|
683
|
-
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
684
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-wiki-links-"));
|
|
685
|
-
const projectRoot = path.join(tempRoot, "wiki-links-project");
|
|
686
|
-
|
|
687
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
688
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
689
|
-
t.after(async () => {
|
|
690
|
-
if (originalRoot === undefined) {
|
|
691
|
-
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
692
|
-
} else {
|
|
693
|
-
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
694
|
-
}
|
|
695
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
const event = await memory.appendEvent({
|
|
699
|
-
content: "Graph views group events by keyword.",
|
|
700
|
-
keywords: ["Knowledge Graph", "obsidian"],
|
|
701
|
-
projectRoot,
|
|
702
|
-
scope: "project",
|
|
703
|
-
sync: false,
|
|
704
|
-
timestamp: "2026-10-16T09:00:00.000Z",
|
|
705
|
-
title: "Graph note",
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
const content = await fs.readFile(path.join(event.root, event.path), "utf8");
|
|
709
|
-
const index = JSON.parse(await fs.readFile(event.indexPath, "utf8"));
|
|
710
|
-
|
|
711
|
-
assert.match(content, /Keywords:\n- \[\[knowledge-graph\]\]\n- \[\[obsidian\]\]/);
|
|
712
|
-
assert.match(JSON.stringify(index), /"knowledge-graph"/);
|
|
713
|
-
assert.doesNotMatch(JSON.stringify(index), /\[\[/);
|
|
714
|
-
|
|
715
|
-
const search = await memory.searchMemory({ projectRoot, query: "obsidian", scope: "project" });
|
|
716
|
-
|
|
717
|
-
assert.deepEqual(search.results[0].keywords, ["knowledge-graph", "obsidian"]);
|
|
718
|
-
});
|