Memory is not "a summary of the chat log" but its own table: with a scope, an importance value and an access counter. Recall scores each candidate with similarity + importance + recency decay + access frequency + a scope bonus. That formula explains why some memories keep resurfacing and others never return.
All three write paths share one vector store and one scoring function: writes decide what exists, scoring and scope decide what the model sees this turn.
Memory model
Field
Notes
Scope
Global / Session / Project, defaulting to Global
ProjectId / TopicId
Scope carriers: Project requires ProjectId, Session requires an existing TopicId
Source
manual / conversation / work
Category
Free-form label passed to the model with the content
Importance
Default 0.5, clamped to 0…1 on create and update
AccessCount / LastAccessedAt
Recall side effects: a hit increments the counter and refreshes the timestamp; both feed scoring
Score
Returned only in search results, never persisted
Three write paths
Path
Behaviour
POST /api/memories
Manual create; Session scope is not allowed here (session memories come from extraction) and Project scope must carry ProjectId
POST /api/memories/extract/{topicId}
Conversation extraction (conversation); the automatic entry point weighs user message count, elapsed time and density
POST /api/memories/extract-work/{sessionId}
Work session extraction (work); fires every 10 user messages and only when the session belongs to a managed project
Both constraints are deliberate: work sessions are noisy and expensive, so skipping extraction beats writing a pile of meaningless memories.
Extra weight per scope; Global always matches, Session matches the same TopicId, Project the same ProjectId
Results are sorted by total score and truncated: topK = 5 when injecting context, topK = 10 on the search page. Every hit immediately writes back LastAccessedAt and AccessCount, so a memory that was used is more likely to be used again. That positive feedback loop is the first thing to check when an irrelevant memory keeps showing up.
Work sessions inject the union of global and current-project memories (passing the project's ManagedProjectId), which keeps project-specific rules inside their project.
Storage backends
SQLite uses the sqlite-vec table vec_memory_embeddings; PostgreSQL uses the vector column on MemoryEmbeddings.
There is currently no full-text path for memories: recall is vector search plus scoring.
Editing a memory re-computes its embedding; deleting soft-deletes the row and removes the embedding and its vector row.
There is no version history for memories: edits overwrite.