memory

package
v0.1.5 Latest Latest
Warning

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

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package memory implements fairpeer's persistent memory. It mirrors Claude Code's two-layer model while honoring fairpeer's cache-first architecture:

  • Hierarchical doc memory: fairpeer.md / AGENTS.md files discovered from the user config dir and up the project tree, with "@path" imports. This is the analog of CLAUDE.md.
  • Auto-memory store: per-project fact files with frontmatter plus a MEMORY.md index, which the model maintains via the `remember` tool (see store.go).

Bitemporal memory (v0.3.0): each stored fact carries both system time (CreatedAt/UpdatedAt — when the record was written) and valid time (ValidFrom/ValidTo — when the fact holds true in the real world). When a fact is updated, the old version is archived as "superseded" rather than deleted, preserving a full history chain. The `memory_query` tool supports time-point queries ("where did I live in March?") via ListAsOf.

All of it folds into the durable system-prompt prefix exactly once at boot (see Compose), so it rides the provider's automatic prefix cache at zero per-turn cost. Mid-session changes never mutate that prefix; they take effect through the controller's transient tail-injection and fold into the prefix on the next session. (some providers do not report cache tokens; the prefix stability still reduces token transmission and prepares for future cache support.)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AppendDoc

func AppendDoc(path, note string) error

AppendDoc appends a one-line note as a bullet under a "## Notes" section in the doc-memory file at path, creating the file (and section) when absent. The note is normalised to a single line so it can't corrupt the section. This is the write side of the "#" quick-add: a plain file edit the user can later reorganise by hand.

func Compose

func Compose(base string, s *Set) string

Compose folds the memory block onto the base system prompt and returns the durable cached-prefix string. Base stays first (it is the most stable text, so it remains a valid cache prefix even when memory changes between sessions); memory follows. With no memory, base is returned unchanged.

func NewForgetTool

func NewForgetTool(store Store) tool.Tool

NewForgetTool returns the `forget` tool bound to store.

func NewRecallTool

func NewRecallTool(store Store) tool.Tool

NewRecallTool returns the `recall` tool bound to store.

func NewRememberTool

func NewRememberTool(store Store) tool.Tool

NewRememberTool returns the `remember` tool bound to store. A zero/disabled store yields a tool that reports the store is unavailable rather than silently dropping saves.

func NormalizeProfile

func NormalizeProfile(s string) string

NormalizeProfile coerces an arbitrary string to a known profile partition, defaulting to "dev" so a sloppy or empty caller never lands memories in a dangling directory. This is for *path* derivation; use NormalizeProfileScope (remember.go) when saving, where unknowns default to "global" instead.

func NormalizeProfileScope

func NormalizeProfileScope(s string) string

NormalizeProfileScope coerces a save's profile argument to a known partition or "" (caller defaults to "global"). Unlike NormalizeProfile (which defaults unknowns to "dev" for *path* derivation), this returns "" for unknowns so the caller can apply the "shared by default" rule distinct from the path floor.

func WithQueue

func WithQueue(ctx context.Context, q Queue) context.Context

WithQueue stamps q onto ctx for the remember/forget tools to find.

Types

type Memory

type Memory struct {
	Name      string    `json:"name,omitempty"`       // kebab-case slug; also the file stem (<name>.md)
	Body      string    `json:"body,omitempty"`       // the fact itself (Markdown)
	Type      Type      `json:"type,omitempty"`       // user/feedback/project/reference (panel tag)
	Profile   string    `json:"profile,omitempty"`    // "global" | "dev" | "cowork" | "project"
	CreatedAt time.Time `json:"created_at,omitempty"` // first write (immutable)
}

Memory is one stored fact.

v0.4: the structure is deliberately small. The 13 bitemporal/lifecycle/ classification fields that existed to govern per-turn injection (status, importance, valid_from/to, ttl, category, tags, supersede*, access tracking) were removed because saved facts are no longer injected — the portrait layer is. Name + Body + Type carry the fact; Profile records which mode it belongs to; CreatedAt supports a light-weight ordering in the panel.

type Options

type Options struct {
	CWD     string
	UserDir string
	Profile string
}

Options configures discovery. CWD defaults to "." and UserDir is the user config root (config.MemoryUserDir()); a "" UserDir disables user-global docs and the auto-memory store. Profile is the active product mode ("dev"|"cowork") and partitions both the portrait layer and the auto-memory store by mode.

