ihmt-memory 1.2.0__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+
7
+ # Your memory and caches: private, never commit them
8
+ ihmt_memory/
9
+ ihmt_projects/
10
+ demo_workspace/
11
+
12
+ # Local machine configuration (absolute paths)
13
+ .mcp.json
14
+ .claude/settings.local.json
15
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gonzalo Román Márquez (gonzaroman)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,609 @@
1
+ Metadata-Version: 2.5
2
+ Name: ihmt-memory
3
+ Version: 1.2.0
4
+ Summary: Long-term memory for AI coding agents as a tree of plain local files, served over MCP.
5
+ Project-URL: Homepage, https://github.com/gonzaroman/IHMT-MEMORY
6
+ Project-URL: Issues, https://github.com/gonzaroman/IHMT-MEMORY/issues
7
+ Author: Gonzalo Román Márquez
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,claude,llm,long-term-memory,mcp,memory
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: mcp[cli]>=2.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # IHMT — Infinite Hierarchical Memory Tree
19
+
20
+ [![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/gonzaroman/ihmt-memory)
21
+
22
+ <!-- mcp-name: io.github.gonzaroman/ihmt-memory -->
23
+
24
+ **Long-term memory for your AI coding agents.** Tell your agent something once — a decision, how
25
+ your setup works, a correction — and it remembers it in every future session, in any project, with
26
+ any of your agents.
27
+
28
+ ## What IHMT does
29
+
30
+ - **Remembers across sessions.** Decisions and their reasons, your environment, your preferences,
31
+ people and projects, corrections. Your agent searches the memory before answering and saves what
32
+ lasts, so you stop repeating yourself.
33
+ - **One memory for all your agents and models.** Claude Code, Codex and opencode can share the same
34
+ memory: what one saves, the others find. Tested with models from Anthropic, OpenAI, Google and Meta.
35
+ - **Saves tokens.** Instead of pasting your notes or re-explaining context every session, the agent
36
+ retrieves only what the question needs — typically 200–900 tokens, whether the memory holds 50
37
+ entries or 50,000, because search walks a tree instead of reading everything.
38
+ [Honest numbers](GUIDE.md#11-honest-numbers), including where it does *not* save.
39
+ - **Understands time.** When something changes ("I moved to Valencia", "staging is on PostgreSQL 17
40
+ now"), the old memory is kept as history, flagged `OUTDATED`, and searches answer with the current
41
+ one first. Where you live, where you work and your stack are tracked automatically; any other
42
+ correction is linked when the agent saves it with `replaces`. When a question is ambiguous ("Luis"
43
+ — which one?), it asks instead of guessing.
44
+ - **Portable.** Your memory is one folder of plain text files. Copy it to another computer, back it
45
+ up, or put it under version control — it works wherever you put it.
46
+ - **Local, private and readable.** No cloud, no database, no account: IHMT stores everything on your
47
+ disk and sends nothing anywhere. (The memories your agent retrieves reach its model like any other
48
+ context.) Every memory is a text file you can open, and each person who installs IHMT starts with
49
+ their own, empty memory.
50
+
51
+ > **Compatibility.** Officially supported: **Claude Code**. Also tested: **Codex** (CLI and the
52
+ > ChatGPT desktop app — [setup](GUIDE.md#56-using-it-with-codex-tested)) and **opencode**
53
+ > ([setup](GUIDE.md#57-using-it-with-opencode-tested)). IHMT is a standard stdio MCP server, so any
54
+ > agent that supports local MCP servers should work — GitHub Copilot, Antigravity, Cursor, Windsurf,
55
+ > Gemini CLI, Claude Desktop… — and [`INSTALL.md`](INSTALL.md) knows how to configure them, but we have
56
+ > not tested those yet. All of them can share one memory.
57
+
58
+ ## Install
59
+
60
+ ### Before you start
61
+
62
+ | You need | Why |
63
+ |---|---|
64
+ | **Python 3.10 or newer** | IHMT is written in Python |
65
+ | **git** | to download IHMT and keep it updated |
66
+ | **An AI agent that can run commands** (Claude Code, Codex, opencode, Copilot in agent mode…) | it installs IHMT and then uses the memory |
67
+ | **Internet, during the installation** | to download the code and the MCP package; not needed afterwards |
68
+
69
+ **Missing Python or git? Your AI agent installs them for you** (it is instructed to ask you first).
70
+ Python goes in your user folder, with no administrator password, so nothing system-wide changes. On a brand-new Mac, git may need one click:
71
+ Apple shows a window asking to install its command-line tools.
72
+
73
+ On a Mac, note that the `python3` that comes with macOS is version 3.9, which is too old — that is why
74
+ your agent may say Python is missing even though `python3` exists.
75
+
76
+ You do **not** need administrator rights, a database, an account, or any paid service beyond your
77
+ agent. Tested on **macOS** and **Linux** (Ubuntu); on **Windows** the instructions are included but
78
+ not tested yet. Details: [GUIDE.md §3](GUIDE.md#3-requirements).
79
+
80
+ ### Let your AI agent install it
81
+
82
+ Paste this into the AI agent you want to give a memory to — **Claude Code**, **Codex** and
83
+ **opencode** are tested; **GitHub Copilot**, **Antigravity**, **Cursor**, **Windsurf**, **Gemini CLI**,
84
+ **Claude Desktop** and other MCP clients should work too:
85
+
86
+ ```
87
+ Install the IHMT memory MCP server for me from https://github.com/gonzaroman/IHMT-MEMORY — follow the instructions in its INSTALL.md.
88
+ ```
89
+
90
+ The agent follows [`INSTALL.md`](INSTALL.md): it downloads IHMT to `~/IHMT-MEMORY`, keeps your
91
+ memory in `~/.ihmt`, registers the server **with itself only**, adds the usage instructions, and
92
+ tells you what it did. Then start a new session so the memory tools load.
93
+
94
+ **Using several agents?** Paste the same prompt in each one, whenever you want. If IHMT is already
95
+ installed — say you have used it with Claude for months and now want it in Codex — the agent finds
96
+ that installation, updates it if it safely can, and connects to the **same memory**, so it knows
97
+ what you told the others from day one.
98
+
99
+ It needs an agent that can run terminal commands or edit files; chat-only assistants in a browser
100
+ cannot install anything.
101
+
102
+ <details>
103
+ <summary><b>Manual install</b></summary>
104
+
105
+ Requirements: Python 3.10+, git, and your agent's CLI.
106
+
107
+ ```bash
108
+ git clone https://github.com/gonzaroman/IHMT-MEMORY.git ~/IHMT-MEMORY
109
+ cd ~/IHMT-MEMORY
110
+ python3 -m venv .venv
111
+ .venv/bin/pip install -r requirements-mcp.txt
112
+ mkdir -p ~/.ihmt
113
+ ```
114
+
115
+ **Claude Code**
116
+
117
+ ```bash
118
+ claude mcp add ihmt-memory -s user -e IHMT_HOME="$HOME/.ihmt" -- "$PWD/.venv/bin/python" "$PWD/mcp_server.py"
119
+ claude mcp list # ihmt-memory … ✔ Connected
120
+ ```
121
+
122
+ The server name must come before `-e`. Then append
123
+ [`templates/memory-instructions.md`](templates/memory-instructions.md) to `~/.claude/CLAUDE.md`.
124
+
125
+ **Codex** — `codex mcp add ihmt-memory --env IHMT_HOME="$HOME/.ihmt" -- "$PWD/.venv/bin/python" "$PWD/mcp_server.py"`,
126
+ then add `default_tools_approval_mode = "approve"` to the `[mcp_servers.ihmt-memory]` table in
127
+ `~/.codex/config.toml` (above its `env` table) and append the template to `~/.codex/AGENTS.md`.
128
+ [Details](GUIDE.md#56-using-it-with-codex-tested).
129
+
130
+ **opencode** — add an `"ihmt-memory"` entry (`"type": "local"`, `"command": [<python>, <mcp_server.py>]`,
131
+ `"environment": {"IHMT_HOME": <memory folder>}`) to the `"mcp"` object of
132
+ `~/.config/opencode/opencode.json`, and append the template to `~/.config/opencode/AGENTS.md`.
133
+ [Details](GUIDE.md#57-using-it-with-opencode-tested).
134
+
135
+ Windows, the project scope, the graphical setup and troubleshooting are all in the
136
+ [guide](GUIDE.md#4-installation-step-by-step).
137
+
138
+ </details>
139
+
140
+ <details>
141
+ <summary><b>Install from PyPI (uvx)</b></summary>
142
+
143
+ No clone and no venv: [uv](https://docs.astral.sh/uv/) downloads and runs the
144
+ [`ihmt-memory`](https://pypi.org/project/ihmt-memory/) package. The memory goes to `~/.ihmt`; set
145
+ `IHMT_HOME` to use another folder (for example, a memory you already have).
146
+
147
+ **Claude Code**
148
+
149
+ ```bash
150
+ claude mcp add ihmt-memory -s user -- uvx ihmt-memory
151
+ claude mcp list # ihmt-memory … ✔ Connected
152
+ uvx ihmt-memory --instructions >> ~/.claude/CLAUDE.md
153
+ ```
154
+
155
+ **Codex** — `codex mcp add ihmt-memory -- uvx ihmt-memory`, add
156
+ `default_tools_approval_mode = "approve"` to the `[mcp_servers.ihmt-memory]` table in
157
+ `~/.codex/config.toml`, and run `uvx ihmt-memory --instructions >> ~/.codex/AGENTS.md`.
158
+
159
+ **opencode** — add an `"ihmt-memory"` entry (`"type": "local"`, `"command": ["uvx", "ihmt-memory"]`) to
160
+ the `"mcp"` object of `~/.config/opencode/opencode.json`, and run
161
+ `uvx ihmt-memory --instructions >> ~/.config/opencode/AGENTS.md`.
162
+
163
+ The last step matters with any install: the instructions are what make the agent search before
164
+ answering and save corrections. `pipx install ihmt-memory` works too, with `ihmt-memory` in place of
165
+ `uvx ihmt-memory`.
166
+
167
+ </details>
168
+
169
+ **New here? Read [`GUIDE.md`](GUIDE.md)** — everyday use, step by step, with real outputs. The rest
170
+ of this README is the technical reference.
171
+
172
+ ---
173
+
174
+ ## How it works
175
+
176
+ A universal, domain-agnostic long-term memory for LLMs, stored as a recursive tree of plain files on
177
+ the local disk. No vector database, no server, **no third-party dependencies** — Python 3.10+ and the
178
+ standard library.
179
+
180
+ Instead of embedding everything into one flat index and scanning it, IHMT organizes knowledge into a
181
+ tree: raw text leaves at the bottom, recursive JSON summaries above them, and a single `root.json`
182
+ trunk at the top. A query walks that tree — root → branch → branch → leaf — so the number of files
183
+ opened grows with the *depth* of the tree (`≈ beam × log_B(n)`), not with the amount stored.
184
+
185
+ | | Flat RAG | IHMT |
186
+ |---|---|---|
187
+ | Retrieval cost | scan / ANN over all *n* chunks | `beam × log_B(n)` file reads |
188
+ | Structure | none — a bag of vectors | explicit hierarchy, inspectable |
189
+ | Chunking | fixed character windows | syntax-, scene- and date-aware |
190
+ | Stale facts | served silently | superseded, dated, and flagged |
191
+ | Ambiguity | returns a plausible guess | asks you for a clue |
192
+ | Storage | binary index | UTF-8 `.txt` + JSON you can read |
193
+
194
+ ## Quick start
195
+
196
+ ```bash
197
+ python3 gui.py # graphical interface: set up, browse, inspect
198
+ python3 init_ihmt.py # or from the terminal: create ./ihmt_memory
199
+ python3 main.py demo # full walkthrough in ./demo_workspace
200
+ python3 -m unittest discover -v # stdlib only; MCP tests skip without the SDK
201
+ ```
202
+
203
+ Then use it on your own material:
204
+
205
+ ```bash
206
+ python3 main.py ingest ~/notes ~/project/src/Main.java
207
+ python3 main.py consolidate --force
208
+ python3 main.py search "how did we handle stock reservations"
209
+ python3 main.py ask "Luis" # interactive clue loop
210
+ python3 main.py conflicts # what changed over time
211
+ ```
212
+
213
+ As a library:
214
+
215
+ ```python
216
+ from ihmt import IHMT
217
+
218
+ memory = IHMT.initialize("./workspace")
219
+ memory.ingest_file("examples/InventoryService.java")
220
+ memory.ingest_file("examples/journal_personal.txt")
221
+ memory.flush() # close the tree up to the root
222
+
223
+ answer = memory.search("reserveStock soft hold")
224
+ print(answer.best.content) # the leaf
225
+ print(answer.best.path) # ['root', 'N1-software.java-…', 'L-software.java-…']
226
+ print(answer.node_reads) # how many branch files were opened
227
+
228
+ for notice in memory.notices():
229
+ print(notice) # "On 2024-03-11 you said user location = 'Madrid', but on 2026-02-03 you updated to 'Valencia'."
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Graphical interface
235
+
236
+ ```bash
237
+ python3 gui.py # opens a browser at 127.0.0.1
238
+ python3 gui.py --path ~/my-memory --port 8765 --no-browser
239
+ ```
240
+
241
+ Still zero dependencies — the server is `http.server` from the standard library, it listens only on
242
+ the loopback interface, and every `/api/*` call needs the random token carried in the URL it opens.
243
+
244
+ Four screens: **Set up** (pick the memory folder with the system dialog, create the store, choose
245
+ between per-project and global registration, preview the exact command or JSON before anything is
246
+ written), **Explore** (collapsible tree down to the stored text, with supersession notices),
247
+ **Diagnose** (a search that reports confidence, files opened vs. total, and the descent path), and
248
+ **Timeline** (active vs. historical values and the detected contradictions). The interface is
249
+ bilingual (ES/EN) and read-only over the memory: it never deletes or edits a leaf.
250
+
251
+ ---
252
+
253
+ ## Use it from Claude Code (MCP)
254
+
255
+ `mcp_server.py` exposes the tree to Claude Code as nine tools in three families: long-term memory,
256
+ project indexes and session scratch memory. The core stays dependency-free; the
257
+ SDK is an optional extra:
258
+
259
+ ```bash
260
+ python3 -m venv .venv
261
+ .venv/bin/pip install -r requirements-mcp.txt # mcp[cli]>=2.0
262
+ ```
263
+
264
+ For a single project, copy `.mcp.json.example` to that project's `.mcp.json` and fill in the absolute
265
+ paths; Claude Code asks you to approve it on the next session there (`claude mcp list` shows it as
266
+ *Pending approval* until then):
267
+
268
+ ```json
269
+ {
270
+ "mcpServers": {
271
+ "ihmt-memory": {
272
+ "command": "/absolute/path/to/IHMT-MEMORY/.venv/bin/python",
273
+ "args": ["/absolute/path/to/IHMT-MEMORY/mcp_server.py"],
274
+ "env": { "IHMT_HOME": "/absolute/path/to/IHMT-MEMORY" }
275
+ }
276
+ }
277
+ }
278
+ ```
279
+
280
+ or in one command:
281
+
282
+ ```bash
283
+ claude mcp add ihmt-memory --scope user \
284
+ -e IHMT_HOME=/absolute/path/to/IHMT-MEMORY \
285
+ -- /absolute/path/to/IHMT-MEMORY/.venv/bin/python /absolute/path/to/IHMT-MEMORY/mcp_server.py
286
+ ```
287
+
288
+ `IHMT_HOME` selects the store (`$IHMT_HOME/ihmt_memory`), created on first use. Point every project at
289
+ one shared directory for a single cross-project memory, or give each project its own.
290
+
291
+ | Tool | Behaviour |
292
+ |---|---|
293
+ | `search_memory(query, clue=None, detail="compact")` | Walks the tree. Compact output: the best memory with its date and `OUTDATED` notices, one line per other match; `detail="full"` adds ids, tree paths and excerpts. An ambiguous query returns an `AMBIGUOUS` block listing the candidates instead of guessing — call again with `clue`. A query that matches nothing says so, and so does one whose closest entry shares only a stray word with it (`NOT FOUND`). |
294
+ | `save_memory(content, domain="general", content_type="auto", replaces="")` | Classifies, splits and stores the text, extracts dated facts, and keeps the tree consolidated. Reports how it was filed and — if the save contradicts something remembered earlier — the notice to relay to the user. With `replaces` (a few words describing an earlier memory) the save is recorded as its correction: the old memory is flagged `OUTDATED` and searches answer with the new one first. |
295
+ | `mark_outdated(old_id, new_id)` | Flags one memory as corrected by another, when `save_memory` found several candidates for `replaces` and listed their ids. |
296
+ | `project_map(path, detail="files", subpath="")` | Compact map of a codebase: files with their size in tokens and, with `detail="symbols"`, each method with its line range. Built from the sync manifest, without opening leaves. |
297
+ | `find_code(query, path, scope="main", subpath="", clue=None, max_tokens=1500)` | Returns only the symbol that answers the query, as `file:first-last` + code. `scope` is `main` (skip tests), `test` or `all`. |
298
+ | `read_file(path, force=False)` | Reads a file and remembers what it handed out this session: a repeated read answers `UNCHANGED` or only a unified diff. |
299
+ | `note(text)` / `recall(query, clue=None)` | Session scratch memory: survives a context compaction, disappears when the session ends. |
300
+ | `digest_output(text, label="output")` | Condenses a long log to its first lines, errors, failures, test totals and last lines; the full text stays recallable. |
301
+
302
+ ### Saving tokens inside a session
303
+
304
+ Long-term memory saves tokens *between* sessions. The project tools save them *within* one, where the
305
+ cost is reading the same files again and again. `ProjectIndex` (`ihmt/project_index.py`) keeps a
306
+ private store per project under `$IHMT_PROJECTS_DIR` (default `$IHMT_HOME/ihmt_projects`):
307
+
308
+ * **one leaf per symbol** — `code_chunk_mode="symbol"`, so a lookup returns a method, not a file;
309
+ * **checksum sync on every call** — size and mtime first, SHA-256 only for what moved; changed files
310
+ are re-ingested, removed ones deleted, and the branches rebuilt. Code is never served stale;
311
+ * **path-ordered branches** — each file is stamped with its rank in path order, so every branch covers
312
+ neighbouring files and its summary stays meaningful for the descent;
313
+ * **code-aware ranking** — the navigator filters by `scope`/`path_prefix` from the catalog, prefers
314
+ the file a query names, demotes tests unless asked and demotes import lines.
315
+
316
+ Measured on a 55-file Spring Boot project (8 typical questions): reading the files that hold the
317
+ answers costs 3,613 tokens; `find_code` returns the exact method for all 8 in 1,071. The map of the
318
+ project costs 660 tokens against 13,157 to read it whole.
319
+
320
+ Those savings are against an agent that **reads whole files**. In an A/B test with 22 real headless
321
+ Claude Code sessions, Claude preferred batched `grep`/`sed -n` and never called the project tools on
322
+ its own; *forcing* them made sessions 42–71 % more expensive. Keeping the server enabled costs about
323
+ 460 tokens per conversation, since Claude Code loads MCP tools on demand. IHMT's main value is memory
324
+ **between** sessions; treat the project tools as optional, and do not mandate them in `CLAUDE.md`.
325
+
326
+ The server transparently supports MCP SDK 2.x (`MCPServer`), 1.x (`FastMCP`) and the standalone
327
+ `fastmcp` package.
328
+
329
+ The usage instructions in your `~/.claude/CLAUDE.md` ([template](GUIDE.md#54-tell-claude-when-to-use-it-recommended)) tell Claude Code *when* to reach for each tool: search before answering anything that
330
+ depends on earlier sessions, save durable facts with their date, never guess on `AMBIGUOUS`, always
331
+ relay `OUTDATED`, and never store secrets.
332
+
333
+ ---
334
+
335
+ ## Storage layout
336
+
337
+ Everything lives in one relocatable directory:
338
+
339
+ ```
340
+ ihmt_memory/
341
+ root.json # the trunk: domains, topics, top branches, counters
342
+ layer_0/<domain>/*.txt # the leaves: raw UTF-8 text + a strict JSON header
343
+ layers/1/*.json # branches: summaries of leaves
344
+ layers/2..N/*.json # branches: summaries of summaries
345
+ state/catalog.json # index: id -> path, domain, parent, timestamp
346
+ state/facts.json # the fact timeline
347
+ ihmt.config.json # branch factor, token budgets, backend
348
+ ```
349
+
350
+ `root.json` lives *inside* `ihmt_memory/` so the store is self-contained: copy the directory and the
351
+ memory travels with it.
352
+
353
+ **A leaf** is a normal text file that describes itself, so it stays meaningful even if the catalog is
354
+ lost:
355
+
356
+ ```
357
+ <<<IHMT-META
358
+ {
359
+ "leaf_id": "L-software.java-0001-84130ee123",
360
+ "timestamp": "2026-09-09T17:45:00Z",
361
+ "data_type": "CODE",
362
+ "domain": "software.java",
363
+ "tags": ["method:reserveStock", "class:InventoryService", "lang:java", "type:code"],
364
+ "parent_id": "N1-software.java-5571a7baac",
365
+ "span": {"start_line": 43, "end_line": 68},
366
+ "checksum": "sha256:…",
367
+ "extra": {"context": "public final class InventoryService {"}
368
+ }
369
+ IHMT-META>>>
370
+ public Optional<String> reserveStock(Sku sku, int quantity) {
371
+ …
372
+ ```
373
+
374
+ **A branch node** embeds each child's title, excerpt and keywords. That is the detail that makes the
375
+ descent cheap: a branch can be *ranked without opening any of its children*.
376
+
377
+ ---
378
+
379
+ ## The five components
380
+
381
+ ### 1. `UniversalIngestor` — detect, split, enrich, store
382
+
383
+ `DomainDetector` classifies each document by **type** (`CODE`, `NARRATIVE`, `CLINICAL`, `PERSONAL`,
384
+ `PROCESS`, `GENERIC`) and **domain** (`software.java`, `medicine.clinical`, `process.cooking`,
385
+ `personal`, …) from its extension plus lexical signatures in English and Spanish. Both can be
386
+ overridden with `--domain` / `--type`.
387
+
388
+ The type selects the splitter, and every splitter obeys one invariant:
389
+
390
+ > **A logical block is never cut.** If a single block exceeds `max_tokens` it is stored whole and
391
+ > flagged `oversized`. Correctness of the block beats hitting the token budget.
392
+
393
+ * **Code** (`chunkers/code.py`) — Python via the stdlib `ast`; Java/JS/TS/C/C++/C#/Go/Rust/Kotlin/
394
+ Swift/PHP via `BraceScanner`, a character-level scanner that tracks brace depth while skipping
395
+ comments, string literals, char literals, template literals and preprocessor lines. Imports
396
+ coalesce; each class/function is a block; an oversized class splits **per member**, and the
397
+ enclosing class header travels in the leaf's `extra.context` rather than being spliced into the
398
+ text. Concatenating a file's leaves reproduces the file **byte for byte** — asserted in the tests.
399
+ * **Narrative** — paragraph-atomic, with `***`, `---`, `Chapter`/`Capítulo` as hard boundaries. Only
400
+ a paragraph larger than `max_tokens` is split, and then at sentence boundaries.
401
+ * **Temporal** (journals, chats, clinical records) — one dated entry is atomic and a change of date
402
+ is a hard boundary, so a leaf never mixes two encounters. **The date found in the text becomes the
403
+ leaf's timestamp**, which is what makes recency weighting mean *when something was true* rather
404
+ than when it was ingested.
405
+ * **Process** (recipes, protocols, runbooks) — steps and ingredient lists stay attached to their
406
+ heading.
407
+
408
+ Leaf ids are derived from `(domain, source, position, content)`, so re-ingesting an unchanged
409
+ document rewrites the same leaves instead of duplicating them.
410
+
411
+ ### 2. `RecursiveSummarizer` — the Summarization Event
412
+
413
+ It watches layer 0. When `branch_factor` leaves of one domain have no parent, it fires a
414
+ **Summarization Event**: they are condensed into a layer-1 node, the node is written, and *only then*
415
+ are the children stamped with their `parent_id` — so an interrupted run re-processes a group instead
416
+ of orphaning it. The same rule applies from layer 1 to layer 2, and so on, until the tree converges;
417
+ then `root.json` is rewritten.
418
+
419
+ `consolidate()` is idempotent. `flush()` (`--force`) also promotes partial groups so the tree closes
420
+ completely. Anything still unconsolidated is referenced directly by the trunk, so **nothing in the
421
+ store is ever unreachable from the root**.
422
+
423
+ ### 3. `SemanticNavigator` — descent and the Interactive Clue Loop
424
+
425
+ Ranking uses Okapi BM25 over each candidate's title, keywords, tags and excerpt, with field weights
426
+ and document frequencies computed **across the siblings of the current level** — exactly the
427
+ discrimination the descent needs, at no extra I/O cost. The walk keeps a beam of `beam_width`
428
+ branches per level.
429
+
430
+ Confidence blends two independent signals:
431
+
432
+ ```
433
+ confidence = 0.6 × coverage + 0.4 × margin
434
+ ```
435
+
436
+ *Coverage* asks "does this leaf actually contain what was asked?"; *margin* asks "is it
437
+ distinguishable from its rivals?". A common first name scores high on the first and near zero on the
438
+ second — which is precisely when the system must not guess:
439
+
440
+ ```
441
+ $ python main.py ask "Luis"
442
+
443
+ "Luis" is ambiguous (3 memories match this query equally well, confidence 0.62).
444
+ It could belong to any of these branches:
445
+ 1. [personal] journal_personal.txt · 2024-07-22 — Vacaciones en Benidorm con Luis, mi primo…
446
+ 2. [personal] journal_personal.txt · 2026-08-30 — Fin de semana en la playa de El Saler con Luis…
447
+ 3. [personal] journal_personal.txt · 2024-11-30 — Cierre de trimestre… Luis Marín revisó el pull request…
448
+ Give me a clue to narrow it down (e.g. a place, a date, a project):
449
+ > vacaciones en Benidorm
450
+
451
+ query: "Luis + vacaciones en Benidorm" · confidence 0.74 · 5 node reads, 3 leaf reads, depth 2
452
+ 1. [personal] journal_personal.txt · 2024-07-22
453
+ path root → N2-personal-ea34db33ae → N1-personal-59bf8bc291 → L-personal-0002-a17b3c8f35
454
+ ```
455
+
456
+ The clue triggers a **joint cross-reference**: candidates matching *both* term groups are boosted
457
+ (×1.6), candidates matching only one are demoted (×0.7). The loop runs up to `max_clue_rounds`
458
+ times, stops early if the user declines, and never silently converts an ambiguous query into a
459
+ confident answer.
460
+
461
+ `clue_provider` is any callable, so the loop works for a human at a terminal (`input`) or for an
462
+ agent that lets the LLM supply its own follow-up.
463
+
464
+ ### 4. `ConflictResolver` — recency weighting and the timeline
465
+
466
+ Facts are `(subject, attribute, value, timestamp, source_leaf)`, recorded programmatically via
467
+ `record_fact()` or extracted at ingest time by pattern rules (`vivo en X` / `I live in X`,
468
+ `mi stack es Y`, `trabajo en Z`, `Diagnóstico:`, `Tratamiento:`, `Medicación:` …; extend with
469
+ `add_pattern`).
470
+
471
+ Each `(subject, attribute)` keeps a dated timeline. The newest value is `ACTIVE`; every earlier one
472
+ becomes `HISTORICAL` with `superseded_by` and a `valid_from`/`valid_to` interval. **Nothing is
473
+ deleted**, so both questions stay answerable:
474
+
475
+ ```python
476
+ memory.resolver.active_state()["user::location"].value # 'Valencia' (now)
477
+ memory.resolver.state_at("2024-12-31")["user::location"].value # 'Madrid' (back then)
478
+ ```
479
+
480
+ Repeating a value at a later date is a confirmation, not a contradiction. A genuine change produces a
481
+ transparent notice — *"On 2024-03-11 you said user location = 'Madrid', but on 2026-02-03 you updated to
482
+ 'Valencia'."* — and the superseded leaf is annotated, so retrieving outdated material always arrives
483
+ with its correction attached (`SearchResult.notices`).
484
+
485
+ A leaf itself stays `ACTIVE`: what it says was true *on its own date*, and that remains the right
486
+ answer to a historical question. What changes is that it can no longer be read as current.
487
+
488
+ ### 5. Summarization backends
489
+
490
+ ```python
491
+ class SummarizerBackend(Protocol):
492
+ name: str
493
+ def summarize(self, children, *, domain: str, layer: int) -> NodeSummary: ...
494
+ ```
495
+
496
+ * `HeuristicSummarizer` (default) — stdlib extractive summarization: TF term ranking with EN/ES stop
497
+ words plus representative-sentence selection. Offline, deterministic, which is what lets the test
498
+ suite assert on tree shape.
499
+ * `AnthropicSummarizer` (optional) — used only when selected *and* the `anthropic` package and
500
+ `ANTHROPIC_API_KEY` are both present. Every failure path (missing SDK, missing key, network error,
501
+ unparseable reply) falls back to the heuristic backend, so a consolidation is never lost because a
502
+ model was unreachable.
503
+
504
+ ```bash
505
+ python3 init_ihmt.py --backend anthropic # model set by summarizer_model in ihmt.config.json
506
+ ```
507
+
508
+ Any other model or local runtime plugs in by implementing the same protocol and passing it as
509
+ `IHMT(..., backend=MyBackend())`.
510
+
511
+ ---
512
+
513
+ ## CLI reference
514
+
515
+ | Command | Purpose |
516
+ |---|---|
517
+ | `init [--branch-factor N] [--target-tokens N] [--force]` | create the store |
518
+ | `ingest <paths…\|-> [--domain D] [--type T] [--tag X] [--no-consolidate]` | ingest files, directories or stdin |
519
+ | `consolidate [--force]` | run pending Summarization Events |
520
+ | `search <query> [--top-k N] [--full]` | walk the tree |
521
+ | `ask <query> [--clue TEXT] [--top-k N]` | search with the clue loop |
522
+ | `tree [--depth N]` | outline of the hierarchy |
523
+ | `stats`, `facts [--subject S]`, `conflicts [--subject S]` | inspection |
524
+ | `rebuild` | rebuild catalog, timeline and trunk from the files |
525
+ | `demo` | end-to-end walkthrough |
526
+
527
+ `--path` selects the store directory and `--json` emits machine-readable output; both work before or
528
+ after the subcommand.
529
+
530
+ ## Configuration
531
+
532
+ `ihmt_memory/ihmt.config.json`:
533
+
534
+ | Key | Default | Meaning |
535
+ |---|---|---|
536
+ | `branch_factor` | 8 | children per branch; the log base of retrieval cost |
537
+ | `target_tokens` / `max_tokens` | 2000 / 3000 | leaf size target and oversize threshold |
538
+ | `beam_width` | 3 | branches kept alive per level |
539
+ | `confidence_threshold` | 0.45 | below this, ask for a clue |
540
+ | `ambiguity_margin` | 0.18 | score gap under which candidates count as tied |
541
+ | `max_clue_rounds` | 3 | clue-loop iterations |
542
+ | `summarizer_backend` / `summarizer_model` | `heuristic` / `claude-sonnet-5` | summarization |
543
+ | `code_chunk_mode` | `pack` | `symbol` stores one leaf per class member (used by project indexes) |
544
+
545
+ Small corpora deserve a small branch factor — the demo uses `branch_factor=4, target_tokens=400` so a
546
+ handful of documents still builds a genuine multi-layer tree.
547
+
548
+ ## Tests
549
+
550
+ ```bash
551
+ python3 -m unittest discover -v # from the project root
552
+ ```
553
+
554
+ 187 tests — 25 of them for the MCP server, skipped without the SDK — covering: byte-exact
555
+ reconstruction and boundary-depth invariants for Java and Python, scene/date/section atomicity,
556
+ oversized-block handling, leaf header round-trips, catalog recovery, summarization thresholds, upward
557
+ propagation, idempotence, full reachability from the root, descent cost bounds, the clue loop,
558
+ recency weighting, historical preservation, the project indexes, the MCP tools, the graphical
559
+ interface and the CLI.
560
+
561
+ ## Design notes and limits
562
+
563
+ * **Retrieval is a descent, not a scan.** That is the whole point, and it means a branch pruned at
564
+ the trunk is not revisited. Domain-level pruning only happens when a query has actual signal at the
565
+ trunk; if it has none, every domain stays in play and the beam applies one level down. The clue
566
+ loop is the recovery mechanism when the descent goes wide.
567
+ * **Lexical, not semantic.** Matching is BM25 over accent-folded, CamelCase-split tokens: it works in
568
+ any language and needs no model, but it will not match a synonym. Plugging an embedding re-ranker
569
+ into `BM25Ranker` is the natural upgrade; the tree structure does not change.
570
+ * **Token counts are estimated** at ~4 characters per token. Budgets only need to be consistent, not
571
+ exact.
572
+ * **Fact extraction is pattern-based.** The bundled rules cover common English/Spanish phrasings and
573
+ clinical headers; `record_fact()` is the reliable path, and `add_pattern()` extends the rules.
574
+ * **Single-writer.** Writes are atomic (`tmp` + `os.replace`) and catalog rebuilds take a lock file,
575
+ but the store assumes one writer at a time.
576
+ * `initialize(force=True)` discards *derived* state only — branches, catalog, timeline — and detaches
577
+ the surviving leaves so the next consolidation rebuilds the hierarchy. Leaf content is never
578
+ deleted.
579
+
580
+ ## Layout
581
+
582
+ ```
583
+ ihmt/
584
+ api.py IHMT facade wiring everything together
585
+ config.py IHMTConfig
586
+ models.py MemoryLeaf, BranchNode, ChildRef, RootIndex, Fact, Contradiction
587
+ storage.py MemoryStore: atomic I/O, catalog, recovery
588
+ textutils.py tokenizing, keywords, entities, extractive summary, timestamps
589
+ detectors.py DomainDetector
590
+ chunkers/ base · code · narrative · temporal · process · generic
591
+ summarizers.py SummarizerBackend · Heuristic · Anthropic
592
+ universal_ingestor.py UniversalIngestor
593
+ recursive_summarizer.py RecursiveSummarizer
594
+ semantic_navigator.py SemanticNavigator, BM25Ranker, ClueRequest, scope/path filters
595
+ project_index.py ProjectIndex: per-project code cache with checksum sync
596
+ conflict_resolver.py ConflictResolver + timeline manager
597
+ ihmt_gui/ local graphical interface (stdlib only)
598
+ mcp_server.py MCP server: the nine tools
599
+ ihmt_launcher.py the `ihmt-memory` command of the PyPI package (~/.ihmt by default)
600
+ pyproject.toml · server.json PyPI package · MCP Registry entry
601
+ init_ihmt.py · main.py · gui.py · examples/ · tests/
602
+ GUIDE.md installation and usage guide
603
+ INSTALL.md installation instructions for AI agents
604
+ templates/ memory-instructions.md: the usage rules agents append to CLAUDE.md / AGENTS.md
605
+ ```
606
+
607
+ ## License
608
+
609
+ [MIT](LICENSE) © 2026 Gonzalo Román Márquez (gonzaroman)