Prompt-cache-bounded memory

Hermes Agent

Hard-bounded Markdown memory frozen into the system prompt at session start, with FTS5 session history, skills as procedural memory, and a pluggable provider interface.

Carries 2 of 7 rubric mechanisms. Most systems here carry none or one (41%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

This report covers Hermes Agent's own built-in memory, not the third-party providers it can mount. The Holographic report covers the first-party HRR plugin shipped in the same repository, at the same commit as this one; this one covers the memory the agent has when no provider is configured at all.

Hermes takes a position almost nothing else in this atlas takes: memory is hard-bounded and frozen, because the prompt cache matters more than completeness.

Two Markdown files under $HERMES_HOME/memories/ hold curated memory — MEMORY.md (the agent's own notes, 2,200 chars) and USER.md (the user profile, 1,375 chars). Both are injected into the system prompt as a frozen snapshot at session start. Mid-session writes land on disk immediately and durably, but deliberately do not change the system prompt, so the prefix cache survives the whole session. The snapshot refreshes on the next start.

The limits are hard, and this is the design's sharpest edge: when an add would exceed the char budget, the write is refused and the tool returns the current entries with an instruction to consolidate and retry within the same turn. Memory compaction is not a background worker; it is a synchronous obligation handed to the model at the moment of overflow.

Around that core sit three more layers:

  • Session history in SQLite (~/.hermes/state.db) with FTS5 full-text search — the unbounded, searchable counterpart to the bounded prompt memory.
  • Skills as procedural memory: Markdown files the agent writes, updates, and deletes itself via skill_manage.
  • A MemoryProvider ABC that mounts exactly one external provider at a time, with first-party adapters for Honcho, Mem0, Hindsight, Supermemory, OpenViking, ByteRover, RetainDB, and Holographic.

The engineering quality of the file layer is high, and much of it is visibly scar tissue from real bugs: external-drift detection with automatic backup, refusal on ambiguous reads, cross-platform file locking, atomic replacement, and threat scanning at the write boundary.

The main weaknesses are the flip side of the same simplicity. Entries are opaque prose with no provenance, status, scope, or supersession; replace/remove address entries by substring match rather than identity; and there is no verification step between "the model decided to remember this" and "this is in every future system prompt."

2. Mental Model

There is no schema. The memory unit is a text entry in a delimited flat file:

MEMORY.md   # agent's personal notes    — 2,200 char whole-file budget
USER.md     # what it knows about you   — 1,375 char whole-file budget

entry
§
another entry, possibly
spanning multiple lines
§
a third entry

The delimiter is \n§\n. Limits are counted in characters, not tokens, explicitly "because char counts are model-independent."

Four memory kinds coexist with very different guarantees:

Layer Store Bounded? In prompt? Searchable
Curated memory MEMORY.md / USER.md Hard char cap Always, frozen at session start n/a — always present
Session history SQLite state.db Unbounded No FTS5
Skills Markdown files Unbounded Names/descriptions only by name
Provider memory External Provider-defined Via prefetch Provider-defined

Write lifecycle:

Diagram — the write gate, threat scan, drift check and budget refusal all run before the disk write — and the system prompt is unchanged for the rest of the session, because the frozen snapshot is only re-rendered at the next session start
Diagram source
%% caption: the write gate, threat scan, drift check and budget refusal all run before the disk write — and the system prompt is unchanged for the rest of the session, because the frozen snapshot is only re-rendered at the next session start
flowchart TB
    A["memory(action = add, replace or remove,<br/>target = memory or user)"] --> G{"write gate"}
    G -->|block| B["refused"]
    G -->|stage| H["held for human approval"]
    G -->|allow| TS["threat scan<br/><i>strict pattern set</i>"]
    TS --> LK["file lock → re-read from disk<br/>→ drift check → budget check"]
    LK --> BUD{"within budget?"}
    BUD -->|no| REF["refuse and demand consolidation"]
    BUD -->|yes| WR["append, replace or remove"]
    WR --> AT["atomic_replace to disk"]
    AT --> SESS["system prompt unchanged<br/><b>this</b> session"]
    SESS --> NEXT["next session start: the frozen snapshot<br/>is re-rendered into the system prompt"]

    style REF fill:#e7efe9,stroke:#3d6b59
    style SESS fill:#f4e2bd,stroke:#b8860b

Two things this diagram makes plain. The budget is a hard refusal — a write that would overflow is rejected and consolidation demanded, rather than the file being silently trimmed. And a write does not change what the model can see until the next session, because the prompt holds a frozen snapshot — so within a session the agent's memory and its context disagree, by design.

3. Architecture

  • tools/memory_tool.py (397 lines): the memory tool surface, dispatch, and the write gate wrapper.
  • tools/memory_tool_store.py (417 lines): MemoryStore — the frozen snapshot, budgets, drift detection, locking, substring editing and batches.
  • agent/memory_manager.py (826 lines): provider registration and lifecycle orchestration.
  • agent/memory_provider.py (165 lines): the pluggable provider ABC.
  • hermes_state.py (1,386 lines) plus twenty-one hermes_state_*.py modules: SQLite session persistence, FTS5 schema, CJK tokenizer extension, corruption probes and repair, each in its own file.
  • tools/write_approval.py: the shared allow/block/stage gate used by memory writes.
  • tools/threat_patterns.py: injection/exfiltration patterns, shared with the context-file scanner.
  • tools/skill_manager_tool.py, tools/skill_provenance.py, agent/skill_*.py: skills as procedural memory.
Diagram — the same gated write beside the two read paths, with session search over FTS5 and one external provider's prefetch block
Diagram source
%% caption: the same gated write beside the two read paths, with session search over FTS5 and one external provider's prefetch block
flowchart TD
  Model["Model"] --> Tool["memory tool<br/>(add/replace/remove)"]
  Tool --> Gate["write_approval gate"]
  Gate -->|stage| Human["Human approval<br/>queue"]
  Gate -->|allow| Scan["threat_patterns<br/>strict scan"]
  Scan --> Lock["file lock +<br/>re-read + drift<br/>check"]
  Lock --> Budget{"within<br/>char limit?"}
  Budget -->|no| Consolidate["refuse: consolidate<br/>in-turn"]
  Budget -->|yes| Disk["atomic_replace<br/>MEMORY.md /<br/>USER.md"]
  Disk -.->|next session start| Snapshot["frozen system-prompt<br/>block"]
  Sessions["state.db +<br/>FTS5"] --> Search["session search"]
  Provider["one external<br/>MemoryProvider"] --> Prefetch["prefetch() block"]

4. Essential Implementation Paths

The frozen snapshot

Documented at the top of tools/memory_tool.py and implemented in MemoryStore.load_from_disk: memory files are rendered into the system prompt once, at session start. Mid-session mutations are durable on disk but invisible to the running session's prompt.

The motivation is economic rather than epistemic — a stable prompt prefix keeps the provider's cache warm for the entire session — but it has a real safety consequence the code calls out: because the snapshot is frozen, a poisoned entry persists for the whole session and across sessions until explicitly removed. That is why memory content is scanned with the broadest ("strict") threat-pattern set at write time, where most systems in this atlas defend at read time instead.

This is an unusual and defensible trade: Hermes filters what may enter durable memory; Verel and RainBox instead fence whatever is recalled as untrusted data at the prompt boundary. Write-time filtering is cheaper per turn and cache-friendly; read-time fencing is more robust, because it does not depend on a pattern list being complete.

Hard budgets and in-turn consolidation

MemoryStore.__init__(memory_char_limit=2200, user_char_limit=1375), both overridable from config under memory.memory_char_limit / memory.user_char_limit. In add, the prospective serialized total is computed before writing; on overflow the tool returns success: False together with current_entries and usage, and instructs the model to merge or remove entries and retry in the same turn.

_MAX_CONSOLIDATION_FAILURES_PER_TURN = 3 bounds the resulting loop, so a model that cannot get under budget degrades instead of spinning.

This is the inverse of the atlas's usual pattern. Most systems here let memory grow and add background consolidation later; Hermes refuses the write and makes compaction the model's immediate problem. The benefit is that prompt cost is known statically and cannot drift. The cost is that the model chooses what to forget under time pressure, with no review and no record of what it dropped.

External drift detection

_detect_external_drift treats the files as tool-shaped and looks for two signals that something else wrote them: a round-trip mismatch (re-parsing and re-serializing does not reproduce the bytes), or any single parsed entry exceeding the whole-file limit — which implies a shell append, patch tool, manual edit, or sibling session dumped free-form content into what the tool would treat as one entry.

On detection it writes a timestamped .bak snapshot and refuses the mutation, returning the backup path. If the backup itself fails, the returned string says so explicitly and the file is left unchanged.

Compare basic-memory, which accepts human edits as canonical and invests heavily in bidirectional reconciliation. Hermes takes the opposite and much cheaper route: detect foreign writes, preserve them, and decline to proceed. For a two-file, few-kilobyte store that is the right call; it would not scale to a knowledge base people are expected to edit directly.

The add path deliberately skips the drift guard — appending cannot clobber — but a comment records the exception that had to be handled anyway: because add rewrites the whole file from parsed entries, a file that exists but reads as empty (transient lock, permission blip, I/O error) would be rewritten down to just the new entry, wiping everything. The code refuses on read failure instead.

The write gate

_apply_write_gate routes every mutating memory action — including a whole apply_batch as one unit — through _gate_or_stage, which calls tools/write_approval.py and gets back allow, block, or stage. Staged writes are persisted with an inline summary and a pending_id for later human approval, and list_pending / get_pending / discard_pending are the surface a person resolves them through — the same confirm-tier write-intent shape RainBox uses for high-impact assistant memory mutations, arrived at independently.

One caveat is explicit in the code: if the gate module fails to import, _gate_or_stage returns None and the write proceeds. It fails open by design, with a docstring saying so.

Session history

The session store maintains messages in SQLite with an FTS5 virtual table, and is unusually defensive about it: it detects legacy inline FTS schemas, probes for FTS5 availability and partial corruption with representative MATCH queries, caps user-controlled query input at 2,048 chars, can load a CJK tokenizer extension, and can drop and rebuild triggers.

Retrieval here is lexical only. There is no vector index over session history and no fusion with the curated memory files — session search is a separate tool surface the model must choose to call.

Skills as procedural memory

Skills are Markdown files the agent creates and edits through skill_manage, with provenance and usage tracking in tools/skill_provenance.py and tools/skill_usage.py. Only names and descriptions occupy prompt space; bodies load on demand. This is a clean separation of "what I know how to do" from "what I know", and it is the layer that makes Hermes self-improving in the sense its documentation claims.

5. Memory Data Model

The curated layer has no data model beyond ordered strings in a file. Consequences:

  • No provenance. An entry does not record when, why, from what turn, or on whose authority it was written.
  • No status. Nothing separates candidate from confirmed; whatever the model writes is immediately authoritative for every future session.
  • No scope. Profiles partition everything — each profile has its own HERMES_HOME with its own config, memory, sessions, and skills — but within a profile there is no project, room, or agent boundary.
  • No identity. replace and remove locate an entry by unique substring, not by ID. The module docstring presents this as a feature ("short unique substring matching (not full text or IDs)"), and for a hand-sized list it is ergonomic. It is nonetheless identity-by-content: as entries are consolidated and reworded, a substring that was unique can become ambiguous or vanish, and the model must re-derive a handle it cannot store.

Durability is handled well: cross-platform locking via fcntl or msvcrt, re-read under lock so concurrent sessions observe each other, atomic_replace for the final write, and explicit refusal rather than best-effort behaviour on ambiguous reads.

6. Retrieval Mechanics

There is essentially no retrieval for curated memory — it is always present, in full, in the system prompt. That is the point of bounding it. Recall latency is zero and recall is guaranteed; the trade is that the budget must be small enough for that to be affordable.

The other layers are retrieved on demand and are not fused:

  • Session history: FTS5 lexical search, explicit tool call.
  • Skills: selected by name/description from the always-present index.
  • Provider memory: whatever the mounted provider returns from prefetch(query), appended as its own block.

Nothing ranks across these layers. There is no unified query that considers curated memory, session history, and provider results together, and no de-duplication between them — a fact can appear in USER.md, in a provider's store, and in session history, and be injected two or three times.

7. Write Mechanics

Curated memory is written only by explicit memory tool calls (or their staged, human-approved equivalents). There is no automatic extraction into MEMORY.md/USER.md — a deliberate choice consistent with the hard budget, since automatic capture would exhaust 2,200 characters almost immediately.

Automatic capture instead happens in the layers that can absorb it: session messages accumulate in SQLite, and a mounted provider's sync_turn/on_session_end hooks may extract whatever it likes into its own store.

apply_batch allows several operations against one target atomically, which is what makes the in-turn consolidate-then-retry flow practical.

8. Agent Integration

The MemoryProvider ABC is the most explicit pluggable-memory contract in the atlas — twenty-one members covering identity and availability, initialization, prompt blocks, prefetch and its queued form, recall status, per-turn sync, tool schemas and dispatch, session end and switch, pre-compression extraction, delegation observation, config, backup paths, and shutdown. MemoryManager enforces a one-external-provider limit to prevent tool-schema bloat and conflicting backends.

Hermes also exposes sessions to MCP clients via hermes mcp serve.

The contract's notable gap — analyzed further in the pluggable memory provider pattern — is that it has no deletion or forgetting hook and no scope parameter. on_memory_write forwards add, replace, and remove for providers that want to mirror built-in writes, but providers are free to implement only add (Holographic does exactly that), so a removal in MEMORY.md need not reach the provider's copy. There is no interface-level answer to "the user asked to be forgotten."

9. Reliability, Safety, and Trust

Strengths:

  • Bounded prompt cost by construction, with a stable cached prefix.
  • Threat scanning at the write boundary, chosen deliberately because frozen snapshots make poisoning persistent.
  • The same scan runs again on every entry at load time, and this is the strongest thing in the file layer. load_from_disk re-scans each entry at the strict scope while building the frozen snapshot; a match is replaced in the snapshot by [BLOCKED: <file> entry contained threat pattern(s): <ids>. Removed from system prompt; use memory(action=remove) to delete the original.] and logged at warning level, while the live entry list keeps the original text. The reasoning is in the docstring: a memory file poisoned on disk by a supply chain, a compromised tool or a sister-session write bypasses the write gate entirely, so the write-time scan is not the last line — and silently dropping the entry instead of naming it "would hide the attack from the user." Two properties make it work rather than merely exist. The placeholder is idempotent, since an entry already opening with [BLOCKED: passes through unwrapped. And the scan is deterministic from disk bytes, so the snapshot is byte-stable for the session and the prefix-cache invariant the whole design is built on survives a security control being added to the hot path.
  • Human approval available for memory writes via a shared staging gate.
  • Foreign-write detection with automatic backup and refusal.
  • Refusal rather than guessing on failed or ambiguous reads, including the case that costs the most: add rewrites the whole file from parsed entries, so a file that exists but reads as empty through a transient lock, a permission blip or an I/O error "would be rewritten down to just the new entry — wiping every prior memory." The reload returns a distinct _READ_FAILED sentinel and the add refuses.
  • Atomic writes, cross-platform locking, cross-session re-read, and order-preserving deduplication of entries at load, so a file that acquired a duplicate through an external append does not spend the budget twice.
  • Extensive FTS5 corruption detection and repair in the session store.
  • Profile isolation for multi-persona use.

Gaps:

  • No verification tier. Anything the model writes is authoritative in every subsequent session. There is no candidate state, no corroboration, and no rejected-value tombstone, so a wrong entry that gets removed can be re-derived and re-added the next session with nothing to stop it.
  • Forgetting is unrecorded and model-driven. A remove deletes the entry from the file and returns success; no event, counter or log records what left or why. Budget pressure is what drives it — a full store refuses the add and hands the model the current entries with an instruction to consolidate and retry in the same turn — so the deletions are proposed by the model under time pressure and land with nothing written down about them. The refusal itself is bounded: _MAX_CONSOLIDATION_FAILURES_PER_TURN is 3, after which the response drops the retry instruction and returns done so a failed side effect cannot hold the turn's reply hostage, which is the right trade and does mean the fact is silently not saved.
  • The fail-closed checkpoint path has no in-tree taker. MemoryProvider.pre_compress_checkpoint_api_version defaults to 1, the historical best-effort contract with the raw message list; a provider that durably checkpoints every successful on_pre_compress opts into version 2 and receives a host-normalized evidence list, with require_checkpoint propagating a failure instead of swallowing it. The host half is complete and tested. None of the eight shipped adapters declares version 2 — rg -n 'checkpoint_api_version|supports_checkpoint' plugins/memory/ returns nothing, the only declaration in the tree is a fake provider inside tests/agent/test_pre_compress_checkpoint_contract.py, and a sibling assertion pins the base class at 1. So every provider a user can actually mount is on best-effort semantics, and a pre-compression checkpoint that fails is a debug log line.
  • Substring identity is fragile under exactly the consolidation the design forces.
  • The write gate fails open if its module cannot be imported.
  • Write-time pattern scanning is a denylist, and unlike read-time fencing its coverage depends on the pattern set being complete.
  • No cross-layer deduplication or precedence between curated memory, provider memory, and session history.

10. Tests, Evals, and Benchmarks

Memory-relevant suites include tests/tools/test_memory_tool.py (851 lines), tests/tools/test_memory_tool_import_fallback.py, tests/tools/test_write_approval.py, tests/tools/test_threat_patterns.py, tests/agent/test_memory_provider.py, tests/agent/test_pre_compress_checkpoint_contract.py, tests/agent/test_memory_boundary_commit.py, tests/agent/test_builtin_memory_disabled_surface.py, tests/agent/test_memory_session_switch.py, and the holographic plugin suites.

The suites were not run for this review. Coverage is failure-path-oriented and several guards cite the issues that produced them (#26045 for drift, #50502 for FTS trigger corruption, #496 for the load-time promptware defence), which is good evidence that the defensive code reflects real incidents rather than speculation.

TestLoadTimeSnapshotSanitization is the part worth copying, and it is what earns negative_eval. Three cases assert that particular material must not reach the system prompt, and each pairs its negative with a positive over the same snapshot, so a snapshot that came back empty fails the test instead of passing it:

assert "Clean fact about the project." in snapshot
assert "[BLOCKED:" in snapshot
assert "ignore previous instructions" not in snapshot
assert "$API_KEY" not in snapshot
assert any("ignore previous instructions" in e for e in s.memory_entries)

That last line is the one most suites in this corpus omit: it pins the difference between withheld from the prompt and deleted from the store, which is the entire design of the mechanism. A second case runs a payload carrying no classic injection verb — a set of imperatives about registering as a node and never writing a script to disk — and asserts it is fenced anyway, so the test exercises the pattern library's reach rather than one canned string. A third asserts an entry already carrying the marker is not double-wrapped, which is the idempotence bug a placeholder substitution invites.

test_memory_boundary_commit.py pins an ordering contract rather than a value: at a /new session boundary, on_session_end must be delivered strictly before on_session_switch, both on one serialized background worker and without blocking the caller. Ordering contracts are usually left to a comment.

No committed memory-quality benchmark was found. There is no evaluation of whether the 2,200/1,375-character budgets are well chosen, and no measurement of what in-turn consolidation discards over long-running use — which is the single most important open empirical question about this design.

11. For Your Own Build

Steal

  • Frozen prompt snapshot. Decoupling durable writes from prompt mutation to preserve the cache is a genuinely useful move for any always-on memory block, and no other system in this atlas does it explicitly.
  • Hard budget with in-turn consolidation. Refusing the write and handing back current entries makes the compaction decision explicit and immediate instead of deferring it to a background job that may never run.
  • Character budgets, not token budgets, for model-independent limits.
  • Foreign-write detection with backup-then-refuse as a cheap alternative to bidirectional sync.
  • Refuse on ambiguous read rather than proceeding from a possibly-empty snapshot.
  • A staged write-approval gate shared across tools, giving humans a checkpoint on durable memory mutations.
  • Skills as separate procedural memory, indexed by name and loaded on demand.

Avoid

  • Model writes are immediately authoritative — no candidate tier, no corroboration, no tombstones.
  • Substring-as-identity for mutation targets.
  • Unlogged forgetting driven by budget pressure.
  • Denylist-based injection defence at write time, with no read-time fence as a second layer.
  • Fail-open write gate.
  • Layer duplication with no precedence rule between curated, provider, and session memory.
  • Provider interface with no deletion contract, making "forget me" unenforceable across a mounted backend.

Fit

Borrow:

  • The frozen-snapshot pattern, especially if prompt caching is a material cost.
  • The hard-budget-plus-consolidate-in-turn loop, with the per-turn failure cap.
  • _detect_external_drift's two signals and its backup-then-refuse response.
  • The staged write-approval gate for durable memory mutations.
  • The bounded-curated / unbounded-searchable split, which is a clean and honest division of labour.

Do not copy:

  • Substring addressing beyond a few dozen entries.
  • Write-time denylist scanning as the only injection defence.
  • The provider ABC as-is if you need deletion, scope, or multi-tenancy — it has none.
  • Unrecorded, model-driven eviction if you ever need to explain why something was forgotten.

12. Open Questions

  • What is actually lost over months of in-turn consolidation? Nothing measures or logs evictions.
  • Should removals write a tombstone so a re-derived entry can be recognized and refused?
  • Should the provider ABC gain forget(scope|id) and a scope parameter? Without them, host-level deletion cannot be honoured by a mounted provider.
  • Should curated memory, provider results, and session hits be fused and de-duplicated rather than concatenated as independent blocks?
  • Are 2,200 and 1,375 characters empirically right, or simply the first values that fit comfortably?
  • Could entries carry a stable hidden ID while preserving the ergonomic substring interface?

Appendix: File Index

  • Curated memory store, budgets, drift, locking: tools/memory_tool_store.py; tool surface and gate wrapper: tools/memory_tool.py.
  • Write gating and staging: tools/write_approval.py.
  • Injection/exfiltration patterns: tools/threat_patterns.py.
  • Provider contract: agent/memory_provider.py; orchestration: agent/memory_manager.py.
  • Load-time snapshot sanitization: MemoryStore.load_from_disk in tools/memory_tool_store.py.
  • Checkpoint API versioning: PRE_COMPRESS_CHECKPOINT_API_VERSION in agent/memory_provider.py, consumed in agent/conversation_compression.py and MemoryManager.supports_pre_compress_checkpoint.
  • Provider-agnostic retrieval query rewriting: plugins/memory/query_rewrite.py.
  • Session persistence and FTS5: hermes_state.py and the hermes_state_*.py modules beside it.
  • Skills as procedural memory: tools/skill_manager_tool.py, tools/skill_provenance.py, tools/skill_usage.py, agent/skill_*.py.
  • Provider adapters: plugins/memory/{holographic,honcho,mem0,hindsight,supermemory,openviking,byterover,retaindb}/.
  • Tests: tests/tools/test_memory_tool*.py, tests/tools/test_write_approval.py, tests/agent/test_memory_provider.py, tests/agent/test_pre_compress_checkpoint_contract.py, tests/agent/test_memory_boundary_commit.py.

Searches recorded for the negative claims

rg -n 'checkpoint_api_version|supports_checkpoint' plugins/memory/    # 0: no shipped adapter opts into v2
rg -n 'audit' tools/memory_tool_store.py tools/memory_tool.py        # 0: no log covers memory mutations
rg -n 'tombstone|rejected|superseded' tools/memory_tool_store.py     # 0: a remove writes no record

History

2026-09-099e6c4100… — third reading, 7,841 commits past the previous pin, at the same commit as the Holographic report so the two describe one repository in one state. Screened again before reading: one auto-run surface, twenty-one build-time execution surfaces, five unpinned surfaces and twenty manifests inside the seven-day cooldown; nothing was installed and no suite was run. The memory layer was split into more files without changing what it does — tools/memory_tool.py is now a 397-line tool surface over a 417-line tools/memory_tool_store.py, and hermes_state.py is 1,386 lines beside twenty-one hermes_state_*.py siblings — so section 3 and the appendix are re-anchored and the load-time sanitization now lives in load_from_disk rather than a separately named helper.

No published claim was wrong at this commit. Both marks were re-checked against the code rather than carried forward: the gate still returns allow, blocked or stage with stage_write recording an origin, still fails open when its module cannot import, and TestLoadTimeSnapshotSanitization still stands at tests/tools/test_memory_tool.py:609-673 with its three paired cases. The fail-closed checkpoint contract still has no in-tree taker, re-established by search rather than by memory of the previous reading, and the search is now recorded in the appendix.

New since the previous pin: entries are deduplicated at load, order-preserving and first-occurrence-wins, so an external append cannot spend the character budget twice; the ABC gained unavailable_reason and recall_status and now carries twenty-one members; the character budgets are configurable rather than fixed. tombstone, trust_state, scope_enforced, audit_log and bitemporal remain absent for the reasons the previous reading gave, all of which still hold.

2026-08-251bbb6e5b… — second reading, 7,068 commits past the previous pin in under a month, on a tree of 25,222 commits; hermes_state.py alone went from roughly 2,250 lines to 14,650. Screened before anything was read: one auto-run surface, twenty build-time execution surfaces, five unpinned surfaces and seven files inside the seven-day cooldown, against a much smaller surface at the previous pin; nothing was installed and no suite was run. negative_eval is added on TestLoadTimeSnapshotSanitization, three paired must-not-inject cases over the frozen system-prompt snapshot, and the mechanism they cover — a second threat scan at load time that fences a poisoned entry out of the prompt while leaving it visible in live state — is new since the previous pin. human_review is unchanged. tombstone, trust_state, scope_enforced, audit_log and bitemporal remain absent: a removal still writes no record of the value it removed, entries carry no status field, separation is a profile directory rather than a scope key filtered on read, and no event log covers memory mutations. The [BLOCKED: marker is not one of the missing marks either — it is computed per load into a derived snapshot and never written back, so nothing on disk records that an entry was judged.

One correction to the previous reading, which described the char budget as producing "unlogged budget-driven eviction". It does not and did not: add refuses at the cap and returns the current entries with a consolidate-and-retry instruction, at the previous pin as well as this one. The body of that report said so; the matrix row contradicted it. The row is corrected, and the accurate version of the risk is that removal is model-driven and unrecorded, which the refusal makes more likely rather than less.

2026-07-270fa5e41c… — first reading.