A live test exposed the problem: when an agent running in OpenProgram was asked to describe “how the current memory system is designed,”
it described Claude Code configuration files and skills on the host and recommended a tool deleted with the previous generation for preserving knowledge,
without mentioning the actual system under openprogram/memory/.
This page examines context assembly and checks the memory clues available to the model each turn against eight reference implementations, then specifies identification text,
introspection-tool placement, and what the model should see when memory fails.
Each layer answers one question: how memory appears in current context, what others do, which lines to change next, and what the design should be without current implementation constraints.
context/components.pyFifteen registered components each declare a layer (L0/L1), within-layer order, condition, and builder.
The assembler collects them by ["L0","L1"], sorts by order, drops false conditions, builds the rest, and joins them with two newlines.
Ascending order places more stable content first, because a stable prefix extends provider cache hits.
identity, inline_prompt, workspace_files, current_dateare all red;
memory_globalis green but conveys only part of its identity, as the next section explains.
Memory enters context through two paths sharing the same delimiter function. The delimiter was designed for recall, preventing old facts from being treated as new user requests. The resident block reuses it and consequently says only “this is recalled content.”
LocalMemoryBackend.system_prompt()
(local_backend.py:59) is return fence_memory(text) if body else "",
so <memory-context>has always existed. Its sentence was written for recall to prevent old facts from being treated as new requests,
which it successfully does. Reusing it for resident content answers the identity question with only “this is recalled content.”
fence_memoryfirst calls sanitize_context,
and sanitize_context has a first regex _FENCE_BLOCK_RE that removes the entire <memory-context>…</memory-context> block,
including its content. Measured fence_memory(fence_memory("hello world")) returns an empty delimited block:
<memory-context>\n[System note…]\n\n\n</memory-context>.
Identification must therefore replace the introduction, not add another wrapper.
From disk to wire, memory crosses four except, each converting exceptions to an empty string. To the assembler, an empty string means the component is absent this turn.
Thus empty memory and failed memory look identical to the model: nothing appears.
Break down each question about what the model can know and check the actual wire input. This fully explains the incorrect answer in the live test.
DEFAULT_TOOLSAmong twenty-four tools is wiki_agent (functions/__init__.py:113),
whose literal description is Maintain a wiki vault — route to ingest, enrich, browse, lint, or search depending on the task.
None of the six memory_* tools is present. Each turn, the model checks its tools for preserving knowledge,
and the listed answer points to a directory already removed during the refactor. This is misleading tool metadata, not model hallucination.
The source review checked functions/__init__.py:72-114 line by line and found zero default memory tools. tool_runtime supplies cwd, while environment supplies OS and Shell.Ask the same six questions of eight references. Mark absent features honestly as absent. All entries come from source with file paths and line numbers.
| Implementation | Injection | Identification | Tells the model “you have memory” | Introspection tools | Empty/failure visibility | Host vs project |
|---|---|---|---|---|---|---|
| OpenProgram Ours |
Resident system-prompt L0 order 50 + recall in each user-message prefixcomponents.py:424 / prep.py:90 | Yes, but incomplete<memory-context> + one System notememory/backend.py:83-94 |
No. No “memory” appears in the fifteen context componentsEntire components.py | Six exist; none is provided by default Only in toolset="memory"or"full"functions/__init__.py:72-114, 195 |
No. Disabled / empty / failed states are identical; four catches return empty stringscomponents.py:269-279 | No.<environment>Only OS + Shellcomponents.py:282-296 |
| hermes-agent | Volatile section of a three-part system prompt, snapshotted once at session startagent/system_prompt.py:277-286 | Yes. A banner of 46 ═ characters + title line + bannertools/memory_tool.py:442-458 |
Yes, the most explicit implementation You have persistent memory across sessions…agent/prompt_builder.py:150-156 |
memoryDefault tool set includes only add/replace/remove; every call returns the full list as a substitute for listmemory_tool.py:649, 425-440 |
The entire block disappears when empty, without a placeholder; only tool calls report Memory is not available…memory_tool.py:444, 582 | Yes, and explicitly identifies this exact problem Your memory and user profile describe the USER, not the system you are running on.prompt_builder.py:306-308 |
| openclaw | System prompt, # Project Context section; MEMORY.md always comes lastagents/system-prompt.ts:176-212, 54-62 |
Yes. Markdown heading + per-file introduction + ## MEMORY.md subheadingsystem-prompt.ts:176-212 |
Yes.## Memory RecallA full paragraph plus the template AGENTS.md’s You wake up fresh each session…memory-core/src/prompt-section.ts:16-26 |
memory_search + memory_get; memory-core is the default plugin slotmemory-core/src/tools.ts:229-484 / plugins/slots.ts:13 |
All three states are visible. Empty workspaces have There is no memory yet…; failed retrieval returns disabled:true + an instruction to inform the usertemplates/BOOTSTRAP.md:12 / tools.shared.ts:120-136 |
Yes.You are a personal assistant running inside OpenClaw. + These user-editable files are loaded by OpenClaw…system-prompt.ts:1016, 1207-1209 |
| opencode | One system-prompt array item; subdirectory AGENTS.md appears as a read-tool result suffixsession/instruction.ts / tool/read.ts:355-356 | At top level only one Instructions from: <path> line; nested content is wrapped in <system-reminder>instruction.ts:166 / read.ts:356 |
Depends on model tier.beast.txt (GPT family) has a full paragraphYou have a memory that stores information about the user…; anthropic.txtNoprompt/beast.txt:113-123 |
No.tool/ contains no memory tools; relies entirely on read/write/greptool/ directory |
No. Missing files produce no item; falsy entries are filteredinstruction.ts:166 / request.ts:64 | <env> block supplies cwd / workspace root / git / platform / date; global and project AGENTS.md have only paths, no labelssession/system.ts:59-70 |
| pi-mono | System prompt, <project_context> section appears between identity and skillscore/system-prompt.ts:132-172 |
Yes.<project_context> + Project-specific instructions and guidelines: + per-file <project_instructions path="…">system-prompt.ts:61-67 |
No. Its introduction says only operating inside pi, a coding agent harness, without mentioning memorysystem-prompt.ts:132 | No. Default tools are read/bash/edit/write onlysystem-prompt.ts:90 | No.contextFiles.length > 0 is required to produce content; read failure prints only a terminal warningresource-loader.ts:57-73 |
Code distinguishes global (~/.pi/agent) and project sources, but prose does not; the model infers from path="…"config.ts:477 / resource-loader.ts:85-110 |
| claude-code | Two separate paths. Usage instructions enter the system prompt; memory content enters message[0] as a synthetic user messageconstants/prompts.ts:495 / utils/api.ts:449-474 | Yes, the most complete implementation<system-reminder> outer wrapper + per-file Contents of {path} ({source annotation})utils/api.ts:463-469 / utils/claudemd.ts:1179-1186 |
Yes.You have a persistent, file-based memory system at {memoryDir}. This directory already exists — write to it directly with the Write tool…memdir/memdir.ts:239 | No dedicated tools; memory is files accessed through generic Write/Read/Grep. Recall uses background sideQuery unavailable to the modelmemdir/findRelevantMemories.ts | Silently absent in main sessions; subagents have a placeholder sentenceYour MEMORY.md is currently empty. When you save new memories, they will appear here.claudemd.ts:652-654 / memdir.ts:307-313 | Five sources each carry an annotation Global private / project checked-in / project private / automatic memory / team sharedclaudemd.ts:1169-1177 |
| codex-cli | Three separate paths: base prompt, AGENTS.md in user-role world-state fragments, and memory summaries in developer-role policy fragmentscontext/user_instructions.rs:11 / ext/memories/src/extension.rs:64-68 | Yes. AGENTS.md uses # AGENTS.md instructions + <INSTRUCTIONS>; memory uses ========= MEMORY_SUMMARY BEGINS =========user_instructions.rs:18-19 / templates/memories/read_path.md:125 |
Present only when the feature is enabled.You have access to a memory folder with guidance from prior runs…; base prompt default.md contains no “memory”read_path.md:3-4 |
Four real tools, but two feature switches default offList / Read / Search / AddAdHocNoteext/memories/src/tools/mod.rs:36-51 / features/src/lib.rs:996-999 |
Missing memory is silent; replaced or removed AGENTS.md is explicitly reportedThe previously provided AGENTS.md instructions no longer apply.prompts.rs:32-43 / agents_md.rs:10-11 | Yes. Global and project sources are separated by --- project-doc ---; multiple workspaces each receive for {env_id} with root {cwd}agents_md.rs:43, 366-370 |
| weclaw | An operations-configured static string, --append-system-prompt or first system messageagent/cli_agent.go:122-124 |
No. Concatenated verbatim with no markerSame as above | No. No memory concept anywhere in the repository | No | N/A. Omit the entire item if the string is emptycli_agent.go:122 | No. cwd passes directly to the subprocess without explanation |
| pi-ai | This directory is a read-only snapshot of pi-mono’s transport layer (OAuth + provider request streaming), with no agent loop, system-prompt assembly, or tool system. All six questions are N/A.references/pi-ai/README.md:1-23 | |||||
Compare each implementation’s memory wrapper verbatim in three groups: tags with explanatory text, tags only, or neither.
message[0], a synthetic user message,
with an outer <system-reminder> wrapper and per-file Contents of {path} ({source annotation}) inside;
five source types have five annotations: user's private global instructions for all projects,
project instructions, checked into the codebase,
user's private project instructions, not checked in,
user's auto-memory, persists across conversations,
shared team memory, synced across the organization.
The model knows this is memory, who owns each file, and its scope. Only this implementation answers authorship at this level of precision.
openclawPlaces memory_search + memory_get in memory-core,
the memory plugin slot’s default implementation, so installation makes it available without selection.
The tool description is itself an instruction: Mandatory recall step…, and explicitly says If response has disabled=true,
memory retrieval is unavailable and should be surfaced to the user.
hermes has memory enabled in the core set, but only add/replace/remove, without list;
returning the full list after each write substitutes for listing.
codex-cliHas four real introspection tools (List / Read / Search / AddAdHocNote),
but both switches default off: Feature::MemoryTool is default_enabled: false,
and an additional memories.dedicated_tools.
claude-codeUses no dedicated tools: memory is files accessed through generic Write/Read/Grep;
the prompt directly instructs the model to write in that directory.
Our position resembles codex: six implemented tools, unavailable by default. Codex uses explicit feature flags;
ours is the easily overlooked fact that they are absent from DEFAULT_TOOLS.
Empty: the BOOTSTRAP.md template explicitly tells the model: There is no memory yet. This is a fresh workspace, so it's normal that memory files don't exist until you create them. This makes emptiness normal rather than suspicious. claude-code has a similar placeholder for subagents: Your MEMORY.md is currently empty. When you save new memories, they will appear here., but not for the main session.
Failed: openclaw retrieval failure returns structured disabled:true + unavailable:true + warning +
action: Check embedding provider configuration and retry, and requires the model to tell the user. Unique among the eight.
Changed: codex alone says that old content is no longer valid: The previously provided AGENTS.md instructions no longer apply. This applies to project documents, however; missing memory remains silent.
The other five, including ours, silently omit the block, leaving the model unable to distinguish never-recorded content from failed retrieval.
This directly caused the observed symptom and deserves separate analysis: can the model tell which visible content belongs to its hosting framework and which belongs to the project it is editing?
Four independently deployable and verifiable changes. Total 85 tokens per turn, including 65 in the L0 cached prefix that are not repeatedly billed from the second turn onward.
The six memory_* enter DEFAULT_TOOLS, and all six also enter DEFERRED_DEFAULT_TOOLS.
The deferred catalog sends only names, not descriptions (deferred_catalog_text is "\n".join(name …));
the six lines measure 20 token.
Each turn, the model scans its tools and sees memory_search, memory_browse, memory_status
names, making memory availability explicit.wiki_agent is no longer the only listed knowledge-system-like tool.
It need not be removed; put the correct answer alongside it.
Alongside fence_memory add fence_core, sharing sanitize_context;
the resident path replaces fence_memory with it. Do not wrap twice, because
_FENCE_BLOCK_RE removes the entire inner block and produces an empty wrapper (measured in section 02).
Leave the recall path unchanged: its introduction correctly prevents old facts from becoming new requests.
_build_memoryStop returning an empty string for all cases. Disabled backend emits nothing because it is the user’s choice;
an empty workspace says memory is enabled, empty, will accumulate automatically, and is healthy;
a provider exception says memory was unavailable this turn and must not imply the user never said something.
In the latter two cases, one line replaces the original 821 tokens, yielding a net reduction.
<environment>Currently only OS and Shell appear. Add two lines: framework name and its own state directory.
Change B’s introduction also states the memory workspace’s absolute path.
Adopt hermes’s meaning: these belong to the framework running you, not the project you edit.
The model then has no reason to search ~/.claude for “my memory system.”
memory_searchandmemory_browse
already state their purpose. Schema parameter instructions are needed only when calling the tool.
tool_search is never deferred and is the sole entry point for loading the others, so the model can act on a known name in one step.
Most sessions never call the six tools. A single status line replaces 821 tokens of stale content, producing a net reduction.The target without current implementation constraints. Four items, each naming the gap and reason for deferral.
Called components.assemble(agent, ["L0","L1"], tools=…),
counted each block’s tokens, and obtained 1638 total with 821 for memory. Called agent_tools(names=DEFAULT_TOOLS, only_available=True)
and printed each tool: zero memory_* and one wiki_agent.
among twenty-four. Delimiter non-idempotence was executed: fence_memory(fence_memory("hello world")) returned an empty-content wrapper.
Every entry cites a path and line number, with literal text copied verbatim.
references/pi-ai/ is a read-only snapshot of pi-mono’s transport layer without an agent layer; all six answers are N/A, not “not found.”
references/openclaw-full/ does not exist; references/openclaw/ is the complete source.
references/claude-code/ contains only five BashTool files; all memory-related content is in
references/claude-code-leaked/; its <system-reminder> literal text matches the actual local input and can be cross-checked.
~/.openprogram/memory/: core.md is 4494 bytes;
topics/andsources/ each contain zero files.
That core.md remains from the previous generation: it starts with a ═══ … 188% (3854/2048 chars) ═══ banner (hermes format),
the body uses [[double brackets]] wiki links, and ends with
[for full context start with memory_browse], while memory_browse is absent from the default tool list.
This is what the model reads every turn, explaining its “wiki” answer.