商业产业 Hacker News AI 2026-10-11T01:02 正文 ok 4069 words

IHMT:以纯文本文件树为 AI 编程 Agent 提供长期记忆

英文标题:🔶 Long-term memory for AI coding agents as a tree of plain files

IHMT 是一款面向 AI 编程 Agent 的本地长期记忆系统,将记忆存储为本地磁盘上的纯文本文件和递归 JSON 树,不依赖云服务、数据库或向量索引。它可让 Claude Code、Codex 和 opencode 共享同一份跨会话、跨项目的记忆,并通过树状检索将典型查询开销控制在约 200—900 tokens,即使记忆条目从 50 条增长到 5 万条。系统能够识别事实更新、将旧信息标记为 `OUTDATED`、在歧义场景主动追问,并支持项目代码索引与会话级临时记忆。对开发者而言,IHMT 提供了一种可审计、可迁移、可版本控制的 Agent 记忆方案,适合减少重复解释上下文和跨工具协作中的信息丢失。
打开原文相关 6 · 机会 1
media2.9 KB media1.2 KB media54.5 KB

中文

IHMT —— 无限层级记忆树(Infinite Hierarchical Memory Tree)

已收录于 mcpservers.org

为你的 AI 编程 Agent 提供长期记忆。 只需告诉 Agent 一次——某个决策、你的环境如何配置,或一项更正——它便会在未来的每次会话、任何项目以及任意 Agent 中记住这些信息。

  • 跨会话记忆。 保存决策及其原因、你的环境、偏好、人员与项目,以及各类更正。Agent 会在回答前搜索记忆,并保存具有长期价值的信息,因此你不必反复说明同一件事。
  • 所有 Agent 和模型共用一份记忆。 Claude Code、Codex 和 opencode 可以共享同一份记忆:一个工具保存的内容,其他工具都能找到。已使用 Anthropic、OpenAI、Google 和 Meta 的模型进行测试。
  • 节省 tokens。 无需在每次会话中粘贴笔记或重复解释上下文,Agent 只会检索当前问题所需的信息——通常为 200—900 tokens;无论记忆中有 50 条还是 50,000 条记录,都是如此,因为搜索会遍历一棵树,而不是读取全部内容。参见真实数据,其中也说明了它在哪些场景下不会节省 token。
  • 理解时间变化。 当信息发生变化时(例如“我搬到瓦伦西亚了”“预发布环境现在使用 PostgreSQL 17”),旧记忆会作为历史保留、标记为 OUTDATED,搜索时优先返回当前信息。你的居住地、工作地点和技术栈会被自动追踪;其他任何更正,只要 Agent 在保存时使用 replaces,也会建立关联。当问题存在歧义时(例如“Luis”——具体是哪一位?),系统会提问,而不是猜测。
  • 可迁移。 你的记忆就是一个存放纯文本文件的文件夹。你可以将其复制到另一台电脑、备份或纳入版本控制——放在哪里都能工作。
  • 本地、私密、可读。 无云端、无数据库、无账号:IHMT 将所有内容保存在你的磁盘上,不会向任何地方发送数据。(Agent 检索到的记忆会像其他上下文一样传给其模型。)每条记忆都是可以直接打开的文本文件,每位安装 IHMT 的用户都从各自独立的空记忆开始。

兼容性。 官方支持:Claude Code。也已测试:Codex(CLI 和 ChatGPT 桌面应用——配置方法)以及 opencode(配置方法)。IHMT 是一个标准的 stdio MCP 服务器,因此任何支持本地 MCP 服务器的 Agent 理论上都可以使用——GitHub Copilot、Antigravity、Cursor、Windsurf、Gemini CLI、Claude Desktop……INSTALL.md 中包含这些工具的配置方法,但尚未实际测试。它们都可以共享同一份记忆。

你需要 原因
Python 3.10 或更高版本 IHMT 使用 Python 编写
git 用于下载 IHMT 并保持更新
能够运行命令的 AI Agent(Claude Code、Codex、opencode、Agent 模式下的 Copilot……) 它会安装 IHMT,随后使用记忆
安装期间可联网 用于下载代码和 MCP 包;之后不再需要联网

缺少 Python 或 git?你的 AI Agent 可以为你安装它们(但会先征求你的同意)。Python 会安装在你的用户目录中,不需要管理员密码,因此不会修改任何系统级内容。在全新的 Mac 上,git 可能需要点击一次:Apple 会弹出一个窗口,询问是否安装其命令行工具。

在 Mac 上请注意,macOS 自带的 python3 版本是 3.9,版本过低——这就是为什么即使 python3 已存在,Agent 仍可能提示缺少 Python。

你不需要管理员权限、数据库、账号,也不需要 Agent 本身之外的任何付费服务。已在 macOS 和 Linux(Ubuntu)上测试;Windows 的说明已提供,但尚未测试。详见:GUIDE.md §3。

让你的 AI Agent 安装

将以下内容粘贴给你希望赋予记忆能力的 AI Agent——已测试 Claude Code、Codex 和 opencode;GitHub Copilot、Antigravity、Cursor、Windsurf、Gemini CLI、Claude Desktop 及其他 MCP 客户端理论上也应可用:

Install the IHMT memory MCP server for me from https://github.com/gonzaroman/IHMT-MEMORY — follow the instructions in its INSTALL.md.

Agent 会按照 INSTALL.md 执行:将 IHMT 下载到 ~/IHMT-MEMORY,将你的记忆保存到 ~/.ihmt,仅在它自身中注册服务器,添加使用说明,并告知你完成了哪些操作。随后请开启一个新会话,以便加载记忆工具。

使用多个 Agent? 只需在每个 Agent 中随时粘贴同一段提示即可。如果 IHMT 已经安装——例如你已经用 Claude 使用了数月,现在希望在 Codex 中使用——Agent 会找到现有安装,在可安全更新时进行更新,并连接到同一份记忆,这样它从一开始就能了解你此前告诉其他 Agent 的内容。

该操作需要能够运行终端命令或编辑文件的 Agent;浏览器中的纯聊天助手无法安装任何内容。

手动安装

要求:Python 3.10+、git,以及你的 Agent CLI。

git clone https://github.com/gonzaroman/IHMT-MEMORY.git ~/IHMT-MEMORY
cd ~/IHMT-MEMORY
python3 -m venv .venv
.venv/bin/pip install -r requirements-mcp.txt
mkdir -p ~/.ihmt

Claude Code

claude mcp add ihmt-memory -s user -e IHMT_HOME="$HOME/.ihmt" -- "$PWD/.venv/bin/python" "$PWD/mcp_server.py"
claude mcp list                     # ihmt-memory … ✔ Connected

服务器名称必须位于 -e 之前。然后将 templates/memory-instructions.md 追加至 ~/.claude/CLAUDE.md。

Codex —— 执行 codex mcp add ihmt-memory --env IHMT_HOME="$HOME/.ihmt" -- "$PWD/.venv/bin/python" "$PWD/mcp_server.py",然后在 ~/.codex/config.toml 的 [mcp_servers.ihmt-memory] 表中(位于其 env 表之前)添加 default_tools_approval_mode = "approve",并将模板追加至 ~/.codex/AGENTS.md。详细说明。

opencode —— 在 ~/.config/opencode/opencode.json 的 "mcp" 对象中添加一个 "ihmt-memory" 条目("type": "local"、"command": [<python>, <mcp_server.py>]、"environment": {"IHMT_HOME": <memory folder>}),并将模板追加至 ~/.config/opencode/AGENTS.md。详细说明。

Windows、项目作用域、图形化配置和故障排除均在指南中。

刚接触 IHMT?请阅读 GUIDE.md ——其中包含日常使用方法、逐步说明和真实输出示例。本 README 的其余部分是技术参考。


一种通用、与领域无关的 LLM 长期记忆方案,以本地磁盘上的纯文件递归树形式存储。无需向量数据库、无需服务器,不依赖第三方组件——只需要 Python 3.10+ 和标准库。

IHMT 不会将所有内容嵌入到单一扁平索引后进行扫描,而是将知识组织为一棵树:底层是原始文本叶节点,其上是递归 JSON 摘要,顶部则是一份 root.json 主干。查询沿该树逐级进行——根 → 分支 → 分支 → 叶节点——因此打开文件的数量随树的深度增长(≈ beam × log_B(n)),而不是随已存储内容的总量增长。

| 扁平 RAG | IHMT
---|---|---
检索成本 | 对全部 n 个分块扫描 / ANN 检索 | beam × log_B(n) 次文件读取
结构 | 无——一袋向量 | 显式层级结构,可检查
分块方式 | 固定字符窗口 | 感知语法、场景和日期
过时事实 | 静默返回 | 替代、标注日期并标记
歧义 | 返回一个看似合理的猜测 | 要求你提供线索
存储 | 二进制索引 | 可读取的 UTF-8 .txt + JSON

python3 gui.py                       # 图形界面:配置、浏览、检查
python3 init_ihmt.py                 # 或通过终端:创建 ./ihmt_memory
python3 main.py demo                 # 在 ./demo_workspace 中运行完整演示
python3 -m unittest discover -v      # 仅标准库;没有 SDK 时会跳过 MCP 测试

然后将其用于你自己的材料:

python3 main.py ingest ~/notes ~/project/src/Main.java
python3 main.py consolidate --force
python3 main.py search "how did we handle stock reservations"
python3 main.py ask "Luis"                    # 交互式线索循环
python3 main.py conflicts                     # 查看哪些信息随时间发生了变化

