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 SavePresets(userDir, profile string, f PresetFile) (string, error)
- func WithQueue(ctx context.Context, q Queue) context.Context
- type Memory
- type Options
- type PresetFile
- type ProfilePreset
- 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.
func SavePresets ¶ added in v0.2.0
func SavePresets(userDir, profile string, f PresetFile) (string, error)
SavePresets normalizes and writes the mode's presets file. Normalization is defensive: trims names, fills duplicate/empty ids, drops blank items, clamps counts/lengths, and clears an Active id that no longer resolves — so the frontend can submit optimistic local state without pre-sanitizing it.
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
// SkipProjectDocs drops the workspace hierarchy (ancestor + project +
// project-local AGENTS.md/fairpeer.md) and keeps only user-global memory.
// Used by profiles whose subject is not the workspace (netdev: the session
// operates on network devices, so a cloned repo's instruction files must
// not steer the session). See NETDEV_SPEC §7.3.
SkipProjectDocs bool
}
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 PresetFile ¶ added in v0.2.0
type PresetFile struct {
Active string `json:"active"`
Items []ProfilePreset `json:"items"`
}
PresetFile is the on-disk shape of <mode>-presets.json: the selectable items plus the id of the one currently in use ("" = none selected → nothing extra injected beyond the portrait files).
func LoadPresets ¶ added in v0.2.0
func LoadPresets(userDir, profile string) PresetFile
LoadPresets reads the mode's presets file. A missing or corrupt file yields the factory defaults (best-effort like the rest of memory: unreadable state degrades to defaults, never to an error the panel can't handle).
func (PresetFile) ActivePreset ¶ added in v0.2.0
func (f PresetFile) ActivePreset() *ProfilePreset
ActivePreset resolves the file's Active id to the preset it points at (nil when none is in use — by choice or because the id dangles). discoverProfile uses this to append the user's current explicit choice to the portrait.
type ProfilePreset ¶ added in v0.2.0
type ProfilePreset struct {
ID string `json:"id"`
Name string `json:"name"`
Content string `json:"content"`
// Builtin marks the factory-seeded presets so the panel can offer a
// "restore defaults" action and tag them in the list. It is a provenance
// tag, not a lock: builtin presets stay fully editable and deletable.
Builtin bool `json:"builtin"`
}
ProfilePreset is one named preference template the user can write once and switch between ("减少AI味", "严格Excel匹配", …). Exactly one preset (the file's Active id) is injected per turn as a clearly labelled section after the portrait files, so the model sees it as the user's explicit current choice — distinct from the dream-maintained portrait that accumulates over time.
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) PresetsPath ¶ added in v0.2.0
PresetsPath returns the absolute path of the active mode's presets file, or "" when there is no user config dir. Read side for the panel that wants to show where preferences live (mirrors Set.ProfilePath).
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.