File-based project memory.
Loads AGENTS.md (preferred) / CLAUDE.md files from a small fixed
hierarchy and turns them into messages the agent sees on every turn.
The hierarchy, in load order, is:
- User-level —
~/.config/ex_athena/AGENTS.md(orCLAUDE.md). Cross-project preferences a user wants every agent to honour. - Project-level —
<cwd>/AGENTS.md(orCLAUDE.md). Repository conventions; usually committed. - Local override —
<cwd>/AGENTS.local.md. Personal scratch on top of project conventions; usually gitignored.
When both AGENTS.md and CLAUDE.md exist at the same level, the
AGENTS.md file wins (matches opencode's behaviour for cross-tool
compatibility).
Where the messages live
Every loaded file becomes a single user-role message tagged
name: "memory" — placed at the front of the conversation so it
precedes the user's first prompt. The Claude Code paper notes that
Claude Code delivers memory as user-context (probabilistic compliance)
rather than as a system prompt (deterministic compliance), and we
copy that pattern.
The compactor pipeline pins these messages: see
ExAthena.Memory.pinned_count/1.
Summary
Functions
Discover and load the memory hierarchy for cwd. Returns a list of
Message.t() in load order.
Is message a memory user-context message produced by discover/2?
Number of pinned-prefix slots the compactor must preserve for memory
messages already prepended to messages. Used by
ExAthena.Compactors.Summary (and the PR2 pipeline) to compute the
effective floor for the pinned prefix.
Functions
@spec discover( String.t(), keyword() ) :: [ExAthena.Messages.Message.t()]
Discover and load the memory hierarchy for cwd. Returns a list of
Message.t() in load order.
Each file's contents are wrapped with a header that names the source so the model can tell them apart. Empty / missing files are skipped.
Options
:user_dir— override the user-level memory directory. Defaults to~/.config/ex_athena/. Useful in tests.:filenames— override the candidate filenames (defaults to["AGENTS.md", "CLAUDE.md"]).
@spec memory_message?(ExAthena.Messages.Message.t() | term()) :: boolean()
Is message a memory user-context message produced by discover/2?
@spec pinned_count([ExAthena.Messages.Message.t()]) :: non_neg_integer()
Number of pinned-prefix slots the compactor must preserve for memory
messages already prepended to messages. Used by
ExAthena.Compactors.Summary (and the PR2 pipeline) to compute the
effective floor for the pinned prefix.