memory

package
v0.5.6 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Package memory is the assistant's persistent memory.

The vault holds what you write down; this holds what the assistant *learns* about you across conversations — your preferences, the people you work with, standing context and priorities — the things a good human assistant just knows and a stateless one forgets the moment a session ends. Memories are embedded and recalled semantically, so the relevant ones surface into a later conversation without you repeating yourself. That continuity is what turns a chatbot into a second brain.

Index

Constants

View Source
const (
	EvCreated     = "created"     // a new memory was stored
	EvReinforced  = "reinforced"  // a re-statement corroborated an existing memory
	EvSuperseded  = "superseded"  // a newer memory replaced this one (ref_id is the newer)
	EvMerged      = "merged"      // a duplicate was folded into this one (ref_id is the dropped)
	EvForgotten   = "forgotten"   // the memory was deleted
	EvUpdated     = "updated"     // an attribute (confidence, project, salience) changed
	EvQuarantined = "quarantined" // a machine proposed this and it is waiting for review
	EvAccepted    = "accepted"    // a quarantined memory was reviewed and released into active memory
	EvRejected    = "rejected"    // a quarantined memory was reviewed and discarded
	// EvAutoAccepted is the review switch accepting a memory with nobody
	// watching, kept apart from EvAccepted so "since your last review" means
	// since a person last looked, not since the switch last ran.
	EvAutoAccepted = "auto-accepted"
	// EvUnaccepted is `logos review --undo-auto` putting one back in the queue.
	EvUnaccepted = "returned-to-review"
)

Event names for the log. Kept as plain strings so the log stays readable and new event types never require a migration.

View Source
const (
	MinCosine = 0.45
	CosineGap = 0.15
)

