Documentation
¶
Overview ¶
Package store defines the storage abstraction memini retrieves memories through. Drivers (sqlite-vec, Postgres/VectorChord) implement Store; the hybrid-search and service layers depend only on this interface.
Index ¶
- Constants
- Variables
- func DurableCtx(ctx context.Context) (context.Context, context.CancelFunc)
- func MemoryTypeLabel(m *memory.Memory) string
- func OrEmptyMap(m map[string]any) map[string]any
- func OrEmptySlice(s []string) []string
- func ValidEventKind(k EventKind) bool
- func ValidRepairState(s RepairState) bool
- type APIKey
- type APIKeyStore
- type ActivityStore
- type ChunkStore
- type ClientSettings
- type ClientSettingsStore
- type EmbedModelStore
- type Event
- type EventFilter
- type EventKind
- type EventLogStore
- type Filter
- type LinkStore
- type MemorySnapshot
- type Metrics
- type NamespaceActivity
- type NamespaceLink
- type Pin
- type PinStore
- type RepairRow
- type RepairStat
- type RepairState
- type RepairStore
- type Scored
- type SettingsLayer
- type Sort
- type SortKey
- type Store
Constants ¶
const DurableWriteTimeout = 10 * time.Second
DurableWriteTimeout bounds one already-decided durable write.
Variables ¶
var ErrConflict = errors.New("id exists in a different namespace")
ErrConflict is returned by Upsert when the given ID already exists in a different namespace, preventing cross-namespace hijacking.
var ErrNotFound = errors.New("memory not found")
ErrNotFound is returned by Get/Delete when no memory matches.
Functions ¶
func DurableCtx ¶ added in v0.7.16
DurableCtx returns a context for a durable write the caller has already decided to perform. It keeps ctx's values (request actor, trace, logger) but drops its cancellation.
Every driver's write path is a transaction, and database/sql and pgx both roll an in-flight transaction back when its context is cancelled. So without this, a client hanging up — or the request deadline landing — between "we decided to store this" and "it committed" loses an accepted write and reports a 500 for it. The window is small but the failure is silent and total, and it is entirely avoidable: by that point nothing about the write depends on the caller still being there.
It is NOT for reads, and NOT for bulk passes. A long scan or a maintenance sweep must still die with its request, so a disconnected client cannot pin a connection or a CPU for a report nobody will read. Use it only where the alternative to finishing is losing an accepted write.
func MemoryTypeLabel ¶ added in v0.2.5
MemoryTypeLabel returns m's typed-extraction class for the memory_type metric label, or "" when absent or unrecognized (keeping cardinality bounded).
func OrEmptyMap ¶ added in v0.2.8
OrEmptyMap returns m, or an empty map when m is nil, so drivers persist an empty JSON object rather than null for absent metadata.
func OrEmptySlice ¶ added in v0.2.8
OrEmptySlice returns s, or an empty slice when s is nil, so drivers persist an empty JSON array rather than null for absent tags.
func ValidEventKind ¶ added in v0.6.8
ValidEventKind reports whether k is one of the recorded kinds, so the REST layer can reject an unknown filter value before it reaches SQL.
func ValidRepairState ¶ added in v0.7.16
func ValidRepairState(s RepairState) bool
ValidRepairState reports whether s is a state this package defines. Repair states are a closed set because they are a metric label and a wire value.
Types ¶
type APIKey ¶ added in v0.6.7
type APIKey struct {
Name string // unique human label, primary key
Hash string // hex SHA-256 of the secret; never the secret itself
HomeNS string // bound home namespace; "" means unbound
// DefaultNS is the namespace applied to a request that presents this key
// but carries no X-Memini-Namespace header; an explicit header always
// wins. "" means no per-key default (the server-wide default applies).
DefaultNS string
CreatedAt time.Time
Disabled bool
// Admin marks this key as an admin credential, the sibling bool to
// Disabled above. It will grant access to the /v1/keys and
// /v1/settings/defaults REST surfaces — the gating itself happens at the
// REST layer (a later change), not here; this field only carries the
// capability through storage and auth. Preserved by rotation, same as
// every other field CLI/REST rotation does not explicitly change.
Admin bool
// ReadOnly marks this key as a read-only credential, the second
// authorization bit and a sibling to Admin above. It denies every mutating
// operation across both HTTP surfaces (REST and MCP) while leaving reads
// untouched — the enforcement itself lives at the API edge, not here; this
// field only carries the capability through storage and auth. It is
// INDEPENDENT of Admin: admin+read_only is a legitimate combination (an
// auditor who may enumerate keys but mutate nothing). Preserved by
// rotation, same as every other field CLI/REST rotation does not explicitly
// change — a rotation that dropped it would silently grant write access.
//
// Note this is "cannot mutate", not "cannot see": a read-only key can still
// name any namespace it likes and read it. Read scoping is a separate axis.
ReadOnly bool
// Settings is this key's per-key ClientSettings override, merged over the
// server's global defaults (ClientSettingsStore) and the built-in
// defaults (DefaultClientSettings) by MergeClientSettings. The zero value
// (every field nil) means no override at all.
Settings ClientSettings
}
APIKey is a persisted API credential: a unique human label, the hex SHA-256 hash of the secret (the secret itself is never stored), an optional home namespace it is bound to, when it was created, and whether it is disabled.
type APIKeyStore ¶ added in v0.6.7
type APIKeyStore interface {
// PutAPIKey inserts or replaces the key keyed by k.Name.
//
// Unlike PutLink (which deliberately overwrites created_at on every
// upsert, since links carry no recency semantics — see its doc above),
// this upsert preserves the existing row's CreatedAt when the incoming
// k.CreatedAt is the zero value: API keys are long-lived identity, and
// rotating a key's hash or home namespace must not reset "when was this
// key first created". Passing a non-zero k.CreatedAt (e.g. import
// restore replaying an original timestamp) still overwrites it, mirroring
// PutLink's own conditional-overwrite convention.
PutAPIKey(ctx context.Context, k APIKey) error
// DeleteAPIKey removes the key by name. The bool reports whether a key
// existed to delete; a missing key is not an error.
DeleteAPIKey(ctx context.Context, name string) (bool, error)
// ListAPIKeys returns every key ordered by name. Never returns secrets —
// only the stored hash — since the plaintext secret is never persisted.
ListAPIKeys(ctx context.Context) ([]APIKey, error)
// GetAPIKeyByHash returns the key whose Hash matches, or nil, nil when
// none does. This is the auth-path lookup, so drivers must maintain an
// index/unique constraint on hash for it to stay fast.
GetAPIKeyByHash(ctx context.Context, hash string) (*APIKey, error)
// RenameAPIKeyNamespaces rewrites every key whose HomeNS or DefaultNS
// equals from to to instead — both columns in one call, since a
// namespace move (maintenance.Move, which also calls
// RenameLinkEndpoints) must not leave either binding pointing at the
// old name. Keys matching in neither column, and the non-matching
// column of a partially-matching key, are untouched; CreatedAt is never
// modified. A no-op when from == to.
RenameAPIKeyNamespaces(ctx context.Context, from, to string) error
}
APIKeyStore is implemented by drivers that persist api_keys, the multiple-API-keys-with-optional-home-namespace feature. It is an optional capability interface — the EmbedModelStore/LinkStore/ActivityStore precedent above — so callers type-assert and degrade gracefully against a driver that predates it. The auth middleware consumes GetAPIKeyByHash on the request path; the CLI consumes Put/Delete/List.
type ActivityStore ¶ added in v0.6.6
type ActivityStore interface {
// NamespaceActivity returns one row per namespace holding at least one
// live memory, ordered by namespace. now is the expiry-evaluation instant
// (mirroring Filter.Now); the zero value means the wall clock.
NamespaceActivity(ctx context.Context, now time.Time) ([]NamespaceActivity, error)
}
ActivityStore is implemented by drivers that can compute per-namespace activity in a single aggregate query (SELECT namespace, COUNT(*), MAX(created_at) ... GROUP BY namespace). It is an optional capability interface — the EmbedModelStore/LinkStore precedent — so callers type-assert and degrade gracefully against a driver that predates it: the briefing child rollup skips itself entirely rather than falling back to per-namespace scans.
type ChunkStore ¶ added in v0.7.3
type ChunkStore interface {
// ChunkVectorSearch returns the k memories whose best-matching chunk is
// nearest to vec, best-first, one row per memory (max-pooled over its
// chunks). Scores are in the same space as VectorSearch's, so a caller can
// compare the two legs directly — which matters because the recall gates
// (semantic floor, min-score) are absolute thresholds, not ranks.
//
// Filter applies to the memories, not the chunks. Rows whose memory is
// filtered out never appear.
ChunkVectorSearch(ctx context.Context, namespace string, vec []float32, f Filter, k int) ([]Scored, error)
// ListUnchunked returns up to limit memories in the namespace whose
// content exceeds minRunes but which have no chunk rows — the backfill's
// work queue — ordered by id, starting after afterID ("" starts from the
// beginning). The cursor lets the backfill page past rows it cannot
// process this tick (declined by the splitter, rejected by the embedder)
// instead of re-listing them into every batch until they starve it.
// Namespace "" means every namespace.
//
// Expired and superseded rows are returned too, deliberately: recall's
// AsOf and IncludeSuperseded modes flow through the chunk leg, and
// tombstones are retained precisely so time-travel queries can reach them.
//
// This is a query rather than a metadata flag because rows that predate
// chunking carry no flag to find them by, and adding one would mean
// rewriting every row before the feature could do anything.
ListUnchunked(ctx context.Context, namespace string, minRunes int, afterID string, limit int) ([]*memory.Memory, error)
// CountUnchunked reports the total size of ListUnchunked's queue — the
// real backlog, where ListUnchunked shows at most one batch of it.
CountUnchunked(ctx context.Context, namespace string, minRunes int) (int, error)
// PutChunks replaces the chunk rows of the memory identified by
// (namespace, id), guarded by updatedAt: the write happens only when the
// row still exists with exactly that updated_at, and reports false
// otherwise. Guard and write are one transaction, so a concurrent content
// change cannot slip between them — the check-then-act race a re-read
// outside the store can never close. Nothing else on the row is touched:
// not the document vector, not the FTS index, and not the columns a
// concurrent Reinforce may be bumping (Reinforce deliberately does not
// advance updated_at, and this guard must not punish it).
PutChunks(ctx context.Context, namespace, id string, updatedAt time.Time, chunks []memory.Chunk) (bool, error)
// CountChunks reports how many chunk rows exist in the namespace (""
// counts every namespace). It exists so tests and operators can see
// orphaned rows that ChunkVectorSearch's join back to memories
// structurally hides; on backends without referential cascades, orphans
// (which belong to no namespace) are included in every count.
CountChunks(ctx context.Context, namespace string) (int, error)
}
ChunkStore is the optional chunked-embedding capability: per-segment vectors for content that runs past the per-item embed budget, so recall can match text the document vector does not cover.
It is a SEPARATE capability rather than a change to VectorSearch, and that separation is load-bearing. VectorSearch has six callers and three of them destroy data: write-dedup (coalesce/supersede tombstones the loser), contradiction routing (closes a fact's validity window), and the maintenance dedup sweep (tombstones). Chunk similarity is max-pooled, which makes a long memory a near-duplicate of anything matching any one of its paragraphs — so pooling inside VectorSearch would have those three tombstone unrelated memories. VectorSearch's semantics are therefore frozen, and recall unions the two legs instead. Chunks are purely additive: they can only add hits, never remove or rewrite a memory.
Because the document vector stays authoritative, a store that implements this needs no migration to be correct, and dropping back to a build without it loses only the extra recall.
type ClientSettings ¶ added in v0.7.0
type ClientSettings struct {
// CaptureTurns enables capturing each user→assistant turn as episodic memory.
CaptureTurns *bool `json:"capture_turns,omitempty"`
// SessionDigest enables recording a session-end/stop/pre-compact digest memory.
SessionDigest *bool `json:"session_digest,omitempty"`
// InlineExtract enables injecting the directive asking the agent to save
// durable facts via memory_remember.
InlineExtract *bool `json:"inline_extract,omitempty"`
// AutoSave enables periodically nudging the agent to persist durable memories.
AutoSave *bool `json:"auto_save,omitempty"`
// AutoSaveInterval is the user-message interval between auto-save nudges;
// must be >= 1.
AutoSaveInterval *int `json:"auto_save_interval,omitempty"`
// AutoSaveMinEvents is the minimum number of buffered state-changing tool
// events since the last auto-save baseline for the interval nudge to fire;
// 0 disables the activity gate (interval-only cadence). Must be >= 0.
AutoSaveMinEvents *int `json:"auto_save_min_events,omitempty"`
// InjectBriefingPinned caps pinned memories in the session-start briefing.
InjectBriefingPinned *int `json:"inject_briefing_pinned,omitempty"`
// InjectBriefingFacts caps durable semantic facts in the session-start briefing.
InjectBriefingFacts *int `json:"inject_briefing_facts,omitempty"`
// InjectBriefingProcedures caps procedural how-tos in the session-start briefing.
InjectBriefingProcedures *int `json:"inject_briefing_procedures,omitempty"`
// InjectBriefingRecent caps recent episodic entries in the session-start briefing.
InjectBriefingRecent *int `json:"inject_briefing_recent,omitempty"`
// InjectBriefingMaxTok is a hard ceiling on briefing injection tokens; 0 is uncapped.
InjectBriefingMaxTok *int `json:"inject_briefing_max_tok,omitempty"`
// InjectPretoolItems caps recalled items injected per file on PreToolUse.
InjectPretoolItems *int `json:"inject_pretool_items,omitempty"`
// InjectPretoolMaxTok is a hard ceiling on per-tool injection tokens; 0 is uncapped.
InjectPretoolMaxTok *int `json:"inject_pretool_max_tok,omitempty"`
// InjectPretoolMinScore floors the composite post-rerank score — the final
// [0,1) scale the response score field and the activity feed show — for a
// PreToolUse injection. All bundled integrations enforce it server-side
// via min_rank_score (floored hits appear in the feed marked as filtered);
// a custom or older caller may still apply it as a pre-rank fused-score
// floor. 0 disables the floor.
InjectPretoolMinScore *float64 `json:"inject_pretool_min_score,omitempty"`
// InjectPretoolTools is the tool-name allowlist that triggers a PreToolUse
// injection. Glob and Grep are deliberately not in the default:
// pattern-derived queries are near-zero-signal and each ungated call costs
// a server embed+rerank — listing them restores the old behavior.
InjectPretoolTools *[]string `json:"inject_pretool_tools,omitempty"`
// InjectPretoolGateMs skips the PreToolUse recall server call entirely for
// a file whose last call was younger than this many milliseconds; 0 always
// calls. Must be >= 0.
InjectPretoolGateMs *int `json:"inject_pretool_gate_ms,omitempty"`
// InjectDedupe suppresses re-injecting a recalled memory still within its
// injection cooldown window (InjectCooldownMs / InjectCooldownPrompts);
// with both windows 0 an unchanged memory stays suppressed for the rest of
// the session. It gates the injection; whether the PreToolUse recall call
// runs follows InjectPretoolGateMs, but turning InjectDedupe off also
// disables that call gate, because the gate's clock lives in the dedupe
// state.
InjectDedupe *bool `json:"inject_dedupe,omitempty"`
// InjectTelemetry reports what each hook actually injected vs suppressed
// back to the server (POST /v1/activity/injected) so the activity feed and
// metrics reflect reality instead of pre-suppression serves. Best-effort
// and bounded; off disables the beacon entirely.
InjectTelemetry *bool `json:"inject_telemetry,omitempty"`
// InjectCooldownMs is the time window (ms) within which an already-injected
// memory is not re-injected; 0 disables the time dimension. Must be >= 0.
InjectCooldownMs *int `json:"inject_cooldown_ms,omitempty"`
// InjectCooldownPrompts is the prompt-count window within which an
// already-injected memory is not re-injected; 0 disables the prompt
// dimension. Must be >= 0.
InjectCooldownPrompts *int `json:"inject_cooldown_prompts,omitempty"`
// InjectLabels selects which annotation labels to render alongside an
// injected memory; each must be one of tier, confidence, age, reason.
InjectLabels *[]string `json:"inject_labels,omitempty"`
// Recall enables recall-driven injection at all.
Recall *bool `json:"recall,omitempty"`
// Capture enables capture (turns/digests) at all.
Capture *bool `json:"capture,omitempty"`
// RecallLimit caps memories per recall call.
RecallLimit *int `json:"recall_limit,omitempty"`
// InjectRecallMaxTok is a hard ceiling on recall injection tokens; 0 is uncapped.
InjectRecallMaxTok *int `json:"inject_recall_max_tok,omitempty"`
// InjectRecallMinScore floors the composite post-rerank score — the final
// [0,1) scale the response score field and the activity feed show — for a
// recall injection. All bundled integrations enforce it server-side via
// min_rank_score (floored hits appear in the feed marked as filtered); a
// custom or older caller may still apply it as a pre-rank fused-score
// floor. 0 disables the floor.
InjectRecallMinScore *float64 `json:"inject_recall_min_score,omitempty"`
// MinCaptureChars is the minimum content length worth bothering to capture a turn.
MinCaptureChars *int `json:"min_capture_chars,omitempty"`
// CaptureUserMaxChars truncates the user side of a captured turn to this
// many characters, marking the cut; 0 captures it whole.
CaptureUserMaxChars *int `json:"capture_user_max_chars,omitempty"`
// CaptureAssistantMaxChars truncates the assistant side of a captured turn
// to this many characters, marking the cut; 0 captures it whole.
CaptureAssistantMaxChars *int `json:"capture_assistant_max_chars,omitempty"`
// RequestTimeoutMs is how long a client waits on one memini HTTP call before
// giving up; it must stay above the server's own RerankTimeout, or a slow
// reranker returns nothing at all instead of the composite-order fallback
// finalizeRecall degrades to. Must be >= 100.
RequestTimeoutMs *int `json:"request_timeout_ms,omitempty"`
// NamespaceScope is "repo" or "owner_repo": "repo" derives the namespace
// from the bare repo name; "owner_repo" disambiguates same-named repos
// across owners with an owner-repo slug (owner + "-" + repo).
NamespaceScope *string `json:"namespace_scope,omitempty"`
// NamespacePrefix is a namespace path prepended ahead of the
// derived/declared namespace; "" means no prefix.
NamespacePrefix *string `json:"namespace_prefix,omitempty"`
}
ClientSettings is the behavioral/injection configuration surface a caller's effective settings resolve to: a layered merge of the built-in defaults (DefaultClientSettings), the server's global defaults (ClientSettingsStore), and a per-API-key override (APIKey.Settings, below). Every field is a pointer; nil means "unset — inherit from the next layer down". Only MergeClientSettings' result is guaranteed to have every field non-nil.
The field set, JSON tags (the wire-level snake_case keys), and defaults are exactly the ClientSettings schema in api/openapi.yaml (config-handshake redesign) — keep the two in sync; the schema wins on any disagreement.
func DefaultClientSettings ¶ added in v0.7.0
func DefaultClientSettings() ClientSettings
DefaultClientSettings returns the built-in default ClientSettings, every field set from the ClientSettings schema's `default:` in api/openapi.yaml. It is the bottom layer of MergeClientSettings — passing it first guarantees the merge result has every field non-nil even when every other layer is empty.
func MergeClientSettings ¶ added in v0.7.0
func MergeClientSettings(layers ...SettingsLayer) (ClientSettings, map[string]string)
MergeClientSettings flattens layers into one ClientSettings: later layers win field-by-field — a nil field never overrides, only an explicitly-set field in a later layer replaces an earlier one. Passing SettingsLayer{Source: "default", S: DefaultClientSettings()} first guarantees every field of the result is non-nil regardless of what (if anything) later layers set.
The second return maps each wire-key (the JSON tag) to the Source label of whichever layer's value it took, for the /v1/self settings_sources provenance the REST layer (a later phase) surfaces to callers.
gocyclo is silenced: the body is a flat per-field applyPtr enumeration (one branch per ClientSettings field), not genuinely complex control flow.
func (ClientSettings) Validate ¶ added in v0.7.0
func (s ClientSettings) Validate() error
Validate returns a non-nil error when a set (non-nil) field violates the range/enum constraints the ClientSettings schema in api/openapi.yaml declares. Unset (nil) fields are never checked — validation is purely per-field, so a partial layer (e.g. one key's override) validates independently of any other layer.
namespace_prefix's check calls httputil.ValidateNamespace directly rather than accepting an injected validator func: internal/httputil has no dependency on internal/store (it only imports the standard library), so importing it here does not create an import cycle — confirmed by reading internal/httputil/httputil.go before wiring this up.
type ClientSettingsStore ¶ added in v0.7.0
type ClientSettingsStore interface {
// GlobalClientSettings returns the stored global defaults, or the zero
// ClientSettings (every field nil) when none has been set yet.
GlobalClientSettings(ctx context.Context) (ClientSettings, error)
// SetGlobalClientSettings replaces the stored global defaults wholesale.
SetGlobalClientSettings(ctx context.Context, s ClientSettings) error
}
ClientSettingsStore is implemented by drivers that persist the server's global default ClientSettings — the layer between the built-in defaults (DefaultClientSettings) and any per-API-key override (APIKey.Settings). It is an optional capability interface — the EmbedModelStore precedent, which stores its single value the same way, over the same meta key/value table.
type EmbedModelStore ¶ added in v0.3.9
type EmbedModelStore interface {
// EmbedModel returns the recorded embedding model name, or "" when none has
// been recorded yet (a fresh store, or one created before this was tracked).
EmbedModel(ctx context.Context) (string, error)
// SetEmbedModel records model as the embedding model the stored vectors were
// produced with, overwriting any previous value.
SetEmbedModel(ctx context.Context, model string) error
}
EmbedModelStore is implemented by drivers that record which embedding model produced their stored vectors. It lets the bootstrap detect a silent model swap — a new MEMINI_EMBED_MODEL at the same dimensionality, which the dims guard cannot catch — where old and new vectors share a width but live in incomparable spaces, quietly degrading recall.
type Event ¶ added in v0.6.8
type Event struct {
// ID is assigned by the store, monotonic within a driver.
ID int64
// OpID groups the rows of one logical operation.
OpID string
Kind EventKind
// Namespace is the namespace the request was made against, which for a
// cascading recall may differ from the served memory's own MemoryNS.
Namespace string
// Query is the recall query; "" for every other kind.
Query string
// MemoryID is "" for the sentinel row of a recall that returned nothing —
// "the query found nothing" is itself worth recording.
MemoryID string
MemoryNS string
MemoryTier memory.Tier
MemorySummary string
// Rank is the 1-based position the memory was served at; 0 when not applicable.
Rank int
// Score is the composite relevance score the memory was served with; nil
// when not applicable (every non-recall kind).
Score *float64
// Detail carries kind-specific context — a recall's degraded mode, a
// briefing row's section, a supersession's replacement id.
Detail map[string]any
// Actor is who performed the operation: the name of the NAMED API key that
// authenticated the request, or "" for the admin env key, an
// unauthenticated dev-mode request, or a legacy row written before
// attribution existed. ActorKind disambiguates the empty cases.
Actor string
// ActorKind classifies the actor: "key" (a named API key, Actor holds its
// name), "env" (the admin env key, Actor is ""), "none" (unauthenticated
// dev mode, Actor is ""), or "" (a legacy row predating attribution —
// unknown). Attribution is automatic and unconditional, stamped on every
// event row from the request context (service.WithActor).
ActorKind string
CreatedAt time.Time
}
Event is one (operation, memory) row of the activity log: what was served or written, when, and — for a recall — the query that pulled it and where it ranked. Memories served by one operation share an OpID, so a recall that returned five memories is five rows the reader regroups into one event.
The memory fields are a snapshot taken at event time, not a live join: they keep the feed renderable in one query (no N+1 fetch per row) and keep a forget event readable after its memory is gone.
type EventFilter ¶ added in v0.6.8
type EventFilter struct {
// Namespace restricts to events recorded against this namespace; "" means all.
Namespace string
// Namespaces restricts to events recorded against any of these namespaces
// (OR); empty means no constraint. Used to narrow an all-namespaces feed;
// ignored when Namespace is set.
Namespaces []string
// Kinds restricts to these kinds; empty means all.
Kinds []EventKind
// Actor restricts to events performed by the named API key (exact match on
// Event.Actor); empty means no constraint. Matches the key name only — the
// admin env key and dev-mode requests carry no name and so are never
// selected by this filter.
Actor string
// Tiers restricts to operations that touched a memory of one of these tiers
// (OR); empty means no constraint.
Tiers []memory.Tier
// Text restricts to operations whose query or any served memory's summary
// contains it, case-insensitively. Empty means no constraint.
Text string
// Since restricts to events recorded at or after the instant; zero means no
// constraint.
Since time.Time
// Before and BeforeID are a keyset cursor: only rows strictly older than
// (Before, BeforeID) in the (created_at DESC, id DESC) ordering are
// returned. A zero Before starts from the newest row.
Before time.Time
BeforeID int64
// Limit caps the returned rows (not operations); <= 0 means no cap.
Limit int
}
EventFilter narrows an activity-log read. The zero value returns the newest events across every namespace and kind.
Tiers and Text select whole operations, not individual rows: an event is a group of rows, so dropping some of a recall's rows would misreport what that recall actually served ("served 2 memories" when it served five). A matching operation is therefore returned intact, with every memory it touched.
type EventKind ¶ added in v0.6.8
type EventKind string
EventKind names an operation the activity log records. Reads and writes are both logged; the kinds are the wire-level values the REST filter accepts.
const ( EventRecall EventKind = "recall" EventGet EventKind = "get" EventBriefing EventKind = "briefing" EventRemember EventKind = "remember" EventUpdate EventKind = "update" EventForget EventKind = "forget" EventSupersede EventKind = "supersede" EventPin EventKind = "pin" EventUnpin EventKind = "unpin" EventSettings EventKind = "settings" // EventInject is a client injection-telemetry report (POST // /v1/activity/injected): which served memories a hook actually injected // into model context, and what its local gates suppressed. EventInject EventKind = "inject" )
EventPin/EventUnpin/EventSettings are part of the config-handshake wire contract (api/openapi.yaml's EventKind enum) landed ahead of the pin/settings write paths that will actually emit them — see internal/api/rest/config_stubs.go. Recognizing them here now means the GET /v1/activity ?kind= filter never 400s on a value the spec itself advertises as valid.
type EventLogStore ¶ added in v0.6.8
type EventLogStore interface {
// AppendEvents inserts the rows of one operation as a single batch, so they
// land contiguously. ListEvents' ordering then keeps an operation's rows
// adjacent, which is what lets the reader regroup a flat row page into
// whole events without a join table.
AppendEvents(ctx context.Context, events []Event) error
// ListEvents returns rows matching f, newest first (created_at DESC, id DESC).
ListEvents(ctx context.Context, f EventFilter) ([]Event, error)
// PruneEvents deletes rows older than olderThan (a zero olderThan prunes
// none by age) and, when keepMax > 0, the oldest rows beyond the newest
// keepMax. Returns the number of rows deleted.
PruneEvents(ctx context.Context, olderThan time.Time, keepMax int) (int64, error)
// ServedSnapshots returns, per ID, the newest memory snapshot a SERVE row
// (recall or briefing) recorded for it against namespace, ignoring rows
// older than since and rows that carry no snapshot. An ID with no covering
// serve is absent from the map — never an error.
//
// It backs the inject event's hydration: an injection beacon carries bare
// IDs, so the snapshot is borrowed from the serve that produced them. That
// also makes it the SAFE source. Injected IDs are taken on faith and never
// authorized, so resolving them against the memories table would let a
// buggy or hostile client read back summaries of memories it was never
// served; scoping to serves already logged against the caller's namespace
// bounds what a report can learn to what it was already shown.
//
// Borrowing rather than re-reading is also what recovers the memory's own
// namespace, which for a cascading recall differs from the request
// namespace (see Event.Namespace) and is recorded nowhere else.
ServedSnapshots(ctx context.Context, namespace string, ids []string, since time.Time) (map[string]MemorySnapshot, error)
}
EventLogStore is implemented by drivers that persist the activity log. It is an optional capability interface — the EmbedModelStore/LinkStore/APIKeyStore precedent — so callers type-assert and degrade gracefully: the service skips logging and the REST layer answers 501 against a driver that lacks it.
type Filter ¶
type Filter struct {
// Tiers restricts results to these tiers; empty means all tiers.
Tiers []memory.Tier
// Levels restricts results to memories whose derivation level (explicit or
// deduced) matches one of the listed values; empty means no level constraint.
Levels []memory.Level
// Tags restricts results to memories carrying every listed tag (AND). Empty
// means no tag constraint.
Tags []string
// Metadata restricts results to memories whose top-level metadata contains
// each listed key with the given string value (AND). Empty means no metadata
// constraint. Only top-level string-valued entries are matched.
Metadata map[string]string
// ExcludeMetadata drops memories whose top-level metadata carries any of the
// listed key=value pairs (the inverse of Metadata). Empty means no exclusion.
// Used to keep a caller from recalling its own just-written memories — e.g.
// the OpenClaw plugin tags each captured turn with its session id and excludes
// that session on the pre-turn auto-recall, so a turn already in the live
// transcript is not echoed back as "long-term memory".
ExcludeMetadata map[string]string
// ExcludeIDs drops memories with the listed ids. Applied in SQL, before
// ranking and the caller's limit, so an excluded hit never consumes a
// result slot — a client that filters already-injected ids after the fact
// would instead lose those slots. Empty means no exclusion.
ExcludeIDs []string
// IncludeExpired includes memories past their TTL (default excludes them).
IncludeExpired bool
// IncludeSuperseded includes contradiction-tombstoned memories.
IncludeSuperseded bool
// Now is the instant expiry is evaluated at; the zero value means the wall
// clock. Callers with an injected clock (service.WithClock) should set it so
// store-level expiry filtering agrees with their notion of "now".
Now time.Time
// AsOf, when non-zero, switches to time-travel recall: results are the
// memories whose validity window contained AsOf (valid_from <= AsOf < valid_to,
// treating NULL bounds as open). It overrides the superseded exclusion, so a
// fact that was true then but has since been replaced is still returned.
AsOf time.Time
// MemoryTypes restricts results to memories whose top-level
// metadata["memory_type"] equals one of the listed values (OR). Empty means
// no constraint. Unlike Metadata (AND, one value per key), this expresses the
// multi-select the UI's memory-type filter needs.
MemoryTypes []string
// CreatedAfter restricts results to memories created at or after the instant.
// The zero value means no constraint.
CreatedAfter time.Time
// AccessedAfter restricts results to memories last accessed at or after the
// instant. The zero value means no constraint.
AccessedAfter time.Time
// Sort orders the results. It is honored by List only: the search methods
// return results best-first by relevance and ignore it.
Sort Sort
}
Filter narrows a search to a subset of memories. The zero value matches all live (non-expired, non-superseded) memories in the namespace.
type LinkStore ¶ added in v0.6.6
type LinkStore interface {
// PutLink inserts or replaces the link keyed by (l.Src, l.Dst). An
// upsert overwrites created_at — deliberately, unlike memory Put's
// preserve-on-upsert: links have no recency semantics, and import
// restore relies on this to replay a link's original CreatedAt rather
// than stamping "now".
PutLink(ctx context.Context, l NamespaceLink) error
// DeleteLink removes the link from src to dst. The bool reports whether a
// link existed to delete; a missing link is not an error.
DeleteLink(ctx context.Context, src, dst string) (bool, error)
// ListLinks returns the links whose Src is src, ordered by Dst. Empty (not
// an error) when src has no outgoing links.
ListLinks(ctx context.Context, src string) ([]NamespaceLink, error)
// ListAllLinks returns every link in the store, for the CLI/UI.
ListAllLinks(ctx context.Context) ([]NamespaceLink, error)
// RenameLinkEndpoints rewrites every link whose Src or Dst equals from to
// to instead, used when a namespace is moved (maintenance.Move). When a
// rewritten link collides with a pre-existing row at its new key, the
// pre-existing row wins and the renamed link is dropped: the target
// namespace's own explicit grant must never be silently widened or
// narrowed by an inherited one. A no-op when from == to.
RenameLinkEndpoints(ctx context.Context, from, to string) error
}
LinkStore is implemented by drivers that persist namespace_links, the cross-namespace read-linking table (namespace-cascade design). It is an optional capability interface — the EmbedModelStore precedent above — so callers type-assert and degrade gracefully against a driver that predates it.
type MemorySnapshot ¶ added in v0.7.9
MemorySnapshot is the denormalized (namespace, tier, summary) triple an event row carries for the memory it touched. Looked up by memory ID so a writer that knows only an ID can record the same snapshot an earlier serve already took — see EventLogStore.ServedSnapshots and the injection beacon, whose client sends bare IDs and nothing else.
type Metrics ¶
type Metrics interface {
// Upsert records one Upsert outcome. op is "insert" or "update"; tier
// is the memory's tier (working/episodic/semantic/procedural); memoryType
// is the typed-extraction class (decision/preference/problem) or "".
Upsert(op, tier, memoryType string)
// Delete records a hard delete from the Forget API path.
Delete()
// SoftDelete records a tombstone written by consolidation.
SoftDelete()
// SweepExpired records one memory removed by the decay sweeper.
SweepExpired(tier string)
// ActiveByTier sets the current count of live (non-superseded,
// non-expired) memories per tier. Called periodically after sweeps/fsck.
ActiveByTier(tier string, n int)
// DedupTombstoned records the number of memories tombstoned by one
// dedup pass (the periodic vector-cluster job or a one-shot dedup call).
// Called once per pass with the pass total.
DedupTombstoned(n int)
}
Metrics receives store events for observability. Methods must be safe for concurrent use; a nil Metrics is replaced by a no-op. Implementations live alongside the Prometheus registry in cmd/memini.
type NamespaceActivity ¶ added in v0.6.6
NamespaceActivity summarizes one namespace's live activity: the count of live memories (excluding expired, superseded, and closed-validity rows — the same liveness a default List filter applies) and the most recent created_at among them (the "last write" column Stats.LastWriteAt uses; unlike Stats, tombstoned rows do not advance it, since a single aggregate query shares one liveness WHERE for both figures).
type NamespaceLink ¶ added in v0.6.6
type NamespaceLink struct {
Src, Dst string
Tiers []memory.Tier // nil = durable default applied by service
Note string
CreatedAt time.Time
}
NamespaceLink is a directed cross-namespace read link: recall scoped to Src additionally reads durable memories from Dst. Tiers restricts which tiers cross the boundary; nil means the service layer applies its durable-tier default (only semantic/procedural cross namespace boundaries — episodic and working never do, in either direction). Note is a free-text annotation for operators explaining why the link exists.
type Pin ¶ added in v0.7.3
type Pin struct {
Key string
Namespace string
Note string
// CreatedBy is the API key name that created the pin; "" for the admin
// key or dev mode, which carry no named principal.
CreatedBy string
CreatedAt time.Time
UpdatedAt time.Time
}
Pin is a persisted project→namespace binding: an operator-created binding from a project's identity to the namespace every handshake for that project resolves to, overriding derivation. Key is the lookup key: "remote:<canonical-remote>" for a git-remote pin (canonical = normalized, credential-stripped) or "path:<absolute-toplevel>" for a path pin (used for remoteless repos and bare directories).
type PinStore ¶ added in v0.7.3
type PinStore interface {
// PutPins upserts entries in a single transaction, keyed by
// each entry's Key. An update preserves the existing row's CreatedAt and
// CreatedBy — a pin's provenance is fixed at creation, unlike APIKey's
// CreatedAt (which import restore can still overwrite by design) — while
// Namespace, Note, and UpdatedAt take the incoming values.
PutPins(ctx context.Context, entries []Pin) error
// GetPins returns the entries matching the given keys, in no
// particular order; a key with no matching row is simply absent from the
// result (not an error).
GetPins(ctx context.Context, keys []string) ([]Pin, error)
// DeletePins removes the entries with the given keys and
// returns the number of rows actually deleted; a key with no matching row
// does not count and is not an error.
DeletePins(ctx context.Context, keys []string) (int64, error)
// ListPins returns every entry ordered by Key, for the CLI/UI.
ListPins(ctx context.Context) ([]Pin, error)
// RenamePinNamespaces rewrites every entry whose Namespace exactly
// equals from to to instead (maintenance.Move, alongside
// RenameLinkEndpoints/RenameAPIKeyNamespaces); a namespace that merely
// starts with from (e.g. "memini2" against from="memini") is untouched.
RenamePinNamespaces(ctx context.Context, from, to string) error
}
PinStore is implemented by drivers that persist pins, the config-handshake redesign's project→namespace pin table. It is an optional capability interface — the EmbedModelStore/LinkStore/APIKeyStore precedent above — so callers type-assert and degrade gracefully against a driver that predates it.
type RepairRow ¶ added in v0.7.16
type RepairRow struct {
ID string
Namespace string
Tier memory.Tier
Content string
Fingerprint string
State RepairState
// Attempts already counts the claim that produced this row, so a handler
// comparing it against a ceiling is reading the cost of the run it is
// about to perform, not the one before it.
Attempts int
}
RepairRow is one claimed unit of deferred repair work: the fields a repair handler needs, without the cost of loading a whole memory.
Fingerprint is the optimistic-concurrency token. Every write that finishes a repair passes it back, and the store applies the change only if it still matches — so a repair that raced a content edit is discarded rather than stamping a vector for text the memory no longer holds. Content hash beats a timestamp here: it cannot false-match two writes landing in the same tick, and it states directly why a re-embed is needed.
type RepairStat ¶ added in v0.7.16
type RepairStat struct {
State RepairState
Count int
// OldestAt is the creation time of the oldest memory in the bucket — the
// signal that the backlog is filling faster than it drains.
OldestAt time.Time
// LastError is the most recent error recorded in the bucket, empty when
// none. Only meaningful for RepairFailed.
LastError string
}
RepairStat is one state's bucket of the repair backlog, for gauges, health and doctor.
type RepairState ¶ added in v0.7.16
type RepairState string
RepairState names the deferred work a memory still owes after a degraded write — one that stored without a vector because the embedder was unreachable or too slow.
It lives in a column on the memory itself rather than in a separate job table, and that is load-bearing: the state commits in the same transaction as the memory, so there is no second write to lose and no window in which a stored-but-unqueued row can exist. A separate table would reintroduce exactly the dual write a transactional outbox exists to prevent.
const ( // RepairNone is a healthy memory owing nothing. It is the zero value, so // every pre-existing row migrates into it for free. RepairNone RepairState = "" // RepairPending needs a vector. RepairPending RepairState = "pending" // RepairEnrich has its vector but still owes the write-time enrichment a // healthy write would have run (dedup, corroboration, contradiction // routing, consolidation). Kept distinct from RepairPending so an embedder // recovery does not silently drop the enrichment half of the repair. RepairEnrich RepairState = "enrich" // RepairFailed exhausted its attempt ceiling. The row is retained with its // last error rather than dropped — the diagnosis is the point, and the // memory is still degraded either way — and excluded from claims so a // wedged row cannot burn the embedder forever. RearmRepairs returns it to // RepairPending after a rest, making the ceiling a circuit breaker with an // auto-close rather than a dead end needing an operator. RepairFailed RepairState = "failed" )
type RepairStore ¶ added in v0.7.16
type RepairStore interface {
// ClaimRepairs atomically takes up to limit memories in the given state
// whose next-run time has passed, oldest-due first. Each claimed row has
// its attempt count incremented and its next run pushed to now+lease
// BEFORE being returned, in one statement.
//
// next_run_at IS the lease: a second claimant (another replica, or a
// `memini mcp` process sharing the sqlite file) sees a future timestamp and
// skips the row, and a worker that crashed mid-repair has its rows become
// claimable again with the attempt already charged — which is the right
// accounting, since a repair that killed the process should count against
// the ceiling. The caller must keep lease greater than
// limit*(per-repair timeout) or a slow batch can be double-claimed; every
// handler is idempotent, so that is waste rather than corruption.
//
// now is honoured by backends without a server clock (sqlite, and every
// test) and ignored by postgres, which computes the lease in SQL: with
// multiple replicas the lease must be a statement about the database's
// clock, not about how well N machines agree on the time.
ClaimRepairs(ctx context.Context, state RepairState, now time.Time,
lease time.Duration, limit int) ([]RepairRow, error)
// SetEmbeddingIfUnchanged attaches vec to a memory and moves it to next,
// but only while its content fingerprint still matches — reporting false
// when it does not, or when the row is gone. Guard and write are one
// statement, so a concurrent content change cannot slip between them.
//
// It deliberately does NOT advance updated_at: a system-initiated re-embed
// is index maintenance, not a logical edit, and bumping it would perturb
// every "prefer the most recent" recency decision in recall and answer.
SetEmbeddingIfUnchanged(ctx context.Context, namespace, id, fingerprint string,
vec []float32, next RepairState) (bool, error)
// SetRepairState moves a memory to next without touching its vector,
// under the same fingerprint guard. Separate from SetEmbeddingIfUnchanged
// precisely so no caller can express "advance the state" with a nil vector
// and have it read as "drop the vector" — the trap that makes a
// Get-then-Upsert round trip lossy (see GetEmbedding).
SetRepairState(ctx context.Context, namespace, id, fingerprint string, next RepairState) (bool, error)
// FailRepair records a recoverable failure: it sets the next run time and
// the last error. It does NOT touch the attempt count — that was charged at
// claim time, so a crashed run and a failed run cost exactly the same,
// which is what keeps the ceiling honest.
FailRepair(ctx context.Context, namespace, id, lastErr string, nextRunAt time.Time) error
// ParkRepair moves a memory to RepairFailed with lastErr, excluding it from
// claims until RearmRepairs returns it. now is the park instant, which
// RearmRepairs later compares against — passed in rather than read from a
// store clock so a test can drive the circuit breaker deterministically.
ParkRepair(ctx context.Context, namespace, id, lastErr string, now time.Time) error
// RearmRepairs returns RepairFailed memories last touched before
// failedBefore to RepairPending with a zeroed attempt count, due
// immediately, and reports how many moved. This is what turns the attempt
// ceiling into a circuit breaker: during a long outage each row costs a
// couple of probes an hour instead of continuous hammering, and recovery
// still needs no human.
RearmRepairs(ctx context.Context, failedBefore time.Time, now time.Time) (int64, error)
// MarkRepairNeeded puts memories into state when they are not already in a
// repair state, due immediately, and reports how many moved. It is the
// migration and safety-net path: rows written by a release that predates
// these columns, or by a write path that bypasses Remember, carry no state
// and would otherwise never be repaired. Rows already mid-repair are left
// alone so a sweep cannot reset a backoff or unpark a wedged row.
MarkRepairNeeded(ctx context.Context, namespace string, ids []string, state RepairState) (int64, error)
// RepairStats returns one entry per non-empty repair state, ordered by
// state. Empty (never an error) when nothing is outstanding.
RepairStats(ctx context.Context) ([]RepairStat, error)
// RepairStateOf returns a memory's repair state, attempt count and last
// error. Used by doctor, by the health endpoint and by tests; not on the
// hot path. Returns ErrNotFound when the memory is absent.
RepairStateOf(ctx context.Context, namespace, id string) (RepairState, int, string, error)
}
RepairStore is the durable deferred-repair queue. It is an optional capability interface — the EventLogStore/ChunkStore pattern — so callers type-assert and degrade: a store without it falls back to the metadata-marker backfill loop, which is the pre-queue behaviour.
Delivery is at-least-once. Every repair handler must be idempotent; the fingerprint guard on each completing write is what makes that true here.
type Scored ¶
type Scored struct {
Memory *memory.Memory
Score float64
// MatchedChunk is the text of the chunk that produced this hit, set only by
// ChunkVectorSearch and empty for every other search. Callers that rerank
// should judge this rather than the whole memory when it is set: it is the
// passage that actually matched, and a reranker handed the whole memory sees
// only a prefix that need not contain it.
MatchedChunk string
}
Scored is a memory paired with a relevance score for the query that produced it. Results are always returned best-first; Score is higher-is-better and is only comparable within a single method's result set.
type SettingsLayer ¶ added in v0.7.0
type SettingsLayer struct {
Source string
S ClientSettings
}
SettingsLayer is one input to MergeClientSettings: a (possibly partial) ClientSettings plus a human-readable label for where it came from (e.g. "default", "global", "key:ci-bot"). Source is echoed back in MergeClientSettings' provenance map for whichever fields this layer wins.
type Sort ¶ added in v0.6.8
type Sort struct {
// Key is the column to order by; "" means SortCreatedAt.
Key SortKey
// Asc orders ascending; the zero value orders descending.
Asc bool
}
Sort orders a listing. The zero value means created_at descending (newest first) — the order the UI browser wants by default, and the order the all-namespaces aggregate has always merged in.
type SortKey ¶ added in v0.6.8
type SortKey string
SortKey names a column List can order by. Values are the wire-level names the REST layer accepts, so an unknown key never reaches SQL — drivers map keys through a whitelist switch and fall back to SortCreatedAt.
type Store ¶
type Store interface {
// Upsert inserts or replaces a memory (matched by ID within its namespace),
// including its keyword-search index entry. When len(m.Embedding) == 0 the
// row is stored with no vector-index entry — kept keyword-searchable only,
// the write path used when embedding generation is unavailable; a stale
// vector-index entry from a prior upsert of the same ID is removed. Any
// other embedding length must equal the store's configured dims, or
// Upsert errors. VectorSearch never returns a vectorless row. Returns
// ErrConflict when the given ID already exists under a different namespace.
Upsert(ctx context.Context, m *memory.Memory) error
// Get returns a memory by ID, or ErrNotFound.
Get(ctx context.Context, namespace, id string) (*memory.Memory, error)
// IDsByPrefix returns the IDs in the namespace that begin with prefix,
// ordered ascending and bounded at limit rows — an indexed prefix scan
// backing short-id resolution (service.Get accepts an 8+ hex-char id
// prefix). LIKE/glob metacharacters in prefix match literally, never as
// wildcards. An empty prefix or non-positive limit returns no rows: the
// caller is resolving a specific handle, never enumerating a namespace.
IDsByPrefix(ctx context.Context, namespace, prefix string, limit int) ([]string, error)
// GetEmbedding returns the stored vector for a memory, for a write that must
// preserve a vector it is not recomputing (see service.Remember's reuse of
// the stored vector when an update leaves content unchanged). Returns
// (nil, nil) when the row exists but is vectorless — a degraded write
// awaiting backfill — and ErrNotFound when the memory is absent.
//
// Kept off Get deliberately: a vector is dims*4 bytes and Get/List/search run
// on every read path, so Memory.Embedding is left empty there (see
// memory.Memory.Embedding). That makes a Get-then-Upsert round trip lossy for
// the vector, which is why a write that skips embedding must read it back
// through here rather than off the Memory it just loaded.
GetEmbedding(ctx context.Context, namespace, id string) ([]float32, error)
// PredecessorIDs returns the IDs of memories in the namespace whose
// SupersededBy points at id — the versions id replaced. Empty when none.
// Used to walk a memory's supersession lineage backwards.
PredecessorIDs(ctx context.Context, namespace, id string) ([]string, error)
// GetByFingerprint returns a live (non-superseded, non-expired) memory in the
// namespace and tier whose content fingerprint (memory.Fingerprint) matches,
// for exact-restatement dedup at write time. Returns ErrNotFound when none
// matches. Now is the instant expiry is evaluated at (zero means wall clock).
GetByFingerprint(ctx context.Context, namespace string, tier memory.Tier, fingerprint string, now time.Time) (*memory.Memory, error)
// Delete removes a memory by ID, or returns ErrNotFound.
Delete(ctx context.Context, namespace, id string) error
// SetSuperseded tombstones a memory by recording the ID that replaced it,
// excluding it from default search results. Returns ErrNotFound if missing.
SetSuperseded(ctx context.Context, namespace, id, supersededBy string) error
// Restore clears superseded_by/valid_to so a tombstoned memory is live
// again. Returns ErrNotFound if missing.
Restore(ctx context.Context, namespace, id string) error
// VectorSearch returns the k memories nearest to vec, best-first.
VectorSearch(ctx context.Context, namespace string, vec []float32, f Filter, k int) ([]Scored, error)
// KeywordSearch returns the k best full-text matches for query, best-first.
KeywordSearch(ctx context.Context, namespace, query string, f Filter, k int) ([]Scored, error)
// Reinforce records that the given memories were just recalled: it bumps
// access_count and last_accessed_at to accessedAt. When newExpiry is non-nil
// it also slides the TTL forward for those that already expire (short-term
// memories), so frequently-recalled memories don't go stale. Missing IDs are
// ignored.
Reinforce(ctx context.Context, namespace string, ids []string, accessedAt time.Time, newExpiry *time.Time) error
// DeleteIfExpiredBefore removes a memory only when its expiry is still at or
// before cutoff. Returns ErrNotFound when the memory was absent or when its
// TTL was slid past cutoff by Reinforce, so the caller never over-deletes a
// memory that was accessed between ListExpired and the delete call.
DeleteIfExpiredBefore(ctx context.Context, namespace, id string, cutoff time.Time) error
// ListExpired returns up to limit memories whose TTL has passed, for the
// decay sweeper.
ListExpired(ctx context.Context, now time.Time, limit int) ([]*memory.Memory, error)
// List returns memories in a namespace matching f (without embeddings),
// for maintenance tasks (short-term capacity, fsck). limit <= 0 means all.
// Results are ordered by f.Sort — newest-created first by default — with
// ties broken by id ascending, so a capped listing is deterministic.
List(ctx context.Context, namespace string, f Filter, limit int) ([]*memory.Memory, error)
// ListNamespaces returns the distinct namespaces that hold memories.
ListNamespaces(ctx context.Context) ([]string, error)
// DeleteNamespace removes every memory in a namespace (including embeddings
// and keyword-search index entries). Returns the number of memories deleted.
DeleteNamespace(ctx context.Context, namespace string) (int64, error)
// Reassign moves the given memories from fromNS to toNS (recovery from a
// botched import or a shared-pool migration). IDs absent from fromNS are
// skipped; since IDs are globally unique a move never collides in toNS.
// Returns the number of memories moved.
Reassign(ctx context.Context, fromNS string, ids []string, toNS string) (int64, error)
// Retier changes a memory's tier and expiry in place (used by retro-tiering
// to demote stale durable memories). Tier lives only in the main row, so no
// vector/keyword reindex is needed. Returns ErrNotFound if missing.
Retier(ctx context.Context, namespace, id string, tier memory.Tier, expiresAt *time.Time) error
// SetConfidence updates a memory's corroboration confidence in place and
// bumps updated_at to now (resetting the lazy-decay baseline), used when a
// durable fact is re-observed. Returns ErrNotFound if missing.
SetConfidence(ctx context.Context, namespace, id string, confidence float64, now time.Time) error
// SetAssessedImportance stamps the LLM-assessed intrinsic importance in
// place. Deliberately does NOT bump updated_at: assessment is a system
// annotation, not a re-observation, and touching updated_at would reset
// confidence lazy-decay and demote eligibility. now only scopes the
// validity predicate. Returns ErrNotFound if missing.
SetAssessedImportance(ctx context.Context, namespace, id string, v float64, now time.Time) error
// MarkContradicted invalidates a durable fact a newer write contradicts: it
// sets confidence, stamps valid_to=now (unless already set) so the fact
// drops out of live recall while staying reachable via AsOf time-travel, and
// records the contradicting id plus the pre-update confidence in metadata for
// audit and reversal (Restore clears valid_to). Non-destructive: the row and
// its history are kept. Bumps updated_at to now. Returns ErrNotFound if
// missing.
MarkContradicted(ctx context.Context, namespace, id, contradictedBy string, confidence float64, now time.Time) error
// Ping verifies the backend is reachable, for readiness checks.
Ping(ctx context.Context) error
// Close releases backend resources.
Close() error
}
Store is the persistence and retrieval contract for memories. Implementations must be safe for concurrent use.