type Queue

type Queue interface{ QueueMemory(note string) }

Queue receives a one-line note about a memory change a tool just made, so the controller can fold it into the current turn — taking effect this session without touching the cache-stable system prefix. (some providers do not report cache tokens; the prefix stability still reduces token transmission.) The remember/forget tools read it from their call context the same way background tools read the job manager.

func QueueFromContext

func QueueFromContext(ctx context.Context) (Queue, bool)

QueueFromContext returns the memory queue the agent stamped, if any.

type Scope

type Scope string

Scope labels where a doc source was discovered, so the assembled block can attribute each chunk and callers (e.g. the `#` quick-add picker) can offer meaningful targets.

const (
	ScopeUser     Scope = "user"     // ~/.config/fairpeer/fairpeer.md
	ScopeAncestor Scope = "ancestor" // a fairpeer.md above the project root
	ScopeProject  Scope = "project"  // ./fairpeer.md (committed, shared)
	ScopeLocal    Scope = "local"    // ./fairpeer.local.md (personal, git-ignored)
)

type Set

type Set struct {
	Docs        []Source // fairpeer.md / AGENTS.md, ascending precedence
	Store       Store    // auto-memory store (may be a zero/disabled Store)
	Index       string   // MEMORY.md contents at load time
	Profile     string   // rendered portrait text (global + active mode), injected each turn
	ProfileName string   // raw active profile ("dev"|"cowork"), kept for reload
	CWD         string   // project working dir used for discovery
	UserDir     string   // user config root (may be "")
}

Set is everything memory loaded for one session: the hierarchical docs, the profile-layer portrait, and a handle to the auto-memory store. It is assembled once at boot and folded into the system prompt by Compose. CWD, UserDir and ProfileName are retained so the controller can re-discover (reload) without re-deriving discovery context — losing ProfileName on reload would drop the mode partition and let dev/cowork memories leak together.

func Load

func Load(opts Options) *Set

Load discovers all memory for a session: the hierarchical docs, the profile-layer portrait, and the auto-memory index. It is best-effort and never errors — missing files just mean less memory — so boot can call it unconditionally.

func (*Set) Block

func (s *Set) Block() string

Block renders the memory as a single Markdown section, or "" when empty. It is deterministic given the same files, which is what keeps it a stable cache prefix across sessions that don't change their memory.

Design (v0.4 rewrite): only the portrait + doc hierarchy are injected. The scattered saved-memories index and the "how to use remember/forget" operating instructions were removed — they diluted the actual memory with management overhead and bloated every turn. Saved facts are no longer injected; the model reaches them on demand via recall instead. This keeps the block small and direct.

func (*Set) DocPath

func (s *Set) DocPath(scope Scope) string

DocPath returns the doc-memory file a given scope writes to. To avoid splitting a project's memory across conventions, it prefers a file that already exists (fairpeer.md / AGENTS.md / CLAUDE.md, in that order); when none exists it creates the universal default (AGENTS.md / AGENTS.local.md). ScopeUser → <userDir>, ScopeLocal → <cwd> with the *.local.md names, anything else → <cwd>. Returns "" for ScopeUser when no user dir is configured.

func (*Set) Empty

func (s *Set) Empty() bool

Empty reports whether the set carries nothing to inject, so Compose can leave the base prompt byte-for-byte untouched (and the cache prefix maximal) when there is no memory at all. The portrait counts — a user with only a profile portrait (and no docs) still gets it in the prefix.

func (*Set) ProfileContent

func (s *Set) ProfileContent() string

ProfileContent returns the current body of the active mode's portrait file, or "" when the file does not exist yet (a fresh install, or a mode the user has never written to). It is the read side the preference panel pairs with WriteDoc.

func (*Set) ProfilePath

func (s *Set) ProfilePath() string

ProfilePath returns the absolute path of the active mode's portrait file (<userDir>/profile/<mode>.md), or "" when there is no user config dir. This is the file the workspace preference panel reads and writes; it is also added to the WriteDoc whitelist so SaveDoc accepts it. The shared portrait files (user.md, memory.md) are intentionally not exposed here — only the mode file is user-editable for now.

func (*Set) WriteDoc

func (s *Set) WriteDoc(path, body string) (string, error)

WriteDoc overwrites a doc-memory file with body, after checking path is a recognized memory file (see allowedDocPaths). It is the save side of the desktop panel's in-place editor. The write lands on disk immediately but does NOT mutate the cache-stable system prefix — the edit folds into the prefix on the next session; to make it apply this session, the controller separately queues a turn-tail note. Returns the path written.

