Comparison of eight frameworksidentified several areas where other implementations do better. This page answers the next question: how each practice fits our structure, or why it does not. The authoritative text is in overview.md, and mechanism diagrams are in Memory subsystem: mechanisms illustrated.
Verified against the running code in openprogram/memory/, with line numbers recorded verbatim. Measured results include numbers.
The original approach in the referenced implementation and why it takes that form. The form may not apply; the motivation usually does.
The adapted design, or the reason not to adopt it. Each item has a decision, not a “worth investigating” conclusion.
The conclusions come first. Three practices are adopted directly or after adaptation; one is rejected because our structure already handles its purpose elsewhere or places its cost on the wrong operation.
claude-code puts a one-line description at the start of each memory file. Retrieval lists those descriptions and uses one inexpensive small-model call to choose five filenames, without building an index. Our retrieval runs BM25 every turn between the user message and model call.
topics/ is immediately rejected by strict parsing
(TopicFormatError: memory block ID required); blockquotes are also rejected, and only HTML comments pass.
A file-header description would therefore require changing our parser contract, while the path already provides that information.
inspect.search uses persist=False,
fully rehashing and parsing both directory trees every turn.Implication: Per-turn retrieval’s time-budget problem is local
and can be fixed locally; an added model round trip can only be avoided.
Rejected because it duplicates an existing capability. claude-code has no index; this approach avoids building one and helps minimize its implementation size in the same comparison. We have already paid the indexing cost. Adding another model round trip duplicates that capability at the cost of waiting every turn. The first two retrieval-quality improvements are local: avoid full reparsing on every retrieval, and add a meaningful minimum result score.
When openclaw trims MEMORY.md, it removes only automatically promoted paragraphs, preserves handwritten content unchanged, and returns removed dates to the caller. Adopting this first requires answering: what is core.md in the new structure? Once answered, ownership itself need no longer be distinguished.
Answer to the prerequisite question: core.md is a derived view, not independent content.
It renders the single topic file topics/core.md, in the same category as timeline/,
recent_events.jsonl, relations.json; manual edits to the view have no effect,
so edit the canonical source. The writer prompt changes from “update core.md” to “put stable facts that should always be visible in
topics/core.md”, leaving only one file type for the writer to understand. The old statement “core.md mirrors a designated page” remains true;
the designated page is now a topic file rather than a wiki page.
| Dimension | Before our change | openclaw | Our current implementation |
|---|---|---|---|
| Who writes | The writer agent edits core.md directly | Automatic promotion and manual edits share one file | Nobody writes it. Write topics/core.md; Runtime renders |
| Limit | 2000 tokens; exceeding it raisesmanagement/config.py:7 | 10000 characters | 2000 tokens becomes a rendering budget, not an exception |
| When full | Reject the transaction; the repair instruction directs the writer elsewhere, permanently freezing core.md | Remove automatic paragraphs, preserve handwritten ones | Render in canonical file order up to budget; overflow remains in the source |
| Does removal destroy content | Not applicable; nothing was ever removed | Yes, so ownership must be distinguished first | No; the canonical source is complete and remains retrievable |
| When it runs | Never. Nightly reorganization lists only topics/ | Trim to budget during writes | Rebuild with other derived views after each write transaction installs successfully |
| What it reports | reorganize returns {"status":"ok","topics":N}, with no place for core |
Return the list of removed dates | Return rendered token count and excluded block IDs |
Also report whether anything actually changed. Nightly reorganization depends on model judgment,
whose failure mode is silently doing nothing while satisfying its own criterion:
under the same reorganization prompt, a 520,000-character single-topic conversation produced a 34.4k-character file that was never split despite repeated reorganization,
because the criterion was “covers two topics,” not size. Therefore reorganize must also return the files actually changed in the run;
an empty list visibly means “nothing changed.” Revising the criterion is separate, but without visibility we cannot even assess whether revision is needed.
pi-mono summarizes a branch when the user leaves it and attaches the summary to the tree. This addresses the gap acknowledged in overview.md: both write paths use only the branch beneath head, and nobody revisits branches abandoned midway. Keep the objective, omit summary nodes.
Why not summary nodes.
pi-mono’s session JSONL tree serves replay and branching; across sessions, only AGENTS.md remains, so abandoned branches truly disappear without summaries.
We have a full topic-organized memory layer. Facts stated on a branch belong in topics/people/dave.md,
not in a summary node attached to the DAG, where they have no footnotes or block IDs and cannot be retrieved.
We already have a DAG-attached summary type (the compaction summary in context/summary), serving the context window rather than memory.
Another type would add another location to search for a statement.
Furthermore, leaving a branch is not currently an observable event in our system.
The event registry has no head-movement or branch-switch event. Head writes are spread across five locations
(chain advancement in set_head, append_message, direct index modification in SessionNodeWriter.append,
update_session, _rewind.py); their only common call is
SessionMemoryIndex.set_head, which discards the old value.
Copying pi-mono would require a new event in this path and retention of the previous head,
solely to trigger work that is equally useful at session end. The session-boundary entry already exists:
session_watcher.py:103 currently queries only the head branch.
One criterion must change together.
should_incremental_write currently has two conditions: pending tokens exceed the threshold, or something is pending and the last message is over an hour old.
An abandoned branch’s last message is necessarily old, so the second condition would also write one-turn retry siblings.
Non-head branches must therefore use only the first condition and disable idle_after. This is the only additional decision this design needs.
When claude-code cannot find the cursor’s message, it treats all messages as unwritten again. Its comment explains that returning zero messages would stop extraction for the remainder of the session. Seeding from source archives in our write-cursor design makes the same tradeoff, but covers only migration; a similar ordinary-runtime failure remains.
How this differs from migration. “Migrating from positional cursors” in overview.md addresses a one-time event: the old state cannot reconstruct the ledger, so source archives seed it, preferring a repeated batch to dropping everything. The tradeoff is the same, but occurs only during upgrade. Here, the ledger itself fails during ordinary operation, independent of migration. Both paths are needed and follow the same principle.
Where this applies after the ledger changes shape.
The final write-cursor design uses “written” markers on nodes, rather than a separate memory-owned ID set,
so there is no cursor file to become unreadable. The principle still applies to a different object:
unreadable accounting means “nothing written,” rather than an exception that prevents the session from ever writing.
For node markers, a missing marker causes repeated work; additionally, RuntimeStateStore.load
encountering an unparseable runtime.json should return empty counters rather than raise.
The two approaches are compared side by side in layer 3 of written-marker.md.
These are the file locations for the three adopted practices. The rejected practice changes no files.
Node markers have replaced positional cursors.
Implementation resides in SessionDB node metadata, runtime/online.pyandwriting.py at the
_records boundary. Legacy migration restores verifiable nodes from strictly parsed Source prefixes.
Session boundaries now process the current head first, then enumerate other live branches; node markers deduplicate shared prefixes.
For the full definition, see written-marker.md. The derived-view change replaces _synchronize’s exception above 2000 tokens with a rendering budget and reports excluded block IDs. WRITABLE_FILES previously listed core.md as manually writable; the derived file is read-only.
Implementation status (2026-08-11): Section 01 rejects index-free model selection based on measured latency;
section 02 implements the canonical topics/core.md source and deterministic core.md derived view; section 03 implements live-branch enumeration
at session boundaries; section 04 recovers corrupt state as empty state together with strict legacy migration and node markers.
The current implementation still uses post-turn thresholds and an idle watcher, with no direct writer notification from head-change events.