hippocamp 0.2.11 → 0.2.13
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 +10 -2
- package/README.md +2 -0
- package/docs/development.md +21 -0
- package/docs/how-it-works.md +1 -1
- package/docs/results.md +52 -0
- package/docs/setup.md +1 -1
- package/package.json +2 -1
- package/scripts/hippocamp-memory.cjs +67 -0
- package/scripts/install-claude.cjs +8 -4
- package/scripts/install-common.cjs +30 -1
- 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 -129
- 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
package/AGENTS.md
CHANGED
|
@@ -29,6 +29,13 @@ This repository is a local-first MCP package for Git-backed agent memory. Keep c
|
|
|
29
29
|
- `scripts/eval-lagoon.cjs`: read-only retrieval check against real Lagoon cases in `<Lagoon root>/evals/retrieval-cases.json`.
|
|
30
30
|
- `scripts/eval-qa.cjs`: LongMemEval-S answer-accuracy check (reader and grader models) for Hippocamp snippets against the labeled answer sessions.
|
|
31
31
|
- `scripts/eval-retrieval.cjs`: retrieval-only LongMemEval-S check for `searchMemory` against a BM25 reference.
|
|
32
|
+
- `scripts/eval-continuity.cjs`: work continuity benchmark runner and CLI: real headless Claude Code or Codex through successive sessions per arm (`cc-none`, `cc-native`, `cc-hippocamp`, `codex-native`, `codex-hippocamp`) in isolated sandboxes, with a frozen run manifest and `--report`.
|
|
33
|
+
- `scripts/continuity-adapters.cjs`: one adapter per benchmark arm, each installed the way a developer has it (Hippocamp through its real `install-claude` or `install-codex` installer).
|
|
34
|
+
- `scripts/continuity-judge.cjs`: blind task judge, miss attribution, and the memory-quality judge.
|
|
35
|
+
- `scripts/continuity-report.cjs`: outcome, efficiency and memory-quality tables, pooled over runs, with a probe-level bootstrap.
|
|
36
|
+
- `scripts/continuity-dataset.cjs`: loader and validator for the continuity dataset, including the retrieval-hint check on prompts.
|
|
37
|
+
- `scripts/eval-locomo.cjs`: LoCoMo answer-accuracy check with the MemoryTax protocol; its `complete` model client is shared with the continuity judge.
|
|
38
|
+
- `bench/continuity/`: synthetic continuity dataset: two seed repos, session tickets, planted facts and the run timeline.
|
|
32
39
|
- `scripts/install-common.cjs`: shared installer helpers: the MCP launch command and Lagoon repo preparation.
|
|
33
40
|
- `scripts/install-claude.cjs`: one-step installer for Claude Code MCP setup and user-level `CLAUDE.md` guidance.
|
|
34
41
|
- `scripts/install-codex.cjs`: one-step installer for the Hippocamp skill and MCP server in Codex.
|
|
@@ -48,7 +55,7 @@ This repository is a local-first MCP package for Git-backed agent memory. Keep c
|
|
|
48
55
|
- Global memory lives under `HIPPOCAMP_GLOBAL_ROOT/`.
|
|
49
56
|
- Project memory lives under `HIPPOCAMP_GLOBAL_ROOT/projects/<slug>/`.
|
|
50
57
|
- The current project slug is inferred from `HIPPOCAMP_PROJECT_ROOT` or the current working directory. An existing folder with nothing but `.git` has no project; project scope then resolves to the global root.
|
|
51
|
-
- Event logs are append-only daily Markdown files under `events/YYYY-MM-DD.md`, with sibling `events/YYYY-MM-DD.index.json` keyword indexes.
|
|
58
|
+
- Event logs are append-only daily Markdown files under `events/YYYY-MM-DD.md`, with sibling `events/YYYY-MM-DD.index.json` keyword indexes. The one exception is the installer's keyword-link format upgrade of older events.
|
|
52
59
|
- Curated files such as `current_state.md` and `open_threads.md` use explicit file writes.
|
|
53
60
|
- MCP `append_event` requires a title and 1-8 keywords; MCP `write_memory_file` rejects paths under `events/`; an explicit `projectRoot` must be a directory inside a Git repository.
|
|
54
61
|
- `append_event` auto-stamps `Agent:` / `Session:` provenance when resolvable (installer env, MCP client name, or process session id). No manual user config.
|
|
@@ -79,10 +86,11 @@ When changing code, use the smallest verification that proves the change:
|
|
|
79
86
|
- Run `npm run dream -- --help` when changing Dream CLI arguments.
|
|
80
87
|
- Run `npm run eval:retrieval -- --limit 100` when changing search ranking or snippets; report Recall@5 against the BM25 row and snippet evidence. Also run `npm run eval:lagoon` when a Lagoon is available.
|
|
81
88
|
- Run `npm run eval:qa -- --limit 3 --dry-run` when changing `scripts/eval-qa.cjs`; a full run calls paid models, so run it only when asked.
|
|
89
|
+
- Run `npm test` and `npm run eval:continuity -- --dry-run` when changing the continuity benchmark or its dataset; a real run spawns paid Claude Code sessions, so run it only when asked.
|
|
82
90
|
|
|
83
91
|
## Notes For Future Agents
|
|
84
92
|
|
|
85
93
|
- The cloud/API version is intentionally deferred.
|
|
86
94
|
- Do not reintroduce Next.js, GitHub API token auth, or GitHub App auth for the local MVP.
|
|
87
95
|
- Keep Dream minimal: scheduled updates and coherent compaction, two curated files plus bounded indexed evidence, one PR per project.
|
|
88
|
-
- The local install story should stay: `npx hippocamp@latest install`, a local Lagoon repo created if missing, normal Git auth. Installers never create remotes or push.
|
|
96
|
+
- The local install story should stay: `npx hippocamp@latest install`, a local Lagoon repo created if missing, normal Git auth. Installers never create remotes or push; their only event rewrite is one local keyword-link format upgrade of older events, the exception to the append-only event invariant.
|
package/README.md
CHANGED
|
@@ -52,6 +52,8 @@ Reproducible with the scripts in this repo.
|
|
|
52
52
|
| Text returned per search | 5.9k chars | Whole sessions: 65.5k |
|
|
53
53
|
| Answer accuracy, Claude Sonnet 5, top 10 results | 77% | Correct sessions given directly: 90% |
|
|
54
54
|
| Unrelated questions that return nothing (our own test set) | 10 / 10 | |
|
|
55
|
+
| Work continuity, 21 real Claude Code sessions, checks passed | 100% (20 / 20) | Claude Code native memory: 96% (25 / 26) |
|
|
56
|
+
| Work continuity, same 21 sessions in Codex, checks passed | 86% (24 / 28) | Codex native memory: 86% (24 / 28) |
|
|
55
57
|
|
|
56
58
|
Full tables, method, and limits: [docs/results.md](docs/results.md).
|
|
57
59
|
|
package/docs/development.md
CHANGED
|
@@ -10,6 +10,7 @@ npm run mcp:help
|
|
|
10
10
|
npm run mcp:smoke
|
|
11
11
|
npm run dream
|
|
12
12
|
npm run dream:railway
|
|
13
|
+
npm run eval:continuity
|
|
13
14
|
npm run eval:lagoon
|
|
14
15
|
npm run eval:qa
|
|
15
16
|
npm run eval:retrieval
|
|
@@ -21,6 +22,26 @@ npm run upgrade:claude
|
|
|
21
22
|
npm run upgrade:grok
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
## Work continuity benchmark
|
|
26
|
+
|
|
27
|
+
`npm run eval:continuity` measures what Hippocamp is built for: a coding agent that keeps working on a project across independent sessions and acts on earlier decisions, constraints and state without being told to look them up.
|
|
28
|
+
|
|
29
|
+
- The dataset in `bench/continuity/` is synthetic: two small engineering projects (`relay`, a TypeScript webhook relay, and `ingest`, a Python CSV pipeline) on one 21-session timeline. Work sessions are ordinary tickets that plant facts; probe sessions are ordinary tickets with hidden checklists. No prompt hints at retrieval.
|
|
30
|
+
- Arms, each set up the way a developer has it: `cc-none` (auto-memory off), `cc-native` (Claude Code defaults) and `cc-hippocamp` (Hippocamp through its real installer, auto-memory off). Codex arms run `codex exec` (`gpt-6.1-sol`) on the ChatGPT sign-in copied from `~/.codex/auth.json`: `codex-native` (Codex `memories` on, with `min_rollout_idle_hours = 0` because sessions run back to back) and `codex-hippocamp` (Hippocamp through `install-codex`). Codex reports no dollar cost, so its cost column stays at $0.
|
|
31
|
+
- Each session is a fresh headless `claude -p --model claude-opus-5-5` process. The repo is committed after each session with the agent's own summary as the message. If the agent ends with an offer or a question, the simulated user answers once with "yes, go ahead". A retried session restores both the repo and the memory store.
|
|
32
|
+
- Sandboxes live in `HIPPOCAMP_BENCH_DIR` (default `~/.cache/hippocamp-bench`), each with its own `HOME` and `CLAUDE_CONFIG_DIR`, so your real `~/.claude` and `~/.lagoon` are never read or written, and runs survive reboots.
|
|
33
|
+
- Judging: a blind judge grades each check from the diff and a tool log with memory tools and paths anonymized. A second call explains each miss (never stored, stale memory, wrong scope, not recalled, ignored, asked instead of acting). At the end, a third grades every stored memory entry for coverage, precision, staleness, noise and duplicates. Prompts and replies are kept next to each transcript, and `validation.csv` lists every verdict for hand-checking.
|
|
34
|
+
- Each session records tokens, time, memory tool calls and memory bytes read or auto-loaded.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run eval:continuity -- --dry-run # free: validates and prints the cost range
|
|
38
|
+
npm run eval:continuity -- --systems cc-native,cc-hippocamp --limit 3 --run smoke
|
|
39
|
+
npm run eval:continuity -- --run final-1 # full run
|
|
40
|
+
npm run eval:continuity -- --report final-1,final-2 # pooled report, spawns nothing
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Sessions and the Claude judge run through Claude Code, so they use your Claude subscription when `CLAUDE_CODE_OAUTH_TOKEN` is set (create it with `claude setup-token` and put it in `.env`). Without it they bill `ANTHROPIC_API_KEY`. `--dry-run` prints the API-price range, and `--budget` caps each call. Results go to `.context/continuity/<run>/`. A run is frozen by `manifest.json` (dataset, harness, model and Claude Code version): reruns with the same `--run` skip finished sessions, new sessions appended to the timeline extend it, and any frozen change is refused unless `--force-manifest` records it. The report shows n next to every percentage, defines memory-required items as those `cc-none` fails in every run, and gives bootstrap intervals over probes.
|
|
44
|
+
|
|
24
45
|
## Releases
|
|
25
46
|
|
|
26
47
|
Pull requests and pushes to `main` run syntax and MCP smoke checks in GitHub Actions.
|
package/docs/how-it-works.md
CHANGED
|
@@ -78,7 +78,7 @@ Event writes update a sibling `events/YYYY-MM-DD.index.json` file. The Markdown
|
|
|
78
78
|
|
|
79
79
|
- Keep memory concise.
|
|
80
80
|
- Prefer curated summaries over raw event history.
|
|
81
|
-
- Give events a short `Keywords:` section; keywords weigh the most in search ranking. `append_event` writes each keyword as a `[[keyword]]` wiki link, so a graph view such as Obsidian's groups events by topic.
|
|
81
|
+
- Give events a short `Keywords:` section; keywords weigh the most in search ranking. `append_event` writes each keyword as a `[[keyword]]` wiki link, so a graph view such as Obsidian's groups events by topic. Install and upgrade convert clear keyword bullets in older events, including legacy `Cues:` sections, to this format and commit the change in the Lagoon; the next memory sync pushes it. A file is rewritten only if search reads exactly the same keywords from it afterwards.
|
|
82
82
|
- Do not duplicate GitHub-owned facts such as commits, PRs, issues, reviews, or CI results.
|
|
83
83
|
- Store artifact references plus the missing rationale, preference, assumption, or follow-up context.
|
|
84
84
|
- Use project scope for project-specific state.
|
package/docs/results.md
CHANGED
|
@@ -39,6 +39,56 @@ The same reader reaches 90% with the correct sessions, so the remaining gap is r
|
|
|
39
39
|
| Right event in top 5, 43 answerable questions | 42 / 43 |
|
|
40
40
|
| No results for 10 questions no memory is about | 10 / 10 |
|
|
41
41
|
|
|
42
|
+
## Work continuity
|
|
43
|
+
|
|
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
|
+
|
|
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 35% of the planted facts against 4%.
|
|
47
|
+
|
|
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
|
+
|
|
50
|
+
Run `full-1`, one run per arm:
|
|
51
|
+
|
|
52
|
+
| | Claude Code native memory | Hippocamp in Claude Code |
|
|
53
|
+
| --- | ---: | ---: |
|
|
54
|
+
| Checks passed | 96% (25 / 26) | 100% (20 / 20) |
|
|
55
|
+
| Checks marked as needing memory | 92% (11 / 12) | 100% (10 / 10) |
|
|
56
|
+
| Tokens per session, median | 179k | 224k |
|
|
57
|
+
| Time per session, median | 62 s | 73 s |
|
|
58
|
+
| Agent cost per run, API price | $8.84 | $9.19 |
|
|
59
|
+
| Memory entries at the end | 8 | 15 |
|
|
60
|
+
| Planted facts covered by memory | 22% | 30% |
|
|
61
|
+
| Memory entries that are useful (precision) | 88% | 80% |
|
|
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.
|
|
64
|
+
|
|
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
|
+
|
|
67
|
+
The diffs, judge prompts, grades with evidence and memory snapshots of this run are in [results/continuity-full-1](results/continuity-full-1/) (start with `report.md` and `validation.csv`). Full transcripts are left out for size.
|
|
68
|
+
|
|
69
|
+
### Same benchmark in Codex
|
|
70
|
+
|
|
71
|
+
Run `codex-1` repeats the 21 sessions in Codex (`codex exec`, codex-cli 0.159.2, `gpt-6.1-sol`, ChatGPT sign-in), same checks and same blind Opus judge. Codex native memory is its `memories` feature, turned on (it is off by default) with `min_rollout_idle_hours = 0`, because the benchmark runs sessions back to back instead of days apart. Hippocamp is installed with `install-codex`; nothing else changes.
|
|
72
|
+
|
|
73
|
+
| | Codex native memory | Hippocamp in Codex |
|
|
74
|
+
| --- | ---: | ---: |
|
|
75
|
+
| Checks passed | 86% (24 / 28) | 86% (24 / 28) |
|
|
76
|
+
| Checks marked as needing memory | 67% (8 / 12) | 75% (9 / 12) |
|
|
77
|
+
| Tokens per session, median | 166k | 238k |
|
|
78
|
+
| Time per session, median | 63 s | 82 s |
|
|
79
|
+
| Memory calls per session | 1.0 | 4.9 |
|
|
80
|
+
| Memory entries at the end | 4 | 17 |
|
|
81
|
+
| Planted facts covered by memory | 4% | 35% |
|
|
82
|
+
| Memory entries that are useful (precision) | 25% | 76% |
|
|
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 about as many planted facts as in Claude Code (35% vs 30%). The two Codex arms tie on checks (bootstrap: 0 points, -14 to +14).
|
|
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).
|
|
87
|
+
|
|
88
|
+
Codex reports no dollar cost, so this run has token and time numbers only.
|
|
89
|
+
|
|
90
|
+
The run data is in [results/continuity-codex-1](results/continuity-codex-1/).
|
|
91
|
+
|
|
42
92
|
## Why less text matters
|
|
43
93
|
|
|
44
94
|
Memory results share the agent's context with code, diffs, and tool output, and agents search several times per task. About 6k characters per search is roughly 1.5k tokens; whole sessions would be about 16k. Fewer tokens make each turn cheaper and faster, and keep the relevant lines from being buried. Less text is only useful when the answer is still in it, which is why we report answer accuracy next to text size.
|
|
@@ -56,4 +106,6 @@ Memory results share the agent's context with code, diffs, and tool output, and
|
|
|
56
106
|
|
|
57
107
|
`npm run eval:lagoon` runs the same kind of check, read-only, against your real Lagoon. It reads cases from `<Lagoon root>/evals/retrieval-cases.json`: a project, a query, and the event ids that answer it. An empty list means no memory should be returned. Keep the cases in Lagoon, not in this repo, because they quote private work.
|
|
58
108
|
|
|
109
|
+
`npm run eval:continuity -- --systems cc-native,cc-hippocamp --run <name>` (or `codex-native,codex-hippocamp`, which needs a Codex ChatGPT sign-in) reruns the work continuity benchmark with your Claude subscription (`CLAUDE_CODE_OAUTH_TOKEN` in `.env`); `--report <name>` prints the tables, and `--dry-run` shows the cost estimate. A run stops cleanly at a usage limit and resumes where it stopped.
|
|
110
|
+
|
|
59
111
|
`npm run eval:qa` measures answer accuracy on the same LongMemEval-S data with the official LongMemEval reader and grader prompts. It compares Hippocamp top 5 (`hippocamp`), top 10 (`hippocamp-k10`), and the labeled answer sessions (`oracle`). It calls an OpenAI-compatible endpoint from `MANIFEST_BASE_URL` and `MANIFEST_API_KEY`; set `QA_READER_MODEL` and `QA_GRADER_MODEL` to change models. Results are cached in `.context/qa-results.jsonl`, and `--dry-run` shows the jobs and token estimates without any API call.
|
package/docs/setup.md
CHANGED
|
@@ -16,7 +16,7 @@ npx hippocamp@latest install-codex
|
|
|
16
16
|
npx hippocamp@latest install-grok
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Upgrade with the same command: `npx hippocamp@latest upgrade`. Installers default to `~/.lagoon` as the memory repo; pass `--global-root /absolute/path/to/lagoon` to use another clone.
|
|
19
|
+
Upgrade with the same command: `npx hippocamp@latest upgrade`. When older events need it, an upgrade also brings them to the current keyword format, as one local Lagoon commit. Event files with uncommitted changes are left for a later upgrade. Restart your agents afterwards to load the new version. Installers default to `~/.lagoon` as the memory repo; pass `--global-root /absolute/path/to/lagoon` to use another clone.
|
|
20
20
|
|
|
21
21
|
Installed through npx, the installer copies the package to `~/.hippocamp/<version>/` and registers that copy, so the server starts without npx or network and keeps working if npm cleans its cache. From a source checkout, `npm install` then `npm run install:claude` (or `install:codex`, `install:grok`) registers the checkout directly.
|
|
22
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hippocamp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local Git-backed memory for AI agents.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"dream": "node scripts/hippocamp-dream.cjs",
|
|
32
32
|
"dream:railway": "bash assets/railway/run-dream.sh",
|
|
33
|
+
"eval:continuity": "node --env-file-if-exists=.env scripts/eval-continuity.cjs",
|
|
33
34
|
"eval:lagoon": "node scripts/eval-lagoon.cjs",
|
|
34
35
|
"eval:qa": "node scripts/eval-qa.cjs",
|
|
35
36
|
"eval:retrieval": "node scripts/eval-retrieval.cjs",
|
|
@@ -758,6 +758,72 @@ function parseEventBlocks(content) {
|
|
|
758
758
|
});
|
|
759
759
|
}
|
|
760
760
|
|
|
761
|
+
// Older events used a "Cues:" header, inline comma lists, or plain bullets. Rewrite them in the
|
|
762
|
+
// format append_event writes now: a "Keywords:" header and one [[wiki link]] bullet per keyword.
|
|
763
|
+
// Only clear keyword bullets change: short, no spaces or backticks, before the first other line.
|
|
764
|
+
function linkKeywordsInEventContent(content) {
|
|
765
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
766
|
+
const lines = content.split(/\r?\n/);
|
|
767
|
+
const output = [];
|
|
768
|
+
let inKeywords = false;
|
|
769
|
+
|
|
770
|
+
for (const line of lines) {
|
|
771
|
+
const header = line.match(/^(\s*)(?:Keywords|Cues):\s*(.*?)\s*$/i);
|
|
772
|
+
|
|
773
|
+
if (header) {
|
|
774
|
+
const inline = normalizeKeywordList(splitInlineKeywordValues(header[2]));
|
|
775
|
+
output.push(`${header[1]}Keywords:`, ...inline.map((keyword) => `${header[1]}- [[${keyword}]]`));
|
|
776
|
+
inKeywords = true;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const bullet = inKeywords && line.match(/^(\s*-\s+)(.+?)\s*$/);
|
|
781
|
+
|
|
782
|
+
if (!bullet) {
|
|
783
|
+
inKeywords = false;
|
|
784
|
+
output.push(line);
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const value = bullet[2];
|
|
789
|
+
const keyword = normalizeKeyword(value);
|
|
790
|
+
const plain = !value.startsWith("[[") && value.length <= 60 && !/[\s`]/.test(value);
|
|
791
|
+
output.push(plain && keyword ? `${bullet[1]}[[${keyword}]]` : line);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const linked = output.join(eol);
|
|
795
|
+
const sameKeywords = (text) => JSON.stringify(parseEventBlocks(text).map((event) => event.keywords));
|
|
796
|
+
|
|
797
|
+
// Never change what search reads: keep the file as it is if any keyword would differ.
|
|
798
|
+
return linked !== content && sameKeywords(linked) === sameKeywords(content) ? linked : content;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Link keywords in every event file under the Lagoon root, except paths `skip` returns true for.
|
|
802
|
+
// Reads every file before writing any. Returns the changed paths, relative to the root.
|
|
803
|
+
async function linkEventKeywords(globalRoot, skip = () => false) {
|
|
804
|
+
const rewrites = [];
|
|
805
|
+
|
|
806
|
+
for (const relativePath of await walkMarkdownFiles(globalRoot)) {
|
|
807
|
+
if (!/(?:^|\/)events\/\d{4}-\d{2}-\d{2}\.md$/.test(relativePath) || skip(relativePath)) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const absolutePath = path.join(globalRoot, relativePath);
|
|
812
|
+
const content = await fs.readFile(absolutePath, "utf8");
|
|
813
|
+
const linked = linkKeywordsInEventContent(content);
|
|
814
|
+
|
|
815
|
+
if (linked !== content) {
|
|
816
|
+
rewrites.push({ relativePath, absolutePath, linked });
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
for (const rewrite of rewrites) {
|
|
821
|
+
await fs.writeFile(rewrite.absolutePath, rewrite.linked);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return rewrites.map((rewrite) => rewrite.relativePath);
|
|
825
|
+
}
|
|
826
|
+
|
|
761
827
|
function buildEventIndex(markdownContent, markdownPath) {
|
|
762
828
|
return {
|
|
763
829
|
version: EVENT_INDEX_VERSION,
|
|
@@ -1643,6 +1709,7 @@ module.exports = {
|
|
|
1643
1709
|
readMemoryFile,
|
|
1644
1710
|
writeMemoryFile,
|
|
1645
1711
|
appendEvent,
|
|
1712
|
+
linkEventKeywords,
|
|
1646
1713
|
listDirectoryEntries,
|
|
1647
1714
|
searchMemory,
|
|
1648
1715
|
readRecentEvents,
|
|
@@ -273,7 +273,11 @@ async function main() {
|
|
|
273
273
|
printHints(lagoon);
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
if (require.main === module) {
|
|
277
|
+
main().catch((error) => {
|
|
278
|
+
console.error("Failed to install Hippocamp into Claude Code:", error.message);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { buildClaudeManagedBlock, ensureClaudeInstructionsInstalled, ensureClaudeSkillInstalled };
|
|
@@ -4,6 +4,7 @@ const os = require("node:os");
|
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { execFile } = require("node:child_process");
|
|
6
6
|
const { promisify } = require("node:util");
|
|
7
|
+
const { linkEventKeywords } = require("./hippocamp-memory.cjs");
|
|
7
8
|
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
|
|
@@ -43,6 +44,7 @@ async function git(args, cwd) {
|
|
|
43
44
|
|
|
44
45
|
// Make sure the Lagoon memory repo exists, and say what is still missing for sync.
|
|
45
46
|
// It never creates a remote or pushes: those reach outside this machine.
|
|
47
|
+
// It may commit a local format upgrade of older events.
|
|
46
48
|
async function prepareLagoon(globalRoot) {
|
|
47
49
|
const hints = [];
|
|
48
50
|
let created = false;
|
|
@@ -75,6 +77,33 @@ async function prepareLagoon(globalRoot) {
|
|
|
75
77
|
hints.push('Set a Git identity so memory can be committed: git config --global user.name "Your Name" && git config --global user.email you@example.com');
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
// Upgrades bring older events to the current keyword format: [[wiki links]] for graph views.
|
|
81
|
+
// Best effort, committed locally only; the next memory sync pushes it. Files with uncommitted
|
|
82
|
+
// or untracked changes are left alone, and a failed commit is undone so the next install retries.
|
|
83
|
+
let linked = [];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const changes = await Promise.all([
|
|
87
|
+
git(["ls-files", "-z", "--modified", "--others", "--exclude-standard"], globalRoot),
|
|
88
|
+
git(["diff", "--cached", "--name-only", "-z"], globalRoot),
|
|
89
|
+
]);
|
|
90
|
+
const known = changes.every((change) => change.ok);
|
|
91
|
+
const dirty = new Set(changes.flatMap((change) => change.stdout.split("\0").filter(Boolean)));
|
|
92
|
+
linked = await linkEventKeywords(globalRoot, (relativePath) => !known || dirty.has(relativePath));
|
|
93
|
+
|
|
94
|
+
if (linked.length) {
|
|
95
|
+
const commit = await git(["commit", "-m", "hippocamp: link event keywords", "--", ...linked], globalRoot);
|
|
96
|
+
|
|
97
|
+
if (!commit.ok) {
|
|
98
|
+
await git(["checkout", "--", ...linked], globalRoot);
|
|
99
|
+
linked = [];
|
|
100
|
+
hints.push("Older event keywords were not linked because Git could not commit. Run the upgrade again once Git is set up.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
hints.push(`Older event keywords were not linked: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
const remote = await git(["remote", "get-url", "origin"], globalRoot);
|
|
79
108
|
let push = "no_remote";
|
|
80
109
|
|
|
@@ -88,7 +117,7 @@ async function prepareLagoon(globalRoot) {
|
|
|
88
117
|
}
|
|
89
118
|
}
|
|
90
119
|
|
|
91
|
-
return { root: globalRoot, created, remote: remote.ok ? remote.stdout : null, push, hints };
|
|
120
|
+
return { root: globalRoot, created, remote: remote.ok ? remote.stdout : null, push, linkedEvents: linked.length, hints };
|
|
92
121
|
}
|
|
93
122
|
|
|
94
123
|
function printHints(lagoon) {
|
package/scripts/eval-lagoon.cjs
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fsSync = require("node:fs");
|
|
4
|
-
const fs = require("node:fs/promises");
|
|
5
|
-
const os = require("node:os");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
const memory = require("./hippocamp-memory.cjs");
|
|
8
|
-
|
|
9
|
-
const MAX_TOP_K = 20;
|
|
10
|
-
const DEFAULT_CASES = "evals/retrieval-cases.json";
|
|
11
|
-
|
|
12
|
-
function printHelp() {
|
|
13
|
-
console.log(`Hippocamp Lagoon eval
|
|
14
|
-
|
|
15
|
-
Read-only retrieval check against your real Lagoon. No model or API key is used.
|
|
16
|
-
Each case names a project, a query, and the event ids that answer it.
|
|
17
|
-
An empty expect list means no memory should be returned.
|
|
18
|
-
|
|
19
|
-
Usage:
|
|
20
|
-
npm run eval:lagoon -- [--cases PATH] [--top-k K]
|
|
21
|
-
|
|
22
|
-
Options:
|
|
23
|
-
--cases PATH Cases JSON. Default: <Lagoon root>/${DEFAULT_CASES}
|
|
24
|
-
--top-k K Results retrieved per query, at most ${MAX_TOP_K}. Default: 5
|
|
25
|
-
--help Show this help
|
|
26
|
-
|
|
27
|
-
Cases file:
|
|
28
|
-
{ "cases": [ { "project": "<slug>", "query": "...", "expect": ["<event id>"] } ] }
|
|
29
|
-
`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseArgs(argv) {
|
|
33
|
-
const options = { cases: path.join(memory.getGlobalRoot(), DEFAULT_CASES), topK: 5, help: false };
|
|
34
|
-
|
|
35
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
36
|
-
const token = argv[index];
|
|
37
|
-
|
|
38
|
-
if (token === "--help" || token === "-h") {
|
|
39
|
-
options.help = true;
|
|
40
|
-
} else if (token === "--cases") {
|
|
41
|
-
options.cases = argv[++index];
|
|
42
|
-
} else if (token === "--top-k") {
|
|
43
|
-
const number = Number(argv[++index]);
|
|
44
|
-
|
|
45
|
-
if (!Number.isInteger(number) || number < 1 || number > MAX_TOP_K) {
|
|
46
|
-
throw new Error(`--top-k must be an integer from 1 to ${MAX_TOP_K}.`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
options.topK = number;
|
|
50
|
-
} else {
|
|
51
|
-
throw new Error(`Unknown argument: ${token}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return options;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function run(options) {
|
|
59
|
-
if (!fsSync.existsSync(options.cases)) {
|
|
60
|
-
throw new Error(`Missing ${options.cases}. Run with --help for the file format.`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const { cases } = JSON.parse(await fs.readFile(options.cases, "utf8"));
|
|
64
|
-
// searchMemory derives the project slug from the folder name, so a temp path per
|
|
65
|
-
// slug reads that project's memory without touching any real repo. The folder is
|
|
66
|
-
// not created: an existing empty folder has no project and reads global memory.
|
|
67
|
-
const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-eval-lagoon-"));
|
|
68
|
-
const answerable = { count: 0, hit: 0, mrr: 0, chars: 0, topScores: [] };
|
|
69
|
-
const empty = { count: 0, correct: 0, topScores: [] };
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
for (const item of cases) {
|
|
73
|
-
if (!/^[a-z0-9][a-z0-9._-]*$/.test(String(item.project))) {
|
|
74
|
-
throw new Error(`Invalid project slug: ${item.project}`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const projectRoot = path.join(sandboxRoot, item.project);
|
|
78
|
-
|
|
79
|
-
const search = await memory.searchMemory({
|
|
80
|
-
query: item.query,
|
|
81
|
-
scope: "project",
|
|
82
|
-
projectRoot,
|
|
83
|
-
maxResults: options.topK,
|
|
84
|
-
});
|
|
85
|
-
const topScore = search.results[0]?.score ?? 0;
|
|
86
|
-
|
|
87
|
-
if (!item.expect.length) {
|
|
88
|
-
empty.count += 1;
|
|
89
|
-
empty.correct += search.results.length ? 0 : 1;
|
|
90
|
-
empty.topScores.push(topScore);
|
|
91
|
-
console.log(`${search.results.length ? "MISS" : "ok "} empty top ${topScore} [${item.project}] ${item.query}`);
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const rank = search.results.findIndex((result) => item.expect.includes(result.id));
|
|
96
|
-
answerable.count += 1;
|
|
97
|
-
answerable.hit += rank === -1 ? 0 : 1;
|
|
98
|
-
answerable.mrr += rank === -1 ? 0 : 1 / (rank + 1);
|
|
99
|
-
answerable.chars += search.results.reduce((sum, result) => sum + result.snippet.length, 0);
|
|
100
|
-
answerable.topScores.push(topScore);
|
|
101
|
-
console.log(
|
|
102
|
-
`${rank === -1 ? "MISS" : "ok "} rank ${rank === -1 ? "-" : rank + 1} top ${topScore} [${item.project}] ${item.query}`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
} finally {
|
|
106
|
-
await fs.rm(sandboxRoot, { recursive: true, force: true });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const percent = (part, whole) => `${((part / Math.max(whole, 1)) * 100).toFixed(1)}%`;
|
|
110
|
-
const range = (scores) => (scores.length ? `${Math.min(...scores)}-${Math.max(...scores)}` : "n/a");
|
|
111
|
-
|
|
112
|
-
console.log(`\nLagoon retrieval, K=${options.topK}, ${cases.length} cases`);
|
|
113
|
-
console.log(
|
|
114
|
-
`answerable (${answerable.count}) Hit@K ${percent(answerable.hit, answerable.count)} MRR ${(answerable.mrr / Math.max(answerable.count, 1)).toFixed(3)} context ${Math.round(answerable.chars / Math.max(answerable.count, 1))} chars top score ${range(answerable.topScores)}`,
|
|
115
|
-
);
|
|
116
|
-
console.log(
|
|
117
|
-
`no memory (${empty.count}) empty result ${percent(empty.correct, empty.count)} top score ${range(empty.topScores)}`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function main() {
|
|
122
|
-
const options = parseArgs(process.argv.slice(2));
|
|
123
|
-
|
|
124
|
-
if (options.help) {
|
|
125
|
-
printHelp();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
await run(options);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
main().catch((error) => {
|
|
133
|
-
console.error(error.message);
|
|
134
|
-
process.exit(1);
|
|
135
|
-
});
|