memdir

package
v1.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 9 Imported by: 0

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

View Source
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
)
View Source
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"
)
View Source
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
)
View Source
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.

View Source
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

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

func Age(mtime time.Time) string

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

func AgeDays(mtime time.Time) int

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

func EnsureMemoryDir(appHome string) error

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

func FreshnessNote(mtime time.Time) string

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

func FreshnessText(mtime time.Time) string

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

func IsInMemoryDir(appHome, absPath string) bool

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

func MemoryDir(appHome string) string

MemoryDir returns <appHome>/memory — the one global memory store. Empty appHome yields "".

func MemoryIndexPath added in v1.4.3

func MemoryIndexPath(appHome string) string

MemoryIndexPath returns <appHome>/memory/MEMORY.md. Empty appHome yields "".

func ParseFrontmatter added in v1.4.3

func ParseFrontmatter(content string) (map[string]string, string)

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

func ProjectKey(absPath string) string

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

func ReadIndex(appHome string) (body string, warning string)

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

func Load(workdir, appHome string, autoMemory bool) Snapshot

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.

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL