@bcmyguest/openmem 0.0.0-stage → 0.8.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 (230) hide show
  1. package/.agents/skills/openmem/SKILL.md +34 -0
  2. package/LICENSE +190 -0
  3. package/README.md +140 -2
  4. package/bin/hindsight-project-index +530 -0
  5. package/cli/coding-agent/access-policy.mjs +38 -0
  6. package/cli/coding-agent/agents.mjs +34 -0
  7. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  8. package/cli/coding-agent/capabilities.mjs +34 -0
  9. package/cli/coding-agent/context-store.mjs +81 -0
  10. package/cli/coding-agent/context-trim.mjs +897 -0
  11. package/cli/coding-agent/file-io.mjs +72 -0
  12. package/cli/coding-agent/hook-harnesses.mjs +88 -0
  13. package/cli/coding-agent/hook-runner.mjs +284 -0
  14. package/cli/coding-agent/install-access.mjs +119 -0
  15. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  16. package/cli/coding-agent/install-rtk.mjs +60 -0
  17. package/cli/coding-agent/install-shim.mjs +201 -0
  18. package/cli/coding-agent/opencode-config.mjs +107 -0
  19. package/cli/coding-agent/opencode-plugin.mjs +132 -0
  20. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  21. package/cli/coding-agent/recall-frame.mjs +29 -0
  22. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  23. package/cli/coding-agent/runtime.mjs +40 -0
  24. package/cli/coding-agent/session-db.mjs +626 -0
  25. package/cli/coding-agent/session-facade.mjs +86 -0
  26. package/cli/coding-agent/toml-edit.mjs +161 -0
  27. package/cli/cognee-mcp.mjs +260 -0
  28. package/cli/context-management.mjs +254 -0
  29. package/cli/feed-sync.mjs +27 -0
  30. package/cli/install-coding-agent.mjs +298 -0
  31. package/cli/install-memory-mcp.mjs +426 -0
  32. package/cli/is-main.mjs +18 -0
  33. package/cli/mcp/context-sql-runner.mjs +58 -0
  34. package/cli/mcp/context-store.mjs +670 -0
  35. package/cli/mcp/context.mjs +61 -0
  36. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  37. package/cli/mcp/export/cognee.mjs +119 -0
  38. package/cli/mcp/export/hindsight.mjs +48 -0
  39. package/cli/mcp/export/index.mjs +26 -0
  40. package/cli/mcp/export/mem0.mjs +31 -0
  41. package/cli/mcp/knowledge.mjs +333 -0
  42. package/cli/mcp/llm-synthesize.mjs +50 -0
  43. package/cli/mcp/mem0-scope-id.mjs +22 -0
  44. package/cli/mcp/memory-facade.mjs +521 -0
  45. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  46. package/cli/mcp/notebooklm-banks.mjs +21 -0
  47. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  48. package/cli/mcp/notebooklm-provider.mjs +225 -0
  49. package/cli/mcp/pmm-records.mjs +23 -0
  50. package/cli/mcp/portability.mjs +158 -0
  51. package/cli/mcp/stdio.mjs +65 -0
  52. package/cli/mcp/unified-mcp-install.mjs +65 -0
  53. package/cli/mem0-mcp.mjs +108 -0
  54. package/cli/notebooklm-mcp.mjs +236 -0
  55. package/cli/openmem-mcp.mjs +38 -0
  56. package/cli/openmem.mjs +222 -0
  57. package/cli/roots.mjs +85 -0
  58. package/cli/schedule.mjs +176 -0
  59. package/cli/stack.mjs +438 -0
  60. package/cli/switch-memory-provider.mjs +358 -0
  61. package/cli/wiring-report.mjs +125 -0
  62. package/compose.bench.yaml +12 -0
  63. package/compose.yaml +227 -0
  64. package/config/coding-agent.json +13 -0
  65. package/config/hindsight.env.example +26 -0
  66. package/config/index.yml +54 -0
  67. package/config/litellm.lemond.yaml +40 -0
  68. package/config/litellm.ollama.yaml +33 -0
  69. package/config/notebooklm.yml +33 -0
  70. package/config/openmem.toml.example +124 -0
  71. package/docs/OPENMEM.md +23 -0
  72. package/docs/architecture.md +245 -0
  73. package/docs/assets/README.md +10 -0
  74. package/docs/assets/setup-demo.gif +0 -0
  75. package/docs/coding-agent-hooks.md +75 -0
  76. package/docs/coding-agent-scopes.md +22 -0
  77. package/docs/coding-agents.md +71 -0
  78. package/docs/cognee.md +37 -0
  79. package/docs/compatibility.md +80 -0
  80. package/docs/concepts.md +135 -0
  81. package/docs/context-management.md +255 -0
  82. package/docs/context-proxy.md +115 -0
  83. package/docs/context-research.md +215 -0
  84. package/docs/context-store.md +277 -0
  85. package/docs/hindsight.md +77 -0
  86. package/docs/index-sources-roadmap.md +38 -0
  87. package/docs/index-sources.md +39 -0
  88. package/docs/indexer-sinks.md +122 -0
  89. package/docs/indexing.md +57 -0
  90. package/docs/manual-stack-setup.md +74 -0
  91. package/docs/mcp-install-guide.md +4 -0
  92. package/docs/mcp-setup.md +104 -0
  93. package/docs/mem0.md +141 -0
  94. package/docs/memory-portability.md +73 -0
  95. package/docs/memory-providers.md +246 -0
  96. package/docs/meridian-litellm.md +198 -0
  97. package/docs/model-configuration.md +106 -0
  98. package/docs/notebooklm.md +83 -0
  99. package/docs/npx-install.md +172 -0
  100. package/docs/openmem-config.md +194 -0
  101. package/docs/provider-wizard-hooks.md +222 -0
  102. package/docs/quickstart.md +107 -0
  103. package/docs/setup-wizard.md +117 -0
  104. package/docs/sources-feeds.md +39 -0
  105. package/docs/sources-readwise.md +23 -0
  106. package/docs/sources-zotero.md +22 -0
  107. package/glama.json +4 -0
  108. package/indexer/Dockerfile +38 -0
  109. package/indexer/code_outline.py +146 -0
  110. package/indexer/feed_sync.py +107 -0
  111. package/indexer/index_config.py +290 -0
  112. package/indexer/pytest.ini +5 -0
  113. package/indexer/sinks/__init__.py +148 -0
  114. package/indexer/sinks/_http.py +44 -0
  115. package/indexer/sinks/cognee.py +329 -0
  116. package/indexer/sinks/hindsight.py +107 -0
  117. package/indexer/sinks/mem0.py +247 -0
  118. package/indexer/watch.py +240 -0
  119. package/package.json +96 -4
  120. package/providers/_template/compose.yaml +52 -0
  121. package/providers/cognee/OPERATOR.md +82 -0
  122. package/providers/cognee/compose.yaml +89 -0
  123. package/providers/cognee/lib/clack.mjs +157 -0
  124. package/providers/cognee/lib/coding-agent.mjs +26 -0
  125. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  126. package/providers/cognee/lib/config.mjs +98 -0
  127. package/providers/cognee/lib/env-file.mjs +194 -0
  128. package/providers/cognee/lib/env-setup.mjs +83 -0
  129. package/providers/cognee/lib/llm-env.mjs +382 -0
  130. package/providers/cognee/lib/ports.mjs +25 -0
  131. package/providers/cognee/lib/smoke.mjs +246 -0
  132. package/providers/cognee/lib/stack-env.mjs +134 -0
  133. package/providers/cognee/lib/state.mjs +101 -0
  134. package/providers/cognee/lib/summary.mjs +56 -0
  135. package/providers/cognee/lib/validate.mjs +97 -0
  136. package/providers/cognee/manifest.json +56 -0
  137. package/providers/cognee/wizard.mjs +134 -0
  138. package/providers/graphiti/manifest.json +15 -0
  139. package/providers/hindsight/lib/clack.mjs +347 -0
  140. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  141. package/providers/hindsight/lib/config.mjs +79 -0
  142. package/providers/hindsight/lib/env-setup.mjs +72 -0
  143. package/providers/hindsight/lib/litellm-routes.mjs +196 -0
  144. package/providers/hindsight/lib/meridian.mjs +307 -0
  145. package/providers/hindsight/lib/model-backend.mjs +243 -0
  146. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  147. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  148. package/providers/hindsight/lib/ports.mjs +30 -0
  149. package/providers/hindsight/lib/probe.mjs +54 -0
  150. package/providers/hindsight/lib/stack-env.mjs +125 -0
  151. package/providers/hindsight/lib/state.mjs +80 -0
  152. package/providers/hindsight/lib/summary.mjs +57 -0
  153. package/providers/hindsight/lib/validate.mjs +81 -0
  154. package/providers/hindsight/manifest.json +58 -0
  155. package/providers/hindsight/wizard.mjs +150 -0
  156. package/providers/letta/manifest.json +29 -0
  157. package/providers/mem0/OPERATOR.md +143 -0
  158. package/providers/mem0/compose.yaml +79 -0
  159. package/providers/mem0/lib/coding-agent.mjs +58 -0
  160. package/providers/mem0/lib/config.mjs +45 -0
  161. package/providers/mem0/lib/configure.mjs +201 -0
  162. package/providers/mem0/lib/dotenv.mjs +71 -0
  163. package/providers/mem0/lib/env-file.mjs +110 -0
  164. package/providers/mem0/lib/http.mjs +69 -0
  165. package/providers/mem0/lib/litellm.mjs +457 -0
  166. package/providers/mem0/lib/mcp-install.mjs +165 -0
  167. package/providers/mem0/lib/secret.mjs +30 -0
  168. package/providers/mem0/lib/smoke.mjs +179 -0
  169. package/providers/mem0/lib/stack-env.mjs +255 -0
  170. package/providers/mem0/lib/state.mjs +67 -0
  171. package/providers/mem0/lib/summary.mjs +88 -0
  172. package/providers/mem0/lib/url.mjs +106 -0
  173. package/providers/mem0/lib/validate.mjs +158 -0
  174. package/providers/mem0/manifest.json +47 -0
  175. package/providers/mem0/wizard.mjs +146 -0
  176. package/providers/zep/manifest.json +29 -0
  177. package/proxy/config.yaml +33 -0
  178. package/proxy/openmem_context/__init__.py +6 -0
  179. package/proxy/openmem_context/adapters.py +504 -0
  180. package/proxy/openmem_context/compress.py +53 -0
  181. package/proxy/openmem_context/core.py +606 -0
  182. package/proxy/openmem_context/embed.py +212 -0
  183. package/proxy/openmem_context/recall.py +49 -0
  184. package/proxy/openmem_context/session.py +70 -0
  185. package/proxy/openmem_context/sessiondb.py +276 -0
  186. package/proxy/openmem_context/settings.py +134 -0
  187. package/proxy/openmem_context/store.py +129 -0
  188. package/proxy/openmem_context/tokens.py +123 -0
  189. package/proxy/openmem_context/ttl.py +78 -0
  190. package/proxy/openmem_context_hook.py +34 -0
  191. package/scripts/check-pack.mjs +219 -0
  192. package/scripts/gen-masks.mjs +83 -0
  193. package/scripts/gen-masks.sh +4 -0
  194. package/scripts/promotion-provider-smoke.mjs +34 -0
  195. package/scripts/run-setup.sh +30 -0
  196. package/scripts/setup.mjs +107 -0
  197. package/scripts/setup.sh +5 -0
  198. package/scripts/spin-up.sh +45 -0
  199. package/scripts/wizard.sh +5 -0
  200. package/server.json +34 -0
  201. package/sync/Dockerfile +27 -0
  202. package/sync/memory_backend.py +87 -0
  203. package/sync/notebooklm_sync.py +664 -0
  204. package/wizard/bin/pm.mjs +97 -0
  205. package/wizard/bin/setup.mjs +33 -0
  206. package/wizard/lib/compose-version.mjs +32 -0
  207. package/wizard/lib/compose.mjs +70 -0
  208. package/wizard/lib/context-step.mjs +140 -0
  209. package/wizard/lib/curl.mjs +103 -0
  210. package/wizard/lib/docker-build.mjs +148 -0
  211. package/wizard/lib/exec.mjs +41 -0
  212. package/wizard/lib/indexer.mjs +6 -0
  213. package/wizard/lib/litellm-env.mjs +55 -0
  214. package/wizard/lib/load-machine-env.mjs +4 -0
  215. package/wizard/lib/log.mjs +32 -0
  216. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  217. package/wizard/lib/notebooklm.mjs +214 -0
  218. package/wizard/lib/parse-args.mjs +87 -0
  219. package/wizard/lib/paths.mjs +39 -0
  220. package/wizard/lib/pm-config.mjs +371 -0
  221. package/wizard/lib/ports.mjs +94 -0
  222. package/wizard/lib/prompts.mjs +85 -0
  223. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  224. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  225. package/wizard/lib/providers/registry.mjs +262 -0
  226. package/wizard/lib/run-clack.mjs +108 -0
  227. package/wizard/lib/run-wizard.mjs +750 -0
  228. package/wizard/lib/validate-setup.mjs +62 -0
  229. package/wizard/lib/wizard-action.mjs +34 -0
  230. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,135 @@
