LemonMemory

Durable memory for agents: what an agent did, written down and searchable later.

lemon_memory is one of the packages that make up the Lemon agent platform. It is usable on its own — its only Lemon dependency is lemon_core.

What is in it

Module Purpose
LemonMemory.Document The record every provider reads and writes: content, scope, metadata, timestamps
LemonMemory.Store SQLite-backed store with OR/BM25 lexical search; the built-in durable backend
LemonMemory.Query Builds sanitized OR-joined FTS5 search terms
LemonMemory.Recall Bounded local lexical search used for automatic conversation recall
LemonMemory.Project Bounded repository/assistant-home/agent-scoped coding learnings in the same durable store
LemonMemory.Providers Fan-out registry — queries every registered provider, isolates failures and timeouts
LemonMemory.Providers.Local The built-in provider over LemonMemory.Store
LemonMemory.Ingest Writes finished runs into memory, invoked from the store's finalize-run hook
LemonMemory.Safety Redaction and size limits applied before anything is persisted
LemonMemory.Lifecycle Bounded/redacted browse, provenance inspection, and exact revision-bound delete for operator clients
LemonMemory.SessionSearch Session-scoped conversation search API used by recall
LemonMemory.TaskFingerprint Stable fingerprints for recognising repeated work
LemonMemory.Doctor.Checks.Memory lemon doctor check of session_search against store health; registered via config :lemon_core, :doctor_checks

Explicit review/confirm workflows can use LemonMemory.Store.put_sync/1 when they must not report success before SQLite accepts a document, or put_new_sync/1 for an atomic create-if-absent boundary that never overwrites an exact document ID. get_document/1 and delete_document/1 inspect and remove one exact record; deletion removes its FTS row in the same SQLite transaction. Ordinary run ingest remains asynchronous. LemonMemory.Safety.redact/1 removes common secret-shaped values before a caller builds the bounded document summary.

Authenticated operator surfaces use LemonMemory.Lifecycle, not raw Store rows. It lists or searches at most 50 results from a bounded recent window, filters by scope, safe agent label, one-way workspace digest, and memory kind, and displays only Safety-redacted summaries plus digest-only learned-source provenance. Deletion is non-mutating until previewed and re-confirmed with the exact fresh digest. Store.delete_document_if_unchanged/3 performs the final constant-time revision comparison and removes both the SQLite row and FTS row inside one transaction.

mix lemon.memory inspects and searches the store from the command line.

Installation

def deps do
  [{:lemon_memory, "~> 0.2"}]
end

SQLite (exqlite) is a hard dependency here, unlike in lemon_core where it is optional: a durable store is this package's reason to exist.

Adding a provider

Implement LemonMemory.Provider and register it at boot:

LemonMemory.Providers.register_provider(%{
  id: "vector",
  module: MyApp.VectorMemory,
  label: "Vector store",
  timeout_ms: 2_000
})

Searches then fan out to it alongside the local store. Read the behaviour's moduledoc first — it documents the contract in prose, including the rules that are easy to get wrong (search/2 returns a bare list, must not raise on user input, and must ignore options it does not recognise).

Verify your provider against the platform's compliance suite:

defmodule MyApp.VectorMemoryTest do
  use LemonPlatformTest.ProviderCase, provider: MyApp.VectorMemory
end

Configuration

# `:path` is a directory; memory.sqlite3 is created inside it.
config :lemon_memory, LemonMemory.Store, path: "/var/lib/my_app/store"

# Ingest options; `start_link/1` opts take precedence over these.
config :lemon_memory, LemonMemory.Ingest, config_ttl_ms: 30_000

LemonMemory.Application starts Providers always, and Store plus Ingest when SQLite is available, both registered under their module names.

Running more than one instance

Store and Ingest both take :name, and every public function on them takes the server as an optional first argument, so an embedding application can run isolated pipelines side by side in one node:

children = [
  Supervisor.child_spec(
    {LemonMemory.Store, name: :tenant_a_store, path: "/var/lib/my_app/tenant_a"},
    id: :tenant_a_store
  ),
  Supervisor.child_spec(
    {LemonMemory.Ingest, name: :tenant_a_ingest, memory_store: :tenant_a_store},
    id: :tenant_a_ingest
  )
]

A non-default ingest worker binds its name into the finalize-run hook: {LemonMemory.Ingest, :handle_finalize_run, [:tenant_a_ingest]}.