hippocamp 0.2.13 → 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 +1 -1
- package/README.md +8 -2
- package/docs/how-it-works.md +1 -1
- package/docs/results.md +14 -10
- package/package.json +1 -1
- package/scripts/hippocamp-memory.cjs +53 -8
package/AGENTS.md
CHANGED
|
@@ -45,7 +45,7 @@ This repository is a local-first MCP package for Git-backed agent memory. Keep c
|
|
|
45
45
|
- `assets/railway/run-dream.sh`: short-lived hosted Dream runner that clones Lagoon and opens one Dream PR per project.
|
|
46
46
|
- `Dockerfile.railway` and `railway.json`: optional Railway cron deployment for overnight Dream runs.
|
|
47
47
|
- `assets/brand/hippocamp-mascot.png`: README mascot asset.
|
|
48
|
-
- `assets/brand/
|
|
48
|
+
- `assets/brand/lagoon-graph.jpg`: README Obsidian graph view of a real Lagoon, loaded from GitHub and kept out of the npm package by `.npmignore` (keep `.npmignore` in sync with `.gitignore`).
|
|
49
49
|
- `docs/`: user docs (setup, how it works, Dream, results, development). The README stays a short overview that links to them.
|
|
50
50
|
|
|
51
51
|
## Project Invariants
|
package/README.md
CHANGED
|
@@ -18,9 +18,11 @@ npx hippocamp@latest install
|
|
|
18
18
|
|
|
19
19
|
This installs Hippocamp into every supported agent it finds and creates your memory repo, `~/.lagoon`. Restart your agent and it starts each task by waking up from memory.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
## Build and own your agents' memory
|
|
22
22
|
|
|
23
|
-
A
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
A real `~/.lagoon` opened in Obsidian. Every dot is a Markdown note your agents wrote, linked by keywords. It's plain files in your own Git repo, so you can open them in any editor.
|
|
24
26
|
|
|
25
27
|
## Why Hippocamp?
|
|
26
28
|
|
|
@@ -55,6 +57,10 @@ Reproducible with the scripts in this repo.
|
|
|
55
57
|
| Work continuity, 21 real Claude Code sessions, checks passed | 100% (20 / 20) | Claude Code native memory: 96% (25 / 26) |
|
|
56
58
|
| Work continuity, same 21 sessions in Codex, checks passed | 86% (24 / 28) | Codex native memory: 86% (24 / 28) |
|
|
57
59
|
|
|
60
|
+
**Work continuity** tests memory on real engineering work, not chat questions. A real Claude Code or Codex agent works through 21 coding sessions on two small projects. Early sessions establish facts, such as a rejected library, a pinned Node version, a blocked vendor, a parked branch or a user preference. Later tickets only go well if the agent remembers them, and the prompts never mention memory. A blind judge grades 28 checks from the diff and the tool calls. Each arm is installed the way a developer has it.
|
|
61
|
+
|
|
62
|
+
The same Hippocamp server, plugged into either agent with no changes, keeps up with that agent's own memory. In Codex, whose built-in memory stored almost nothing, Hippocamp's memory covered 57% of the planted facts against 4%. Hippocamp uses about 25–45% more tokens per session. This is one run per arm, so the differences are within noise. Every grade, diff and judge prompt is published in [docs/results](docs/results/).
|
|
63
|
+
|
|
58
64
|
Full tables, method, and limits: [docs/results.md](docs/results.md).
|
|
59
65
|
|
|
60
66
|
## Docs
|
package/docs/how-it-works.md
CHANGED
|
@@ -70,7 +70,7 @@ Typical agent flow:
|
|
|
70
70
|
4. Use `append_event` for meaningful milestones, with a title and 1-8 concise keywords.
|
|
71
71
|
5. Update curated files like `current_state.md` and `open_threads.md` before finishing.
|
|
72
72
|
|
|
73
|
-
Writes sync by default. If sync fails or is skipped, call `sync_memory`.
|
|
73
|
+
Writes sync by default. If sync fails or is skipped, call `sync_memory`. `wake_up` and a `sync_memory` with nothing to send first pull what other machines pushed (fast-forward only, without credential prompts; when the remote cannot be reached, the pull gives up after 10 seconds and the agent continues with local memory), so a second machine starts from current memory. Sync only commits when the Lagoon is the top of its own Git repository; a Lagoon folder inside another repository, such as a code checkout, is never committed or pushed.
|
|
74
74
|
|
|
75
75
|
Event writes update a sibling `events/YYYY-MM-DD.index.json` file. The Markdown event remains the canonical memory; the sidecar is rebuildable. Search ranks indexed events and curated files with BM25 and typo tolerance ([MiniSearch](https://github.com/lucaong/minisearch)), weighting keywords above titles and titles above bodies. It returns small coherent evidence blocks from indexed events; an event found only by its keywords or title returns its first paragraphs. If no indexed result is strong enough, it returns no event result instead of scanning whole event logs.
|
|
76
76
|
|
package/docs/results.md
CHANGED
|
@@ -43,7 +43,7 @@ The same reader reaches 90% with the correct sessions, so the remaining gap is r
|
|
|
43
43
|
|
|
44
44
|
Does memory carry real engineering work from one coding session to the next? `npm run eval:continuity` runs real headless Claude Code (`claude-opus-5-5`, Claude Code 2.1.318 to 2.1.319) through 21 sessions over two small synthetic projects (a TypeScript webhook relay and a Python ingest job). Early sessions plant facts the way work does: a rejected library, a pinned Node version, a blocked vendor, a parked branch, a user preference. Later sessions give tickets that only go well if the agent remembers them. The prompts never say "remember" or point at memory.
|
|
45
45
|
|
|
46
|
-
In short: the same Hippocamp server, plugged unchanged into Claude Code or Codex, keeps up with each one's own memory (100% vs 96% in Claude Code, 86% vs 86% in Codex), with fewer than 5 points of difference and one run per arm. In Codex, whose built-in memory stored almost nothing, Hippocamp's memory covered
|
|
46
|
+
In short: the same Hippocamp server, plugged unchanged into Claude Code or Codex, keeps up with each one's own memory (100% vs 96% in Claude Code, 86% vs 86% in Codex), with fewer than 5 points of difference and one run per arm. In Codex, whose built-in memory stored almost nothing, Hippocamp's memory covered 57% of the planted facts against 4%.
|
|
47
47
|
|
|
48
48
|
Each arm is installed the way a developer has it, in its own sandbox home: Claude Code defaults with native auto-memory, or Hippocamp through its real installer (`install-claude`) with auto-memory off. A blind judge (Opus, arm names and memory paths scrubbed) grades 28 checks against the diff and the tool calls. A check is n/a when the session wrote no code it applies to.
|
|
49
49
|
|
|
@@ -56,11 +56,11 @@ Run `full-1`, one run per arm:
|
|
|
56
56
|
| Tokens per session, median | 179k | 224k |
|
|
57
57
|
| Time per session, median | 62 s | 73 s |
|
|
58
58
|
| Agent cost per run, API price | $8.84 | $9.19 |
|
|
59
|
-
| Memory entries at the end | 8 |
|
|
60
|
-
| Planted facts covered by memory | 22% |
|
|
61
|
-
| Memory entries that are useful (precision) | 88% |
|
|
59
|
+
| Memory entries at the end | 8 | 27 |
|
|
60
|
+
| Planted facts covered by memory | 22% | 57% |
|
|
61
|
+
| Memory entries that are useful (precision) | 88% | 81% |
|
|
62
62
|
|
|
63
|
-
What it shows: Hippocamp, plugged into Claude Code as a plain MCP server, does as well as Claude Code's own memory, at about the same cost (+4% dollars, +25% tokens per session). Native memory's one miss was a parked `fix/late-dedupe` branch it no longer knew about, so it rewrote the fix from scratch. Hippocamp's 8 n/a checks come mostly from one session where it correctly held a blocked vendor back and wrote no code; native also held the vendor back, but with gated code the checks could grade.
|
|
63
|
+
What it shows: Hippocamp, plugged into Claude Code as a plain MCP server, does as well as Claude Code's own memory, at about the same cost (+4% dollars, +25% tokens per session). Native memory's one miss was a parked `fix/late-dedupe` branch it no longer knew about, so it rewrote the fix from scratch. Hippocamp's 8 n/a checks come mostly from one session where it correctly held a blocked vendor back and wrote no code; native also held the vendor back, but with gated code the checks could grade. A check is only counted when the session did work it applies to, so the two arms are scored on different numbers of checks (20 vs 26); this favors an arm that holds work back.
|
|
64
64
|
|
|
65
65
|
What it does not show: that Hippocamp is better. The difference is one check, from one run per arm (bootstrap 95% interval over probes: +4 points, 0 to +16). There is no no-memory run, so the "needing memory" label comes from the dataset, not from a measured baseline. An earlier development run (27 / 27 vs 24 / 27) is not used: its cases were added after a tie and its harness was unfair to native memory.
|
|
66
66
|
|
|
@@ -77,16 +77,20 @@ Run `codex-1` repeats the 21 sessions in Codex (`codex exec`, codex-cli 0.159.2,
|
|
|
77
77
|
| Tokens per session, median | 166k | 238k |
|
|
78
78
|
| Time per session, median | 63 s | 82 s |
|
|
79
79
|
| Memory calls per session | 1.0 | 4.9 |
|
|
80
|
-
| Memory entries at the end | 4 |
|
|
81
|
-
| Planted facts covered by memory | 4% |
|
|
82
|
-
| Memory entries that are useful (precision) | 25% |
|
|
80
|
+
| Memory entries at the end | 4 | 31 |
|
|
81
|
+
| Planted facts covered by memory | 4% | 57% |
|
|
82
|
+
| Memory entries that are useful (precision) | 25% | 61% |
|
|
83
83
|
|
|
84
|
-
What it shows: the same Hippocamp server, with no Codex-specific code, works in Codex as it does in Claude Code. Codex called `wake_up`, `search_memory` and `append_event` on its own, and the memory it built covers
|
|
84
|
+
What it shows: the same Hippocamp server, with no Codex-specific code, works in Codex as it does in Claude Code. Codex called `wake_up`, `search_memory` and `append_event` on its own, and the memory it built covers as many planted facts as in Claude Code (57% in both). The two Codex arms tie on checks (bootstrap: 0 points, -14 to +14).
|
|
85
85
|
|
|
86
|
-
Codex's own memory stored almost nothing in this run: its background summaries never produced a memory from a past session, and the one real entry is a changelog preference the agent wrote as a note. The other three entries are Codex's empty templates, which the judge counts as noise. Codex still passed most checks without it, from the repository itself (code, commit messages, notes it wrote in the repo); that is why the score does not drop more. Hippocamp's misses include one stale next step and one fact it never saved (a blocked vendor).
|
|
86
|
+
Codex's own memory stored almost nothing in this run: its background summaries never produced a memory from a past session, and the one real entry is a changelog preference the agent wrote as a note. The other three entries are Codex's empty templates, which the judge counts as noise. The harness turns Codex memory on but does not wait for or confirm its background step between sessions, so this arm measures Codex memory as it behaved here, not a verified working pipeline. Hippocamp's Codex memory also holds more duplicates (5) and stale entries (4) than in Claude Code, so its precision is lower (61%). Codex still passed most checks without it, from the repository itself (code, commit messages, notes it wrote in the repo); that is why the score does not drop more. Hippocamp's misses include one stale next step and one fact it never saved (a blocked vendor).
|
|
87
87
|
|
|
88
88
|
Codex reports no dollar cost, so this run has token and time numbers only.
|
|
89
89
|
|
|
90
|
+
### How memory quality is scored
|
|
91
|
+
|
|
92
|
+
At the end of each run, a judge labels every memory entry, across both projects, as a current fact, a stale fact, other useful context, noise or a duplicate. Coverage is the share of planted facts with at least one current entry. Each entry is credited with one fact at most, so a native memory file that lists several facts in one entry gets credit for one of them: coverage favors memories that store one fact per entry, like Hippocamp's events, and the native coverage figures are a lower bound. A first version of this report judged only one project of Hippocamp's memory and gave it 30% (Claude Code) and 35% (Codex); this was fixed and re-judged without re-running any session.
|
|
93
|
+
|
|
90
94
|
The run data is in [results/continuity-codex-1](results/continuity-codex-1/).
|
|
91
95
|
|
|
92
96
|
## Why less text matters
|
package/package.json
CHANGED
|
@@ -438,9 +438,9 @@ async function ensureParentDirectory(filePath) {
|
|
|
438
438
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
async function runGit(args, cwd) {
|
|
441
|
+
async function runGit(args, cwd, options = {}) {
|
|
442
442
|
try {
|
|
443
|
-
const result = await execFileAsync("git", args, { cwd });
|
|
443
|
+
const result = await execFileAsync("git", args, { cwd, ...options });
|
|
444
444
|
return {
|
|
445
445
|
ok: true,
|
|
446
446
|
stdout: result.stdout.trim(),
|
|
@@ -477,17 +477,54 @@ function realpathExisting(targetPath) {
|
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
|
|
480
|
+
// The Lagoon's own repository, or null: a repository that only encloses the Lagoon (a code
|
|
481
|
+
// checkout, a home-directory repo) must never receive memory commits or pushes.
|
|
482
|
+
async function getLagoonRepo() {
|
|
481
483
|
const repoCandidate = getGlobalRepoRoot();
|
|
482
484
|
const repoRoot = await getGitRepoRoot(repoCandidate);
|
|
483
|
-
const scopeRoot = getScopeRoot(scope, projectRoot);
|
|
484
485
|
|
|
485
486
|
if (!repoRoot) {
|
|
487
|
+
return { repoCandidate, reason: "not_git_repo" };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (realpathExisting(repoRoot) !== realpathExisting(repoCandidate)) {
|
|
491
|
+
return { repoCandidate, repoRoot, reason: "lagoon_not_repo_root" };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return { repoCandidate, repoRoot };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Brings in memory pushed from other machines. Fast-forward only: local commits that have not
|
|
498
|
+
// been pushed yet are reconciled by the push path, never here. Offline or slow remotes are skipped.
|
|
499
|
+
async function pullMemory() {
|
|
500
|
+
const { repoRoot, reason } = await getLagoonRepo();
|
|
501
|
+
|
|
502
|
+
if (!repoRoot) {
|
|
503
|
+
return { pulled: false, reason };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const upstream = await runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot);
|
|
507
|
+
|
|
508
|
+
if (!upstream.ok) {
|
|
509
|
+
return { pulled: false, reason: "no_upstream" };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Never prompt: saved credentials (credential helpers, SSH agent) still work; anything that would ask fails fast.
|
|
513
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "true", SSH_ASKPASS: "true", GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND || "ssh -o BatchMode=yes" };
|
|
514
|
+
const result = await runGit(["pull", "--ff-only", "--quiet"], repoRoot, { timeout: 10000, env });
|
|
515
|
+
return result.ok ? { pulled: true } : { pulled: false, reason: "pull_failed", error: result.stderr };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function syncMemory({ scope, projectRoot, paths, message }) {
|
|
519
|
+
const { repoCandidate, repoRoot, reason } = await getLagoonRepo();
|
|
520
|
+
const scopeRoot = getScopeRoot(scope, projectRoot);
|
|
521
|
+
|
|
522
|
+
if (reason) {
|
|
486
523
|
return {
|
|
487
524
|
attempted: false,
|
|
488
525
|
skipped: true,
|
|
489
|
-
reason
|
|
490
|
-
repoRoot: repoCandidate,
|
|
526
|
+
reason,
|
|
527
|
+
repoRoot: repoRoot || repoCandidate,
|
|
491
528
|
};
|
|
492
529
|
}
|
|
493
530
|
|
|
@@ -530,11 +567,13 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
|
|
|
530
567
|
const aheadResult = await runGit(["rev-list", "--count", "@{u}..HEAD"], repoRoot);
|
|
531
568
|
|
|
532
569
|
if (!aheadResult.ok || aheadResult.stdout === "0") {
|
|
570
|
+
// Nothing to send: an explicit sync still brings in what other machines pushed.
|
|
533
571
|
return {
|
|
534
572
|
attempted: true,
|
|
535
573
|
skipped: true,
|
|
536
574
|
reason: "no_changes_to_commit",
|
|
537
575
|
repoRoot,
|
|
576
|
+
pull: await pullMemory(),
|
|
538
577
|
};
|
|
539
578
|
}
|
|
540
579
|
|
|
@@ -1084,7 +1123,10 @@ async function readMemoryFile({ scope, path: relativePath, projectRoot }) {
|
|
|
1084
1123
|
async function writeMemoryFile({ scope, path: relativePath, content, projectRoot, sync = true }) {
|
|
1085
1124
|
const target = resolveScopedPath(scope, relativePath, projectRoot);
|
|
1086
1125
|
|
|
1087
|
-
|
|
1126
|
+
// Event logs are append-only in every scope, including a project's log reached from the global root.
|
|
1127
|
+
const fromGlobal = path.relative(getGlobalRoot(), target.absolutePath).split(path.sep);
|
|
1128
|
+
|
|
1129
|
+
if (target.path === "events" || target.path.startsWith("events/") || fromGlobal[0] === "events" || (fromGlobal[0] === "projects" && fromGlobal[2] === "events")) {
|
|
1088
1130
|
throw new Error("events/ is append-only. Use append_event with a title and keywords instead.");
|
|
1089
1131
|
}
|
|
1090
1132
|
|
|
@@ -1273,7 +1315,8 @@ async function collectEventDocuments({ scope, eventFiles }) {
|
|
|
1273
1315
|
index = null;
|
|
1274
1316
|
}
|
|
1275
1317
|
|
|
1276
|
-
|
|
1318
|
+
// The log always sits next to its index; a path field inside the index is never trusted.
|
|
1319
|
+
const markdownName = indexName.replace(/\.index\.json$/, ".md");
|
|
1277
1320
|
|
|
1278
1321
|
if (!index || !Array.isArray(index.events)) {
|
|
1279
1322
|
continue;
|
|
@@ -1622,6 +1665,7 @@ async function searchMemory({ query, scope = "both", projectRoot, maxResults = 5
|
|
|
1622
1665
|
}
|
|
1623
1666
|
|
|
1624
1667
|
async function wakeUp({ projectRoot } = {}) {
|
|
1668
|
+
const pull = await pullMemory();
|
|
1625
1669
|
const globalRoot = getScopeRoot("global", projectRoot);
|
|
1626
1670
|
const resolvedProjectRoot = getProjectRoot(projectRoot);
|
|
1627
1671
|
const projectSlug = getProjectSlug(projectRoot);
|
|
@@ -1695,6 +1739,7 @@ async function wakeUp({ projectRoot } = {}) {
|
|
|
1695
1739
|
missing,
|
|
1696
1740
|
globalFiles: globalFiles.map((file) => file.path),
|
|
1697
1741
|
projectFiles: projectFiles.map((file) => file.path),
|
|
1742
|
+
pull,
|
|
1698
1743
|
text: `${sections.join("\n").trim()}\n`,
|
|
1699
1744
|
};
|
|
1700
1745
|
}
|