type Source

type Source struct {
	Path  string
	Scope Scope
	Body  string
}

Source is one loaded memory file with provenance and @import-expanded body.

type Store

type Store struct {
	Dir       string // .../fairpeer/projects/<slug>/<profile>/memory (mode-partitioned)
	GlobalDir string // .../fairpeer/memory/<profile> (shared facts for this mode)
}

Store is the auto-memory archive: a directory of one-fact-per-file Markdown notes with frontmatter, plus a MEMORY.md index of one line per fact. The model maintains it through the `remember`/`forget` tools. The index is NOT injected into the per-turn prompt (the portrait layer is) — it exists so the memory panel can list saved facts and the model can reach them on demand. The whole thing is plain files the user can edit by hand.

v0.4: this is the slimmed store. The bitemporal model, decay/TTL, supersede chaining, FTS index, archive/restore, and conflict detection were removed — saved facts are no longer injected per turn, so the machinery that governed injection (status/importance/decay/compact) had no remaining job. Same-name save overwrites; history is the user's VCS.

func StoreFor

func StoreFor(userDir, cwd, profile string) Store

StoreFor resolves the auto-memory directories for a project working dir under the user config root, partitioned by profile so dev/cowork memories never leak across modes. profile is the active product mode ("dev" | "cowork" | ""); "" is normalised to "dev" (the unprofiled floor). The layout:

GlobalDir = <userDir>/memory/<profile>   shared facts for this mode
Dir       = <userDir>/projects/<slug>/<profile>/memory   project-scoped facts

so switching profile points every Store method at a disjoint subtree — remember/forget/List all follow without per-call plumbing. A "" userDir (config dir unresolvable) yields a zero Store, which all methods treat as a disabled no-op.

func (Store) Delete

func (s Store) Delete(name string) error

Delete removes a memory file and its MEMORY.md line — the model's `forget` path and the user's way to prune a stale fact. A missing file is not an error; the goal state (gone) holds either way. v0.4: this is a hard delete (the old .archive/ traceability layer was removed with the bitemporal machinery).

func (Store) DirFor

func (s Store) DirFor(profile string) string

DirFor returns the directory a memory of the given profile should be stored in. A "project" profile lands in the project-scoped Dir; everything else (global/dev/cowork) lands in GlobalDir. When GlobalDir is empty, all facts fall back to Dir.

func (Store) Index

func (s Store) Index() string

Index returns the MEMORY.md contents (the per-line index of saved memories), or "" if there are none yet. GlobalDir entries come first, then Dir entries, each group sorted alphabetically by name; a name present in both resolves to its global entry (global is the broader truth).

func (Store) List

func (s Store) List() []Memory

List returns all saved memories across the active mode's directories (global + project), sorted alphabetically by name. It is what the memory panel and the model's on-demand lookups read. Only .md files in the directory roots are scanned (no recursion, no .archive — that layer is gone).

func (Store) Path

func (s Store) Path(name string) string

Path returns the absolute file path a memory with the given name lives at. It checks GlobalDir first, then Dir; not found returns the GlobalDir default (or Dir when GlobalDir is empty) so a save always has a concrete target.

func (Store) Save

func (s Store) Save(m Memory) (string, error)

Save writes (or overwrites) a memory file and refreshes its MEMORY.md index line. It is the single mutation entry point — the `remember` tool and the desktop editor both go through here so the index never drifts from the files. Same-name overwrite is a plain overwrite (no archive/supersede); prior versions live in the user's VCS if tracked. Returns the path written.

type Type

type Type string

Type classifies a memory, mirroring the auto-memory taxonomy. It is kept as a lightweight tag for the memory panel; it no longer drives storage routing (profile partitioning does that now) or injection.

const (
	TypeUser      Type = "user"      // who the user is: role, preferences, expertise
	TypeFeedback  Type = "feedback"  // guidance on how to work
	TypeProject   Type = "project"   // ongoing work / goals / constraints
	TypeReference Type = "reference" // pointers to external resources (URLs, tickets)
)

func NormalizeType

func NormalizeType(s string) Type

NormalizeType coerces an arbitrary string to a known Type, defaulting to TypeProject so a sloppy tool argument never blocks a save.

Jump to

Keyboard shortcuts

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