The READ path for agent memory
The READ path for agent memory
Card t_3841e128 asks four questions before any provider is switched on. This note answers
all four from live probes, then names the one metric. The honest headline is uncomfortable and
is stated up front:
A read path exists in the built-in store, but it is unconditional — and an unconditional read produces no signal. The only conditional read (a query that returns results) is the external-provider prefetch path, and it is switched off on every profile. So today memory is provably written and unprovably read.
That is the September failure mode reproduced in miniature, and it is the reason the card is a card.
0. What is actually running (measured 2026-10-21, LAB)
| Check | Result |
|---|---|
hermes memory status — min / mira / sol / echo / nova | Provider: (none — built-in only) on all five |
| Built-in memory | Memory injection: enabled ✓ · Memory tool: enabled ✓ |
| External provider plugins installed | byterover, holographic, retaindb — installed, not configured |
Listening ports :6333 (Qdrant) · :8888 (hindsight) · :8000 (mem0) | none |
Central Registry :8432 — the September bus's read endpoint | not listening; curl → Connection refused |
Live hindsight on :8888 | {"status":"healthy"} — but it is Sol's scratch probe (profiles/sol/cache/scratch/hs-probe/.venv), not a deployed service |
memory tool available to a CLI session | yes — it is in my own tool list this session |
Tool memory calls in the full log history (agent.log + 2 rotations) | 6 — and all 6 are write failures (char-limit overflows, unmatched old_text) |
Successful memory writes logged | 0 |
| Memory reads logged | 0 |
Two facts carry the whole note. The memory tool is offered to every agent, and the log
shows it being attempted and failing — never reading. And the one named cross-agent read
endpoint from September is not running at all.
1. WHO queries memory — which agent, in which loop
Three loops exist, and they are not equal.
| Loop | Reader | Does it query? |
|---|---|---|
| Interactive session (CLI / Telegram / Discord) | the agent, via the frozen memory block injected at session start | yes, but unconditionally — the whole store is injected, not selected |
Worker (kanban-dispatched, hermes -p <profile>) | the worker process | same as interactive — memory is in the cli toolset, so the worker gets the write-only tool and the same injection |
| Cron (no-agent script jobs) | a shell script | no — scripts do not run an agent; nothing reads memory |
There is no agent whose job is to read memory. No cron reads it, no agent queries it on another's behalf, and no worker is tasked with recall. The reader is always the same agent that is about to answer — and it reads by having the store dumped into its prompt, not by asking for anything.
This is the structural difference from September. The bus had per-agent writers and no reader role. Today the reader role is still empty; what changed is that the store is small enough to be injected whole.
2. WHAT they query for — the retrieval trigger
The built-in store has no retrieval trigger. That is the sharpest finding in this note.
tools/memory_tool.py defines exactly three actions — add, replace, remove (plus the
atomic operations batch). There is no search, no query, no recall. The tool is
write-only by construction; it cannot read its own store.
What the agent actually gets is in the tool's own description:
"Memory is injected into every future turn… a FROZEN snapshot at session start; mid-session writes hit disk but never change the prompt."
So the real retrieval trigger is session start, and the query is empty. Every entry is returned every time, whether or not it is relevant. A memory written in session N is not visible until session N+1.
The conditional read exists only in the external-provider path:
turn start → _memory_query_text(message) # the user's words ARE the query
→ is_trivial_prompt() gate # greetings/acks skipped
→ prefetch_all(query) # provider.search(query)
→ results injected into the turn
→ describe_recall() # "🧠 Provider — recalled N memories"
That is a genuine query-triggered read. It is dead on every profile, because no provider is enabled.
Consequence worth stating plainly: an unconditional read is not a read signal. You cannot measure whether memory helped from a store that is always present — the same way you cannot measure a cache hit rate on a cache that is always warm. This is why the metric in §4 has to count conditional reads, not injections.
3. WHEN it is written
Three writers, all real, all measured:
| Writer | Trigger | Evidence |
|---|---|---|
Explicit memory tool call | the agent decides a fact is durable | in my tool list; 6 attempts in the logs, all rejected |
| Background review fork | after every turn — agent/background_review.py, fail-open enabled | pending/memory/*.json staging files: min 18 · mira 38 · sol 22 · echo 4 · nova 1 |
nudge_interval: 10 | every 10 user turns, the agent is reminded to consider saving | profiles/min/config.yaml:1081 |
The background review is the real writer. It forks the agent after a turn, asks it to consolidate,
and stages the result to pending/memory/<id>.json for approval. That is why the store is
chronically at its ceiling:
| Profile | MEMORY.md | Limit |
|---|---|---|
| min | 3,302 chars | 2,200 |
| mira | 3,070 chars | 2,200 |
| echo | 2,184 chars | 2,200 |
| sol | 1,441 chars | 2,200 |
| nova | 1,745 chars | 2,200 |
min and mira are over budget — which is exactly why their memory calls fail in the log
("memory would be at 2,226/2,200 chars — over the limit"). The store is not merely being
written; it is full and unable to accept more.
That reframes the card's question. It is not "will anyone read what we write?" so much as "we are writing into a store that is already full, and the only reader is unconditional." A provider that adds capacity without adding a conditional read makes this worse, not better.
4. WHAT proves it is read — the metric
The one metric
recalls_with_results— the count of conditional read events that returned ≥1 result, per agent, over a trailing 7 days.
Zero means the store is accumulating. Non-zero means it is being read. This single number separates "memory is working" from "memory is accumulating", and today it is unmeasurable because nothing counts it.
Why this metric and not a simpler one
| Candidate | Why it fails |
|---|---|
| "Memory is enabled ✓" | A config flag. It was ✓ in September too, with 1,548 unread rows. |
| "Rows written per week" | Measures accumulation — the opposite of what we want. |
| "Store size" | Grows whether or not anyone reads. |
| "Injection happened" | Unconditional, so it fires even for an empty or irrelevant store. |
recalls_with_results | Conditional on a query, so zero is meaningful. |
The gap that makes it unmeasurable today
Hermes already has the hook point — MemoryManager.prefetch_all() and describe_recall()
(agent/memory_manager.py:502,559) — and turn_context.py:920 already calls it. But
describe_recall() emits a UI status line, not a log line. Nothing is persisted. So the read
event happens (when a provider is on) and leaves no durable trace.
This is the concrete engineering ask, and it is small: persist one line per prefetch —
(ts, agent, provider, query_hash, result_count) — to a file or table. Then recalls_with_results
is a SELECT count(*) ... WHERE result_count > 0. The read path is already wired; only the
counter is missing.
The second-order check (not the metric, but the proof)
A counter proves memory was read, not that it mattered. The stronger proof is "did a recalled fact change an answer", measured by spiking 2–3 real sessions on one agent with and without recall and comparing the output. That is Echo's sequence step 6, and it should stay a manual experiment — it is not a standing metric.
5. Recommendation
Do not enable a provider until recalls_with_results exists.
Enabling hindsight or mem0 without the counter reproduces September exactly: writes accumulate,
reads are unprovable, and the store dies quietly again. The provider choice (Echo's
recommendation: hindsight, MIT, no open-core gating, local_external fits LAB+Pulse) is
not the blocker — and Docker is no longer the blocker either. The counter is.
Sequenced:
| # | Step | Owner | Why now |
|---|---|---|---|
| 1 | Build the read counter — persist (ts, agent, provider, query_hash, result_count) from the existing prefetch_all hook | Sol (engineering lane) | It is the gate for everything below; without it no provider decision is falsifiable |
| 2 | Name the reader role explicitly in the fleet rules — who reads, in which loop | Min | Today the reader is implicit; September died because nobody owned it |
| 3 | Fix the full store — min and mira are over the 2,200-char ceiling and their writes are failing | Min | A store that cannot accept writes is not a memory system |
| 4 | Enable one provider on one agent (hindsight, local_external, LAN-bound, telemetry off) | Sol | Only after 1–3; a single agent is the pilot |
| 5 | Spike 2–3 sessions, measure recalls_with_results and whether recall changed an answer | Echo | Vendor benchmarks are their claims; ours must be ours |
Two things this note does NOT change: the provider recommendation (still hindsight over mem0, per Echo's §8), and the SAG/security findings (§6 of the extension study). Both stand.
6. Not verified (read as limits, not findings)
- I did not enable a provider. Every claim above is from source, config, and live host probes. No memory was written or read by me.
- I did not read the built-in injection path's source in full — I confirmed the frozen-snapshot
behaviour from the tool's own description and the
memory_managerbuiltin no-op, not by tracingagent_init. The "no query, no trigger" claim rests on the tool schema (three write actions, no read action) and the description text, both quoted directly. recalls_with_resultsis a design, not a measurement. I have not built it, so I cannot show it working. Step 1 is the proof.- The
:8888hindsight instance is Sol's scratch probe, not a service — I confirmed it is healthy but did not check its data, config, or whether it survives a reboot. - I did not check Pulse-side profiles. The five profiles probed are all LAB. Pulse agents (tam/eva/eve) run on the Pulse laptop and were not reachable from here.
🎯 Min's Read: the read path exists in the built-in store but it is unconditional — the
whole store is injected at session start, so it produces no signal and cannot be measured. The
only conditional read is the provider prefetch path, and it is switched off on all five
profiles. The memory tool is write-only by construction (add/replace/remove, no query), the
one named cross-agent read endpoint (:8432) is not listening, and the log shows 6 memory-tool
attempts — all write failures, zero reads. Meanwhile min and mira are over their 2,200-char
ceiling. The one metric is recalls_with_results (conditional reads returning ≥1 result, per
agent, 7-day trailing) — and it is unmeasurable today because describe_recall() emits a UI
line instead of a log line. Recommendation: do not enable a provider until the counter exists.
Memory extension — mem0 vs hindsight
Young, 2026-10-20: "Revisit Memory study… We need to extend our agents memory as Hermes had limitation. mem0 would be great candidate, check self host and license. Goal is to host on LAB, Pulse can connect to it."
Skills registration — 33 skills, measured
Young, 2026-10-20: "pls study these features of Hermes we would need to use it some day. Register these skills for our agents to use." — 33 URLs across bundled + optional.

