Documentation
¶
Overview ¶
Package memdir loads the on-disk memory that seeds the agent's system prompt at session start, and provides the read primitives for evva's typed-memory directory:
- <workdir>/EVVA.md workdir memory — repo conventions (user-authored)
- <appHome>/memory/ one global auto-memory directory of typed *.md files
- <appHome>/memory/MEMORY.md the always-injected index (maintained by the model)
Individual memory files carry frontmatter (name / description / type) and are pulled into a turn on demand by the relevance retriever in the recall sub-package; only the MEMORY.md index injects statically into the prompt. The model writes memory files itself with the standard write/edit tools — a permission write carve-out auto-allows writes confined to the memory dir.
All files are optional. Missing files yield zero-value Snapshot fields and no warning; the prompt builder skips empty sections cleanly. Any non-missing read failure (permission, oversize) is recorded in Snapshot.Warnings — Load itself never returns an error so the agent can always boot.
This package depends only on stdlib. It is not imported by the sysprompt package; the caller threads Snapshot fields into the prompt context, keeping the dependency arrow one-way. The relevance retriever needs llm.Client, so it lives in the internal/memdir/recall sub-package rather than here.
Index ¶
- Constants
- Variables
- func Age(mtime time.Time) string
- func AgeDays(mtime time.Time) int
- func EnsureMemoryDir(appHome string) error
- func FormatManifest(hs []MemoryHeader) string
- func FreshnessNote(mtime time.Time) string
- func FreshnessText(mtime time.Time) string
- func IsInMemoryDir(appHome, absPath string) bool
- func MemoryDir(appHome string) string
- func MemoryIndexPath(appHome string) string
- func ParseFrontmatter(content string) (map[string]string, string)
- func ProjectKey(absPath string) string
- func ReadIndex(appHome string) (body string, warning string)
- type MemoryHeader
- type MemoryType
- type Snapshot
Constants ¶
const ( // MaxIndexLines caps how many lines of MEMORY.md inject into the prompt. // Port of memdir.ts:MAX_ENTRYPOINT_LINES. MaxIndexLines = 200 // MaxIndexBytes caps the injected index size (~125 chars/line × 200 lines). // It catches long-line indexes that slip past the line cap. Port of // memdir.ts:MAX_ENTRYPOINT_BYTES. MaxIndexBytes = 25_000 )
const ( // MemoryDirName is the single global auto-memory directory under AppHome. // Port of paths.ts:AUTO_MEM_DIRNAME. evva diverges from ref's per-git-root // keying — one global store, no project key (PRD §5.2). MemoryDirName = "memory" // MemoryIndexFile is the always-loaded index inside the memory dir. The // model maintains it (one `- [Title](file.md) — hook` line per memory); Go // only reads it. Port of paths.ts:AUTO_MEM_ENTRYPOINT_NAME. MemoryIndexFile = "MEMORY.md" )
const ( // MaxMemoryFiles caps how many headers a scan returns (newest-first). Port // of memoryScan.ts:MAX_MEMORY_FILES. MaxMemoryFiles = 200 // FrontmatterMaxLines bounds how many leading lines a scan reads per file // when looking for frontmatter. Port of memoryScan.ts:FRONTMATTER_MAX_LINES. FrontmatterMaxLines = 30 )
const MaxFileBytes = 64 * 1024
MaxFileBytes caps each read memory file at 64 KiB. Past that the user is almost certainly using EVVA.md for the wrong thing (knowledge base, not conventions doc); we truncate and warn rather than refuse outright so a bloated file doesn't break the session.
const ProjectMemoryFile = "EVVA.md"
ProjectMemoryFile is the basename of the user-authored, repo-scoped memory file read from the workdir. (EVVA.md is conventions, NOT auto-memory — it is out of scope for the typed-memory directory and left exactly as-is.)
Variables ¶
var MemoryTypes = []MemoryType{TypeUser, TypeFeedback, TypeProject, TypeReference}
MemoryTypes is the canonical ordered type list. Consumed by ParseMemoryType and by the prompt's frontmatter example.
Functions ¶
func Age ¶ added in v1.4.3
Age renders a human-readable age string ("today" / "yesterday" / "N days ago"). Models reason about staleness from "47 days ago" far better than from a raw timestamp. Port of memoryAge.ts:memoryAge.
func AgeDays ¶ added in v1.4.3
AgeDays returns whole days elapsed since mtime, floored: 0 for today, 1 for yesterday, 2+ for older. A future mtime (clock skew) clamps to 0. Port of memoryAge.ts:memoryAgeDays.
func EnsureMemoryDir ¶ added in v1.4.3
EnsureMemoryDir creates the memory dir (mkdir -p). Idempotent; called once at session start so the model can write a memory file without first checking the dir exists — the prompt's "this directory already exists" claim depends on it (ensureMemoryDirExists parity, memdir.ts:129). Never fatal in spirit: a real failure (EACCES/EROFS) is returned for the caller to log, and the model's own Write would surface the underlying error anyway. Empty appHome is a no-op.
func FormatManifest ¶ added in v1.4.3
func FormatManifest(hs []MemoryHeader) string
FormatManifest renders the header list as one line per file:
- [type] filename (RFC3339): description
The `[type] ` tag and `: description` suffix are omitted when absent (formatMemoryManifest parity, memoryScan.ts:84). Timestamps are UTC RFC3339.
func FreshnessNote ¶ added in v1.4.3
FreshnessNote wraps FreshnessText in <system-reminder> tags with a trailing newline, for callers that don't add their own wrapper. "" for memories ≤1 day old. Port of memoryAge.ts:memoryFreshnessNote.
func FreshnessText ¶ added in v1.4.3
FreshnessText is the plain-text staleness caveat for memories older than one day; "" for today/yesterday (a caveat there is just noise). The wording is copied verbatim from memoryAge.ts:memoryFreshnessText — it was tuned against real incidents where a stale file:line citation made a wrong claim sound more authoritative, not less.
Use this when the consumer adds its own wrapping (the recall path wraps the whole batch in one <system-reminder>, so it embeds FreshnessText, not the self-wrapping FreshnessNote).
func IsInMemoryDir ¶ added in v1.4.3
IsInMemoryDir reports whether absPath is confined within MemoryDir(appHome). Same filepath.Rel containment shape as permission.IsPlanFilePath: rejects the dir root itself, "..", siblings, and absolute-elsewhere. Empty appHome or a path that can't be proven contained → false.
Used by the per-turn recall read-file guard (the permission write carve-out uses permission.IsAutoMemPath, which keeps pkg/permission free of an internal/memdir import — same logic, two homes by design).
func MemoryDir ¶ added in v1.4.3
MemoryDir returns <appHome>/memory — the one global memory store. Empty appHome yields "".
func MemoryIndexPath ¶ added in v1.4.3
MemoryIndexPath returns <appHome>/memory/MEMORY.md. Empty appHome yields "".
func ParseFrontmatter ¶ added in v1.4.3
ParseFrontmatter splits a YAML-ish frontmatter block from a markdown body.
A memory file may open with a block delimited by a `---` line on each side:
--- name: user-role description: senior Go engineer, new to this repo's frontend type: user --- <body…>
The parser is deliberately minimal — flat `key: value` pairs, string values only — so the base memdir package stays stdlib-only (no YAML dependency; see the package doc). It is legacy-tolerant by design: a file with no opening `---`, or an unterminated block, yields an empty map and the *entire* input as the body, never an error. A malformed memory file therefore stays readable (the scanner simply skips it) instead of breaking a session.
Keys are lowercased + trimmed; values are trimmed and stripped of one pair of surrounding quotes. Indentation is tolerated, so a nested `metadata:` block — some ref templates write `type:` under it — is flattened into the same map. The flat form is canonical (MEMORY_FRONTMATTER_EXAMPLE), so a top-level key always wins over an indented duplicate of the same name.
func ProjectKey ¶ added in v1.4.3
ProjectKey turns an absolute filesystem path into a stable directory key used under <APP_HOME>/projects/. Mirrors the convention Claude Code uses at ~/.claude/projects/ — the path's separators are flattened to "-" so the result is one segment that round-trips losslessly enough for human inspection.
Examples (POSIX):
/Users/johnny/lab/evva -> "-Users-johnny-lab-evva" /home/alice/work/api -> "-home-alice-work-api"
On Windows the volume colon is dropped and backslashes are flattened the same way:
C:\Users\Alice\proj -> "C-Users-Alice-proj"
An empty input yields "". Inputs are cleaned via filepath.Clean first so "/a//b/../c" and "/a/c" produce the same key.
func ReadIndex ¶ added in v1.4.3
ReadIndex reads <appHome>/memory/MEMORY.md and returns it ready for prompt injection: trimmed, truncated to the line + byte caps, with a named-cap warning appended when either cap fired. The second return is a short, bare truncation message for Snapshot.Warnings logging ("" when nothing was truncated). A missing or empty index returns ("", "").
Go only READS the index — the model maintains MEMORY.md itself as part of its two-step save (PRD Task 2: a Go-side index writer would fight the model for ownership of the same file). Port of memdir.ts:truncateEntrypointContent.
Types ¶
type MemoryHeader ¶ added in v1.4.3
type MemoryHeader struct {
// Filename is the path RELATIVE to the memory dir (e.g. "feedback/foo.md").
// This same string is the key everywhere: in the manifest, in the recall
// selector's valid-filename set, and in the alreadySurfaced de-dup — it is
// the real hallucinated-path safety net (PRD Task 1 note).
Filename string
Path string // absolute path on disk
ModTime time.Time // file mtime; drives newest-first ordering + freshness
Description string // frontmatter `description`; "" when absent
Type MemoryType // frontmatter `type`; "" when absent/unknown
}
MemoryHeader is the lightweight view of one memory file — enough to build the recall manifest and surface freshness without reading the full body. Port of memoryScan.ts:MemoryHeader.
func ScanMemoryFiles ¶ added in v1.4.3
func ScanMemoryFiles(dir string) []MemoryHeader
ScanMemoryFiles walks dir recursively for *.md files (excluding MEMORY.md), reads each one's frontmatter header, and returns the headers sorted newest-first, capped at MaxMemoryFiles.
Never errors: a missing dir, an unreadable file, or malformed frontmatter is skipped — mirroring memoryScan.ts (Promise.allSettled + catch → []). A missing dir or a dir with no .md files returns an empty slice.
type MemoryType ¶ added in v1.4.3
type MemoryType string
MemoryType is the closed taxonomy a memory file declares in its `type:` frontmatter. Ported from ref/src/memdir/memoryTypes.ts: four types that capture context NOT derivable from the current project state. Code patterns, architecture, and git history are derivable (grep/git/EVVA.md) and must not be saved as memories.
const ( TypeUser MemoryType = "user" TypeFeedback MemoryType = "feedback" TypeProject MemoryType = "project" TypeReference MemoryType = "reference" )
func ParseMemoryType ¶ added in v1.4.3
func ParseMemoryType(raw string) (MemoryType, bool)
ParseMemoryType resolves a raw frontmatter value to a MemoryType. Returns ("", false) for an unknown or empty value — files without a `type:` keep working, and files with an unknown type degrade gracefully rather than erroring (parseMemoryType parity, memoryTypes.ts:28). Callers treat the false case as "untyped".
type Snapshot ¶
type Snapshot struct {
WorkdirMemory string // raw contents of <workdir>/EVVA.md (user-authored, repo-scoped)
MemoryIndex string // <appHome>/memory/MEMORY.md body, truncated + inject-ready; "" when absent or auto-memory off
MemoryDir string // absolute <appHome>/memory when auto-memory is on; "" otherwise (the per-turn recall + carve-out gate on this)
Warnings []string // non-fatal: oversize-truncation, permission errors
}
Snapshot is one session's view of the on-disk memory. Any field may be empty when the underlying file is missing, empty, or unreadable; callers treat empty as "skip the section."
func Load ¶
Load reads the memory that seeds a session. EVVA.md is always read (it is user-authored conventions, independent of the auto-memory toggle). When autoMemory is true and appHome is set, Load also ensures the global memory dir exists (so the model can write without checking), records its path, and reads the MEMORY.md index. The function never returns an error.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package dream is the background memory-consolidation ("dream") subsystem: a gated, fenced pass that merges, prunes, and re-indexes the global memory store.
|
Package dream is the background memory-consolidation ("dream") subsystem: a gated, fenced pass that merges, prunes, and re-indexes the global memory store. |
|
Package recall finds the memory files relevant to a user query via a cheap LLM side-query, so only the few memories that matter get pulled into a turn (the rest stay on disk, surfaced on demand).
|
Package recall finds the memory files relevant to a user query via a cheap LLM side-query, so only the few memories that matter get pulled into a turn (the rest stay on disk, surfaced on demand). |