1
+ # Concepts
2
+
3
+ OpenMem is memory and context management for coding agents: smart compaction that keeps every turn searchable, pluggable memory providers (Hindsight, mem0, Cognee), one setup wizard.
4
+
5
+ Benchmarks: [smart compaction against Claude Code's native compaction and keep-everything](context-management.md#benchmarks).
6
+
7
+ ## What’s coding-agent memory?
8
+
9
+ Coding agents forget between sessions. A memory system stores durable facts, decisions, and project context so the next turn (or agent) can recall them without re-deriving everything from the repo.
10
+
11
+ In practice that means:
12
+
13
+ - **Write** — retain notes, session transcripts, indexed sources
14
+ - **Read** — search, recall, or synthesize an answer from stored memories
15
+ - **Scope** — keep personal notes, vaults, and per-repo banks from colliding
16
+ - **Operate** — run a backend locally (or point at one), keep secrets off the wire you did not choose
17
+
18
+ Full use of memory needs a **memory system**: an API (or library) plus storage, extraction, and usually an admin or ops path. Hindsight, mem0, Cognee, and in-house stores are examples. Each has its own API, deploy story, and agent wiring.
19
+
20
+ ## What’s OpenMem?
21
+
22
+ OpenMem is **not** a memory engine. It is the layer that sits between coding agents and whichever engine you run:
23
+
24
+ | Layer | Role |
25
+ | --- | --- |
26
+ | **Agents** | Claude Code, Codex, Cursor Agent, OpenCode — call stable `memory_*` tools and share one skill |
27
+ | **OpenMem** | Wizard, Compose, indexer, unified MCP, session hooks, import/export formats |
28
+ | **Memory provider** | The selected memory system; owns storage, semantics, and its native API. See [Provider status](provider-status.md). |
29
+
30
+ One active provider at a time. `openmem switch` rewires the stack; it does not pretend to migrate your data. Portability is explicit through the format layer (PMM today).
31
+
32
+ ```mermaid
33
+ flowchart TB
34
+ AG[Coding agents]
35
+ OM["OpenMem surface<br/>memory_* · skill · session hooks · wizard · indexer"]
36
+ AG --> OM
37
+ OM --> HS[Hindsight]
38
+ OM --> M0[mem0]
39
+ OM --> CG[Cognee]
40
+ OM --> NX["…next provider manifest"]
41
+ ```
42
+
43
+ ## Why an interface layer?
44
+
45
+ Without a shared front door, every agent and indexer hard-codes one vendor’s SDK or HTTP shape. Switching providers means rewriting hooks, MCP entries, and indexing. Trying a second system means a second install path.
46
+
47
+ Standardizing the **agent and ops surface** means:
48
+
49
+ - Keep using your current memory product
50
+ - Try another provider without rewriting agent prompts and MCP tool names
51
+ - Share indexer, wizard, and install plumbing across backends
52
+ - Grow a community of providers behind one contract
53
+
54
+ OpenMem owns machine plumbing. Providers own memory quality.
55
+
56
+ ## Core pieces
57
+
58
+ ### Providers
59
+
60
+ A **memory provider** is the selected memory system, such as Hindsight, mem0, or Cognee. OpenMem's adapter under `providers/<id>/` maps its manifest, Compose fragment, wizard hooks, and indexer sink to that provider's API.
61
+
62
+ Ship a provider as `providers/<id>/` (manifest + hooks + compose). Status (`supported` / `experimental` / `planned`) comes from the manifest. See [Provider status](provider-status.md) and [Pluggable memory providers](memory-providers.md).
63
+
64
+ Only one provider is active (`MEMORY_PROVIDER`). Switching updates Compose, the indexer sink, and unified MCP to use it.
65
+
66
+ ### Evaluation API (`memory_*`)
67
+
68
+ Agents do not call Hindsight or mem0 tool names by default. They call a **stable tool set** on the `openmem` MCP server:
69
+
70
+ | Tool | Intent |
71
+ | --- | --- |
72
+ | `memory_add` | Store a durable record |
73
+ | `memory_search` / `memory_recall` | Retrieve |
74
+ | `memory_reflect` / `memory_consolidate` | Synthesize (provider-dependent) |
75
+ | `memory_forget` | Delete (scoped; confirm before bulk) |
76
+ | `memory_export` / `memory_import` | Portability (PMM) |
77
+ | `memory_provider_status` / `memory_project_scope` | Ops / bank resolution |
78
+ | `memory_pages_list` / `memory_pages_search` / `memory_pages_read` | Curated knowledge pages (Hindsight) |
79
+ | `memory_capture_initiative` | Track an approved feature plan (Hindsight) |
80
+ | `memory_status` | Ingest drain state (Hindsight; mem0 always ready) |
81
+ | `smart_compact_search` / `smart_compact_get` | Recover trimmed session context and compressed tool results (local, every provider) |
82
+
83
+ The facade (`cli/mcp/memory-facade.mjs`) maps each call onto the active provider’s HTTP API. Native provider MCP (`install-mcp --provider-only`) remains available when you want vendor tools.
84
+
85
+ ### Context and banks
86
+
87
+ **Bank** (or provider-equivalent `user_id` / dataset) is the scope key for a write or read. Default MCP scope is `openmem-mcp`. Project work should pass the resolved project bank so session noise does not land in the default.
88
+
89
+ Resolution order: longest matching `index.yml` bank prefix → `~/.config/personal-memory/coding-agent.json` `mapPathToBank` → legacy Hindsight mapping → `coding-agent::<repo-directory-name>`. Details: [Coding-agent scopes](coding-agent-scopes.md).
90
+
91
+ ### Formats
92
+
93
+ Import/export goes through a **format layer**, not a provider-specific dump. **PMM** (Personal Memory Manifest) is the first format; more formats plug into the same orchestrator. See [Portable memory (PMM)](memory-portability.md).
94
+
95
+ ### Session hooks and skill
96
+
97
+ OpenMem installs one **skill** and **session hooks** (SessionStart / prompt / Stop) so agents recall and retain without remembering a vendor SDK. Hook sets are exclusive: `[agents] integration` is `openmem` (default) or `provider`, which swaps in the upstream Hindsight plugin (`--provider-plugin`; `--no-provider-plugin` switches back).
98
+
99
+ ### Indexer
100
+
101
+ A shared **indexer** watches git repos, Obsidian vaults, notes trees, and feed caches, then writes through the active provider’s sink. Sources are provider-agnostic; only the sink changes with `MEMORY_PROVIDER`.
102
+
103
+ ## What OpenMem is not
104
+
105
+ - A replacement for Hindsight, mem0, or Cognee product UIs
106
+ - A guarantee that memories move losslessly between backends (export/import is explicit)
107
+ - A multi-provider fan-out in one process (one active provider; switch to change)
108
+
109
+ ## Related projects and inspiration
110
+
111
+ OpenMem is not affiliated with these projects.
112
+
113
+ | Project | Role here |
114
+ | --- | --- |
115
+ | [OpenFeature](https://openfeature.dev/) | Vendor-agnostic interface over pluggable providers — the model for `memory_*` over memory backends |
116
+ | [mise](https://mise.jdx.dev/) / [asdf](https://asdf-vm.com/) | One CLI over many backends with declarative config |
117
+ | [Hindsight](https://github.com/vectorize-io/hindsight) · [mem0](https://github.com/mem0ai/mem0) · [Cognee](https://github.com/topoteretes/cognee) | Memory providers |
118
+ | [Hindsight coding agents](https://www.npmjs.com/package/@vectorize-io/hindsight-coding-agents) | Reference for knowledge pages, initiatives and ingest status; replaced by OpenMem hooks in `openmem` mode |
119
+ | [LiteLLM](https://github.com/BerriAI/litellm) | Model proxy for Hindsight; hosts the [context proxy](context-proxy.md) |
120
+ | [Mainplane](https://github.com/mainplane-ai/mainplane) | Smart compaction model: fresh context plus a pointer to the saved transcript, retrieved on demand |
121
+ | [rtk](https://github.com/rtk-ai/rtk) | Shell-output compaction (`rtk init` when installed); its proxy's pointer-and-retrieve compression of large tool results inspired the context proxy's |
122
+ | [Meridian](https://github.com/rynfar/meridian) | Claude subscription as an Anthropic-compatible API; optional Hindsight model backend ([guide](meridian-litellm.md)) |
123
+ | [OpenCode](https://opencode.ai/) · [Claude Code hooks](https://code.claude.com/docs/en/hooks) · [Codex hooks](https://developers.openai.com/codex/hooks) | Agent APIs OpenMem translates between ([compatibility](compatibility.md)) |
124
+
125
+ ## Next
126
+
127
+ | Doc | Use when |
128
+ | --- | --- |
129
+ | [Quickstart](quickstart.md) | First install |
130
+ | [MCP setup](mcp-setup.md) | Unified `memory_*` MCP, native mem0/Cognee, NotebookLM |
131
+ | [Portable memory (PMM)](memory-portability.md) | Export/import between providers |
132
+ | [Architecture](architecture.md) | Runtime data paths |
133
+ | [Provider status](provider-status.md) | Which backends ship |
134
+ | [Compatibility](compatibility.md) | What each agent and provider supports, with evidence level |
135
+ | [mem0](mem0.md) · [Hindsight](hindsight.md) · [Cognee](cognee.md) | Provider operator guides |
@@ -0,0 +1,255 @@
1
+ # Context management
2
+
3
+ Context management (smart compaction) keeps long coding-agent sessions under a token limit without an LLM summary. Every message goes into a local SQLite session store; older turns leave the context and the turn they precede carries a pointer to them; the agent searches the store when it needs a detail back.
4
+
5
+ <p align="center">
6
+ <picture>
7
+ <source media="(prefers-color-scheme: dark)" srcset="https://bcmyguest.github.io/openmem/media/smart-compaction-dark.gif" />
8
+ <img src="https://bcmyguest.github.io/openmem/media/smart-compaction.gif" alt="Animation: the model context fills past trim_at_tokens; the system prompt, tools and task turn stay, older turns and the oldest tool step of the in-flight turn slide into the saved transcript, the reply before the cut stays, the cut turn gets the continuing-session pointer prepended, the last three tool steps stay, the context drops to the target, and one turn-2 excerpt returns through smart_compact_search" width="720" />
9
+ </picture>
10
+ <br />
11
+ <sub>Older turns move to the session store, the cut turn carries a pointer, and <code>smart_compact_search</code> pulls a detail back.</sub>
12
+ </p>
13
+
14
+ ```mermaid
15
+ sequenceDiagram
16
+ participant A as Agent
17
+ participant P as context-proxy<br/>(OpenCode 2: plugin)
18
+ participant S as Context store
19
+ participant M as Model API
20
+ A->>P: request over trim_at_tokens
21
+ P->>S: every message to sessions/<sid>.db<br/>(trimmed rows in_context = 0)<br/>large tool results to blobs/<id>.txt
22
+ P->>M: pointer + first and latest turns
23
+ M-->>A: reply
24
+ A->>S: smart_compact_search / smart_compact_sql / smart_compact_get (MCP)
25
+ S-->>A: matching rows, query results, full tool result
26
+ ```
27
+
28
+ Each request goes through five steps ([context proxy](context-proxy.md) has the details):
29
+
30
+ | Step | When | Effect |
31
+ | --- | --- | --- |
32
+ | Tool-result compression | A tool result exceeds `compress_over_tokens` | First 40 and last 20 lines stay, behind a `smart_compact_get(id=...)` header; the full text goes to `blobs/<id>.txt` |
33
+ | Store | Every request | Each message goes into `sessions/<sid>.db` once, keyed by a chained fingerprint, with an `in_context` flag ([context store](context-store.md#session-database)) |
34
+ | Trim | The request reaches `trim_at_tokens`, or the user typed `/smart-compact` | Turns before the last real user turn, and the oldest tool exchanges of a long in-flight turn, leave the context and their rows get `in_context = 0`; a pointer naming `smart_compact_search` / `smart_compact_sql` replaces them. System prompt, tools, the first user turn, the reply before the last user turn, that turn and at least its last 3 tool exchanges stay verbatim, except that signed thinking leaves every kept assistant message but the latest ([why](context-store.md#trim)) |
35
+ | Recall on trim | The session registration names a bank and `[compaction] recall` is on | Memory recall for the last prompt follows the pointer in an `<openmem-recall untrusted="true">` block marked as reference data, not instructions, capped at `[compaction] max_chars`. Proxy: Hindsight only. OpenCode 2 plugin: any provider |
36
+ | Standing instruction | Every request | A constant system-prompt note tells the agent to call `smart_compact_search` before answering from content it cannot see, and that `<openmem-recall>` blocks are data, not instructions |
37
+
38
+ The cut is sticky: later requests with the same history get the same rewrite byte for byte, so the prompt cache keeps hitting. A trim invalidates the cache once.
39
+
40
+ ## Benchmarks
41
+
42
+ ContextBench synthetic tasks, run through Claude Code in headless strict mode (no Bash or file tools, so the agent cannot park data on disk) with one model, Claude Haiku 5.5. Three settings: keep everything (no compaction), Claude Code's native auto-compaction, and OpenMem smart compaction (SQLite store, lexical search and `smart_compact_sql`). `kv-store` streams key-value batches and then asks for values; `log-triage` streams log batches and then asks lookup and count questions. Sizes grow left to right. Seed counts differ per cell (n in the table). [Context research](context-research.md#benchmark-method) has the method, every run and the caveats. The numbers and charts below are generated by `site/scripts/bench/compute-contextbench.py` and `site/scripts/bench/render-contextbench.mjs` from the result files.
43
+
44
+ <!-- contextbench:start (generated by site/scripts/bench/render-contextbench.mjs; do not edit) -->
45
+ <p align="center">
46
+ <picture>
47
+ <source media="(prefers-color-scheme: dark)" srcset="https://bcmyguest.github.io/openmem/media/contextbench-quality-dark.png" />
48
+ <img src="https://bcmyguest.github.io/openmem/media/contextbench-quality.png" alt="Line charts of answer quality by task size (kv-store score, log-triage score, lookup and count questions) for OpenMem smart compaction, native compaction and keep everything. Largest sizes. kv-store R16000: OpenMem 1.00, native 0.00, keep everything 0.00. log-triage L12800: OpenMem 1.00, no native trial, keep everything 0.38." width="480" />
49
+ </picture>
50
+ </p>
51
+
52
+ Source: [ContextBench](https://github.com/RulinShao/ContextBench) synthetic tasks, Claude Haiku 5.5. Score is reward_lenient (tag-insensitive), the mean over the completed seeds of each cell. † a seed dropped the `#tag` from its kv answers and scored 0 under the strict reward; the values were right. ‡ the harness step limit stopped the trial before any question, so the 0 is not a recall score.
53
+
54
+ | Task, size | OpenMem smart compaction | Native compaction | Keep everything |
55
+ | --- | --- | --- | --- |
56
+ | kv-store R1000 | 1.00, n = 3 | 1.00, n = 1 | 1.00, n = 1 |
57
+ | kv-store R4000 | 1.00 †, n = 3 | 0.04, n = 1 | 1.00, n = 2 |
58
+ | kv-store R16000 | 1.00 †, n = 3 | 0.00, n = 1 | 0.00 ‡, n = 2 |
59
+ | log-triage L1600 | 0.90 (lookup 1.00, count 0.81), n = 3 | 0.50 (lookup 1.00, count 0.00), n = 1 | 0.71 (lookup 1.00, count 0.42), n = 1 |
60
+ | log-triage L6400 | 1.00 (lookup 1.00, count 1.00), n = 3 | 0.08 (lookup 0.17, count 0.00), n = 2 | 0.46 (lookup 0.92, count 0.00), n = 2 |
61
+ | log-triage L12800 | 1.00 (lookup 1.00, count 1.00), n = 3 | no completed trial | 0.38 (lookup 0.75, count 0.00), n = 1 |
62
+
63
+ ### Time
64
+
65
+ <p align="center">
66
+ <picture>
67
+ <source media="(prefers-color-scheme: dark)" srcset="https://bcmyguest.github.io/openmem/media/contextbench-compaction-dark.png" />
68
+ <img src="https://bcmyguest.github.io/openmem/media/contextbench-compaction.png" alt="Bar chart of seconds per compaction event, median bars with 10th to 90th percentile whiskers. Median native summary call 10.7 s; median OpenMem request that carried a trim 4.0 s, other OpenMem requests 1.7 s." width="480" />
69
+ </picture>
70
+ </p>
71
+
72
+ Native compaction is a model call. Claude Code's summary call took median 10.7 s (p10–p90 7.6–16.1 s, n = 22); Claude Code's own compaction timer read median 12.4 s (p10–p90 9.1–17.7 s, n = 22). The summary calls are the requests after which the next prompt drops below half its size; that finds 22 of the 22 compactions Claude Code reported.
73
+
74
+ Smart compaction makes no model call: the proxy cuts the history in its pre-call hook and records no separate hook time. The request that carried a trim took median 4.0 s (p10–p90 3.1–5.4 s, n = 81), against median 1.7 s (p10–p90 1.4–2.5 s, n = 2723) for the other requests of the same run. That request also rewrites the prompt cache for the shortened history, so the difference is an upper bound on the trim's own time, assuming LiteLLM's request timer covers the pre-call hook (not verified).
75
+
76
+ <p align="center">
77
+ <picture>
78
+ <source media="(prefers-color-scheme: dark)" srcset="https://bcmyguest.github.io/openmem/media/contextbench-time-dark.png" />
79
+ <img src="https://bcmyguest.github.io/openmem/media/contextbench-time.png" alt="Line charts of agent wall time per trial by task size for kv-store and log-triage. At log-triage L12800 an OpenMem trial took 24.4 min and a keep-everything trial 13.5 min. The runs had different API load, so treat the gap as rough." width="480" />
80
+ </picture>
81
+ </p>
82
+
83
+ | Setting | Trials | Agent wall time per trial, mean | Requests | Request latency, mean | Prompt-cache hit rate |
84
+ | --- | --- | --- | --- | --- | --- |
85
+ | OpenMem smart compaction | 18 | 12.3 min | 2804 | 1.99 s | 95% |
86
+ | Native compaction | 6 | 8.1 min | 729 | 0.86 s | 93% |
87
+ | Keep everything | 9 | 9.7 min | 1077 | 0.59 s | 99% |
88
+
89
+ Wall time per trial is longer with OpenMem (593 s vs 472 s for native compaction); OpenMem also makes more requests per trial (130 vs 117), which include its store searches.
90
+
91
+ Source: [ContextBench](https://github.com/RulinShao/ContextBench) synthetic tasks, Claude Haiku 5.5.
92
+
93
+ Wall time and request latency come from different runs: the baselines (run B) and smart compaction (run D) used different LiteLLM builds, ran at different times under different API load, and only run D had the OpenMem hook. Treat the comparison as rough.
94
+
95
+ ### Cost and prompt cache
96
+
97
+ <p align="center">
98
+ <picture>
99
+ <source media="(prefers-color-scheme: dark)" srcset="https://bcmyguest.github.io/openmem/media/contextbench-cost-dark.png" />
100
+ <img src="https://bcmyguest.github.io/openmem/media/contextbench-cost.png" alt="Line charts of list-price cost per trial by task size for kv-store and log-triage. At log-triage L12800 keep everything cost $6.12 per trial and OpenMem $0.74. Where both ran, OpenMem cost within 15% of native compaction." width="480" />
101
+ </picture>
102
+ </p>
103
+
104
+ <p align="center">
105
+ <picture>
106
+ <source media="(prefers-color-scheme: dark)" srcset="https://bcmyguest.github.io/openmem/media/contextbench-cache-dark.png" />
107
+ <img src="https://bcmyguest.github.io/openmem/media/contextbench-cache.png" alt="Line charts of prompt-cache hit rate by task size for kv-store and log-triage. Prompt-cache hit rate, cells where native ran: OpenMem 95%, native 93%. A trim rewrites the cache once; the cut is then stable." width="480" />
108
+ </picture>
109
+ </p>
110
+
111
+ Source: [ContextBench](https://github.com/RulinShao/ContextBench) synthetic tasks. Cost applies Claude Haiku 5.5 list prices to each request’s token counts. Cache hit rate is cache reads over all prompt tokens (input, cache reads and cache writes) of a cell’s requests. Peak prompt is the largest single request.
112
+
113
+ | Task, size | List-price cost per trial (smart / native / keep) | Prompt-cache hit rate (smart / native / keep) | Tokens per trial, cache reads in parentheses (smart / native / keep) | Peak prompt (smart / native / keep) |
114
+ | --- | --- | --- | --- | --- |
115
+ | kv-store R1000 | $0.05 / $0.05 / $0.05 | 97% / 97% / 97% | 2.9M (2.8M) / 2.8M (2.7M) / 2.8M (2.7M) | 101k / 97k / 96k |
116
+ | kv-store R4000 | $0.22 / $0.24 / $1.05 | 93% / 88% / 98% | 5.0M (4.6M) / 3.4M (3.0M) / 15.8M (15.4M) | 119k / 120k / 358k |
117
+ | kv-store R16000 | $0.85 / $0.88 / $3.64 | 90% / 88% / 98% | 13.8M (12.4M) / 12.8M (11.2M) / 56.0M (55.0M) | 119k / 119k / 979k |
118
+ | log-triage L1600 | $0.27 / $0.31 / $0.57 | 98% / 99% / 98% | 9.0M (8.9M) / 8.6M (8.4M) / 10.2M (9.9M) | 120k / 120k / 178k |
119
+ | log-triage L6400 | $0.32 / $0.36 / $1.88 | 96% / 95% / 99% | 10.6M (10.2M) / 8.7M (8.2M) / 32.6M (32.2M) | 120k / 123k / 386k |
120
+ | log-triage L12800 | $0.74 / — / $6.12 | 96% / — / 99% | 18.6M (17.7M) / — / 109.4M (108.6M) | 120k / — / 765k |
121
+ <!-- contextbench:end -->
122
+
123
+ ### Latest version: search-tools note and cache-stable trims
124
+
125
+ The charts and tables above are run D. Three changes since, measured on one task (log-triage L1600 seed 44):
126
+
127
+ 1. **Haiku trims at 95k** instead of about 120k, so every request stays under the 100k line where list prices rise 5× per token.
128
+ 2. **The trim note names the search tools.** Without it the model never searched its trimmed history on this task and answered 16 of 24 questions UNKNOWN. With it, every trial searched and answered every question.
129
+ 3. **Trims keep the prompt cache intact.** A trim now drops the model's thinking from before the cut and keeps the thinking after it. The start of the prompt stays identical from one request to the next, so the cache keeps hitting.
130
+
131
+ | Version | Trials | Score | Cost per trial | Cache hit rate | Tokens written to cache | Largest prompt | Wall time |
132
+ | --- | --- | --- | --- | --- | --- | --- | --- |
133
+ | 95k trim | 1 | 0.17 | $0.15 | 93% | 404k | 94k | 7.9 min |
134
+ | + search-tools note | 5 | 0.94 | $0.22 | 92% | 598k | 95k | 9.8 min |
135
+ | + cache-stable trims | 5 | 1.00 | $0.11 | 98% | 163k | 95k | 7.3 min |
136
+
137
+ The cache fix halves mean cost, and all 5 trials scored 1.00. Most of the gap is one search-tools-only trial that trimmed 3 times, wrote 2.2M tokens to the cache and cost $0.59; without it that version averages $0.13.
138
+
139
+ For scale, the same size on seed 42 (one trial each, a different task instance): keep everything scored 0.71 at $0.57 with a 178k largest prompt; native compaction scored 0.50 at $0.31. The latest version costs about a fifth of keep everything and a third of native compaction. Most of that saving is the price tier, not token count: total tokens differ by less than 1.5× (6.9M against 10.2M). Treat the ratios as rough until the baselines run on seed 44. [Context research](context-research.md) has every trial.
140
+
141
+ ### Compact-Bench (real coding sessions)
142
+
143
+ [Compact-Bench](https://github.com/cai-jianfeng/compact-bench)'s Task Preservation track resumes real SWE-bench repair sessions after compaction and checks whether the task is preserved, damaged, rescued or left unsolved, per method: full history, Claude Code summary, Codex summary and OpenMem.
144
+
145
+ <!-- compact-bench:start (generated by site/scripts/bench/render-contextbench.mjs; do not edit) -->
146
+ Compact-Bench results pending. The run is in progress; partial numbers are not published. Methods: Full history, Claude Code summary, Codex summary, OpenMem.
147
+ <!-- compact-bench:end -->
148
+
149
+ ## When to turn it on
150
+
151
+ | Turn it on when | Leave it off when |
152
+ | --- | --- |
153
+ | Sessions grow past `trim_at_tokens` (default 120,000 estimated tokens, 95,000 for Haiku) | Sessions stay under that limit |
154
+ | You want trimmed turns searchable instead of summarized away | You cannot run the `context-proxy` container (Claude Code, OpenCode 1 and Codex send every request to it and fail while it is down) |
155
+ | You use Claude Code, Codex or OpenCode (OpenCode 2 needs no container) | You use only Cursor CLI (not routed through the proxy) |
156
+
157
+ The thresholds are untuned defaults.
158
+
159
+ ## Turn it on or off
160
+
161
+ The switch is per agent. `[context_proxy.agents]` holds one key per agent; a missing key follows the master switch `[context_proxy] enabled`. With neither set, OpenCode 2 is on (it needs no service) and the proxy-routed agents are off.
162
+
163
+ ```toml
164
+ [context_proxy]
165
+ enabled = true # master switch
166
+ [context_proxy.agents] # optional overrides; missing = follow `enabled` (unset: opencode2 on, others off)
167
+ claude-code = true
168
+ opencode1 = true
169
+ codex = false
170
+ opencode2 = true # native plugin, no service
171
+ ```
172
+
173
+ | Key | Served by | Needs the `context-proxy` service |
174
+ | --- | --- | --- |
175
+ | `claude-code` | `context-proxy` | yes |
176
+ | `opencode1` | `context-proxy` | yes |
177
+ | `opencode2` | OpenMem's OpenCode 2 plugin | no |
178
+ | `codex` | `context-proxy` service, ChatGPT login kept (`/backend-api/codex`) | yes |
179
+
180
+ | Where | How |
181
+ | --- | --- |
182
+ | Setup wizard | Step 8 "Context management (smart compaction)": tick the installed agents to trim |
183
+ | CLI | `npx @bcmyguest/openmem context on [--agents a,b]` / `context off [--agents a,b]`; without `--agents`, all four |
184
+ | Config | Edit the keys above, then `install-coding-agent --yes` |
185
+ | Installer flag | `install-coding-agent --yes --context-proxy` / `--no-context-proxy`: every agent on or off for this run; saves `enabled` |
186
+
187
+ - **Wizard.** The step offers Claude Code, OpenCode 1, Codex and OpenCode 2 when installed. Each default is the saved key, else a saved `enabled`, else on for OpenCode 2 and off for the proxy-routed agents. The step saves all four keys and sets `enabled` to whether any is on. When a proxy-routed agent is on, it writes the proxy key file (`~/.config/personal-memory/context-proxy.env`, mode 600) and the store directories, then starts the service with the provider's compose profile plus `--profile context-proxy` (`up -d --no-build context-proxy`). When none is, it stops a running `context-proxy` after the coding-agent step, unless Claude Code, OpenCode 1 or Codex still points at it. `--dry-run` prints the plan and writes nothing.
188
+ - **CLI.** `context on|off` sets the named agents, keeps the others at their current value, sets `enabled` to whether any agent is on, and re-runs `install-coding-agent --yes`. `--dry-run` prints the save and runs the installer with `--dry-run`, which plans from the saved config. `context status [--json]` lists each agent (on/off, saved, following `enabled`, or default; how it is served), whether the proxy answers on its port, and which agents point at it. It then summarizes the last 24 hours of [events](context-store.md#events) from the session store, without docker: trims, estimated tokens saved, messages dropped, fail-opens (request sent untrimmed) with the newest five errors, the prompt cache from `usage` events (requests, how many reported cache counts, hit ratio = cache read / (read + creation + uncached input) over those requests, cache-creation tokens on requests that made a new cut vs the rest with their averages, and, for the proxy's Anthropic route, the `cache_control` breakpoint count distribution and the five most common layouts), and whether the proxy code in the last event matches the checkout (restart the service when it does not).
189
+ - **Installer.** Without a flag, `install-coding-agent` points Claude Code and OpenCode 1 at the proxy according to their keys. When the stack root has a `compose.yaml`, it also starts the service (before pointing agents at it) if `claude-code` or `opencode1` is on, and stops a running service (after unpointing them) if neither is, unless an agent still points at it.
190
+ - **OpenCode 2.** The plugin reads `opencode2`, else `enabled`, else trims (no service needed), when OpenCode starts it. An explicit `opencode2 = false`, or `enabled = false` without an `opencode2` key, turns it off. `CONTEXT_PROXY_ENABLED` in OpenCode's environment overrides the config: `0` turns the trim off, `1` on.
191
+
192
+ `/smart-compact` in a session forces a trim on the next request.
193
+
194
+ ## Agent support
195
+
196
+ From the [compatibility matrix](compatibility.md):
197
+
198
+ | Agent | Trim and compression | How | Evidence |
199
+ | --- | --- | --- | --- |
200
+ | Claude Code | ✅ | `context-proxy` through `ANTHROPIC_BASE_URL` in `~/.claude/settings.json` | Verified live with `claude -p` through LiteLLM v1.106.6; installer wiring test-covered only |
201
+ | Codex CLI | ✅ | `context-proxy` through `model_provider = "openmem"` and `[model_providers.openmem]` in `~/.codex/config.toml` | Verified live once with `codex exec` (ChatGPT login) through LiteLLM v1.109.0-dev.1, the only release with the subscription pass-through; installer wiring test-covered only ([footnote 9](compatibility.md#coding-agents)) |
202
+ | Cursor CLI | ❌ | Not wired | Custom model base URL support unverified |
203
+ | OpenCode 1.x | ◐ | `context-proxy` through `provider.anthropic.options` | Whether OpenCode applies the `headers` option to its built-in Anthropic provider is unverified |
204
+ | OpenCode 2.x | ✅ native | OpenMem's OpenCode 2 plugin trims in its `context` hook; never uses the proxy | Checked once live with OpenCode 2.2.1 against fake model and Hindsight servers |
205
+
206
+ OpenCode 2 differs in two ways:
207
+
208
+ - The plugin trims in-process, so its `opencode2` key needs no service.
209
+ - With `[compaction] native = true` (the default), the plugin also replaces OpenCode's own compaction summary with the pointer plus recall. No other agent has this.
210
+
211
+ Claude Code keeps its own auto-compact on as a safety net: if the proxy is down, nothing else bounds the session. The two overlap. Claude Code counts tokens over its full local transcript, not the trimmed request the proxy sends, so `/context` reads higher than the API's reported usage and Claude Code can compact a session the proxy has already trimmed. Each such compaction starts a new thread in the trim state, and the proxy trims it afresh.
212
+
213
+ ## Tools agents use
214
+
215
+ All four tools are part of the unified `memory_*` MCP server, read local files only, and work with every memory provider.
216
+
217
+ | Tool | Input | Returns |
218
+ | --- | --- | --- |
219
+ | `smart_compact_search` | `query` (words, FTS5) or `pattern` (JavaScript regex); optional `session_id` from the pointer, `role` (`user`, `assistant`, `tool`, a list, or `all`; default user and tool), `in_context` (`true` rows still in context, `false` trimmed rows; default both), `mode` (`hybrid` or `lexical`), `context` (0-5 neighbouring rows), `limit` (default 8) | Matching rows of the session database (`seq`, `role`, `kind`, `in_context`, excerpt), `total_matches`, `line_matches` (exact count of matching lines in the `lines` table across the session, under the same filters), `databases`, and `blob_ids` for compressed results. Without `session_id`, recent sessions under the workspace's git root |
220
+ | `smart_compact_sql` | `session_id`, `sql` (one read-only `SELECT` or `WITH … SELECT`) | At most 200 rows over `messages`, `lines`, `messages_fts`, `lines_fts`, `events`; 2 s limit. For counts, groupings and joins ([context store](context-store.md#sql)) |
221
+ | `smart_compact_get` | `id` (16 hex characters), optional `offset` | Full text of a compressed tool result, at most 100,000 characters per call; page with `next_offset` |
222
+ | `context_status` | optional `hours` (1-720, default 24) | The `context status` events summary: `trims`, `tokensSaved` (estimated), `failOpens`, `lastErrors`, `latest` writer versions, `checkout_proxy_version` and `proxy_stale` (`null` where the checkout has no `proxy/`, as in the installed agent runtime), and `usage`: `requests`, `withCacheCounts`, token sums, `hitRatio` (`null` without cache counts), `newCut` / `other` cache-creation totals, `breakpoints` (count to requests) and `breakpointLayouts` |
223
+
224
+ The installer does not wire the unified MCP for OpenCode 2 ([compatibility](compatibility.md), footnote 1). The pointer names no file path; a shell-capable agent can query `sessions/<sid>.db` under the store root with `sqlite3`.
225
+
226
+ ## Configuration
227
+
228
+ `~/.config/personal-memory/config.toml` (shipped example: `config/openmem.toml.example`). Environment variables override the file.
229
+
230
+ | Key | Default | Env override | Effect |
231
+ | --- | --- | --- | --- |
232
+ | `[context_proxy] enabled` | unset | none | Master switch: the value of every agent key that is not set. Unset: `opencode2` on, the others off |
233
+ | `[context_proxy.agents] claude-code`, `opencode1`, `codex`, `opencode2` | follow `enabled`; with it unset, `opencode2` on and the others off | `CONTEXT_PROXY_ENABLED` for `opencode2` (in OpenCode's environment) | Per-agent on/off; see [Turn it on or off](#turn-it-on-or-off) |
234
+ | `[context_proxy] port` | `4100` | none (copied to the stack `.env` as `CONTEXT_PROXY_PORT`) | Host port, bound to 127.0.0.1 |
235
+ | `[context_proxy] trim_at_tokens` | `95000` for Haiku, `120000` otherwise | `CONTEXT_TRIM_AT_TOKENS` | Estimated prompt tokens that trigger a trim; unset, the threshold follows the request's model ([settings](context-proxy.md)), and a set value applies to every model; the estimate is calibrated from the provider's reported usage ([token estimate](context-proxy.md#token-estimate)) |
236
+ | `[context_proxy] trim_to_fraction` | `0.3` | `CONTEXT_TRIM_TO_FRACTION` | How deep a trim cuts a long in-flight turn: older tool exchanges stay only while the request is under this fraction of `trim_at_tokens`. Between 0 and 1; another value falls back to 0.3 |
237
+ | `[context_proxy] chars_per_token` | `2.5` | `CONTEXT_CHARS_PER_TOKEN` | Characters per token before a session reports usage |
238
+ | `[context_proxy] compress_over_tokens` | `4000` | `CONTEXT_COMPRESS_OVER_TOKENS` | Tool results above this are compressed; `0` disables compression |
239
+ | `[context_proxy] standing_instruction` | `true` | `CONTEXT_STANDING_INSTRUCTION` | Constant system-prompt note to search trimmed turns |
240
+ | `[compaction] max_chars` | `12000` | `CONTEXT_COMPACTION_MAX_CHARS` | Cap on the recall block (frame included) after a trim or compaction pointer |
241
+ | `[compaction] recall` | `true` | `CONTEXT_COMPACTION_RECALL` | Recall in the trim or compaction pointer |
242
+ | `[compaction] native` | `true` | none | OpenCode 2 only: replace OpenCode's compaction summary with the pointer plus recall |
243
+ | `[context_store] ttl_days` | `30` | `CONTEXT_STORE_TTL_DAYS` | Delete sessions idle longer than this; `0` keeps everything |
244
+ | `[context_store] embedding_model` | empty (off) | `CONTEXT_EMBEDDING_MODEL` | Embedding model for hybrid `smart_compact_search` ([vectors](context-store.md#vectors)) |
245
+ | `[context_store] embedding_url` | `http://127.0.0.1:11434/v1` | `CONTEXT_EMBEDDING_URL` | OpenAI-compatible embeddings endpoint as the host sees it |
246
+ | `[context_store] embedding_timeout_s` | `30` | `CONTEXT_EMBEDDING_TIMEOUT_S` | Seconds per row of a background embeddings call |
247
+ | `[context_store] embedding_max_chars` | `4000` | `CONTEXT_EMBEDDING_MAX_CHARS` | Characters of each row's head that are embedded |
248
+
249
+ `install-coding-agent` and `./setup` copy the trim keys into the stack `.env` the proxy container reads. The store lives at `$OPENMEM_STATE_DIR/context` (default `~/.local/share/openmem/context`).
250
+
251
+ ## More
252
+
253
+ - [Context proxy](context-proxy.md): the compose service, its environment variables and limits.
254
+ - [Context store](context-store.md): file layout, session database schema, search and SQL tools, trim rules and inserted text.
255
+ - [Context research](context-research.md).
@@ -0,0 +1,115 @@
1
+ # Context proxy
2
+
3
+ The `context-proxy` compose service is a LiteLLM proxy that sits between a coding agent and its model API (Anthropic for Claude Code and OpenCode 1, ChatGPT's Codex backend for Codex) and rewrites each request before forwarding it:
4
+
5
+ 1. **Tool-result compression.** A tool result over `CONTEXT_COMPRESS_OVER_TOKENS` is replaced by its first 40 and last 20 lines (capped at 4000 and 2000 characters) behind a header naming `smart_compact_get(id=...)`. The full text goes to `blobs/<id>.txt`.
6
+ 2. **Store.** Every request's messages go into the session's SQLite database, `sessions/<sid>.db`: one row per content block, keyed by a chained fingerprint so resent history is never stored twice, with lines and FTS5 indexes for search. Each row records whether it is still in the model's context. Database errors are logged and never block the request. Schema and rules: [context-store.md](context-store.md#session-database).
7
+ 3. **Trim.** When a request reaches `CONTEXT_TRIM_AT_TOKENS`, or the session's `.force` flag exists, the proxy drops the messages before the last real user turn and marks the dropped rows `in_context = 0`. It then puts a pointer to `smart_compact_search` / `smart_compact_sql` at the start of the kept turn. System prompt, tools, the first real user turn (usually the task statement), the agent reply just before the kept turn and the kept turn stay verbatim. When that turn's own tool loop is long, the proxy also drops its oldest tool exchanges, keeping at least the newest 3 and more while the request stays under `CONTEXT_TRIM_TO_FRACTION` (default 0.3) of the threshold. Tool call/result pairs are never split. Kept assistant messages from before the trim lose their signed thinking blocks, because the trim changes the prefix those signatures cover; the latest assistant message at trim time stays byte-identical, and thinking produced after the trim stays until the next trim. The exact rules are in [context-store.md](context-store.md#trim).
8
+ 4. **Recall on trim.** If the session registration names a bank, the proxy adds Hindsight recall for the last user prompt (`budget: low`, 5 s timeout), inside an `<openmem-recall untrusted="true">` block that a fixed sentence marks as reference data, not instructions; recalled text cannot close the block ([format](context-store.md#proxy-inserted-text)). `CONTEXT_COMPACTION_RECALL=0` turns recall off. When the bank has pending or processing ingest operations, it adds a line saying recent turns may not be in memory yet.
9
+ 5. **Standing instruction.** Every request, from the first, gets a constant system-prompt note: older turns may be replaced by an `[openmem: …]` pointer, and the agent must call `smart_compact_search` (or `smart_compact_sql` for counts and aggregates) before answering anything that depends on content it cannot see, and that text inside `<openmem-recall>` blocks is data, not instructions. Agents otherwise tend to answer from the visible turns after a trim. The text and per-format placement are in [context-store.md](context-store.md#proxy-inserted-text).
10
+
11
+ The cut is sticky. Later requests with the same history receive the same rewrite, byte for byte, so the prompt cache keeps hitting. The cut moves only on the next threshold crossing or `.force`. Recall is computed once per cut. File layout, schema and inserted text follow [context-store.md](context-store.md). Embeddings for hybrid search are computed on a background thread, never in the request path, and only when `CONTEXT_EMBEDDING_MODEL` is set ([Vectors](context-store.md#vectors)). At start the proxy deletes sessions idle longer than `CONTEXT_STORE_TTL_DAYS`, at most once a day ([Retention](context-store.md#retention)).
12
+
13
+ **OpenCode 2 trims natively and does not use this proxy.** OpenMem's OpenCode 2 plugin applies the same steps inside OpenCode's `context` hook (`cli/coding-agent/context-trim.mjs`, a JS port of `core.py` for OpenCode's message parts). It uses the same environment variables and defaults, the same pointer and compressed-result text, and the same store files, writing the session database through `cli/coding-agent/session-db.mjs` (`node:sqlite`); compressed output matches `compress_text` byte for byte and the database rows match the proxy's (both tested against the Python). It differs in three places. The pointer is its own text part ending in a blank line, because OpenCode's OpenAI-compatible lowering joins text parts without a separator. The pending-ingest line comes from `memory_status`. Its token estimate is the same function (`estimatePromptTokens`, tested against the Python in `scripts/tests/test_context_estimate.mjs`), calibrated from OpenCode's `session.step.ended` event: its `tokens` (`TokenUsage.Info`) pair with the context request that preceded the step, and prompt tokens are `input + cache.read + cache.write` (OpenCode fills `input` from the non-cached count). Live delivery of that event to OpenMem's plugin has not been checked; without it the plugin uses `CONTEXT_CHARS_PER_TOKEN`. Recall goes through the session facade, so every provider can answer, not only Hindsight. `scripts/tests/test_context_parity.mjs` runs shared fixture requests through both implementations and diffs the views, database rows, schema and blobs. OpenCode's own compaction is replaced the same way; see [coding agents](coding-agents.md). `CONTEXT_PROXY_ENABLED=0` also turns the native trim off. To keep the request growing only at its end, the plugin's per-prompt recall is a leading text part of the user message that prompted it, bound to that message's fingerprint and repeated byte for byte on later requests; the system prompt gets only a constant session banner and the standing instruction. Trimming and database rows see the messages without the recall part. A plugin restart forgets the bindings, so the next request renders older turns without recall and misses the cache once.
14
+
15
+ Related work and benchmark results behind this design: [context research](context-research.md).
16
+
17
+ Code: `proxy/openmem_context/` (stdlib only), `proxy/openmem_context_hook.py` (LiteLLM `async_pre_call_hook` and `async_log_success_event`), `proxy/config.yaml`. Tests: `python3 -m unittest discover -s proxy/tests` (part of `npm test`). Image check (Docker, manual): `python3 proxy/tests/image/run.py` runs the hook inside the compose image against a fake upstream.
18
+
19
+ The service image is the LiteLLM dev build `v1.109.0-dev.1`, the only release with PR #41522 (ChatGPT-subscription pass-through, which Codex needs); stable releases lack it. Hindsight's own `litellm` service stays on stable. Move the pin to the first stable release with #41522 and rerun the image check.
20
+
21
+ ## Configuration
22
+
23
+ Set these in the stack `.env` or in the shell that runs compose.
24
+
25
+ | Variable | Default | Effect |
26
+ | --- | --- | --- |
27
+ | `CONTEXT_PROXY_ENABLED` | `1` | `0` forwards every request unmodified |
28
+ | `CONTEXT_TRIM_AT_TOKENS` | unset: `95000` for Haiku, `120000` otherwise | Prompt tokens (estimated as below) that trigger a trim. Unset or not an integer, the threshold follows the request's model (`MODEL_TRIM_AT_TOKENS` in `proxy/openmem_context/settings.py`): a model id containing `haiku` trims at 95000, under the higher price that Haiku charges for prompts over 100k tokens. A set value applies to every model |
29
+ | `CONTEXT_TRIM_TO_FRACTION` | `0.3` | A trim keeps older in-turn tool exchanges only while the request stays under this fraction of `CONTEXT_TRIM_AT_TOKENS`; must be between 0 and 1, else the default applies (config.toml `[context_proxy] trim_to_fraction`) |
30
+ | `CONTEXT_CHARS_PER_TOKEN` | `2.5` | Characters per token for the estimate until the session reports usage (config.toml `[context_proxy] chars_per_token`) |
31
+ | `CONTEXT_COMPRESS_OVER_TOKENS` | `4000` | Tool results above this are compressed; `0` disables compression |
32
+ | `CONTEXT_COMPACTION_MAX_CHARS` | `12000` | Maximum characters of the recall block (frame included) inserted after a trim |
33
+ | `CONTEXT_COMPACTION_RECALL` | `1` | `0` leaves recall out of the trim pointer (config.toml `[compaction] recall = false`) |
34
+ | `CONTEXT_STANDING_INSTRUCTION` | `1` | `0` leaves out the standing `smart_compact_search` instruction in the system prompt (config.toml `[context_proxy] standing_instruction = false`) |
35
+ | `CONTEXT_PROXY_PORT` | `4100` | Host port, bound to 127.0.0.1 |
36
+ | `CONTEXT_PROXY_ENV_FILE` | `~/.config/personal-memory/context-proxy.env` | Holds `LITELLM_MASTER_KEY` (and keys for extra routes) |
37
+ | `OPENMEM_STATE_DIR` | `~/.local/share/openmem` | The proxy mounts `<dir>/context` read-write |
38
+ | `CONTEXT_STORE_TTL_DAYS` | `30` | Delete session files idle this many days, checked at start at most once a day; `0` keeps them (config.toml `[context_store] ttl_days`) |
39
+ | `CONTEXT_EMBEDDING_MODEL` | empty | Embedding model for hybrid search; empty turns vectors off (config.toml `[context_store] embedding_model`) |
40
+ | `CONTEXT_EMBEDDING_TIMEOUT_S` | `30` | Seconds per row of a background embeddings call (config.toml `[context_store] embedding_timeout_s`) |
41
+ | `CONTEXT_EMBEDDING_MAX_CHARS` | `4000` | Characters of each row's head that are embedded (config.toml `[context_store] embedding_max_chars`) |
42
+ | `CONTEXT_PROXY_EMBEDDING_URL` | `http://host.docker.internal:11434/v1` | OpenAI-compatible embeddings base URL as the container sees it (Ollama on the host); passed in as `CONTEXT_EMBEDDING_URL` |
43
+ | `CONTEXT_PROXY_HINDSIGHT_URL` | `http://hindsight:8888` | Recall endpoint; the default needs the hindsight profile in the same compose project |
44
+
45
+ The defaults are untuned guesses, except `CONTEXT_CHARS_PER_TOKEN`: real Claude Code requests measured about 2.3 characters per token, so the former characters / 4 estimate let a 120000 threshold fire at 203–236k real prompt tokens.
46
+
47
+ ## Token estimate
48
+
49
+ The trim compares `CONTEXT_TRIM_AT_TOKENS` with an estimate calibrated from the provider's reported usage:
50
+
51
+ 1. `async_pre_call_hook` counts the request's characters (system, tools, messages; 6400 per binary block; the standing instruction excluded) and remembers them under the request's `litellm_call_id`.
52
+ 2. `async_log_success_event` reads the prompt tokens of that call, cache reads and cache writes included, and stores `{chars, tokens}` in the session's `.trim.json`, per conversation thread (keyed like the cut, by the first message's fingerprint) and as the session's latest sample.
53
+ 3. The next request in the thread estimates `tokens + ceil((chars now − chars then) / ratio)`, where `ratio` is that sample's chars per token, bounded to 1–8. A thread with no sample yet (a new subagent) uses the session's latest ratio; a session with no usage uses `CONTEXT_CHARS_PER_TOKEN`.
54
+
55
+ The estimate only decides whether a new cut is chosen. It never changes a stored cut or the rendered request, so calibration keeps the rewrite sticky and cache-stable.
56
+
57
+ Usage sources, read in the LiteLLM source and checked with a fake upstream: v1.106.6 for the first three rows; v1.109.0-dev.1 (`proxy/tests/image/run.py`, 2026-10-29) for all four, each plain and streamed, recording the fake's 4321 prompt tokens:
58
+
59
+ | Call | Success-hook response | Prompt tokens |
60
+ | --- | --- | --- |
61
+ | `/v1/messages` (`anthropic_messages`), plain or streamed | `ModelResponse` built from Anthropic's usage (streams: from the SSE usage events) | `usage.prompt_tokens`, which LiteLLM sums from `input_tokens` + `cache_read_input_tokens` + `cache_creation_input_tokens` |
62
+ | `/v1/chat/completions`, plain or streamed | `ModelResponse` | `usage.prompt_tokens` |
63
+ | `/v1/responses`, plain or streamed | `ResponsesAPIResponse`, or the `response.completed` event | `usage.input_tokens` (cached tokens included) |
64
+ | `/backend-api/codex/responses` (call type `pass_through_endpoint`; the call id comes from the logging object, not the forwarded body) | `ResponsesAPIResponse` from the terminal stream event | `usage.input_tokens` |
65
+
66
+ A raw Anthropic usage object is summed the same way, and `standard_logging_object.prompt_tokens` is the last fallback. Limits: when an upstream reports no usage, LiteLLM substitutes its own `token_counter` estimate and calibration learns that estimate; LiteLLM adds `stream_options.include_usage` itself only for OpenAI-backed chat streams. Anthropic responses with server-side `iterations` report input summed over the iterations, which overstates one prompt and trims early. A call that fails, or a proxy restart between request and response, leaves the session on its previous sample. OpenAI-backed routes were not run live. On the Codex route LiteLLM parses the stream only for a model in its price map (`chatgpt/gpt-6-sol` is); for an unmapped model the success hook gets an unparsed stream and calibration falls back to `CONTEXT_CHARS_PER_TOKEN`.
67
+
68
+ ## Enable
69
+
70
+ `install-coding-agent` writes the env file and points Claude Code, OpenCode 1 and Codex at the proxy. By hand:
71
+
72
+ ```sh
73
+ mkdir -p -m 700 ~/.local/share/openmem/context # compose must not create it as root
74
+ docker compose --profile hindsight --profile context-proxy up -d context-proxy
75
+ ```
76
+
77
+ Claude Code settings (`env`):
78
+
79
+ ```json
80
+ {
81
+ "ANTHROPIC_BASE_URL": "http://127.0.0.1:4100",
82
+ "ANTHROPIC_CUSTOM_HEADERS": "x-litellm-api-key: Bearer <LITELLM_MASTER_KEY>"
83
+ }
84
+ ```
85
+
86
+ Leave `ANTHROPIC_API_KEY` out of the proxy environment. The client's own credentials, including Claude subscription OAuth, then pass through to Anthropic.
87
+
88
+ Codex (`~/.codex/config.toml`, written mode 600; a previous `model_provider` is backed up and restored on disable):
89
+
90
+ ```toml
91
+ model_provider = "openmem"
92
+
93
+ [model_providers.openmem]
94
+ name = "OpenMem context proxy"
95
+ base_url = "http://127.0.0.1:4100/backend-api/codex"
96
+ wire_api = "responses"
97
+ requires_openai_auth = true
98
+ supports_websockets = false
99
+ http_headers = { "x-litellm-api-key" = "Bearer <LITELLM_MASTER_KEY>" }
100
+ ```
101
+
102
+ Codex keeps its ChatGPT login: `requires_openai_auth` makes it send the ChatGPT bearer and `ChatGPT-Account-ID` as it does to chatgpt.com, and LiteLLM's `/backend-api/codex/{models,responses}` route forwards both to `https://chatgpt.com/backend-api/codex`. That route also requires the LiteLLM key in `x-litellm-api-key`, separate from the ChatGPT bearer (`codex_passthrough_endpoints.py`). `openai_base_url` cannot carry it: Codex ignores `http_headers` on the built-in `openai` provider. The hook handles the pass-through only on the `/backend-api/codex/responses` route (Responses adapter, `prompt_cache_key` as session id); other pass-through bodies go through unchanged. A custom provider has no remote compaction in Codex 0.162.0, so Codex compacts locally.
103
+
104
+ The service runs as `HOST_UID:HOST_GID` so the context store stays host-owned. It has its own profile, separate from the `litellm` service Hindsight uses, so Hindsight's model traffic never passes through it.
105
+
106
+ ## Limits
107
+
108
+ - **Codex was checked live once** (one `codex exec` turn: answer, stored turn, usage). Trims, compression and recall on the Codex route ran only against the fake upstream. Whether `codex resume` lists sessions recorded under another `model_provider` is unverified.
109
+ - **Dev image.** The service runs a LiteLLM dev build (`v1.109.0-dev.1`); expect to move the pin.
110
+ - **OpenAI-format clients need routes.** The shipped `"*"` route sends every model name to Anthropic. Add one entry per OpenAI model to `proxy/config.yaml` (see the comment there). Whether an exact model name takes precedence over the `"*"` wildcard has not been tested here.
111
+ - **Claude Code persists large outputs itself.** In Claude Code 2.1.280, a 106 KB Bash output reached the API as a file path plus a 2 KB preview; a 24 KB output arrived inline and was compressed. Compression therefore mostly hits mid-size results.
112
+ - **One worker.** Session state uses an in-process lock; do not run LiteLLM with several workers.
113
+ - **Single host.** The session database uses SQLite WAL, which needs the proxy container and the host MCP server to share memory through the bind-mounted `-shm` file. Linux Docker with a bind mount works; Docker Desktop and network filesystems are unverified.
114
+ - **A trim invalidates the prompt cache once**, on the request that moves the cut; requests after it hit again.
115
+ - **Fail open.** Any error prints one line (exception type and location, never content) to the container log and forwards the request unmodified. Once the session id is known, the error is also written to the session database as a `fail_open` [event](context-store.md#events); each trim writes a `trim` event with estimated tokens before and after. `openmem context status` and the `context_status` MCP tool summarize both without docker, and flag a proxy whose code differs from the checkout.