Documentation
¶
Overview ¶
Package memory implements harness pattern 3 (tiered memory): a conservative, cross-session memory facility exposed to the model as two tools (Remember and Recall) backed by a pluggable tool.MemoryStore.
This package contains BOTH the file-backed store adapter AND the tool.Tool values that drive it. They are kept together deliberately: the tools are thin adapters over the store seam (constructor-injected, mirroring NewBashTool), and shipping them as one opt-in unit lets the composition root wire memory with a single import. The tools depend only on the tool.MemoryStore interface, so a fake in-memory store can stand in for tests.
SCOPING: a Store is per-PROJECT. The composition root calls New(dir) once per project/workspace directory; entries persist across process restarts for that directory and are isolated from other projects. See tool.MemoryStore.
Index ¶
- Constants
- func NewCallerStore(store tool.MemoryStore, project bool) tool.MemoryStore
- func NewNamespacedStore(store tool.MemoryStore, namespace string) tool.MemoryStore
- func NewRecallTool(store tool.MemoryStore) tool.Tool
- func NewRememberTool(store tool.MemoryStore) tool.Tool
- func NewSearchMemoryTool(store tool.MemoryStore) tool.Tool
- func NewUserModelTools(store tool.MemoryStore) []tool.Tool
- func Register(cat *tool.Catalog, store tool.MemoryStore) error
- func RegisterUserModel(cat *tool.Catalog, store tool.MemoryStore) error
- func Tools(store tool.MemoryStore) []tool.Tool
- func WithWorkspace(ctx context.Context, workspace string) context.Context
- type CallerStore
- func (s *CallerStore) Forget(ctx context.Context, key string) error
- func (s *CallerStore) Index(ctx context.Context) ([]tool.MemoryEntry, error)
- func (s *CallerStore) List(ctx context.Context, prefix string) ([]tool.MemoryEntry, error)
- func (s *CallerStore) Recall(ctx context.Context, key string) (tool.MemoryEntry, bool, error)
- func (s *CallerStore) RememberEntry(ctx context.Context, entry tool.MemoryEntry) error
- func (s *CallerStore) Search(ctx context.Context, query string, limit int) ([]tool.MemoryEntry, error)
- type NamespacedStore
- func (s *NamespacedStore) Forget(ctx context.Context, key string) error
- func (s *NamespacedStore) Index(ctx context.Context) ([]tool.MemoryEntry, error)
- func (s *NamespacedStore) List(ctx context.Context, prefix string) ([]tool.MemoryEntry, error)
- func (s *NamespacedStore) Recall(ctx context.Context, key string) (tool.MemoryEntry, bool, error)
- func (s *NamespacedStore) RememberEntry(ctx context.Context, entry tool.MemoryEntry) error
- func (s *NamespacedStore) Search(ctx context.Context, query string, limit int) ([]tool.MemoryEntry, error)
- type Store
- func (s *Store) Forget(ctx context.Context, key string) error
- func (s *Store) ForgetVersioned(ctx context.Context, key string, expected tool.MemoryVersion) (tool.MemoryRecord, error)
- func (s *Store) Index(ctx context.Context) ([]tool.MemoryEntry, error)
- func (s *Store) Inspect(ctx context.Context, key string) (tool.MemoryRecord, bool, error)
- func (s *Store) List(ctx context.Context, prefix string) ([]tool.MemoryEntry, error)
- func (s *Store) Recall(ctx context.Context, key string) (tool.MemoryEntry, bool, error)
- func (s *Store) Remember(ctx context.Context, key, value string) error
- func (s *Store) RememberEntry(ctx context.Context, e tool.MemoryEntry) error
- func (s *Store) RememberIfCurrent(ctx context.Context, entry tool.MemoryEntry, expected tool.MemoryCurrent) (tool.MemoryRecord, error)
- func (s *Store) RememberVersioned(ctx context.Context, entry tool.MemoryEntry, expected tool.MemoryVersion) (tool.MemoryRecord, error)
- func (s *Store) RetireDuplicate(ctx context.Context, survivorKey string, survivorVersion tool.MemoryVersion, ...) (tool.MemoryRecord, error)
- func (s *Store) Search(ctx context.Context, query string, k int) ([]tool.MemoryEntry, error)
- func (s *Store) SynthesizeReplacement(ctx context.Context, survivor tool.MemoryEntry, ...) (tool.MemoryRecord, error)
- func (s *Store) UndoLatest(ctx context.Context, key string, expected tool.MemoryVersion) (tool.MemoryRecord, error)
Constants ¶
const ( RememberToolName = "Remember" RecallToolName = "Recall" SearchMemoryToolName = "SearchMemory" InspectMemoryToolName = "InspectMemory" ForgetMemoryToolName = "ForgetMemory" UndoMemoryToolName = "UndoMemory" )
Tool names are kept here for compatibility with root composition callers.
const ( RememberUserToolName = "RememberUser" RecallUserToolName = "RecallUser" SearchUserModelToolName = "SearchUserModel" InspectUserMemoryToolName = "InspectUserMemory" ForgetUserMemoryToolName = "ForgetUserMemory" UndoUserMemoryToolName = "UndoUserMemory" )
Catalog names of the user-scoped portable memory tools.
Variables ¶
This section is empty.
Functions ¶
func NewCallerStore ¶
func NewCallerStore(store tool.MemoryStore, project bool) tool.MemoryStore
NewCallerStore returns a store partitioned by verified caller and, when project is true, by workspace. The returned store advertises MemoryLifecycleStore exactly when the backing store does.
func NewNamespacedStore ¶
func NewNamespacedStore(store tool.MemoryStore, namespace string) tool.MemoryStore
NewNamespacedStore confines a MemoryStore to namespace. Namespace is an adapter-private boundary, not a model-visible key prefix. The returned store advertises MemoryLifecycleStore exactly when the backing store does.
func NewRecallTool ¶
func NewRecallTool(store tool.MemoryStore) tool.Tool
NewRecallTool returns the portable project-scoped Recall implementation.
func NewRememberTool ¶
func NewRememberTool(store tool.MemoryStore) tool.Tool
NewRememberTool returns the portable project-scoped Remember implementation.
func NewSearchMemoryTool ¶
func NewSearchMemoryTool(store tool.MemoryStore) tool.Tool
NewSearchMemoryTool returns the portable project-scoped Search implementation.
func NewUserModelTools ¶
func NewUserModelTools(store tool.MemoryStore) []tool.Tool
NewUserModelTools returns the user-scoped portable family. Lifecycle tools are included only when store implements tool.MemoryLifecycleStore.
func Register ¶
func Register(cat *tool.Catalog, store tool.MemoryStore) error
Register adds the applicable project family to cat.
func RegisterUserModel ¶
func RegisterUserModel(cat *tool.Catalog, store tool.MemoryStore) error
RegisterUserModel adds the applicable user family to cat.
Types ¶
type CallerStore ¶
type CallerStore struct {
// contains filtered or unexported fields
}
CallerStore chooses a namespace from the verified caller on each operation. Project stores additionally bind that namespace to the run's workspace.
func (*CallerStore) Forget ¶
func (s *CallerStore) Forget(ctx context.Context, key string) error
Forget removes key from the verified caller's namespace.
func (*CallerStore) Index ¶
func (s *CallerStore) Index(ctx context.Context) ([]tool.MemoryEntry, error)
Index returns verified caller entries without their values.
func (*CallerStore) List ¶
func (s *CallerStore) List(ctx context.Context, prefix string) ([]tool.MemoryEntry, error)
List returns verified caller entries matching prefix.
func (*CallerStore) Recall ¶
func (s *CallerStore) Recall(ctx context.Context, key string) (tool.MemoryEntry, bool, error)
Recall retrieves key from the verified caller's namespace.
func (*CallerStore) RememberEntry ¶
func (s *CallerStore) RememberEntry(ctx context.Context, entry tool.MemoryEntry) error
RememberEntry stores entry in the verified caller's namespace.
func (*CallerStore) Search ¶
func (s *CallerStore) Search(ctx context.Context, query string, limit int) ([]tool.MemoryEntry, error)
Search ranks entries in the verified caller's namespace.
type NamespacedStore ¶
type NamespacedStore struct {
// contains filtered or unexported fields
}
NamespacedStore presents one logical MemoryStore namespace over a shared backing store. Every operation, including index/search and consolidation, is confined to namespace; callers never see the internal key prefix.
func (*NamespacedStore) Forget ¶
func (s *NamespacedStore) Forget(ctx context.Context, key string) error
Forget removes key from this namespace.
func (*NamespacedStore) Index ¶
func (s *NamespacedStore) Index(ctx context.Context) ([]tool.MemoryEntry, error)
Index returns namespace entries without their values.
func (*NamespacedStore) List ¶
func (s *NamespacedStore) List(ctx context.Context, prefix string) ([]tool.MemoryEntry, error)
List returns namespace entries matching prefix.
func (*NamespacedStore) Recall ¶
func (s *NamespacedStore) Recall(ctx context.Context, key string) (tool.MemoryEntry, bool, error)
Recall retrieves key from this namespace.
func (*NamespacedStore) RememberEntry ¶
func (s *NamespacedStore) RememberEntry(ctx context.Context, entry tool.MemoryEntry) error
RememberEntry stores entry under this namespace.
func (*NamespacedStore) Search ¶
func (s *NamespacedStore) Search(ctx context.Context, query string, limit int) ([]tool.MemoryEntry, error)
Search ranks entries within this namespace.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a file-backed, cross-process-safe tool.MemoryStore. It persists entries as a single JSON document at <dir>/memory.json and commits every write atomically (temp file + rename) so a crash mid-write cannot corrupt or truncate the on-disk file. A fresh Store opened over the same dir sees previously written entries, giving durability across process restarts.
Concurrency / locking. The store is safe for both in-process and cross-process concurrent use, and — critically — does not LOSE updates under either:
- In-process: s.mu serialises every method on a single Store, and is also held across the full read-modify-write of a mutation. This is the only lock that protects the flock handle, which is NOT goroutine-safe when shared across goroutines on one fd.
- Cross-process (several mecated/mecatui instances, agent-team / subagent runs sharing one memory.json): a gofrs/flock advisory lock on the STABLE sentinel <dir>/memory.lock guards the read-modify-write. Writes (RememberEntry, Remember, Forget) take an EXCLUSIVE lock; reads (Recall, List, Index) take a SHARED lock. The lock spans the whole load→mutate→save sequence, so two processes can no longer interleave read-modify-write and clobber each other (the lost-update bug the bare temp+rename did not prevent).
Acquisition order is always s.mu THEN flock; the flock is acquired with a bounded TryLockContext/TryRLockContext (lockRetryDelay / lockTimeout, also honouring ctx cancellation) so a stuck holder fails loud instead of deadlocking, and released (Unlock) before return on every path including errors.
INVARIANT — at most ONE *Store per directory per process. gofrs/flock uses BSD flock(2), which contends across file descriptors even WITHIN a single process. Two *Store values opened over the SAME dir in the SAME process therefore hold DISTINCT fds on the sentinel and would self-deadlock: the second writer's exclusive acquire blocks on the first's lock until the lockTimeout expires (a loud error, but a needless one). The composition root upholds this today by constructing one shared *Store per project (see internal/app). If per-session memory is ever needed, SHARE a single *Store keyed by absolute dir (the way the session-engine map is keyed) rather than calling New per session — do NOT open a second *Store over a dir already owned in-process.
func New ¶
New constructs a file-backed Store rooted at dir, creating dir (and parents) if it does not exist. The store is scoped to dir: it owns <dir>/memory.json and the cross-process lock sentinel <dir>/memory.lock. Pass a per-project directory so memory is isolated per project.
Construct AT MOST ONE *Store per dir per process. Because the cross-process lock uses BSD flock(2) (per-fd, cross-fd-contending even in one process), a SECOND *Store opened over the same dir in this process would self-deadlock its own lock acquisition until the lockTimeout, since the two *Store values hold separate fds on the sentinel. Share one *Store keyed by absolute dir instead. See the Store type doc for the full rationale.
func (*Store) Forget ¶
Forget deletes the active entry for key. It remains idempotent for legacy callers, while an existing value is retained as lifecycle history plus a tombstone.
func (*Store) ForgetVersioned ¶
func (s *Store) ForgetVersioned(ctx context.Context, key string, expected tool.MemoryVersion) (tool.MemoryRecord, error)
ForgetVersioned atomically appends a tombstone when expected is current.
func (*Store) Index ¶
Index returns the tier-0 routing table: every entry with the VALUE OMITTED and Description filled (explicit, else derived from the value's first line), sorted by key. It applies NO size cap — the consumer (the prompt assembler) caps and renders. It is the cheap, always-in-context summary view.
func (*Store) Inspect ¶
Inspect returns the current state and complete history, including deleted tombstones. Legacy flat records are projected as a stable imported baseline without rewriting memory.json.
func (*Store) List ¶
List returns all entries whose key has the given prefix, sorted by key. An empty prefix returns every entry.
func (*Store) Remember ¶
Remember stores value under key with no explicit description (the index derives one from the value), overwriting any existing entry and bumping its UpdatedAt. An empty key is rejected. It is a convenience wrapper over RememberEntry, kept so callers that do not care about descriptions stay unchanged. It is intentionally NOT part of tool.MemoryStore — the interface carries RememberEntry only; this concrete convenience survives for direct *Store users.
func (*Store) RememberEntry ¶
RememberEntry stores e, overwriting any existing entry under e.Key and bumping UpdatedAt to now. e.Description is stored as-is (empty is allowed; the index derives one). An empty key is rejected.
func (*Store) RememberIfCurrent ¶
func (s *Store) RememberIfCurrent(ctx context.Context, entry tool.MemoryEntry, expected tool.MemoryCurrent) (tool.MemoryRecord, error)
RememberIfCurrent performs a presence-and-version CAS in the same flocked transaction.
func (*Store) RememberVersioned ¶
func (s *Store) RememberVersioned(ctx context.Context, entry tool.MemoryEntry, expected tool.MemoryVersion) (tool.MemoryRecord, error)
RememberVersioned atomically creates or replaces a record when expected is the current opaque version. A legacy flat record is materialized as the first imported revision inside the same locked write.
func (*Store) RetireDuplicate ¶
func (s *Store) RetireDuplicate(ctx context.Context, survivorKey string, survivorVersion tool.MemoryVersion, sourceKey string, sourceVersion tool.MemoryVersion) (tool.MemoryRecord, error)
RetireDuplicate atomically verifies the active survivor and source revisions and appends a source tombstone in one exclusive load/mutate/save transaction.
func (*Store) Search ¶
Search ranks entries by BM25 lexical relevance to query (over each entry's key + derived description + value) and returns the top k best-first, with the VALUE OMITTED and Description filled — mirroring Index's result shape. Ranking runs INSIDE the shared lock, over the freshly loaded data, so it sees a consistent snapshot and never races a concurrent write. An empty or whitespace-only query short-circuits to an empty result (no lock, no error); k <= 0 uses the default page size. See bm25Rank for the scoring/ordering rules.
func (*Store) SynthesizeReplacement ¶
func (s *Store) SynthesizeReplacement(ctx context.Context, survivor tool.MemoryEntry, survivorVersion tool.MemoryVersion, sourceKeys []string, sourceVersions []tool.MemoryVersion) (tool.MemoryRecord, error)
SynthesizeReplacement atomically verifies every participant, appends a new survivor revision with the reviewed bytes, and tombstones all sources in one exclusive load/mutate/save transaction. Undo remains per-key and is intentionally not exposed as a grouped inverse of this operation.
func (*Store) UndoLatest ¶
func (s *Store) UndoLatest(ctx context.Context, key string, expected tool.MemoryVersion) (tool.MemoryRecord, error)
UndoLatest appends a compensating revision for the newest mutation not already compensated. Recording the target version makes repeated undo walk backward and prevents concurrent callers from reversing one mutation twice.