hippo-memory 1.61.0 → 1.62.0

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.
Files changed (57) hide show
  1. package/README.md +28 -53
  2. package/dist/agent-memories/apply.d.ts +1 -1
  3. package/dist/agent-memories/claude-code.d.ts +3 -1
  4. package/dist/agent-memories/claude-code.js +53 -9
  5. package/dist/agent-memories/sync.d.ts +3 -3
  6. package/dist/agent-memories/sync.js +14 -6
  7. package/dist/agent-memories/types.d.ts +0 -2
  8. package/dist/api/assemble.js +55 -54
  9. package/dist/api/context-select.d.ts +49 -0
  10. package/dist/api/context-select.js +344 -0
  11. package/dist/api/context.d.ts +2 -2
  12. package/dist/api/context.js +195 -522
  13. package/dist/api/drill-down.js +36 -33
  14. package/dist/api/promote.js +55 -66
  15. package/dist/api/recall.js +303 -438
  16. package/dist/api/sleep.js +203 -218
  17. package/dist/capture/compact.d.ts +1 -1
  18. package/dist/capture/compact.js +2 -2
  19. package/dist/cli/briefs.js +324 -306
  20. package/dist/cli/context.js +44 -34
  21. package/dist/cli/continuity.js +283 -271
  22. package/dist/cli/curate.js +35 -34
  23. package/dist/cli/decisions.js +333 -333
  24. package/dist/cli/explain.js +66 -60
  25. package/dist/cli/maintenance.js +62 -51
  26. package/dist/cli/playbooks.js +387 -370
  27. package/dist/cli/projects.js +8 -5
  28. package/dist/cli/recall.js +28 -43
  29. package/dist/cli/remember.js +113 -70
  30. package/dist/cli/session-hooks.js +100 -90
  31. package/dist/cli/setup.js +267 -246
  32. package/dist/cli/status.js +73 -64
  33. package/dist/cli/transfer.js +85 -99
  34. package/dist/compaction-record.d.ts +0 -2
  35. package/dist/compaction-record.js +1 -1
  36. package/dist/customer-notes.js +77 -68
  37. package/dist/dag.js +222 -186
  38. package/dist/decisions.js +93 -76
  39. package/dist/doctor.js +11 -7
  40. package/dist/goals.js +99 -86
  41. package/dist/incidents.js +45 -38
  42. package/dist/policies.js +85 -68
  43. package/dist/processes.js +87 -71
  44. package/dist/project-briefs.js +135 -108
  45. package/dist/project-merge.d.ts +12 -4
  46. package/dist/project-merge.js +130 -45
  47. package/dist/shared.d.ts +9 -0
  48. package/dist/shared.js +10 -8
  49. package/dist/skills.js +81 -65
  50. package/dist/store/search-rows.d.ts +2 -2
  51. package/dist/store/search-rows.js +15 -9
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  55. package/extensions/openclaw-plugin/package.json +1 -1
  56. package/openclaw.plugin.json +1 -1
  57. package/package.json +1 -1
package/README.md CHANGED
@@ -22,7 +22,7 @@ npm install -g hippo-memory && hippo init
22
22
 
