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 ¶
- func AppendDoc(path, note string) error
- func Compose(base string, s *Set) string
- func NewForgetTool(store Store) tool.Tool
- func NewRecallTool(store Store) tool.Tool
- func NewRememberTool(store Store) tool.Tool
- func NormalizeProfile(s string) string
- func NormalizeProfileScope(s string) string
- func WithQueue(ctx context.Context, q Queue) context.Context
- type Memory
- type Options
- type Queue
- type Scope
- type Set
- type Source
- type Store
- type Type
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AppendDoc ¶
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 ¶
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 ¶
NewForgetTool returns the `forget` tool bound to store.
func NewRecallTool ¶
NewRecallTool returns the `recall` tool bound to store.
func NewRememberTool ¶
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 ¶
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 ¶
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.
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 ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
func NormalizeType ¶
NormalizeType coerces an arbitrary string to a known Type, defaulting to TypeProject so a sloppy tool argument never blocks a save.