作为库使用:

from ihmt import IHMT

memory = IHMT.initialize("./workspace")
memory.ingest_file("examples/InventoryService.java")
memory.ingest_file("examples/journal_personal.txt")
memory.flush()                                # 将树向上闭合至根节点

answer = memory.search("reserveStock soft hold")
print(answer.best.content)                    # 叶节点
print(answer.best.path)                       # ['root', 'N1-software.java-…', 'L-software.java-…']
print(answer.node_reads)                      # 打开了多少个分支文件

for notice in memory.notices():
    print(notice)   # "On 2024-03-11 you said user location = 'Madrid', but on 2026-02-03 you updated to 'Valencia'."

python3 gui.py                       # 在 127.0.0.1 打开浏览器界面
python3 gui.py --path ~/my-memory --port 8765 --no-browser

依然是零依赖——服务器使用标准库中的 http.server,仅监听回环接口,并且每个 /api/* 调用都需要 URL 中携带的随机 token。

提供四个界面:设置(通过系统对话框选择记忆文件夹、创建存储、选择按项目或全局注册,并在写入任何内容前预览准确的命令或 JSON)、浏览(可折叠的树状视图,可一直展开至已存储文本,并显示替代通知)、诊断(展示置信度、已打开文件数与总文件数之比,以及下降路径的搜索功能)和时间线(显示当前值与历史值,以及检测到的矛盾)。界面支持双语(西班牙语/英语),且对记忆为只读:绝不会删除或编辑叶节点。


从 Claude Code(MCP)中使用

mcp_server.py 通过三类共九个工具向 Claude Code 暴露该树:长期记忆、项目索引和会话临时记忆。核心仍保持零依赖;SDK 是可选附加组件:

python3 -m venv .venv
.venv/bin/pip install -r requirements-mcp.txt        # mcp[cli]>=2.0

对于单个项目,将 .mcp.json.example 复制为该项目的 .mcp.json,并填写绝对路径;Claude Code 会在该项目的下一个会话中要求你批准它(在此之前,claude mcp list 会将其显示为 Pending approval):

{
  "mcpServers": {
    "ihmt-memory": {
      "command": "/absolute/path/to/IHMT-MEMORY/.venv/bin/python",
      "args": ["/absolute/path/to/IHMT-MEMORY/mcp_server.py"],
      "env": { "IHMT_HOME": "/absolute/path/to/IHMT-MEMORY" }
    }
  }
}

或者使用一条命令:

claude mcp add ihmt-memory --scope user \
  -e IHMT_HOME=/absolute/path/to/IHMT-MEMORY \
  -- /absolute/path/to/IHMT-MEMORY/.venv/bin/python /absolute/path/to/IHMT-MEMORY/mcp_server.py

IHMT_HOME 选择存储位置($IHMT_HOME/ihmt_memory),首次使用时会自动创建。将每个项目都指向同一个共享目录,即可获得一份跨项目的统一记忆;也可以为每个项目分配各自独立的目录。

工具 行为
search_memory(query, clue=None, detail="compact") 遍历树。紧凑输出包括:最佳记忆及其日期和 OUTDATED 通知、其他每项匹配的一行摘要;detail="full" 会增加 ID、树路径和摘录。歧义查询会返回列出候选项的 AMBIGUOUS 区块,而不是猜测——请携带 clue 再次调用。没有任何匹配的查询会明确提示;与最近条目仅共享一个孤立词语的查询也会提示 NOT FOUND。
save_memory(content, domain="general", content_type="auto", replaces="") 对文本进行分类、拆分并存储,提取带日期的事实,并保持树处于已整合状态。会报告内容如何归档;如果新保存内容与此前记忆相矛盾,也会报告应转告给用户的通知。使用 replaces(几句话描述较早的记忆)时,保存会被记录为对旧记忆的更正:旧记忆被标记为 OUTDATED,搜索优先返回新记忆。
mark_outdated(old_id, new_id) 当 save_memory 为 replaces 找到多个候选项并列出其 ID 时,将一条记忆标记为已被另一条记忆更正。
project_map(path, detail="files", subpath="") 代码库的紧凑地图:显示文件及其 token 大小;使用 detail="symbols" 时,显示每个方法及其行号范围。该地图基于同步清单构建,无需打开叶节点。
find_code(query, path, scope="main", subpath="", clue=None, max_tokens=1500) 仅返回回答查询所需的符号,格式为 file:first-last + 代码。scope 可为 main(跳过测试)、test 或 all。
read_file(path, force=False) 读取文件,并记住本次会话中已提供的内容:重复读取时会返回 UNCHANGED,或仅返回统一 diff。
note(text) / recall(query, clue=None) 会话临时记忆:可跨越上下文压缩,但会在会话结束时消失。
digest_output(text, label="output") 将长日志压缩为开头几行、错误、失败项、测试汇总和末尾几行;完整文本仍可通过 recall 取回。

在单个会话中节省 tokens

长期记忆在会话之间节省 tokens。项目工具则在单个会话之内节省 tokens:在这里,成本来自反复读取相同的文件

English / 原文

IHMT — Infinite Hierarchical Memory Tree

Listed on mcpservers.org

Long-term memory for your AI coding agents. Tell your agent something once — a decision, how your setup works, a correction — and it remembers it in every future session, in any project, with any of your agents.

  • Remembers across sessions. Decisions and their reasons, your environment, your preferences, people and projects, corrections. Your agent searches the memory before answering and saves what lasts, so you stop repeating yourself.
  • One memory for all your agents and models. Claude Code, Codex and opencode can share the same memory: what one saves, the others find. Tested with models from Anthropic, OpenAI, Google and Meta.
  • Saves tokens. Instead of pasting your notes or re-explaining context every session, the agent retrieves only what the question needs — typically 200–900 tokens, whether the memory holds 50 entries or 50,000, because search walks a tree instead of reading everything. Honest numbers, including where it does not save.
  • Understands time. When something changes ("I moved to Valencia", "staging is on PostgreSQL 17 now"), the old memory is kept as history, flagged OUTDATED, and searches answer with the current one first. Where you live, where you work and your stack are tracked automatically; any other correction is linked when the agent saves it with replaces. When a question is ambiguous ("Luis" — which one?), it asks instead of guessing.
  • Portable. Your memory is one folder of plain text files. Copy it to another computer, back it up, or put it under version control — it works wherever you put it.
  • Local, private and readable. No cloud, no database, no account: IHMT stores everything on your disk and sends nothing anywhere. (The memories your agent retrieves reach its model like any other context.) Every memory is a text file you can open, and each person who installs IHMT starts with their own, empty memory.

Compatibility. Officially supported: Claude Code. Also tested: Codex (CLI and the ChatGPT desktop app — setup) and opencode (setup). IHMT is a standard stdio MCP server, so any agent that supports local MCP servers should work — GitHub Copilot, Antigravity, Cursor, Windsurf, Gemini CLI, Claude Desktop… — and INSTALL.md knows how to configure them, but we have not tested those yet. All of them can share one memory.

You need Why
Python 3.10 or newer IHMT is written in Python
git to download IHMT and keep it updated
An AI agent that can run commands (Claude Code, Codex, opencode, Copilot in agent mode…) it installs IHMT and then uses the memory
Internet, during the installation to download the code and the MCP package; not needed afterwards

Missing Python or git? Your AI agent installs them for you (it is instructed to ask you first). 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: Apple shows a window asking to install its command-line tools.

On a Mac, note that the python3 that comes with macOS is version 3.9, which is too old — that is why your agent may say Python is missing even though python3 exists.

You do not need administrator rights, a database, an account, or any paid service beyond your agent. Tested on macOS and Linux (Ubuntu); on Windows the instructions are included but not tested yet. Details: GUIDE.md §3.

Let your AI agent install it

Paste this into the AI agent you want to give a memory to — Claude Code , Codex and opencode are tested; GitHub Copilot , Antigravity , Cursor , Windsurf , Gemini CLI , Claude Desktop and other MCP clients should work too:

Install the IHMT memory MCP server for me from https://github.com/gonzaroman/IHMT-MEMORY — follow the instructions in its INSTALL.md.

The agent follows INSTALL.md: it downloads IHMT to ~/IHMT-MEMORY, keeps your memory in ~/.ihmt, registers the server with itself only , adds the usage instructions, and tells you what it did. Then start a new session so the memory tools load.

Using several agents? Paste the same prompt in each one, whenever you want. If IHMT is already installed — say you have used it with Claude for months and now want it in Codex — the agent finds that installation, updates it if it safely can, and connects to the same memory , so it knows what you told the others from day one.

It needs an agent that can run terminal commands or edit files; chat-only assistants in a browser cannot install anything.

Manual install

Requirements: Python 3.10+, git, and your agent's CLI.

git clone https://github.com/gonzaroman/IHMT-MEMORY.git ~/IHMT-MEMORY
cd ~/IHMT-MEMORY
python3 -m venv .venv
.venv/bin/pip install -r requirements-mcp.txt
mkdir -p ~/.ihmt

Claude Code

claude mcp add ihmt-memory -s user -e IHMT_HOME="$HOME/.ihmt" -- "$PWD/.venv/bin/python" "$PWD/mcp_server.py"
claude mcp list                     # ihmt-memory … ✔ Connected

The server name must come before -e. Then append templates/memory-instructions.md to ~/.claude/CLAUDE.md.

Codex — codex mcp add ihmt-memory --env IHMT_HOME="$HOME/.ihmt" -- "$PWD/.venv/bin/python" "$PWD/mcp_server.py", then add default_tools_approval_mode = "approve" to the [mcp_servers.ihmt-memory] table in ~/.codex/config.toml (above its env table) and append the template to ~/.codex/AGENTS.md. Details.

opencode — add an "ihmt-memory" entry ("type": "local", "command": [<python>, <mcp_server.py>], "environment": {"IHMT_HOME": <memory folder>}) to the "mcp" object of ~/.config/opencode/opencode.json, and append the template to ~/.config/opencode/AGENTS.md. Details.

Windows, the project scope, the graphical setup and troubleshooting are all in the guide.

New here? ReadGUIDE.md — everyday use, step by step, with real outputs. The rest of this README is the technical reference.


A universal, domain-agnostic long-term memory for LLMs, stored as a recursive tree of plain files on the local disk. No vector database, no server, no third-party dependencies — Python 3.10+ and the standard library.

Instead of embedding everything into one flat index and scanning it, IHMT organizes knowledge into a tree: raw text leaves at the bottom, recursive JSON summaries above them, and a single root.json trunk at the top. A query walks that tree — root → branch → branch → leaf — so the number of files opened grows with the depth of the tree (≈ beam × log_B(n)), not with the amount stored.

| Flat RAG | IHMT
---|---|---
Retrieval cost | scan / ANN over all n chunks | beam × log_B(n) file reads
Structure | none — a bag of vectors | explicit hierarchy, inspectable
Chunking | fixed character windows | syntax-, scene- and date-aware
Stale facts | served silently | superseded, dated, and flagged
Ambiguity | returns a plausible guess | asks you for a clue
Storage | binary index | UTF-8 .txt + JSON you can read

python3 gui.py                       # graphical interface: set up, browse, inspect
python3 init_ihmt.py                 # or from the terminal: create ./ihmt_memory
python3 main.py demo                 # full walkthrough in ./demo_workspace
python3 -m unittest discover -v      # stdlib only; MCP tests skip without the SDK

Then use it on your own material:

python3 main.py ingest ~/notes ~/project/src/Main.java
python3 main.py consolidate --force
python3 main.py search "how did we handle stock reservations"
python3 main.py ask "Luis"                    # interactive clue loop
python3 main.py conflicts                     # what changed over time

As a library:

from ihmt import IHMT

memory = IHMT.initialize("./workspace")
memory.ingest_file("examples/InventoryService.java")
memory.ingest_file("examples/journal_personal.txt")
memory.flush()                                # close the tree up to the root

answer = memory.search("reserveStock soft hold")
print(answer.best.content)                    # the leaf
print(answer.best.path)                       # ['root', 'N1-software.java-…', 'L-software.java-…']
print(answer.node_reads)                      # how many branch files were opened

for notice in memory.notices():
    print(notice)   # "On 2024-03-11 you said user location = 'Madrid', but on 2026-02-03 you updated to 'Valencia'."

python3 gui.py                       # opens a browser at 127.0.0.1
python3 gui.py --path ~/my-memory --port 8765 --no-browser

Still zero dependencies — the server is http.server from the standard library, it listens only on the loopback interface, and every /api/* call needs the random token carried in the URL it opens.

Four screens: Set up (pick the memory folder with the system dialog, create the store, choose between per-project and global registration, preview the exact command or JSON before anything is written), Explore (collapsible tree down to the stored text, with supersession notices), Diagnose (a search that reports confidence, files opened vs. total, and the descent path), and Timeline (active vs. historical values and the detected contradictions). The interface is bilingual (ES/EN) and read-only over the memory: it never deletes or edits a leaf.


Use it from Claude Code (MCP)

mcp_server.py exposes the tree to Claude Code as nine tools in three families: long-term memory, project indexes and session scratch memory. The core stays dependency-free; the SDK is an optional extra:

python3 -m venv .venv
.venv/bin/pip install -r requirements-mcp.txt        # mcp[cli]>=2.0

For a single project, copy .mcp.json.example to that project's .mcp.json and fill in the absolute paths; Claude Code asks you to approve it on the next session there (claude mcp list shows it as Pending approval until then):

{
  "mcpServers": {
    "ihmt-memory": {
      "command": "/absolute/path/to/IHMT-MEMORY/.venv/bin/python",
      "args": ["/absolute/path/to/IHMT-MEMORY/mcp_server.py"],
      "env": { "IHMT_HOME": "/absolute/path/to/IHMT-MEMORY" }
    }
  }
}

or in one command:

claude mcp add ihmt-memory --scope user \
  -e IHMT_HOME=/absolute/path/to/IHMT-MEMORY \
  -- /absolute/path/to/IHMT-MEMORY/.venv/bin/python /absolute/path/to/IHMT-MEMORY/mcp_server.py

IHMT_HOME selects the store ($IHMT_HOME/ihmt_memory), created on first use. Point every project at one shared directory for a single cross-project memory, or give each project its own.

Tool Behaviour
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).
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.
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.
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.
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.
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.
note(text) / recall(query, clue=None) Session scratch memory: survives a context compaction, disappears when the session ends.
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.

Saving tokens inside a session

Long-term memory saves tokens between sessions. The project tools save them within one, where the cost is reading the same files again and again. ProjectIndex (ihmt/project_index.py) keeps a private store per project under $IHMT_PROJECTS_DIR (default $IHMT_HOME/ihmt_projects):

  • one leaf per symbol — code_chunk_mode="symbol", so a lookup returns a method, not a file;
  • checksum sync on every call — size and mtime first, SHA-256 only for what moved; changed files are re-ingested, removed ones deleted, and the branches rebuilt. Code is never served stale;
  • path-ordered branches — each file is stamped with its rank in path order, so every branch covers neighbouring files and its summary stays meaningful for the descent;
  • code-aware ranking — the navigator filters by scope/path_prefix from the catalog, prefers the file a query names, demotes tests unless asked and demotes import lines.

Measured on a 55-file Spring Boot project (8 typical questions): reading the files that hold the answers costs 3,613 tokens; find_code returns the exact method for all 8 in 1,071. The map of the project costs 660 tokens against 13,157 to read it whole.

Those savings are against an agent that reads whole files. In an A/B test with 22 real headless Claude Code sessions, Claude preferred batched grep/sed -n and never called the project tools on its own; forcing them made sessions 42–71 % more expensive. Keeping the server enabled costs about 460 tokens per conversation, since Claude Code loads MCP tools on demand. IHMT's main value is memory between sessions; treat the project tools as optional, and do not mandate them in CLAUDE.md.

The server transparently supports MCP SDK 2.x (MCPServer), 1.x (FastMCP) and the standalone fastmcp package.

The usage instructions in your ~/.claude/CLAUDE.md (template) tell Claude Code when to reach for each tool: search before answering anything that depends on earlier sessions, save durable facts with their date, never guess on AMBIGUOUS, always relay OUTDATED, and never store secrets.


Everything lives in one relocatable directory:

ihmt_memory/
  root.json                 # the trunk: domains, topics, top branches, counters
  layer_0/<domain>/*.txt    # the leaves: raw UTF-8 text + a strict JSON header
  layers/1/*.json           # branches: summaries of leaves
  layers/2..N/*.json        # branches: summaries of summaries
  state/catalog.json        # index: id -> path, domain, parent, timestamp
  state/facts.json          # the fact timeline
  ihmt.config.json          # branch factor, token budgets, backend

root.json lives inside ihmt_memory/ so the store is self-contained: copy the directory and the memory travels with it.

A leaf is a normal text file that describes itself, so it stays meaningful even if the catalog is lost:

<<<IHMT-META
{
  "leaf_id": "L-software.java-0001-84130ee123",
  "timestamp": "2026-09-09T17:45:00Z",
  "data_type": "CODE",
  "domain": "software.java",
  "tags": ["method:reserveStock", "class:InventoryService", "lang:java", "type:code"],
  "parent_id": "N1-software.java-5571a7baac",
  "span": {"start_line": 43, "end_line": 68},
  "checksum": "sha256:…",
  "extra": {"context": "public final class InventoryService {"}
}
IHMT-META>>>
    public Optional<String> reserveStock(Sku sku, int quantity) {
        …

A branch node embeds each child's title, excerpt and keywords. That is the detail that makes the descent cheap: a branch can be ranked without opening any of its children.


1. UniversalIngestor — detect, split, enrich, store

DomainDetector classifies each document by type (CODE, NARRATIVE, CLINICAL, PERSONAL, PROCESS, GENERIC) and domain (software.java, medicine.clinical, process.cooking, personal, …) from its extension plus lexical signatures in English and Spanish. Both can be overridden with --domain / --type.

The type selects the splitter, and every splitter obeys one invariant:

A logical block is never cut. If a single block exceeds max_tokens it is stored whole and flagged oversized. Correctness of the block beats hitting the token budget.

  • Code (chunkers/code.py) — Python via the stdlib ast; Java/JS/TS/C/C++/C#/Go/Rust/Kotlin/ Swift/PHP via BraceScanner, a character-level scanner that tracks brace depth while skipping comments, string literals, char literals, template literals and preprocessor lines. Imports coalesce; each class/function is a block; an oversized class splits per member , and the enclosing class header travels in the leaf's extra.context rather than being spliced into the text. Concatenating a file's leaves reproduces the file byte for byte — asserted in the tests.
  • Narrative — paragraph-atomic, with ***, ---, Chapter/Capítulo as hard boundaries. Only a paragraph larger than max_tokens is split, and then at sentence boundaries.
  • Temporal (journals, chats, clinical records) — one dated entry is atomic and a change of date is a hard boundary, so a leaf never mixes two encounters. The date found in the text becomes the leaf's timestamp , which is what makes recency weighting mean when something was true rather than when it was ingested.
  • Process (recipes, protocols, runbooks) — steps and ingredient lists stay attached to their heading.

Leaf ids are derived from (domain, source, position, content), so re-ingesting an unchanged document rewrites the same leaves instead of duplicating them.

2. RecursiveSummarizer — the Summarization Event

It watches layer 0. When branch_factor leaves of one domain have no parent, it fires a Summarization Event : they are condensed into a layer-1 node, the node is written, and only then are the children stamped with their parent_id — so an interrupted run re-processes a group instead of orphaning it. The same rule applies from layer 1 to layer 2, and so on, until the tree converges; then root.json is rewritten.

consolidate() is idempotent. flush() (--force) also promotes partial groups so the tree closes completely. Anything still unconsolidated is referenced directly by the trunk, so nothing in the store is ever unreachable from the root.

3. SemanticNavigator — descent and the Interactive Clue Loop

Ranking uses Okapi BM25 over each candidate's title, keywords, tags and excerpt, with field weights and document frequencies computed across the siblings of the current level — exactly the discrimination the descent needs, at no extra I/O cost. The walk keeps a beam of beam_width branches per level.

Confidence blends two independent signals:

confidence = 0.6 × coverage + 0.4 × margin

Coverage asks "does this leaf actually contain what was asked?"; margin asks "is it distinguishable from its rivals?". A common first name scores high on the first and near zero on the second — which is precisely when the system must not guess:

$ python main.py ask "Luis"

"Luis" is ambiguous (3 memories match this query equally well, confidence 0.62).
It could belong to any of these branches:
  1. [personal] journal_personal.txt · 2024-07-22 — Vacaciones en Benidorm con Luis, mi primo…
  2. [personal] journal_personal.txt · 2026-08-30 — Fin de semana en la playa de El Saler con Luis…
  3. [personal] journal_personal.txt · 2024-11-30 — Cierre de trimestre… Luis Marín revisó el pull request…
Give me a clue to narrow it down (e.g. a place, a date, a project):
> vacaciones en Benidorm

query: "Luis + vacaciones en Benidorm" · confidence 0.74 · 5 node reads, 3 leaf reads, depth 2
  1. [personal] journal_personal.txt · 2024-07-22
     path  root → N2-personal-ea34db33ae → N1-personal-59bf8bc291 → L-personal-0002-a17b3c8f35

The clue triggers a joint cross-reference : candidates matching both term groups are boosted (×1.6), candidates matching only one are demoted (×0.7). The loop runs up to max_clue_rounds times, stops early if the user declines, and never silently converts an ambiguous query into a confident answer.

clue_provider is any callable, so the loop works for a human at a terminal (input) or for an agent that lets the LLM supply its own follow-up.

4. ConflictResolver — recency weighting and the timeline

Facts are (subject, attribute, value, timestamp, source_leaf), recorded programmatically via record_fact() or extracted at ingest time by pattern rules (vivo en X / I live in X, mi stack es Y, trabajo en Z, Diagnóstico:, Tratamiento:, Medicación: …; extend with add_pattern).

Each (subject, attribute) keeps a dated timeline. The newest value is ACTIVE; every earlier one becomes HISTORICAL with superseded_by and a valid_from/valid_to interval. Nothing is deleted , so both questions stay answerable:

memory.resolver.active_state()["user::location"].value      # 'Valencia'  (now)
memory.resolver.state_at("2024-12-31")["user::location"].value  # 'Madrid'  (back then)

Repeating a value at a later date is a confirmation, not a contradiction. A genuine change produces a transparent notice — "On 2024-03-11 you said user location = 'Madrid', but on 2026-02-03 you updated to 'Valencia'." — and the superseded leaf is annotated, so retrieving outdated material always arrives with its correction attached (SearchResult.notices).

A leaf itself stays ACTIVE: what it says was true on its own date , and that remains the right answer to a historical question. What changes is that it can no longer be read as current.

5. Summarization backends

class SummarizerBackend(Protocol):
    name: str
    def summarize(self, children, *, domain: str, layer: int) -> NodeSummary: ...
  • HeuristicSummarizer (default) — stdlib extractive summarization: TF term ranking with EN/ES stop words plus representative-sentence selection. Offline, deterministic, which is what lets the test suite assert on tree shape.

  • AnthropicSummarizer (optional) — used only when selected and the anthropic package and ANTHROPIC_API_KEY are both present. Every failure path (missing SDK, missing key, network error, unparseable reply) falls back to the heuristic backend, so a consolidation is never lost because a model was unreachable.

    python3 init_ihmt.py --backend anthropic # model set by summarizer_model in ihmt.config.json

Any other model or local runtime plugs in by implementing the same protocol and passing it as IHMT(..., backend=MyBackend()).


Command Purpose
init [--branch-factor N] [--target-tokens N] [--force] create the store
`ingest <paths… -> [--domain D] [--type T] [--tag X] [--no-consolidate]`
consolidate [--force] run pending Summarization Events
search <query> [--top-k N] [--full] walk the tree
ask <query> [--clue TEXT] [--top-k N] search with the clue loop
tree [--depth N] outline of the hierarchy
stats, facts [--subject S], conflicts [--subject S] inspection
rebuild rebuild catalog, timeline and trunk from the files
demo end-to-end walkthrough

--path selects the store directory and --json emits machine-readable output; both work before or after the subcommand.

ihmt_memory/ihmt.config.json:

Key Default Meaning
branch_factor 8 children per branch; the log base of retrieval cost
target_tokens / max_tokens 2000 / 3000 leaf size target and oversize threshold
beam_width 3 branches kept alive per level
confidence_threshold 0.45 below this, ask for a clue
ambiguity_margin 0.18 score gap under which candidates count as tied
max_clue_rounds 3 clue-loop iterations
summarizer_backend / summarizer_model heuristic / claude-sonnet-5 summarization
code_chunk_mode pack symbol stores one leaf per class member (used by project indexes)

Small corpora deserve a small branch factor — the demo uses branch_factor=4, target_tokens=400 so a handful of documents still builds a genuine multi-layer tree.

python3 -m unittest discover -v          # from the project root

187 tests — 25 of them for the MCP server, skipped without the SDK — covering: byte-exact reconstruction and boundary-depth invariants for Java and Python, scene/date/section atomicity, oversized-block handling, leaf header round-trips, catalog recovery, summarization thresholds, upward propagation, idempotence, full reachability from the root, descent cost bounds, the clue loop, recency weighting, historical preservation, the project indexes, the MCP tools, the graphical interface and the CLI.

  • Retrieval is a descent, not a scan. That is the whole point, and it means a branch pruned at the trunk is not revisited. Domain-level pruning only happens when a query has actual signal at the trunk; if it has none, every domain stays in play and the beam applies one level down. The clue loop is the recovery mechanism when the descent goes wide.

  • Lexical, not semantic. Matching is BM25 over accent-folded, CamelCase-split tokens: it works in any language and needs no model, but it will not match a synonym. Plugging an embedding re-ranker into BM25Ranker is the natural upgrade; the tree structure does not change.

  • Token counts are estimated at ~4 characters per token. Budgets only need to be consistent, not exact.

  • Fact extraction is pattern-based. The bundled rules cover common English/Spanish phrasings and clinical headers; record_fact() is the reliable path, and add_pattern() extends the rules.

  • Single-writer. Writes are atomic (tmp + os.replace) and catalog rebuilds take a lock file, but the store assumes one writer at a time.

  • initialize(force=True) discards derived state only — branches, catalog, timeline — and detaches the surviving leaves so the next consolidation rebuilds the hierarchy. Leaf content is never deleted.

    ihmt/ api.py IHMT facade wiring everything together config.py IHMTConfig models.py MemoryLeaf, BranchNode, ChildRef, RootIndex, Fact, Contradiction storage.py MemoryStore: atomic I/O, catalog, recovery textutils.py tokenizing, keywords, entities, extractive summary, timestamps detectors.py DomainDetector chunkers/ base · code · narrative · temporal · process · generic summarizers.py SummarizerBackend · Heuristic · Anthropic universal_ingestor.py UniversalIngestor recursive_summarizer.py RecursiveSummarizer semantic_navigator.py SemanticNavigator, BM25Ranker, ClueRequest, scope/path filters project_index.py ProjectIndex: per-project code cache with checksum sync conflict_resolver.py ConflictResolver + timeline manager ihmt_gui/ local graphical interface (stdlib only) mcp_server.py MCP server: the nine tools init_ihmt.py · main.py · gui.py · examples/ · tests/ GUIDE.md installation and usage guide INSTALL.md installation instructions for AI agents templates/ memory-instructions.md: the usage rules agents append to CLAUDE.md / AGENTS.md

MIT © 2026 Gonzalo Román Márquez (gonzaroman)