23
23
  Setting up every git repo under a folder in one go is a second step. The [Quick start](#quick-start) says what it changes, then gives the command.
24
24
 
25
- Package installation alone does not enable automatic preservation on every agent. Complete the documented setup and required host trust; capture and compaction coverage depend on the integration. See [automatic-save coverage](#does-installation-automatically-save-before-compaction).
25
+ Package installation alone does not enable automatic preservation on every agent. Complete the documented setup and required host trust; capture and compaction coverage depend on the integration.
26
26
 
27
27
  Having an AI agent install it? Point it at [llms-install.md](llms-install.md): it installs, wires hippo into the agents it finds, and verifies with `hippo doctor`.
28
28
 
@@ -544,7 +544,7 @@ database to `.hippo/backups/` first and logs every id it touched in the audit lo
544
544
  ```bash
545
545
  hippo projects --global # names, counts, live worktrees of this repo
546
546
  hippo projects merge hippo-wt-fix hippo --global # fold an old worktree name into its repo
547
- hippo projects repair --global # re-tag user-global merges by their parents
547
+ hippo projects repair --global # set aside note copies, fold old worktree names, re-tag merges
548
548
  ```
549
549
 
550
550
  **See what memory costs in tokens.** Every block of memory text hippo hands an agent (the
@@ -936,53 +936,36 @@ For how these mechanisms connect to LLM training, continual learning, and open r
936
936
 
937
937
  ## Comparison
938
938
 
939
- Two tables. The first is plain facts: where the data lives, what each tool needs and runs on, and what it has published. Several rows favour the other tools: a managed multi-user service, a native Python library, a full knowledge graph. The second lists design bets, choices a tool made rather than results it measured; what hippo has measured is under [Benchmarks](#benchmarks). Graph-first systems ([gbrain](https://hermesatlas.com/projects/garrytan/gbrain), [Zep](https://www.getzep.com/), [Cognee](https://www.cognee.ai/)), agent-managed systems ([Letta](https://github.com/letta-ai/letta-code)), and version-control or skill-distillation takes ([Memoria](https://github.com/matrixorigin/Memoria), [EverMind](https://evermind.ai/)) solve adjacent problems with different mechanics.
940
-
941
- ### What each tool is
942
-
943
- The rows from where the data lives to the graph were checked against each tool's own pages on 2026-09-28.
939
+ The AI-memory category matured fast in 2026. Hippo's specific take (bio-decay, strengthen-on-use, outcome-weighted half-lives) is one stance among several. The table below is a feature snapshot, not a verdict: graph-first systems ([gbrain](https://hermesatlas.com/projects/garrytan/gbrain), [Zep](https://www.getzep.com/), [Cognee](https://www.cognee.ai/)), agent-managed systems ([Letta](https://github.com/letta-ai/letta-code)), and version-control / skill-distillation takes ([Memoria](https://github.com/matrixorigin/Memoria), [EverMind](https://evermind.ai/)) all solve adjacent problems with different mechanics.
944
940
 
945
941
  | Feature | Hippo | [MemPalace](https://github.com/milla-jovovich/mempalace) | [Mem0](https://github.com/mem0ai/mem0) | [Basic Memory](https://github.com/basicmachines-co/basic-memory) | [gbrain](https://hermesatlas.com/projects/garrytan/gbrain) | [Zep](https://www.getzep.com/) | [Letta](https://github.com/letta-ai/letta-code) | [Cognee](https://www.cognee.ai/) | [Memoria](https://github.com/matrixorigin/Memoria) | [EverMind](https://evermind.ai/) |
946
942
  |---------|-------|-----------|------|-------------|--------|-----|-------|--------|---------|----------|
947
- | Where your data lives | Your machine or your server (SQLite) | Your machine (ChromaDB by default) | Where you run it, or Mem0's cloud | Your machine (Markdown files); cloud optional | Your machine (PGLite) or your Postgres | Zep's cloud (your own cloud on Enterprise) | Your machine; cloud backup with /login | Your machine by default; Cognee Cloud optional | Memoria Cloud, or self-hosted (Docker or embedded) | Your machine by default; EverOS Cloud optional |
948
- | Managed multi-user service | No (self-hosted, with tenants and API keys; hosted SaaS is planned for the commercial edition) | ? | Yes (hosted platform) | Yes (Teams) | ? (self-hosted server with OAuth) | Yes (Zep Cloud) | ? (cloud backup with /login) | Yes (Cognee Cloud) | ? (Memoria Cloud) | Yes (EverOS Cloud) |
949
- | Needs an account or model key | No | No (core path) | Yes (model key; account for the platform) | No (account for the cloud) | No (keyless mode) | Yes (Zep account; Graphiti needs a model key) | Yes (your own model keys) | No (local) | ? (account for Memoria Cloud) | Yes (an LLM key, OpenRouter) |
950
- | License | MIT | MIT | Apache-2.0 | AGPL-3.0 | MIT | Proprietary cloud (Graphiti: Apache-2.0) | Apache-2.0 | Apache-2.0 | Apache-2.0 | Apache-2.0 (EverOS) + cloud |
951
- | Runtime | Node.js 22.16+, no runtime deps | Python 3.9+ (ChromaDB by default) | Python or Node.js (server: Postgres + pgvector) | Python (SQLite by default) | Bun (PGLite or Postgres + pgvector) | Managed service (Graphiti: Python + a graph database) | Node.js (npm) | Python (graph and vector stores, or Postgres) | A CLI binary + a MatrixOne database | Python (SQLite + LanceDB) |
952
- | SDKs and APIs | CLI, HTTP API, Python SDK over HTTP | Python library, CLI | Python and Node.js libraries, a self-hosted server | CLI, cloud app | CLI, HTTP API | Python, TypeScript and Go SDKs | TypeScript SDK, CLI | Python and TypeScript SDKs, REST API, CLI | Python client, REST API, CLI | Python library, HTTP API, CLI |
953
- | Native Python library | No (the Python SDK calls hippo serve over HTTP) | Yes (mempalace) | Yes (mem0ai) | Yes (basic-memory) | No (installs with Bun) | Yes (Graphiti: graphiti-core) | ? (Letta Code ships on npm) | Yes (cognee) | ? (memoria-client) | Yes (everos) |
954
- | Graph or entity relations | Partial (entities from decisions and policies; recall --hops, off by default) | Yes (temporal entity graph) | Partial (entity linking; graph memory on Pro) | Yes (wikilinks and observations) | Yes (typed knowledge graph) | Yes (temporal knowledge graph) | No | Yes (knowledge graph) | No (typed claims) | ? (graph view in progress) |
955
- | Hybrid search (BM25 + embeddings) | Yes (BM25 by default; embeddings are an optional install) | Embeddings + spatial | Yes (semantic + BM25 + entity) | No | Yes (vec + rerank + graph) | Yes (graph + vec) | ? | Yes (GraphRAG) | Yes (vector + full-text) | Yes (mRAG, multi-modal) |
956
- | MCP server | Yes | Yes | Yes (hosted, needs an account) | Yes | Yes (stdio + HTTP/OAuth) | Yes (hosted, needs an account) | Yes (hosted, needs an API key) | Yes (first-party Claude/LangGraph) | Yes | ? |
957
- | Multi-agent shared memory | Yes | No | No | No | Yes (brain repo, team mounts) | Yes | Yes (shared memory blocks) | Yes | Yes (branch/merge across sessions) | Yes (multi-agent coordination) |
958
- | Auto-hook install | Yes (Claude Code hooks, OpenCode plugin) | No | No | No | No | No | No | No | No | No |
959
- | Cross-tool import (ChatGPT/Claude/Cursor) | Yes | No | No | No | Partial (data sources) | ? | No | Partial (28 data sources) | No (Git ops) | Partial (mRAG: PDFs/images/URLs) |
960
- | Git-friendly | Yes | No | No | Yes | Yes | No | Yes (memory tracked in git) | No | Yes (Git is the model) | ? |
961
- | Framework agnostic | Yes | Yes | Partial | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
962
- | LongMemEval (published) | 85.6% R@5 from hippo recall, 96.8% with the budget lifted (4,000-token default budget; the benchmark scripts' best of five settings: 98.0% local / 99.8% voyage any-evidence, 88.5% local all-evidence; s_cleaned, per-haystack)\* | 96.6% raw / 100% reranked R@5 | 94.4 (hosted platform)\*\* | N/A | 95.53% all-evidence R@5 reranked, 93.19% without (s_cleaned\*) | 90.2% accuracy\*\* (LoCoMo 94.7%) | N/A | N/A | 88.78% overall accuracy w/ reader\*\* | 83.00% overall\*\* (LoCoMo 93.05%, HaluMem 93.04%) |
963
-
964
- \* Hippo's figures are on `longmemeval_s_cleaned` with a per-question haystack. On a default install, `hippo recall` puts an answer session in its top 5 for 85.6% of the 500 questions inside its default 4,000-token budget. With the budget lifted it scores 96.8%, but then it returns every candidate, so that figure measures the ranking ([result](docs/evals/2026-09-28-recall-cli-longmemeval-result.md)). The 98.0% and 99.8% are the benchmark scripts' retrieval, each the best of five settings, not `hippo recall`. Any-evidence R@5 counts a hit when any answer session is in the top 5, over all 500 questions: 98.0% with the free local MiniLM embedder (an optional install) and 99.8% with voyage-3-large (measured 2026-06-09, not re-run). All-evidence R@5 counts a hit only when every answer session is in the top 5, over the 470 questions that have an answer: 86.8 to 88.5% with MiniLM. gbrain first published 97.6%, an any-evidence score over all 500; its [report](https://github.com/garrytan/gbrain-evals/blob/main/docs/benchmarks/2026-05-07-longmemeval-s.md) now leads with all-evidence, 95.53% (449 of 470) with the paid Voyage rerank-2.5 reranker and 93.19% without it. On all-evidence recall gbrain is ahead. The June 2026 build scored 98.6 any-evidence; [`docs/evals/2026-09-23-longmemeval-reproduction.md`](docs/evals/2026-09-23-longmemeval-reproduction.md) has both runs. An older hippo number, 86.8% R@5 on `longmemeval_oracle` under pooled (non-per-haystack) retrieval, is not comparable to per-haystack figures.
965
-
966
- \*\* Different metric: these are end-to-end answer scores, not retrieval R@5. Mem0's 94.4 comes from its hosted platform, which its README says includes optimizations the open-source SDK lacks. Zep's 90.2% and 94.7% are accuracy figures from its homepage. Memoria's 88.78% and EverMind's 83% are overall accuracy with a reader LLM. Higher denominator + LLM helps. Not directly comparable to retrieval-only R@5 numbers above. The Mem0, Zep and Letta columns were last checked against each vendor's own pages on 2026-09-28.
967
-
968
- ### Design bets
969
-
970
- A Yes means the tool made that choice, not that the choice was shown to help. What hippo has measured about its own bets is under [Benchmarks](#benchmarks); decay, for one, tied with decay switched off. Spatial organization and lossless compression are MemPalace's bets.
971
-
972
- | Design bet | Hippo | [MemPalace](https://github.com/milla-jovovich/mempalace) | [Mem0](https://github.com/mem0ai/mem0) | [Basic Memory](https://github.com/basicmachines-co/basic-memory) | [gbrain](https://hermesatlas.com/projects/garrytan/gbrain) | [Zep](https://www.getzep.com/) | [Letta](https://github.com/letta-ai/letta-code) | [Cognee](https://www.cognee.ai/) | [Memoria](https://github.com/matrixorigin/Memoria) | [EverMind](https://evermind.ai/) |
973
- |---------|-------|-----------|------|-------------|--------|-----|-------|--------|---------|----------|
974
943
  | Decay by default | Yes | No | No | No | No | No | No | No | No | No |
975
944
  | Retrieval strengthening | Yes | No | No | No | No | No | No | Partial (recall tuning) | No | Partial (Skill Memory distills patterns) |
976
945
  | Reward-proportional decay | Yes | No | No | No | No | No | No | No | No | No |
977
- | Outcome tracking | Yes | No | No | No | No | No | No | No | No | Partial (Cases: agent trajectories) |
946
+ | Hybrid search (BM25 + embeddings) | Yes | Embeddings + spatial | Yes (semantic + BM25 + entity) | No | Yes (vec + rerank + graph) | Yes (graph + vec) | ? | Yes (GraphRAG) | Yes (vector + full-text) | Yes (mRAG, multi-modal) |
947
+ | Schema acceleration / knowledge graph | Yes (schema) | No | Partial (entity linking; graph memory on Pro) | No | Yes (typed KG, self-wiring) | Yes (temporal KG) | No | Yes (auto-ontologies) | No (typed claims) | Yes (hierarchical: user/group/agent) |
978
948
  | Conflict detection + resolution | Yes | No | Partial (hosted platform marks superseded facts) | No | Yes (eval-surfaced) | Yes (auto-invalidate stale facts) | No | No | Yes (auto-detect + quarantine) | Partial (temporal tracking) |
949
+ | Multi-agent shared memory | Yes | No | No | No | Yes (brain repo, team mounts) | Yes | Yes (shared memory blocks) | Yes | Yes (branch/merge across sessions) | Yes (multi-agent coordination) |
979
950
  | Transfer scoring | Yes | No | No | No | No | No | No | No | No | No |
951
+ | Outcome tracking | Yes | No | No | No | No | No | No | No | No | Partial (Cases: agent trajectories) |
980
952
  | Confidence tiers | Yes | No | No | No | No (typed facts) | No | No | No | No | No |
981
- | Schema acceleration | Yes | No | No | No | No | No | No | No | No | No |
982
953
  | Spatial organization | No | Yes (wings/halls/rooms) | No | No | No | No | No | No | No | No |
983
954
  | Lossless compression | No | Yes (AAAK, 30x) | No | No | No | No | No | No | No | No |
955
+ | Cross-tool import (ChatGPT/Claude/Cursor) | Yes | No | No | No | Partial (data sources) | ? | No | Partial (28 data sources) | No (Git ops) | Partial (mRAG: PDFs/images/URLs) |
956
+ | Auto-hook install | Yes | No | No | No | No | No | No | No | No | No |
957
+ | MCP server | Yes | Yes | Yes (hosted, needs an account) | Yes | Yes (stdio + HTTP/OAuth) | Yes (hosted, needs an account) | Yes (hosted, needs an API key) | Yes (first-party Claude/LangGraph) | Yes | ? |
958
+ | Zero runtime deps | Yes | No (ChromaDB) | No | No | No (PGLite or PG+pgvector) | No (managed service) | No (npm deps) | No (Python deps) | Yes (single Rust binary) | No (managed + OSS) |
959
+ | LongMemEval (best published) | 98.0% local / 99.8% voyage any-evidence R@5; 88.5% local all-evidence R@5 (s_cleaned, per-haystack)\* | 96.6% raw / 100% reranked R@5 | 94.4 (hosted platform)\*\* | N/A | 95.53% all-evidence R@5 reranked, 93.19% without (s_cleaned\*) | 90.2% accuracy\*\* (LoCoMo 94.7%) | N/A | N/A | 88.78% overall accuracy w/ reader\*\* | 83.00% overall\*\* (LoCoMo 93.05%, HaluMem 93.04%) |
960
+ | Git-friendly | Yes | No | No | Yes | Yes | No | Yes (memory tracked in git) | No | Yes (Git is the model) | ? |
961
+ | Framework agnostic | Yes | Yes | Partial | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
962
+ | License | MIT | (open) | Apache-2.0 | (open) | MIT | Proprietary cloud (Graphiti: Apache-2.0) | Apache-2.0 | MIT (core) | Apache-2.0 | Apache-2.0 (OSS) + cloud |
984
963
 
985
- Different tools answer different questions. Mem0 and Basic Memory implement "save everything, search later." MemPalace implements "store everything, organize spatially for retrieval." gbrain, Zep, and Cognee implement "extract typed entities and relationships into a knowledge graph." Letta implements "the agent edits its own memory blocks." Memoria implements "Git-style version control over the memory state itself." EverMind implements "self-evolving Skill Memory + multi-modal retrieval over hierarchical scopes." Hippo implements "learn what is wrong and rank it down." These are complementary takes, not a single-axis ranking: memory lifecycle (Hippo) + GraphRAG (gbrain/Cognee/Zep) + agent-self-edit (Letta) + memory-VCS (Memoria) + skill-distillation (EverMind) cover different parts of the same problem.
964
+ \* Hippo's figures are on `longmemeval_s_cleaned` with a per-question haystack, each the best of five retrieval settings in the benchmark scripts, not `hippo recall`. Any-evidence R@5 counts a hit when any answer session is in the top 5, over all 500 questions: 98.0% with the free local MiniLM embedder (an optional install) and 99.8% with voyage-3-large (measured 2026-06-09, not re-run). All-evidence R@5 counts a hit only when every answer session is in the top 5, over the 470 questions that have an answer: 86.8 to 88.5% with MiniLM. gbrain first published 97.6%, an any-evidence score over all 500; its [report](https://github.com/garrytan/gbrain-evals/blob/main/docs/benchmarks/2026-05-07-longmemeval-s.md) now leads with all-evidence, 95.53% (449 of 470) with the paid Voyage rerank-2.5 reranker and 93.19% without it. On all-evidence recall gbrain is ahead. The June 2026 build scored 98.6 any-evidence; [`docs/evals/2026-09-23-longmemeval-reproduction.md`](docs/evals/2026-09-23-longmemeval-reproduction.md) has both runs. An older hippo number, 86.8% R@5 on `longmemeval_oracle` under pooled (non-per-haystack) retrieval, is not comparable to per-haystack figures.
965
+
966
+ \*\* Different metric: these are end-to-end answer scores, not retrieval R@5. Mem0's 94.4 comes from its hosted platform, which its README says includes optimizations the open-source SDK lacks. Zep's 90.2% and 94.7% are accuracy figures from its homepage. Memoria's 88.78% and EverMind's 83% are overall accuracy with a reader LLM. Higher denominator + LLM helps. Not directly comparable to retrieval-only R@5 numbers above. The Mem0, Zep and Letta columns were last checked against each vendor's own pages on 2026-09-28.
967
+
968
+ Different tools answer different questions. Mem0 and Basic Memory implement "save everything, search later." MemPalace implements "store everything, organize spatially for retrieval." gbrain, Zep, and Cognee implement "extract typed entities and relationships into a knowledge graph." Letta implements "the agent edits its own memory blocks." Memoria implements "Git-style version control over the memory state itself." EverMind implements "self-evolving Skill Memory + multi-modal retrieval over hierarchical scopes." Hippo implements "learn what is wrong and stop repeating it." These are complementary takes, not a single-axis ranking: bio-lifecycle (Hippo) + GraphRAG (gbrain/Cognee/Zep) + agent-self-edit (Letta) + memory-VCS (Memoria) + skill-distillation (EverMind) cover different parts of the same problem.
986
969
 
987
970
  ---
988
971
 
@@ -1090,7 +1073,7 @@ node run.mjs --adapter all
1090
1073
 
1091
1074
  ### How do I give Claude Code memory between sessions?
1092
1075
 
1093
- Run `npm install -g hippo-memory`, then `hippo init` in the project. If the project has a `CLAUDE.md`, init adds a short block telling Claude to run `hippo context --auto` when a session starts. It also adds 7 hook entries to Claude Code's settings that keep your pinned memories plus up to 5 that match the prompt in context, save a task snapshot and the memories a compaction summary lists, store failed tool calls as lessons, and run `hippo sleep` when the session ends, and it sets up a daily 6:15am run. [What hippo init changes](#what-hippo-init-changes) lists everything. The [Claude Code plugin](https://github.com/kitfunso/hippo-memory/tree/master/extensions/claude-code-plugin) is the alternative to these hooks; use one, not both. To set up every git repo up to three folders below your home directory at once, know what the scan changes first: each repo gets its own store, seeded from a year of its git history, the same hooks go in when one of those repos uses Claude Code, and the daily run is set up, but no block goes into any repo's `CLAUDE.md`. The command is `hippo init --scan ~`; run `hippo init` in the projects where you want the block.
1076
+ Run `npm install -g hippo-memory`, then `hippo init` in the project. If the project has a `CLAUDE.md`, init adds a short block telling Claude to run `hippo context --auto` when a session starts. It also adds hooks to Claude Code's settings that keep your pinned memories in context, save a task snapshot before compaction, and run `hippo sleep` when the session ends. `hippo init --scan ~` gives every git repo under your home folder a store and installs the same hooks, but adds no block to any `CLAUDE.md`. The [Claude Code plugin](https://github.com/kitfunso/hippo-memory/tree/master/extensions/claude-code-plugin) is the alternative to these hooks; use one, not both.
1094
1077
 
1095
1078
  ### How do I give Cursor memory between sessions?
1096
1079
 
@@ -1098,19 +1081,11 @@ Run `npm install -g hippo-memory`, then `hippo init` in the project. If the proj
1098
1081
 
1099
1082
  ### How do I give Codex memory across sessions?
1100
1083
 
1101
- `hippo init` adds its instructions to your `AGENTS.md`, which Codex reads before it starts work. When Codex is installed, init also adds two hooks to Codex's `hooks.json`: one puts your pinned memories plus up to 5 that match the prompt into every prompt, the other makes the next prompt send them again after a compaction. Codex asks you to trust each new hook once in `/hooks`, and skips it until you do. Capturing Codex sessions is opt-in: `hippo hook install codex` wraps the Codex launcher, and `hippo hook uninstall codex` removes the wrapper and the hooks.
1084
+ `hippo init` adds its instructions to your `AGENTS.md`, which Codex reads before it starts work. Capturing Codex sessions is opt-in: `hippo hook install codex` wraps the Codex launcher, and `hippo hook uninstall codex` removes the wrapper.
1102
1085
 
1103
1086
  ### Which agents does hippo work with?
1104
1087
 
1105
- `hippo init` detects Claude Code, Codex, Cursor, OpenClaw, OpenCode and Pi. It installs hooks for Claude Code and OpenCode, adds 2 hooks to Codex's `hooks.json` when Codex is installed (Codex runs them once you trust them in `/hooks`), and adds instructions to an existing `AGENTS.md` for Codex, Cursor, OpenClaw and Pi. It only patches instruction files that already exist. Any MCP client can use the [MCP server](#mcp-server), and other tools can call the CLI or the HTTP API that `hippo serve` starts.
1106
-
1107
- ### Does installation automatically save before compaction?
1108
-
1109
- Package installation alone does not enable automatic preservation on every agent. Complete the documented setup and required host trust; capture and compaction coverage depend on the integration.
1110
-
1111
- With the Claude Code hooks or native plugin configured, PreCompact saves a derivable working-state snapshot and a compaction record; PostCompact extracts the lessons listed in the compaction summary. A snapshot, a compaction record and a useful memory are different outputs, and post-compaction extraction is not a guarantee that every earlier lesson was saved before loss. Hippo's Codex hooks currently provide prompt delivery and post-compaction re-injection, with one-time trust in `/hooks`; they install no PreCompact save hook. Codex session-end capture requires the opt-in wrapper. MCP tool access and instruction files alone do not provide automatic lifecycle capture.
1112
-
1113
- Follow the [integration recipes](https://github.com/kitfunso/hippo-memory/tree/master/integrations) for the exact agent and mode. Automatic pre-loss preservation across all supported agents is planned under AZ4-AZ6 in the [roadmap](https://github.com/kitfunso/hippo-memory/blob/master/ROADMAP.md#current-execution-index); it is not a universal install-only capability today.
1088
+ `hippo init` detects Claude Code, Codex, Cursor, OpenClaw, OpenCode and Pi, and wires itself into each one's instruction file, hooks or plugin. It only patches instruction files that already exist. Any MCP client can use the [MCP server](#mcp-server), and other tools can call the CLI or the HTTP API that `hippo serve` starts.
1114
1089
 
1115
1090
  ### Can I use hippo as an MCP memory server?
1116
1091
 
@@ -1118,7 +1093,7 @@ Yes. `hippo mcp` runs the server over stdio, and `npx -y hippo-memory mcp` runs
1118
1093
 
1119
1094
  ### How is hippo different from mem0?
1120
1095
 
1121
- mem0 uses a language model to extract memories, OpenAI by default in its open-source library, and memories stored through its hosted MCP server live in your Mem0 account ([mem0 docs](https://docs.mem0.ai/platform/mem0-mcp), checked 2026-09-28). Hippo stores memories in SQLite on your machine, needs no account and no model, and `hippo init` wires it into the coding agents it finds. mem0's platform marks an older fact superseded when a newer one replaces it; in hippo you run `hippo supersede` yourself. Hippo also lets you mark a recalled memory wrong with `hippo outcome --bad`, and it drops out of the top results.
1096
+ mem0 uses a language model to extract memories, OpenAI by default in its open-source library, and memories stored through its hosted MCP server live in your Mem0 account ([mem0 docs](https://docs.mem0.ai/platform/mem0-mcp), checked 2026-09-28). Hippo stores memories in SQLite on your machine, needs no account and no model, and `hippo init` wires it into the coding agents it finds. mem0's platform and hippo both mark an older fact superseded when a newer one replaces it. Hippo also lets you mark a recalled memory wrong with `hippo outcome --bad`, and it drops out of the top results.
1122
1097
 
1123
1098
  ### Is this just RAG?
1124
1099
 
@@ -1126,7 +1101,7 @@ No. RAG searches a fixed corpus. Hippo's store changes as your agent works: a me
1126
1101
 
1127
1102
  ### Does it need embeddings?
1128
1103
 
1129
- No. Recall runs on BM25 out of the box, with no model and no network call, and a default install has no embedder. Embeddings are an optional install for hybrid search. On LongMemEval-S, where each question gets its own haystack, `hippo recall` on a default install puts an answer session in its top five for 85.6% of questions inside its 4,000-token budget, and 87.6% with the free local MiniLM embedder; the budget, not the embedder, is most of the gap to the benchmark scripts. Those scripts, which are not `hippo recall`, fuse BM25 with MiniLM and reach 98.0% recall@5 at their best of five settings, counting a hit when any answer session is in the top five. On LongMemEval's oracle split with one pooled store, BM25 alone scored 74.0% recall@5 in v0.11. These runs use different setups, so they are not a before and after.
1104
+ No. Recall runs on BM25 out of the box, with no model and no network call, and a default install has no embedder. Embeddings are an optional install for hybrid search. On LongMemEval-S, where each question gets its own haystack, the benchmark scripts (not `hippo recall`) fuse BM25 with the free local MiniLM embedder and reach 98.0% recall@5, counting a hit when any answer session is in the top five. On LongMemEval's oracle split with one pooled store, BM25 alone scored 74.0% recall@5 in v0.11. The two runs use different setups, so they are not a before and after.
1130
1105
 
1131
1106
  ### Do I still need CLAUDE.md?
1132
1107
 
@@ -1138,11 +1113,11 @@ Mark it, and it drops out of the top results. `hippo outcome --bad` weakens the
1138
1113
 
1139
1114
  ### Where does hippo keep my data?
1140
1115
 
1141
- On your machine, in SQLite: `.hippo/hippo.db` in each project, plus a global store in `~/.hippo/` for lessons shared across projects, with markdown mirrors you can read and commit. Recall makes no network call by default. Text goes to an outside provider only through features that use one: an API embedder, the Jev, CLEF or LLM reranker, `hippo refine`, and the fact extraction `hippo sleep` runs through Anthropic's API whenever `ANTHROPIC_API_KEY` is set in its environment. To turn that last one off, set `{"extraction":{"enabled":false}}` in `.hippo/config.json`.
1116
+ On your machine, in SQLite: `.hippo/hippo.db` in each project, plus a global store in `~/.hippo/` for lessons shared across projects, with markdown mirrors you can read and commit. Recall makes no network call by default. Text goes to an outside provider only through features that use one: an API embedder, the Jev or LLM reranker, `hippo refine`, and the fact extraction `hippo sleep` runs through Anthropic's API whenever `ANTHROPIC_API_KEY` is set in its environment. To turn that last one off, set `{"extraction":{"enabled":false}}` in `.hippo/config.json`.
1142
1117
 
1143
1118
  ### What does hippo cost?
1144
1119
 
1145
- Nothing. Hippo is MIT-licensed and needs no account or API key. Optional features that call an outside provider bill through it: the Jev reranker costs about 0.0004 USD a recall, and API embedders and sleep's fact extraction bill your own keys. Memory text handed to your agent uses context tokens when it is sent, and again, usually at the cheaper cached-input rate, on each later model call until the host compacts. `hippo tokens` shows both for the hook and compact-resume blocks, and the sent tokens for the rest.
1120
+ Nothing. Hippo is MIT-licensed and needs no account or API key. Optional features that call an outside provider bill through it: the Jev reranker costs about 0.0004 USD a recall, and API embedders and sleep's fact extraction bill your own keys. Memory text handed to your agent uses context tokens, and `hippo tokens` shows how many.
1146
1121
 
1147
1122
  ### Is it production-ready?
1148
1123
 
@@ -1150,7 +1125,7 @@ Judge it by what is tested. 3,500+ tests run against a real database, with no mo
1150
1125
 
1151
1126
  ### Has hippo been shown to make agents better at their work?
1152
1127
 
1153
- No. The published numbers measure retrieval: whether the right memory comes back, and whether a memory marked wrong stays out of the results. Decay and sleep are design choices, not measured wins. In the [mechanism audit](https://github.com/kitfunso/hippo-memory/blob/master/docs/evals/2026-09-23-mechanism-audit-round2-result.md), decay had no measurable effect against decay switched off (-0.7 points [-1.4, 0.1] on a 20-session synthetic test, too short for a 365-day half-life to act), and sleep lowered LongMemEval hit@5 by 3.6 points [-5.8, -1.4] under the audit's declared scorer; no scorer there showed sleep helping recall. Every measurement, including failed runs and one retracted claim, is indexed in [docs/evals](https://github.com/kitfunso/hippo-memory/blob/master/docs/evals/README.md).
1128
+ Not yet. The published numbers measure retrieval: whether the right memory comes back, and whether a memory marked wrong stays out of the results. A paired test that runs real agent sessions with and without hippo is under way. Every measurement, including failed runs and one retracted claim, is indexed in [docs/evals](https://github.com/kitfunso/hippo-memory/blob/master/docs/evals/README.md).
1154
1129
 
1155
1130
  ---
1156
1131
 
@@ -34,7 +34,7 @@ export interface ContainerOutcome {
34
34
  }
35
35
  /** Throws SQLITE_BUSY when another writer holds the store past its busy timeout; nothing is written then. */
36
36
  export declare function syncContainer(s: StoreSession, work: ContainerWork): ContainerOutcome;
37
- export type SetAsideWhy = 'note-gone' | 'note-changed' | 'handover' | 'project-merge';
37
+ export type SetAsideWhy = 'note-gone' | 'note-changed' | 'handover' | 'project-merge' | 'project-repair';
38
38
  export type SetAsideResult = {
39
39
  readonly kind: 'untagged';
40
40
  readonly entry: MemoryEntry;
@@ -6,6 +6,8 @@ export declare function claudeCheckoutRoot(top: string, gitDir: string, common:
6
6
  /** Claude Code's folder name for a path: non-alphanumerics made '-', and a name over 200 characters cut to 200 plus a base-36 hash of the whole path. */
7
7
  export declare function claudeFolderName(root: string): string;
8
8
  export declare const claudeCodeAdapter: Adapter;
9
- /** Post-compact's read: the session's own notes folder and nothing else, so no git call runs inside the hook's time limit. */
9
+ /** A session's own notes folder and nothing else, with no git call, so post-compact can read it inside the hook's time limit. */
10
10
  export declare function claudeTranscriptListing(ctx: AdapterContext, transcriptPath: string): Listing;
11
+ /** The project a session folder's notes belong to: the one Claude named the folder for, the session's start folder, else cwd or a parent; null when none matches. */
12
+ export declare function transcriptNotesOrigin(transcriptPath: string, cwd: string | null, machine: Pick<AdapterContext, 'platform' | 'env'>): string | null;
11
13
  //# sourceMappingURL=claude-code.d.ts.map
@@ -1,7 +1,7 @@
1
1
  // Claude Code's auto memory: frontmatter `.md` notes in a per-project folder, plus the `autoMemoryDirectory` user folder.
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { realpathOrResolve } from '../project-identity.js';
4
+ import { deriveOriginProject, realpathOrResolve } from '../project-identity.js';
5
5
  import { isStringValue } from '../capture-contract.js';
6
6
  import { isJsonObject } from '../hooks/shared.js';
7
7
  import { expandHome, frontmatterField, itemTime, readTextFile, splitFrontmatter } from './files.js';
@@ -45,23 +45,67 @@ export const claudeCodeAdapter = {
45
45
  return { tool: 'claude-code', home: config, containers: readFolders(folders, scope, ctx.platform), warnings };
46
46
  },
47
47
  };
48
- /** Post-compact's read: the session's own notes folder and nothing else, so no git call runs inside the hook's time limit. */
48
+ /** A session's own notes folder and nothing else, with no git call, so post-compact can read it inside the hook's time limit. */
49
49
  export function claudeTranscriptListing(ctx, transcriptPath) {
50
50
  const config = ctx.env.CLAUDE_CONFIG_DIR || path.join(ctx.home, '.claude');
51
51
  const folder = path.join(path.dirname(transcriptPath), 'memory');
52
52
  return { tool: 'claude-code', home: config, containers: readFolders([folder], 'project', ctx.platform), warnings: [] };
53
53
  }
54
+ /** The project a session folder's notes belong to: the one Claude named the folder for, the session's start folder, else cwd or a parent; null when none matches. */
55
+ export function transcriptNotesOrigin(transcriptPath, cwd, machine) {
56
+ const fold = (name) => (machine.platform === 'win32' ? name.toLowerCase() : name);
57
+ const folder = fold(path.basename(path.dirname(transcriptPath)));
58
+ const start = transcriptStartCwd(transcriptPath);
59
+ const pinned = pinnedProjectDirName(machine.env);
60
+ if (pinned !== null && fold(pinned) === folder) {
61
+ // A pinned name stands for whatever project the session ran in, so its start folder decides.
62
+ const from = start ?? cwd;
63
+ return from !== null && fs.existsSync(from) ? deriveOriginProject(from) : null;
64
+ }
65
+ for (const from of [start, cwd]) {
66
+ for (let dir = from === null ? null : path.resolve(from); dir !== null; dir = path.dirname(dir) === dir ? null : path.dirname(dir)) {
67
+ // A folder gone from disk resolves to its bare name, never the project it was in, so it decides nothing.
68
+ if ([dir, realpathOrResolve(dir)].some((d) => fold(claudeFolderName(d)) === folder))
69
+ return fs.existsSync(dir) ? deriveOriginProject(dir) : null;
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+ // Claude writes the cwd on each message line after a few header lines; 64 KB holds the first with room to spare.
75
+ const START_SCAN_BYTES = 64 * 1024;
76
+ /** The cwd on the transcript's first line that has one: the folder Claude named the session folder for, which a folder name alone cannot give back. */
77
+ function transcriptStartCwd(transcriptPath) {
78
+ if (!fs.existsSync(transcriptPath))
79
+ return null;
80
+ const fd = fs.openSync(transcriptPath, 'r');
81
+ try {
82
+ const buf = Buffer.alloc(START_SCAN_BYTES);
83
+ const lines = buf.subarray(0, fs.readSync(fd, buf, 0, buf.length, 0)).toString('utf8').split('\n');
84
+ for (const line of lines) {
85
+ const cwd = /"cwd":"((?:[^"\\]|\\.)*)"/.exec(line)?.[1];
86
+ if (cwd === undefined)
87
+ continue;
88
+ // SAFETY: the match is the body of one JSON string, so parsing it in quotes yields a string.
89
+ return JSON.parse(`"${cwd}"`);
90
+ }
91
+ return null;
92
+ }
93
+ finally {
94
+ fs.closeSync(fd);
95
+ }
96
+ }
54
97
  function projectFolders(ctx, config) {
55
98
  const projects = path.join(config, 'projects');
56
99
  const names = ctx.projectRoot === undefined ? [] : [...claudeMemoryFolderNames(ctx.projectRoot, ctx.platform)];
57
- const pinned = ctx.env.CLAUDE_CODE_PROJECT_DIR_NAME;
58
- // Claude reads the pinned name only alongside a pinned config folder.
59
- if (ctx.env.CLAUDE_CONFIG_DIR && pinned !== undefined && PROJECT_DIR_NAME.test(pinned))
100
+ const pinned = pinnedProjectDirName(ctx.env);
101
+ if (pinned !== null)
60
102
  names.push(pinned);
61
- const folders = names.map((name) => path.join(projects, name, 'memory'));
62
- if (ctx.transcriptPath)
63
- folders.push(path.join(path.dirname(ctx.transcriptPath), 'memory'));
64
- return folders;
103
+ return names.map((name) => path.join(projects, name, 'memory'));
104
+ }
105
+ function pinnedProjectDirName(env) {
106
+ const pinned = env.CLAUDE_CODE_PROJECT_DIR_NAME;
107
+ // Claude reads the pinned name only alongside a pinned config folder.
108
+ return env.CLAUDE_CONFIG_DIR && pinned !== undefined && PROJECT_DIR_NAME.test(pinned) ? pinned : null;
65
109
  }
66
110
  function userFolders(ctx, config, warnings) {
67
111
  const dir = autoMemoryDirectory(path.join(config, 'settings.json'), ctx.home, warnings);
@@ -24,10 +24,10 @@ export declare function importForStore(hippoRoot: string, opts: SyncOptions): Im
24
24
  export declare function importProjectMemories(hippoRoot: string, opts: SyncOptions): ImportReport;
25
25
  /** Every tool's user-level memory into the global store, created on demand, with no origin. */
26
26
  export declare function importUserMemories(invokingRoot: string, opts: SyncOptions): ImportReport;
27
- /** Session end in a folder with no store of its own: the session's project into the global store with its origin, then the user pass. */
27
+ /** Session end in a folder with no store of its own: the session's project into the global store with its origin, the session folder's notes, then the user pass. */
28
28
  export declare function importAtSessionEnd(cwd: string, transcriptPath: string | undefined, opts: SyncOptions): ImportReport;
29
- /** Post-compact: the transcript folder's notes only, with no git call, no legacy adoption and no user pass, as the hook has 10 seconds. */
30
- export declare function importAtCompaction(hippoRoot: string, transcriptPath: string, originProject: string | undefined, opts: SyncOptions): ImportReport;
29
+ /** The session folder's notes under the project Claude filed them for, never cwd's: a session begun at home keeps its home notes user-global wherever it ends. No git call, so post-compact can run it. */
30
+ export declare function importSessionFolder(hippoRoot: string, transcriptPath: string, cwd: string | null, opts: SyncOptions): ImportReport;
31
31
  /** The variable when set (empty or `none` is off); else the tools both the invoking and the target store's config allow. */
32
32
  export declare function allowedTools(env: Machine['env'], invoking: string, target: string, warnings: string[]): Set<ToolId>;
33
33
  //# sourceMappingURL=sync.d.ts.map
@@ -15,7 +15,7 @@ import { selectLiveEntriesBySourcePrefix } from '../store/entry-reads.js';
15
15
  import { updateStats } from '../store/index-and-stats.js';
16
16
  import { resolveTenantId } from '../tenant.js';
17
17
  import { setAsideRow, syncContainer } from './apply.js';
18
- import { claudeCodeAdapter, claudeTranscriptListing } from './claude-code.js';
18
+ import { claudeCodeAdapter, claudeTranscriptListing, transcriptNotesOrigin } from './claude-code.js';
19
19
  import { codexAdapter } from './codex.js';
20
20
  import { copilotAdapter } from './copilot.js';
21
21
  import { geminiAdapter } from './gemini.js';
@@ -53,21 +53,29 @@ export function importUserMemories(invokingRoot, opts) {
53
53
  scope: 'user', target: resolveGlobalRootDir(), invoking: invokingRoot, list: (a) => a.list(ctx, 'user'), legacy: false, originProject: '', handover: false,
54
54
  }, opts);
55
55
  }
56
- /** Session end in a folder with no store of its own: the session's project into the global store with its origin, then the user pass. */
56
+ /** Session end in a folder with no store of its own: the session's project into the global store with its origin, the session folder's notes, then the user pass. */
57
57
  export function importAtSessionEnd(cwd, transcriptPath, opts) {
58
58
  const globalRoot = resolveGlobalRootDir();
59
- const ctx = context(opts.machine, { projectRoot: cwd, transcriptPath });
59
+ const ctx = context(opts.machine, { projectRoot: cwd });
60
60
  const report = runPass({
61
61
  scope: 'project', target: globalRoot, invoking: globalRoot, list: (a) => a.list(ctx, 'project'), legacy: false, originProject: deriveOriginProject(cwd), handover: false,
62
62
  }, opts);
63
+ if (transcriptPath !== undefined)
64
+ mergeReports(report, importSessionFolder(globalRoot, transcriptPath, cwd, opts));
63
65
  mergeReports(report, importUserMemories(globalRoot, opts));
64
66
  return report;
65
67
  }
66
- /** Post-compact: the transcript folder's notes only, with no git call, no legacy adoption and no user pass, as the hook has 10 seconds. */
67
- export function importAtCompaction(hippoRoot, transcriptPath, originProject, opts) {
68
+ /** The session folder's notes under the project Claude filed them for, never cwd's: a session begun at home keeps its home notes user-global wherever it ends. No git call, so post-compact can run it. */
69
+ export function importSessionFolder(hippoRoot, transcriptPath, cwd, opts) {
70
+ const origin = transcriptNotesOrigin(transcriptPath, cwd, opts.machine);
71
+ if (origin === null)
72
+ return emptyReport();
73
+ // A project store takes its own project's notes; another project's, or home's, go to the global store, which parts them by origin.
74
+ const own = isGlobalStoreRoot(hippoRoot) || origin === deriveOriginProject(path.dirname(hippoRoot));
75
+ const target = own ? hippoRoot : resolveGlobalRootDir();
68
76
  const ctx = context(opts.machine, {});
69
77
  return runPass({
70
- scope: 'project', target: hippoRoot, invoking: hippoRoot, legacy: false, originProject, handover: false,
78
+ scope: 'project', target, invoking: hippoRoot, legacy: false, originProject: origin, handover: false,
71
79
  list: (a) => (a.tool === 'claude-code' ? claudeTranscriptListing(ctx, transcriptPath) : null),
72
80
  }, opts);
73
81
  }
@@ -6,8 +6,6 @@ export interface AdapterContext {
6
6
  readonly env: Readonly<Record<string, string | undefined>>;
7
7
  readonly platform: NodeJS.Platform;
8
8
  readonly projectRoot?: string;
9
- /** Claude Code's transcript at a hook: its folder's `memory/` holds that session's own notes. */
10
- readonly transcriptPath?: string;
11
9
  }
12
10
  export interface MemoryItem {
13
11
  /** A path inside the container with '/' separators, or `<heading slug>/<text hash>` in a single-file store. */
@@ -59,58 +59,9 @@ export function assemble(ctx, sessionId, opts = {}) {
59
59
  const olderRows = scoped.slice(0, tailStartIdx);
60
60
  const tailRows = scoped.slice(tailStartIdx);
61
61
  // Substitute parent summaries for older rows that share one.
62
- const olderItems = [];
63
- let summarized = 0;
64
- if (summarizeOlder && olderRows.length > 0) {
65
- const olderByParent = new Map();
66
- for (const r of olderRows) {
67
- if (!r.dag_parent_id)
68
- continue;
69
- const list = olderByParent.get(r.dag_parent_id) ?? [];
70
- list.push(r);
71
- olderByParent.set(r.dag_parent_id, list);
72
- }
73
- const eligibleParentIds = Array.from(olderByParent.keys()).filter((pid) => (olderByParent.get(pid)?.length ?? 0) >= 2);
74
- const parents = eligibleParentIds.length > 0
75
- ? loadEntriesByIds(ctx.hippoRoot, eligibleParentIds, ctx.tenantId)
76
- .filter((p) => (p.dag_level ?? 0) === 2 && !p.superseded_by)
77
- .filter((p) => passesScopeFilterForRecall(p.scope ?? null, opts.scope))
78
- : [];
79
- const claimedRawIds = new Set();
80
- for (const parent of parents) {
81
- const claimed = (olderByParent.get(parent.id) ?? []).map((r) => r.id);
82
- claimed.forEach((id) => claimedRawIds.add(id));
83
- olderItems.push({
84
- id: parent.id,
85
- content: parent.content,
86
- createdAt: parent.earliest_at ?? parent.created,
87
- isSummary: true,
88
- substitutedFor: claimed,
89
- strength: parent.strength,
90
- });
91
- summarized += claimed.length;
92
- }
93
- for (const r of olderRows) {
94
- if (claimedRawIds.has(r.id))
95
- continue;
96
- olderItems.push({
97
- id: r.id,
98
- content: r.content,
99
- createdAt: r.created,
100
- strength: r.strength,
101
- });
102
- }
103
- }
104
- else {
105
- for (const r of olderRows) {
106
- olderItems.push({
107
- id: r.id,
108
- content: r.content,
109
- createdAt: r.created,
110
- strength: r.strength,
111
- });
112
- }
113
- }
62
+ const { olderItems, summarized } = summarizeOlder && olderRows.length > 0
63
+ ? substituteSummaries(ctx, olderRows, opts.scope)
64
+ : { olderItems: olderRows.map(rawItem), summarized: 0 };
114
65
  const tailItems = tailRows.map((r) => ({
115
66
  id: r.id,
116
67
  content: r.content,
@@ -124,9 +75,59 @@ export function assemble(ctx, sessionId, opts = {}) {
124
75
  const cmpIso = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
125
76
  olderItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
126
77
  tailItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
127
- let items = [...olderItems, ...tailItems];
128
78
  const itemCost = opts.cost?.item ?? ((it) => estimateTokens(it.content));
129
79
  const room = budget - (opts.cost?.fixed(Math.max(budget, totalRaw)) ?? 0);
80
+ const { items, tokens, evicted } = evictToBudget([...olderItems, ...tailItems], itemCost, room);
81
+ return { sessionId, items, tokens, totalRaw, summarized, evicted, truncated };
82
+ }
83
+ function rawItem(r) {
84
+ return {
85
+ id: r.id,
86
+ content: r.content,
87
+ createdAt: r.created,
88
+ strength: r.strength,
89
+ };
90
+ }
91
+ function substituteSummaries(ctx, olderRows, scope) {
92
+ const olderItems = [];
93
+ let summarized = 0;
94
+ const olderByParent = new Map();
95
+ for (const r of olderRows) {
96
+ if (!r.dag_parent_id)
97
+ continue;
98
+ const list = olderByParent.get(r.dag_parent_id) ?? [];
99
+ list.push(r);
100
+ olderByParent.set(r.dag_parent_id, list);
101
+ }
102
+ const eligibleParentIds = Array.from(olderByParent.keys()).filter((pid) => (olderByParent.get(pid)?.length ?? 0) >= 2);
103
+ const parents = eligibleParentIds.length > 0
104
+ ? loadEntriesByIds(ctx.hippoRoot, eligibleParentIds, ctx.tenantId)
105
+ .filter((p) => (p.dag_level ?? 0) === 2 && !p.superseded_by)
106
+ .filter((p) => passesScopeFilterForRecall(p.scope ?? null, scope))
107
+ : [];
108
+ const claimedRawIds = new Set();
109
+ for (const parent of parents) {
110
+ const claimed = (olderByParent.get(parent.id) ?? []).map((r) => r.id);
111
+ claimed.forEach((id) => claimedRawIds.add(id));
112
+ olderItems.push({
113
+ id: parent.id,
114
+ content: parent.content,
115
+ createdAt: parent.earliest_at ?? parent.created,
116
+ isSummary: true,
117
+ substitutedFor: claimed,
118
+ strength: parent.strength,
119
+ });
120
+ summarized += claimed.length;
121
+ }
122
+ for (const r of olderRows) {
123
+ if (claimedRawIds.has(r.id))
124
+ continue;
125
+ olderItems.push(rawItem(r));
126
+ }
127
+ return { olderItems, summarized };
128
+ }
129
+ function evictToBudget(start, itemCost, room) {
130
+ let items = start;
130
131
  let tokens = items.reduce((acc, it) => acc + itemCost(it), 0);
131
132
  let evicted = 0;
132
133
  while (tokens > room && items.length > 0) {
@@ -147,6 +148,6 @@ export function assemble(ctx, sessionId, opts = {}) {
147
148
  tokens -= cost;
148
149
  evicted++;
149
150
  }
150
- return { sessionId, items, tokens, totalRaw, summarized, evicted, truncated };
151
+ return { items, tokens, evicted };
151
152
  }
152
153
  //# sourceMappingURL=assemble.js.map
@@ -0,0 +1,49 @@
1
+ import type { AmbientLoadResult } from '../store/candidates.js';
2
+ import { type MemoryEntry } from '../memory.js';
3
+ import { type HippoConfig } from '../config.js';
4
+ import type { DeliveryObserver } from '../delivery-recorder.js';
5
+ import type { ContextCost, ContextOpts, ContextResultEntry } from './context-types.js';
6
+ import type { Context } from './types.js';
7
+ /** What getContext resolved from opts and config before it read a row. */
8
+ export interface ContextPlan {
9
+ pinnedOnly: boolean;
10
+ limit: number;
11
+ includeRecent: number;
12
+ activeScope: string;
13
+ exactScope: string | undefined;
14
+ query: string;
15
+ hasLocal: boolean;
16
+ hasGlobal: boolean;
17
+ globalRoot: string;
18
+ primaryIsGlobal: boolean;
19
+ hasLocalTaskState: boolean;
20
+ config: HippoConfig;
21
+ currentProjectName: string;
22
+ includeCrossProject: boolean;
23
+ originProject: string | undefined;
24
+ promptRecallPending: boolean;
25
+ cost: ContextCost | undefined;
26
+ price: (entry: MemoryEntry, isGlobal: boolean, promptRecall?: boolean) => number;
27
+ obs: DeliveryObserver | undefined;
28
+ }
29
+ /** The ambient admit rules; `digestHidden` is read late because a prompt-recall eligibility check can still set it. */
30
+ export interface ContextAdmission {
31
+ ambientAdmit: (e: MemoryEntry) => boolean;
32
+ admit: (e: MemoryEntry) => boolean;
33
+ /** What the two-store search admits: `admit` without the own-session compaction rule. */
34
+ bothStoresAdmit: (e: MemoryEntry) => boolean;
35
+ digestHidden: () => boolean;
36
+ }
37
+ export interface ContextPools {
38
+ local: AmbientLoadResult;
39
+ global: AmbientLoadResult;
40
+ }
41
+ export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global: readonly MemoryEntry[], now: Date): [MemoryEntry[], MemoryEntry[]];
42
+ export declare const finiteOr: (v: number, dflt: number, min: number) => number;
43
+ /** Pins plus the prompt-recall or recent-N backfill; null means the block is empty. */
44
+ export declare function selectPinned(ctx: Context, opts: ContextOpts, plan: ContextPlan, left: number, pools: ContextPools, admission: ContextAdmission): ContextResultEntry[] | null;
45
+ /** No query: the strongest memories by strength, up to budget. */
46
+ export declare function selectStrongest(plan: ContextPlan, left: number, pools: ContextPools): ContextResultEntry[];
47
+ /** Real query: hybrid search over both stores, or physics/hybrid over the local rows; emits the 'recall' audit row. */
48
+ export declare function selectBySearch(ctx: Context, plan: ContextPlan, left: number, pools: ContextPools, admission: ContextAdmission): Promise<ContextResultEntry[]>;
49
+ //# sourceMappingURL=context-select.d.ts.map