Relevance floor for recall. Fuse ranks, and a rank says nothing about whether anything matched: the top candidate always scores 1.0, so every query returned up to its limit and reinforced all of it (#44). Measured with nomic-embed-text on ten short memories and ten queries: unrelated pairs sit at a median cosine of 0.36 with a maximum of 0.48, a clear answer at 0.73-0.87, and the one vague query ("which package manager") put its answer at 0.48 — level with the noise. So no absolute threshold separates them alone. MinCosine drops what no query should reach (nonsense tops out at 0.39); CosineGap drops the band of noise behind a strong answer, and leaves a vague query's flat field whole rather than guess at it. The nomic search_query/search_document prefixes were measured too and widened the overlap, so they are not sent.

View Source
const (
	// LexicalShare is how much of the best match's shared-word weight a memory
	// must carry before its words are evidence on their own.
	LexicalShare = 0.7
	// VectorOnlyGap is CosineGap for a memory sharing no word with the query.
	// 0.12, not 0.10, which measured 2 points more precise on the 32 facts but
	// sits exactly on a paraphrase 0.10 below the answer, the case
	// TestAMemoryFarBelowTheBestMatchIsLeftOut keeps.
	VectorOnlyGap = 0.12
)

Measured on a scratch vault of 432 memories about one project (its own 400 commit subjects plus 32 labelled facts) and 18 labelled queries: any shared word admitted a memory, so a project's common vocabulary — "written", "claude code", "release" — let most of the vault through, and the 0.15 gap admitted the rest of a flat field. Weighting the shared words, and holding a vector-only match closer, took precision from 27% to 33% with the same recall there, and from 31% to 39% on the 32 facts alone, where three answers that had got in on a coincidental word ("tag" in "-tags") were lost. Swept 0.3-0.9 and 0.05-0.15: lower LexicalShare or a wider gap gave the noise back, a higher share or narrower gap started dropping answers. Ranking was not the lever: score fusion, qwen3-embedding:0.6b, and a 4B chat model as judge were all measured and none ranked the answers higher.

View Source
const (
	PinNone   = 0 // ranked normally
	PinAlways = 1 // always included in a context pack, budget permitting — see spendMemories
	PinNever  = 2 // excluded from recall entirely; still on record, still listed by All
)

Pin states. PinNone is the default: ranked normally, neither forced in nor held out. Deliberately two states rather than a bool, because "always show me this" and "never show me this" are different user intents, not opposites of the same one — a memory can need overriding in either direction, and a single flag can only say one of them.

View Source
const DedupThreshold = 0.87

DedupThreshold is how close a new memory must be to an existing one to be treated as the same fact. High, so genuinely distinct facts about the same subject are kept; only near-restatements collapse.

The threshold alone was not enough — records that share a sentence frame and differ only in a number sit above it. It is now one of two conditions, the other being sameFact.

View Source
const Dir = "memories"

Dir is where memories live inside the vault.

View Source
const FadingDays = 60

FadingDays is how long a memory can go unrecalled before `logos show` asks whether it is still wanted — two-thirds of a half-life, while it still ranks well enough to be worth deciding about rather than already out of sight.

View Source
const HalfLifeDays = 90.0

HalfLifeDays is how long an untouched memory takes to lose half its salience. Long, because a preference stated once is meant to persist — but not forever if it is never relevant again.

View Source
const LogFile = "log.md"

LogFile is the timeline's name inside the memory directory. Not one of the kinds, for the reason PendingFile is not: Import iterates `kinds`, so this file is invisible to the path that decides what counts as known.

View Source
const MemSimilarityThreshold = 0.60

MemSimilarityThreshold gates "similar" edges. High enough that the graph shows genuine clusters, not a cobweb.

View Source
const OutcomeNoop = "noop"

OutcomeNoop means nothing was stored — there was nothing to store.

View Source
const PendingFile = "pending.md"

PendingFile is the review queue's name inside the memory directory. Not one of the kinds, on purpose: Import iterates `kinds`, so this file is invisible to the path that decides what counts as known.

View Source
const Schema = `` /* 4108-byte string literal not displayed */
View Source
const SourceCheckpoints = "checkpoints"

SourceCheckpoints marks a memory promoted from a decision that held across checkpoints (see ops.PromoteDecisions). Resume lists it with the decisions, not the memories, because that is where its sources were.

View Source
const (
	// StaleAgeDays: a memory older than this, never recalled, and faded in
	// effective salience is stale — still true, perhaps, but no longer earning
	// its keep.
	StaleAgeDays = 60
)

Variables

View Source
var ErrOnlySecret = errors.New("nothing to remember once the credential or <private> span in it was kept out — neither is stored as a memory")

ErrOnlySecret is Store refusing a memory that was nothing but a credential, or a span the person marked <private>. Its own value so a bulk caller can count these as refusals rather than stopping on the first one, or reporting them as failures to write.

View Source
var Ks = []int{1, 3, 5, 10}

Ks are the recall@k thresholds reported in one pass.

Functions

func Accept

func Accept(db *sql.DB, id int64) error

Accept releases a quarantined memory into active memory: it becomes recallable and gets written to the vault, same as anything Store creates directly. Rejects an id that is not actually pending, rather than silently no-op-ing on a typo or a double-accept.

func AcceptAuto added in v0.5.2

func AcceptAuto(db *sql.DB, id int64, at time.Time) error

AcceptAuto is Accept done by the review switch at the given time. The memory is marked as auto-accepted in its vault line, which is what lets a person list and undo what the switch did — after a rebuild as well as before one.

func AcceptedSince added in v0.5.3

func AcceptedSince(db *sql.DB, since int64, project string) (byHand, auto int, err error)

AcceptedSince counts the review queue's accepts at or after since, a Unix time, by a person and by the review switch apart: an accept nobody watched is not the same evidence as one somebody read. project "" counts every scope; a project counts its worktrees' scopes ("brain/feature") with it, as the usage ledger totals them. By prefix with the slash, not LIKE, so a project named with _ or % matches only itself. Read from the log, which ImportLog rebuilds from memories/log.md, so the count survives deleting the index.

func Consolidate

func Consolidate(db *sql.DB, rt *router.Router) (merged int, superseded int, err error)

Consolidate merges near-duplicate memories and supersedes ones a newer memory updates, so the store holds each fact once and holds its current version.

For each highly-similar pair, the model classifies the relation:

  • duplicate → keep the more-salient, fold in salience, supersede the other
  • update → the newer memory replaces the older; the older is superseded (this is what the knowledge-update case needs: "I moved to Boston" supersedes "I live in NYC")
  • distinct → leave both

Only pairs above the similarity gate are shown to the model, so the number of calls stays proportional to genuine overlap, not to the store size squared.

func ContestedNow added in v0.5.2

func ContestedNow(db *sql.DB, id int64, text string) (int64, bool)

ContestedNow is contestedMemory for a memory already stored: whether its text contradicts another memory, by text alone. The review switch asks it at the moment it would accept, not only when the proposal arrived, because a memory pinned or stored since can be the one it now disputes.

func Count

func Count(db *sql.DB) (int, error)

func DismissedTexts added in v0.5.3

func DismissedTexts(db *sql.DB) (map[string]bool, error)

DismissedTexts is every text the timeline records a person rejecting from the review queue or forgetting. Narrower than SeenTexts on purpose: a text met only as the reinforcement of a memory worded differently was not thrown out, and a caller that reports these as the user's rejections would say so falsely. Restored from log.md, like the rest of the timeline.

func EffectiveSalience

func EffectiveSalience(m Memory, now int64) float64

EffectiveSalience is a memory's stored salience decayed by how long since it was last useful, then lifted by how often it has proven useful. This is what ranking should trust, not the raw stored number: memory that earns its keep through recall stays prominent; memory that never helps recedes.

func EscapeField added in v0.4.8

func EscapeField(s string) string

EscapeField and UnescapeField are the same encoding for code outside this package that rewrites these files — a project rename — so it matches the name as written rather than as typed.

func Evidence added in v0.5.0

func Evidence(query string, qVec []float32, cands []Candidate) []bool

Evidence reports, per candidate, whether anything ties it to the query: enough of the query's distinctive words, a few of them backed by a vector close to the best match, or a vector alone that is closer still. Without evidence a candidate is not an answer, however it ranks among the others.

func Exclude

func Exclude(db *sql.DB, id int64) error

Exclude marks a memory never-include: it is dropped from recall entirely, but stays on record rather than being deleted. For the case Forget is too blunt for — a memory that is not wrong, just not something that should surface (something sensitive, something the user would rather manage by hand) — where the honest action is "stop showing me this", not "erase it".

func Export

func Export(db *sql.DB, dir string) error

Export writes every kind to the vault.

func ExportKind

func ExportKind(db *sql.DB, dir string, kind Kind) error

ExportKind writes one kind's memories to memories/<kind>.md.

Superseded memories are left out. They are history, not knowledge — the memory_log already records that they existed and what replaced them, and carrying them here would put values the user has already corrected back into a file the user reads.

Quarantined memories are left out too, and for a stronger reason: they are not yet knowledge at all. A machine proposed them and nobody has said yes. Writing them to the file the user reads as "what the assistant knows" would make quarantine theatre — reviewable in principle, already believed in practice. They start appearing here the moment Accept clears the flag.

func Faded added in v0.4.8

func Faded(db *sql.DB, now int64) (int, error)

Faded counts the memories that disuse has pushed well below their stored salience. It only counts: decay is applied where salience is read, by EffectiveSalience, and nowhere else.

It used to write the decayed value back as the stored salience. Every run then re-applied all the decay since last_used to a number that already had it, so `logos memory consolidate` run twice in a row halved an unused fact twice, and every ranking site decayed the result a third time. The write also never reached the kind file, so a rebuild quietly undid it, and ranking depended on whether the cache had survived.

func Fingerprint added in v0.5.3

func Fingerprint(text string) string

Fingerprint is the key Store deduplicates on, for a caller that has to say beforehand what a Store will skip: a preview that counted by exact text promised writes the Store then folded into an existing row.

func Forget

func Forget(db *sql.DB, id int64) error

func ForgetBySource

func ForgetBySource(db *sql.DB, source string) (int, error)

ForgetBySource removes every memory learned a particular way and reports how many went. It returns the count rather than nothing so a caller can tell "I removed forty" from "there was nothing there".

This is what makes a bulk seeding reversible. `logos bootstrap` writes several memories at once from git history, and a user who dislikes the result should not have to forget them one id at a time — nor should they have to guess which ids were the machine's and which were theirs. Source is the record of how a fact arrived, so it is the honest handle for undoing a whole arrival.

Each row goes through Forget, so the timeline records every removal individually and the vault files are flushed. A bulk DELETE would be faster and would leave the vault holding notes the database no longer knows about.

func Fuse

func Fuse(query string, qVec []float32, cands []Candidate) []float64

Fuse returns a reciprocal-rank-fusion score per candidate (aligned to cands), combining vector similarity and BM25 lexical rankings, normalised to 0..1 by the top score so callers can blend it with other signals like salience.

func HasProject added in v0.4.8

func HasProject(db *sql.DB, project string) (bool, error)

HasProject reports whether any memory — including a superseded or quarantined one — is filed under this exact project name. It is how a caller tells "this project has recorded nothing about that" apart from "there is no such project", which otherwise produce the same empty result and so the same wrong conclusion for the agent reading it.

func HybridRank

func HybridRank(query string, qVec []float32, cands []Candidate, k int) []string

HybridRank fuses vector and BM25 rankings, returning candidate IDs best-first.

func Import

func Import(db *sql.DB, p *provider.Provider, embedModel, dir string) (imported, rescued int, err error)

Import rebuilds the store from the vault. This is what makes `rm -rf .logos` survivable.

The files win. Where a memory exists in both places the text, confidence and provenance are taken from markdown, because the whole point is that a person can open the file, correct a fact, and have the correction stick. Memories present in the cache but absent from the file are deleted — that is how deleting a line forgets something.

Vectors are not stored in the file; anything without one is re-embedded here, which is the same work `logos index` already does for notes.

func ImportLog

func ImportLog(db *sql.DB, dir string) (int, error)

ImportLog restores the timeline from the vault, and is what makes deleting the cache survivable for the history as well as for the memories themselves.

Runs after Import, so a memory the log refers to is already back in the table. Events are matched on what they are rather than on a row id — a log id is local to the database that issued it and means nothing after a wipe — so re-running this is a no-op rather than a second copy of every line.

Returns how many events it put back.

func ImportPending

func ImportPending(db *sql.DB, dir string) (int, int, error)

ImportPending restores the review queue from the vault, and is what makes deleting the cache survivable for proposals as well as for memories.

Ordering matters: this runs after Import, so an id the queue claims is one Import has already had its chance to restore as an active memory. If a proposal was accepted between the last flush and now, the row exists and is no longer quarantined — re-quarantining it would un-accept a decision the user already made, so an existing row is left exactly as it is.

A damaged file is refused rather than acted on, for the reason Import gives: a truncated queue read as authoritative would silently reject every proposal the missing tail held.

Returns how many proposals it put back into the cache, and how many it rescued the other way — out of a cache that was their only copy and into the vault. They are separate numbers because they are opposite events, and a caller that reported a rescue as a restore would tell the user their queue had been recovered on a run where it was merely, finally, written down.

func Init

func Init(db *sql.DB) error

func LastReview added in v0.5.2

func LastReview(db *sql.DB) (int64, error)

LastReview is when a person last accepted or rejected a proposal, or 0. It reads the memory timeline, which log.md restores after a rebuild, so "since your last review" survives deleting the index like the marks it counts.

func Learn

func Learn(db *sql.DB, rt *router.Router, exchange, source string) (int, error)

Learn extracts durable memories from a conversation excerpt and stores the ones worth keeping. Returns how many were newly stored. Runs at T1 — a narrow classification, not reasoning.

func LearnedBetween

func LearnedBetween(db *sql.DB, since, until int64) (int, error)

LearnedBetween counts memories first created in [since, until] (unix seconds), read from the append-only log so the count is stable even after some of those memories were later superseded or forgotten. Used for growth over time.

func MarkDrift added in v0.5.3

func MarkDrift(mems []Memory, dir string)

MarkDrift sets Drift on each memory recorded at a commit of the repository at dir whose named files have changed since. The same measurement, and the same "may", as a ruling's (deadend.MarkDrift): a memory written in another repository, or before commits were recorded, is left alone.

func PendingCount

func PendingCount(db *sql.DB) (int, error)

PendingCount is the number `logos doctor` surfaces (see internal/health). The whole point of quarantine is that nothing sits there unseen, and a count nobody ever looks at is exactly that — so this has to be cheap enough to run on every health check, which is why it is a COUNT rather than len(Pending()).

func Pin

func Pin(db *sql.DB, id int64) error

Pin marks a memory always-include: a context pack carries it regardless of relevance score. For the handful of things that are always worth having on hand — the standing instruction, the fact that governs everything else — and that a ranking model might otherwise rate as unremarkable on any given query.

func ProjectOf added in v0.5.3

func ProjectOf(db *sql.DB, id int64) (string, error)

ProjectOf is the project memory id is filed under, "" for a global one.

func ReconcilePending added in v0.5.1

func ReconcilePending(db *sql.DB) (restored, rejected int, err error)

ReconcilePending adopts hand edits to the review queue file now, and reports how many proposals the file put back and how many it rejected.

Exported for `logos review`. Only writes used to reconcile, so the review offered a proposal the user had already rejected by deleting its line — and a write that did adopt the deletion said nothing about it.

func Reconfirm added in v0.5.5

func Reconfirm(db *sql.DB, id int64, commit string) error

Reconfirm records that a memory still holds at commit: `logos review --stale` after a person looked at the files that moved since it was written. Its drift is measured from there on, so the warning they answered does not come back for the same commits. Written through to the memory's file like setPin, because a commit kept only in the index would be lost with it and every answered warning would return.

func Reject

func Reject(db *sql.DB, id int64) error

Reject discards a quarantined memory outright. Unlike Forget, this only works on something still pending: rejecting an already-active memory is not what this call means, and forcing the caller through Forget for that keeps the two operations from being silently interchangeable.

func Render

func Render(mems []Memory) string

Context renders the recalled memories as a compact block for injection into a prompt — what the assistant "already knows" about the user, so it does not ask again and can act on preferences unprompted.

func SeenTexts added in v0.5.2

func SeenTexts(db *sql.DB) (map[string]bool, error)

SeenTexts is every text the timeline records a memory being stored, proposed or folded into another with — what Logos has met before, whatever became of it since. A fold too: a re-worded import is logged under its own words only as the reinforcement of the memory it restated, and forgetting that memory overrules it as much as forgetting one it created. The import switch skips these, so a memory the user forgot or rejected is not brought back by the next import. The timeline is restored from log.md, so this survives deleting the index.

func SetVault

func SetVault(db *sql.DB, dir string)

SetVault tells one store where its durable copy belongs. An empty dir unbinds it, which disables writing through — what tests that only care about the cache want, and what index.Close does so a closed handle leaves nothing behind.

func UnattributedCount

func UnattributedCount(db *sql.DB) (int, error)

UnattributedCount reports how many log lines carry no project at all. These are pre-migration events for memories that were forgotten before the backfill could reach them; they belong to some project, and there is no longer any way to say which. A project timeline that quietly omits them is accurate about what it shows and incomplete about the past, and a caller that says so is more trustworthy than one that does not.

func UndoAuto added in v0.5.2

func UndoAuto(db *sql.DB, id int64) error

UndoAuto puts a memory the review switch accepted back in the review queue. Back in the queue rather than rejected: the switch's mistake is to have decided without the user, and the undo returns the decision to them instead of making a second one on their behalf.

func UnescapeField added in v0.4.8

func UnescapeField(s string) string

func Unflushed added in v0.4.8

func Unflushed(db *sql.DB) (int, error)

Unflushed counts the memories the cache holds that the vault does not, which is what `logos doctor` needs in order to say so. Zero is the normal answer.

func UnflushedIDs added in v0.4.8

func UnflushedIDs(db *sql.DB) map[int64]bool

UnflushedIDs is the set of memories the cache holds and the vault does not, for a reader that has already loaded some memories and needs to say which of them are not durable.

A set rather than a column on Memory because every recall path scans its own explicit column list, and threading one more field through all of them would touch far more code than the question is worth. One query answers it for a whole page of results, and the normal answer is an empty map.

func UnflushedPending added in v0.5.2

func UnflushedPending(db *sql.DB) (int, error)

UnflushedPending counts the proposals in the review queue the cache holds and memories/pending.md does not — a separate number from Unflushed because the file at risk is a different one, and so is the noun doctor names it by.

func Unpin

func Unpin(db *sql.DB, id int64) error

Unpin returns a memory to normal ranking, whichever direction it was set — pinned or excluded.

Types

type BenchResult

type BenchResult struct {
	Category string
	N        int
	HitsAt   map[int]int // k -> number of questions whose evidence was in the top k
}

BenchResult is the outcome for one category (or overall): hits at each k.

func RunLongMemEval

func RunLongMemEval(p *provider.Provider, embedModel, path string, limit int, hybrid bool, progress func(done, total int)) ([]BenchResult, error)

RunLongMemEval evaluates retrieval recall at several k thresholds (Ks) over up to `limit` instances of a LongMemEval file, in one embedding pass.

func (BenchResult) RecallAt

func (r BenchResult) RecallAt(k int) float64

RecallAt returns recall@k for this result.

type Candidate

type Candidate struct {
	ID   string
	Text string
	Vec  []float32
}

Candidate is one thing being ranked — a stored memory or a benchmark session.

type Correction added in v0.5.3

type Correction struct {
	ID       int64 `json:"id"`
	Replaced int64 `json:"replaced"`
	// Existing is set when the corrected text was already an active memory, so
	// the old one was retired in its favour instead of a twin being written.
	// "Corrected — #12 is new" would be false, and the caller says so.
	Existing   bool               `json:"existing,omitempty"`
	Redactions []secret.Redaction `json:"redactions,omitempty"`
}

A Correction says what Correct did: which memory now holds the truth, and which one it replaced.

func Correct added in v0.5.3

func Correct(db *sql.DB, p *provider.Provider, embedModel string, id int64, m *Memory) (Correction, error)

Correct replaces memory id with m in one step: m is stored, and the old memory is marked superseded by it.

Forget-then-remember was the only way to do this before, and it is two mistakes. Forgetting erases what was believed, so the history can no longer say "this used to be 8080"; and remembering the new fact on its own runs it through the contested check, which finds the very memory being corrected and queues the correction for review. A person who said "that is wrong, it is now X" has already reviewed it. So the new memory skips semantic dedup and the contested check, and the old one is kept on record, out of recall.

The link survives deleting the index without a vault format change: the superseded memory leaves memories/<kind>.md, and the superseded event in memories/log.md — the one ImportLog replays — names its replacement.

type DiffEntry

type DiffEntry struct {
	MemID int64  `json:"mem_id"`
	Event string `json:"event"`
	Text  string `json:"text"` // snapshot from the log at the time of the change
	TS    int64  `json:"ts"`
	// The project the memory belonged to, "" for a global fact. Carried so a
	// reader can tell whose work an entry describes: a flat list of every
	// project's changes reads as a list of changes to the one in front of you.
	Project string `json:"project,omitempty"`
}

DiffEntry is one change to one memory within the window.

type DiffResult

type DiffResult struct {
	Subject      string      `json:"subject,omitempty"`
	Since        int64       `json:"since"`
	Until        int64       `json:"until"`
	Added        []DiffEntry `json:"added"`        // newly learned facts
	Removed      []DiffEntry `json:"removed"`      // superseded or forgotten
	Corroborated []DiffEntry `json:"corroborated"` // re-stated, merged, or updated — belief firmed
}

DiffResult is what changed between two moments.

func Diff

func Diff(db *sql.DB, subject string, since, until int64) (DiffResult, error)

Diff computes what changed in the store between since and until (unix seconds, inclusive). If subject is non-empty, it narrows to changes whose snapshot text mentions it, case-insensitively — "what changed about Sarah?". Matching on the snapshot (not the live memories table) is deliberate: it still catches facts that were forgotten or superseded inside the window.

func (DiffResult) Empty

func (d DiffResult) Empty() bool

type Edits added in v0.5.3

type Edits struct {
	Added, Forgotten []int64
	// Held is a line left out of memory, each with why and what to do: its
	// text belongs to a memory awaiting review or superseded, so it can be
	// neither that memory nor a new one.
	Held []string
	// Migrated counts memories moved from an old one-file-per-kind vault to
	// their own file under memories/<kind>/, this run. Zero on every run after
	// the first for a given kind — migration is idempotent, keyed off which
	// per-memory files already exist, not off a flag that could get stuck if
	// a run is interrupted.
	Migrated int
	// SetAside and Orphaned are sync conflict copies under memories/<kind>/,
	// vault-relative, left out of memory. Returned rather than printed so
	// every caller of an import can say them — a copy announced only by
	// `logos index` was invisible to setup, which imports the same files.
	// Orphaned are copies whose original is gone: a memory forgotten,
	// corrected or quarantined removes its own file and cannot know a sync
	// client made a second one, and reading that copy as live brought the
	// forgotten memory back on the next import without a word.
	SetAside, Orphaned []string
}

Edits is what an import changed because of a hand edit: lines with no id comment that became memories, and memories whose line is gone. Said by `logos index`, because the two together are what a line retyped without its comment does — the old memory is forgotten with its project, confidence and pin, and a bare one takes its place — and the summary line counts neither.

func ImportEdits added in v0.5.3

func ImportEdits(db *sql.DB, p *provider.Provider, embedModel, dir string) (imported, rescued int, e Edits, err error)

ImportEdits is Import, also saying which memories a hand edit made or removed. A memory restored under its own id after a wipe is neither.

type Fetched added in v0.5.3

type Fetched struct {
	ID         int64
	Memory     Memory
	Found      bool
	Superseded bool
}

Fetched is one id asked for by number, and what became of it. An id is something an agent copied out of an earlier answer, so the memory may have been replaced, queued or excluded since; each of those is a different thing to tell the agent, and none of them is the text served as current.

func ByIDs added in v0.5.3

func ByIDs(db *sql.DB, ids []int64) ([]Fetched, error)

ByIDs looks up memories by id, in the order asked. Unlike All it reads superseded and quarantined rows too, because the caller has to be able to say why an id it was given is not being served.

type GraphEdge

type GraphEdge struct {
	Src    string  `json:"src"`
	Dst    string  `json:"dst"`
	Rel    string  `json:"rel"` // mentions | similar | supersedes
	Weight float64 `json:"weight"`
}

GraphEdge connects two nodes.

type GraphNode

type GraphNode struct {
	ID         string  `json:"id"`   // "m<id>" for a memory, the note slug for a note
	Type       string  `json:"type"` // "memory" | "note"
	Label      string  `json:"label"`
	Kind       string  `json:"kind"`
	Confidence float64 `json:"confidence,omitempty"`
	Salience   float64 `json:"salience,omitempty"`
	Project    string  `json:"project,omitempty"`
	Superseded bool    `json:"superseded,omitempty"`
	Degree     int     `json:"degree"`
}

GraphNode is a memory or a note in the memory graph.

type HealthReport

type HealthReport struct {
	Total          int        `json:"total"`
	Duplicates     int        `json:"duplicates"`      // memories that are part of a near-duplicate pair
	DuplicatePairs [][2]int64 `json:"duplicate_pairs"` // a sample, for `consolidate` to act on
	Stale          int        `json:"stale"`
	StaleIDs       []int64    `json:"stale_ids"` // a sample
	Orphans        int        `json:"orphans"`   // linked to no note (informational)
	LowConfidence  int        `json:"low_confidence"`
	Score          float64    `json:"score"` // 0..1, fraction with no defect flag
}

HealthReport is the computed diagnostic.

func Health

func Health(db *sql.DB) (HealthReport, error)

Health computes the report over the active store. It loads the columns the checks need directly (activeMemories omits confidence), including the vectors the duplicate check compares.

type Kind

type Kind string

Kind categorises a memory so it can be weighted and surfaced appropriately.

const (
	// Preference: how the user likes things done ("prefers short emails",
	// "no meetings before 10am"). The heart of the "emotional intelligence" a
	// secretary is expected to have.
	Preference Kind = "preference"
	// Person: a relationship fact ("Sarah is the CFO", "Alex is sensitive
	// about deadlines").
	Person Kind = "person"
	// Fact: a durable fact about the user or their work.
	Fact Kind = "fact"
	// Context: standing situational context ("launching in Q4", "hiring two
	// engineers") — what lets the assistant anticipate.
	Context Kind = "context"
	// Procedure: how to do something in this codebase, with a trap — what
	// goes wrong if you don't. The mirror of a recorded dead end: negative
	// knowledge stops an agent repeating a mistake, this stops it repeating
	// the discovery that the mistake was avoidable. Never surfaced by
	// ordinary recall or in a context pack (see recallScoped, Pinned) — its
	// only reader is before_you_try, via RecallProcedures. See
	// internal/procedure for the record shape and retrieval ranking.
	Procedure Kind = "procedure"
)

func KindOf added in v0.5.3

func KindOf(db *sql.DB, id int64) (Kind, error)

KindOf is the kind memory id was stored as, which a correction of it keeps.

type LogEntry

type LogEntry struct {
	ID     int64  `json:"id"`
	TS     int64  `json:"ts"`
	MemID  int64  `json:"mem_id"`
	Event  string `json:"event"`
	Detail string `json:"detail"`
	RefID  int64  `json:"ref_id"`
	// Project is the memory's project as it stood when the event happened,
	// which is not always its project now — a memory can be re-scoped, and the
	// timeline of a project should show what was true at the time rather than
	// retroactively rewriting itself.
	Project string `json:"project,omitempty"`
}

LogEntry is one line of the timeline.

func History

func History(db *sql.DB, memID int64) ([]LogEntry, error)

History returns the timeline for a single memory, oldest first — its whole life, from first learned to now.

func ParseLog added in v0.5.3

func ParseLog(raw string) []LogEntry

ParseLog is parseLog for a reader outside this package: `logos watch` reads the timeline's lines as they are appended, from the vault, so it shows what happened even to a memory stored by another process's index.

func Timeline

func Timeline(db *sql.DB, limit int) ([]LogEntry, error)

Timeline returns the most recent log entries, newest first, up to limit (limit <= 0 means all).

func TimelineInProject

func TimelineInProject(db *sql.DB, project string, limit int) ([]LogEntry, error)

TimelineInProject is Timeline narrowed to one project: what changed in what the assistant knows about *this* work, newest first.

It is a separate function rather than a filter the caller applies afterwards because the limit has to be applied after the narrowing, not before. Asking for "the last 20 events in kestrel" and getting two, because the other eighteen of the global last-20 belonged to other projects, is the kind of wrong that looks like an empty project rather than a bad query.

Unattributed lines — written before the log carried a project, by a memory since forgotten — match nothing and are simply absent. UnattributedCount reports how many there are so a caller can say the timeline is partial instead of implying it is complete.

type MemGraph

type MemGraph struct {
	Nodes []GraphNode `json:"nodes"`
	Edges []GraphEdge `json:"edges"`
}

MemGraph is the whole thing.

func BuildGraph

func BuildGraph(db *sql.DB, withSimilarity bool) (MemGraph, error)

BuildGraph assembles the memory graph. withSimilarity adds the (view-time, never-stored) similarity edges; they are the densest, so they are opt-in.

func (MemGraph) Mermaid

func (g MemGraph) Mermaid() string

Mermaid renders the graph as a Mermaid flowchart — a portable way to actually see it (terminal tools, markdown, the artifact viewer) without the widget.

type Memory

type Memory struct {
	ID int64 `json:"id"`
	// Uid is a content-derived id — sha256(kind + normalized text + the
	// repository's normalized remote), truncated to uidLen hex characters —
	// computed once, by computeUID, the first time this memory is stored, and
	// never recomputed after. ID is an autoincrement counter, local to one
	// vault's SQLite cache and reused once a row is deleted; Uid is what lets
	// the same fact, written independently on two machines, be recognised as
	// the same memory once 0.6.0 merges vaults across users. A hand edit to
	// text, confidence or salience must not change it — see upsert, which
	// never writes this column on an UPDATE.
	Uid      string  `json:"uid,omitempty"`
	Text     string  `json:"text"`
	Kind     Kind    `json:"kind"`
	Salience float64 `json:"salience"` // 0..1, how much it matters
	// Confidence is how sure we are the fact is true and current, as distinct
	// from salience (how much it matters). A preference you stated by hand is
	// near-certain; one the model inferred from a passing remark is not.
	// Corroboration raises it; being superseded or going stale lowers it.
	Confidence float64 `json:"confidence"` // 0..1
	// Project scopes a memory to one piece of work, so an assistant helping with
	// ÉlyséeBot recalls ÉlyséeBot's context and not the whole life. Empty means
	// global — it applies everywhere.
	Project string `json:"project,omitempty"`
	Source  string `json:"source"` // where it was learned (conversation, manual, mcp)
	// Agent names which coding agent learned this — "claude-code", "cursor",
	// "codex" — when the MCP host said so at handshake. Empty means either the
	// fact predates this field or arrived somewhere that isn't one agent among
	// several (the CLI, a dream pass). Never asked of the model: see
	// internal/mcpserver's clientInfoFromInitialize, which is the only writer.
	Agent        string `json:"agent,omitempty"`
	Created      int64  `json:"created"`
	LastUsed     int64  `json:"last_used"`
	Uses         int    `json:"uses"`
	SupersededBy int64  `json:"superseded_by,omitempty"` // id of the memory that replaced this one
	// Supersedes is the opposite direction from SupersededBy, and the one that
	// has to survive a wipe: SupersededBy is this row's own id-based pointer,
	// set on the OLD memory by carryLocked, and never written to the vault —
	// it dies with the index, which is fine, because the superseded row's file
	// is deleted too (see removeMemoryFile) and nothing recreates either on
	// reimport. Supersedes is the NEW memory's forward link, written here as
	// the old memory's content-derived Uid rather than its numeric id, so a
	// reader of this memory's own per-memory file can say what it replaced
	// without the database at all. Set once by Correct, at creation, exactly
	// like Uid itself.
	Supersedes string `json:"supersedes,omitempty"`
	// Quarantined means a machine proposed this and nobody has looked at it yet.
	// A quarantined memory is a real row — same table, same shape — it is just
	// invisible to Recall/All until a human accepts it. See quarantine.go.
	Quarantined bool `json:"quarantined,omitempty"`
	// ReviewIfContested queues this memory only when it contradicts something
	// already stored, instead of queueing it unconditionally. Set on the way
	// in; Quarantined is the answer on the way out. See contestedMemory.
	ReviewIfContested bool `json:"-"`
	// Turn cites the transcript turn a queued proposal was made in, as TurnRef
	// writes it. Contests is the memory it contradicted when it queued. Both
	// are what the review switch decides by (see internal/ops/autoreview.go),
	// and both are written into the queue file, because a decision the index
	// alone remembered would be forgotten by the rebuild that follows.
	Turn     string `json:"turn,omitempty"`
	Contests int64  `json:"contests,omitempty"`
	// AutoAccepted is when the review switch accepted this memory, or 0 when a
	// person did or nobody had to. Written into the memory's vault line, so
	// `logos review --undo-auto` still finds it after the index is deleted.
	AutoAccepted int64   `json:"auto_accepted,omitempty"`
	Score        float64 `json:"score"` // set on recall: relevance to the query
	// Pin overrides ranking entirely: PinAlways forces inclusion in a context
	// pack regardless of relevance score, PinNever removes a memory from
	// recall while leaving it on record. See PinAlways and PinNever.
	Pin int `json:"pin,omitempty"`
	// Commit is the repository's HEAD where the memory was written, when it
	// was written inside the repository its project is. A procedure's trap and
	// a decision's reason are about the code as it was then; recall measures
	// the files the text names against this, the way a checkpoint's rulings
	// are measured against the checkpoint's commit. Empty for every memory
	// written before it existed and for one written outside a repository —
	// neither is ever flagged.
	Commit string `json:"commit,omitempty"`
	// Remote is the repository's normalized origin URL (see
	// gitstate.NormalizeRemote), set by ops.Anchor from Dir before Store is
	// called. It exists only to seed Uid at creation — not persisted to the
	// vault or the index, and never read back once Uid is set. A memory
	// written outside a repository, or one whose caller never resolved a
	// Dir, leaves this "", which computeUID treats as the explicit no-remote
	// case rather than a repository that happens to have an empty identity.
	Remote string `json:"-"`
	// Drift is set on the way out, by MarkDrift, when files the memory names
	// have changed since Commit. A reason to say so, never to drop it.
	Drift *gitstate.Drift `json:"drift,omitempty"`
	// contains filtered or unexported fields
}

Memory is one remembered thing.

func All

func All(db *sql.DB) ([]Memory, error)

All returns every active memory, most salient first — the fallback and the CLI view. Superseded memories are excluded (they are history, not truth).

func AllInProject

func AllInProject(db *sql.DB, project string) ([]Memory, error)

AllInProject is All narrowed to one project's memories plus the global ones, which is what a project-scoped view wants: the work's own facts, and the standing truths that apply to every project. An empty project means no narrowing at all, so this degrades to All.

func AutoAccepted added in v0.5.2

func AutoAccepted(db *sql.DB, since int64) ([]Memory, error)

AutoAccepted lists the memories the review switch accepted at or after since, newest first, that still carry the mark: one a person has since forgotten, or whose mark they deleted by hand, is no longer the switch's.

func Fading added in v0.5.5

func Fading(mems []Memory, now int64) []Memory

Fading picks out the memories no recall has touched in FadingDays (never recalled counts from when it was written), the longest untouched first.

Decay already ranked these down, and said so to nobody: a memory that drifted out of every context pack looked, from the outside, like one Logos had lost. Listing them gives the person the decision decay was making for them. A pinned memory is left out — the pin already answered the question.

So are the two kinds that reach an agent without a recall, so their clock says nothing about whether they are read: a procedure is shown by before_you_try, and preferences are surfaced into every context pack. Listing those beside "let one go" would invite deleting what is used every session.

func Pending

func Pending(db *sql.DB) ([]Memory, error)

Pending returns quarantined memories awaiting review, oldest first — a backlog should be worked in the order it arrived, not have the newest arrival jump the queue.

It carries Agent, which the review queue needs more than active memory does: the question in front of a reviewer is whether to believe a proposal, and which agent proposed it is part of the answer.

func Pinned

func Pinned(db *sql.DB, project string) ([]Memory, error)

Pinned returns every active PinAlways memory in scope for a project — its own plus the global ones, the same scoping AllInProject uses. This is the list contextpack must include regardless of relevance score; ranking never gets a vote on whether a pinned memory appears; only the budget does, and only in the sense of how much of it fits — see spendMemories.

kind != Procedure is not optional here the way it is a design choice elsewhere: recallScoped's exclusion only stops a procedure from being ranked into the pack, but Pinned is contextpack's separate, unconditional path around ranking entirely (see recallScoped's own comment on why no special case lives there for PinAlways). Without this line, pinning a procedure would be the one way to put instruction-shaped vault content in front of a model with no boundary line in sight — the opposite of "procedures never enter the pack."

func Recall

func Recall(db *sql.DB, p *provider.Provider, embedModel, query string, k int) ([]Memory, error)

Recall returns the memories most relevant to a query, by embedding similarity. The used memories have their reinforcement bumped, so what proves useful stays fresh — memory that is exercised persists, memory that never helps fades in the ranking.

func RecallInProject

func RecallInProject(db *sql.DB, p *provider.Provider, embedModel, query, project string, k int) ([]Memory, error)

RecallInProject is Recall narrowed to a project's memory (plus global memory).

func RecallProcedures

func RecallProcedures(db *sql.DB, p *provider.Provider, embedModel, query string, k int) ([]Memory, error)

RecallProcedures returns the vault's procedure-kind memories bearing on a query, unscoped by project — deliberately, mirroring deadend.Collect, which searches every project on purpose because the procedure that would help was quite possibly recorded on a different project than the one being asked about. internal/procedure.Check takes it from here to rank and flag cross-project hits; this function's only job is "every stored procedure", the way Collect's is "every recorded dead end".

func Surface

func Surface(db *sql.DB, kinds []Kind, n int) ([]Memory, error)

Surface returns the top non-superseded memories of the given kinds by effective salience — what the assistant should proactively keep in mind (the standing preferences and context that make a brief feel like it knows you), with no query needed.

func (Memory) DriftNote added in v0.5.3

func (m Memory) DriftNote() string

DriftNote is the clause a surface appends to a drifted memory's line, or "".

type PipelineCase

type PipelineCase struct {
	Name     string
	Exchange string   // the conversation to learn from
	Probe    string   // a later question that should recall the fact
	Expect   []string // any of these substrings appearing in a recalled memory counts as a hit
}

PipelineCase is one conversation and what should be learnable from it.

type PipelineReport

type PipelineReport struct {
	Cases       int
	Learned     int           // memories stored
	RecallHits  int           // probes that recalled the expected fact
	Duplicates  int           // re-learning the same case stored nothing new (good)
	LearnTime   time.Duration // total extraction time
	RecallTime  time.Duration // total recall time
	MemoryCount int           // final store size
	Details     []string      // per-case notes
}

PipelineReport is the outcome of a pipeline run.

func RunPipeline

func RunPipeline(db *sql.DB, rt *router.Router, cases []PipelineCase) (PipelineReport, error)

RunPipeline learns from each case, then probes recall, over a fresh store. It also re-learns every case once to confirm dedup holds (no growth).

func (PipelineReport) Accuracy

func (r PipelineReport) Accuracy() float64

type QAResult added in v0.5.1

type QAResult struct {
	Category string
	N        int
	Correct  int
}

QAResult is end-to-end accuracy for one LongMemEval category: a generated answer, graded against the gold answer, after retrieval — as opposed to BenchResult, which only asks whether the right session was surfaced. A system can have perfect recall and a wrong answer; this is the number that is actually comparable to another memory system's published LongMemEval score, because every competitor reports QA accuracy, not recall@k.

func RunLongMemEvalQA added in v0.5.1

func RunLongMemEvalQA(p *provider.Provider, embedModel, genModel, judgeModel, path string, limit int, hybrid bool, depth int, progress func(done, total int)) ([]QAResult, error)

RunLongMemEvalQA runs the full pipeline LongMemEval actually measures: retrieve, generate an answer from what was retrieved, grade it against the gold answer. genModel answers the question from retrieved context alone; judgeModel (often the same model) grades the generated answer against the gold one. Both run through the local router, per this project's no-cloud-call rule — there is no hosted grading step hiding in here.

func (QAResult) Accuracy added in v0.5.1

func (r QAResult) Accuracy() float64

Accuracy returns the fraction of this category's questions answered correctly.

type Receipt

type Receipt struct {
	Outcome string `json:"outcome"` // EvCreated, EvReinforced, EvQuarantined, or OutcomeNoop
	ID      int64  `json:"id"`
	// Ref is the memory that was reinforced, when the store folded into an
	// existing one instead of adding a twin.
	Ref int64 `json:"ref,omitempty"`
	// StillQueued is set when the memory reinforced is itself waiting in
	// quarantine. Dedup matches queued rows too, and "already knew that" about
	// a fact recall will not return is a receipt contradicted one call later.
	StillQueued bool `json:"still_queued,omitempty"`
	// Contested names the memory this one contradicts, when that is why it
	// queued. "Queued for review" with no reason is the receipt that trains a
	// user to stop reading receipts; the one fact that makes the queue worth
	// opening is which existing memory is now in dispute.
	Contested     int64  `json:"contested,omitempty"`
	ContestedText string `json:"contested_text,omitempty"`
	// Redactions is what Store masked out of the text before keeping it.
	Redactions []secret.Redaction `json:"redactions,omitempty"`
	// QueueRestored and QueueRejected are what a queued store adopted from a
	// hand edit to the review queue file before inserting: lines the user added,
	// and lines they deleted, which rejects those proposals. The store does it
	// on the way to rewriting the file, and a caller that cannot see it reports
	// "queued" on a call that also discarded something in the user's name.
	QueueRestored int `json:"queue_restored,omitempty"`
	QueueRejected int `json:"queue_rejected,omitempty"`
}

A Receipt says what actually happened to a memory that was stored.

"Saved" is not one outcome. The same call can create a memory, corroborate one that already existed, land in quarantine awaiting review, or do nothing — and a caller that cannot tell those apart will report all four as success. That matters most across MCP, where an agent tells the user it remembered something: it should be able to say whether it learned a new fact, confirmed an old one, or just queued something for the user to look at.

func Store

func Store(db *sql.DB, p *provider.Provider, embedModel string, m *Memory) (Receipt, error)

Store embeds a memory and saves it, deduplicated on its normalised text so the same fact learned twice does not accumulate. The receipt distinguishes a new memory from a corroborated one.

func (Receipt) Created

func (r Receipt) Created() bool

Created reports whether the store added a new, immediately-active memory, which is what most callers actually want to branch on. A quarantined arrival is deliberately excluded — it exists in the store, but nothing may treat it as learned until a human accepts it.

func (Receipt) Queued

func (r Receipt) Queued() bool

Queued reports whether the store landed in quarantine rather than going active — the signal a caller uses to tell the user "queued for review" instead of "remembered".

Jump to

Keyboard shortcuts

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