A drop-in persistent memory store for AI agents built on plain PostgreSQL. Works in any Lua 5.1+ runtime — pgvector is auto-detected and used when available, but not required — the default backend is a pure-Lua brute-force search that runs on any Postgres 15+.
Give your agent a real memory that survives session crashes, JSON-overflow errors in chat clients, container restarts, and device switches — without taking on any new runtime services beyond what your app already runs.
Lua-first. Every component — embedder, store, routes, Web UI, MCP
server, summarizer — is written in Lua. Hard dependencies are PostgreSQL,
lua-openssl (AES-256-CBC crypto), and luasocket (HTTP outside OpenResty).
OpenResty is optional — the library runs in any Lua 5.1+ runtime.
- Hybrid search: cosine vector similarity (pgvector / HNSW) + Postgres full-text search, blended via configurable weights.
- Local-first: ships with a pure-Lua in-process embedder (
hash) — zero network, zero Python, zero model files, zero new dependencies. Latency is microseconds. - Embedder-agnostic upgrade path: when you outgrow lexical similarity,
swap to Ollama, OpenAI, or any HTTP service that returns a JSON
vector — without touching the schema, API, or clients. See
EMBEDDERS.md for the 5-minute switch and the dim-mismatch trap,
eval/results/recall_bench.md for a
measured hash-vs-Ollama recall comparison on a synthetic corpus, and
eval/results/longmemeval.md for the
full 500-question LongMemEval
_sretrieval-recall run. - Auth-agnostic: you supply an
auth_fn(self) -> bool. The library never assumes a session model, role table, or CSRF mechanism. - Scoped: every memory has a
scope(global,repo:<name>,session:<id>, or anything you want). Multiple projects safely share one DB. - Session continuity: end-of-session
promote()rolls a hotsession:<uuid>scope into a single summary in a long-term scope — the next session starts knowing what the previous one knew. See SESSION_CONTINUITY.md. Auto-capture hooks**:luamemo.hookswires chat agents into the store with two lines per turn — user/assistant messages, tool calls, and durable decisions land in the right scope with sane dedup defaults. See HOOKS.md. - Built-in reranker: opt-in second pass over the hybrid top-N
using
noop(lexical),ollama, oropenaiadapters. Lifted R@1 by +12 pts on LongMemEval_s(n=50) at ~4 % overhead with the freenoopadapter. See RETRIEVAL_TUNING.md §4. - Tiny surface: one
setup()call, oneroutes.register()call, six HTTP endpoints, six Lua functions. - CLI included:
memo write,memo search,memo recentfrom any shell. - MCP-native: a pure-Lua Model Context Protocol
stdio server lets Claude Desktop, Cursor, Continue.dev, and Copilot Agent Mode
read/write memories directly. See
mcp/README.md. - Importance + time decay: each memory carries an
importanceweight (0..10) and an optionaldecay_rate(0..1 per day). Search ranks by(hybrid_score × importance × e^(-decay_rate · days))so fresh, high-value notes outrank stale low-value ones automatically. See examples/decay_importance.md.
┌──────────────────────────────────┐
│ Caller surfaces │
│ HTTP routes │ CLI (memo) │ │
│ Web UI │ MCP server │ │
└──────┬───────────┬───────────────┘
│ │
▼ ▼
┌──────────────────────────────────┐
│ init.lua — setup() + config │
└───┬──────────────────────────────┘
│
┌────────┼───────────────┐
▼ ▼ ▼
store.lua embed.lua summarizer.lua
(write/ (in-process (timer + adapters
search/ hash OR in summarizers/)
recent/ HTTP via
delete/ http.lua)
dedup)
│ │
│ └──► http.lua ─► resty.http (OpenResty)
│ └► socket.http (plain Lua)
▼
db.lua ──► lapis.db (OpenResty) OR pgmoon (plain Lua)
│
▼
PostgreSQL (pgvector if present, REAL[] otherwise)
| Module | Role |
|---|---|
luamemo.init |
Public entry point. setup(), re-exports, start_background_jobs(). |
luamemo.store |
All SQL. Write / get / search / recent / update / delete / dedup / summary replacement. |
luamemo.db |
Portable PostgreSQL adapter. Delegates to lapis.db under OpenResty; falls back to pgmoon in plain Lua 5.1+. All other library modules go through this layer — no direct lapis.db dependency. |
luamemo.http |
Portable HTTP client. Uses resty.http (non-blocking cosockets) under OpenResty; falls back to socket.http / ssl.https (LuaSocket) in plain Lua 5.1+. Used by embedder adapters, rerankers, and execute_with_secret. |
luamemo.embed |
Embedder dispatcher. Picks in-process embedder or HTTP adapter. |
luamemo.embedders.hash |
Pure-Lua feature-hashing embedder. Zero deps. |
luamemo.adapters.* |
HTTP embedder adapters (Ollama, OpenAI, Voyage, Cohere, generic). |
luamemo.routes |
Lapis route factory. 7 endpoints under one prefix. |
luamemo.web |
Server-rendered admin browser. Pure-Lua HTML, double-submit-cookie CSRF. |
luamemo.secrets |
AES-256-CBC encrypted secret storage + execute_with_secret. Requires master_key_* config. |
luamemo.kg |
Knowledge-graph fact store (lm_kg_facts table). |
luamemo.summarizer |
Selection + adapter dispatch + transactional replacement. |
luamemo.summarizers.* |
LLM adapters for summarisation (noop, ollama, openai). |
luamemo.rerankers.* |
Reranker adapters (noop, ollama, openai, cross_encoder). |
luamemo.schema.sql |
Fresh-install schema for the pgvector backend. |
luamemo.schema_bruteforce.sql |
Fresh-install schema for the brute-force backend (no extension). |
luamemo.migrations/ |
Idempotent ALTERs for live DBs. |
cli/memo |
Bash CLI; calls the HTTP API with bearer token. |
mcp/server.lua |
Pure-Lua stdio MCP server. 11 tools. |
- Caller hits
POST /api/memory/write(ormemory.write{...}directly). routes.luarunsbefore_request(CSRF/rate-limit hook) thenauth_fn.store.writevalidates inputs, callsembed.embed(title."\n".body)to get the vector.- If
dedup_enabled,store.writeruns a top-1 similarity search in the same scope. Abovededup_thresholdit merges the existing row instead of inserting. - INSERT (or UPDATE on merge) returns the row.
- Caller hits
GET /api/memory/search?q=.... embed.embed(q)produces a query vector.store.searchissues a single SQL query that:- selects candidates (vector ANN if pgvector available, FTS-ranked scope-scoped fetch otherwise),
- normalises vector and FTS scores per batch,
- blends them by
hybrid_weights, - multiplies by
importance · exp(-decay_rate · days_since_updated), - sorts and limits.
- Rows returned in a backend-agnostic shape so HTTP / Web UI / CLI / MCP are unaffected by which backend ran.
auto(default) — probepg_extensionatsetup()time and pickpgvectorif the extension is installed, otherwisebruteforce.pgvector—embedding vector(N)column, HNSW index,ORDER BY embedding <=> $vecfor ANN.bruteforce—embedding REAL[]column, no extension. SQL pre-filters byscope/kind/FTS intobruteforce_candidate_limitrows; Lua computes cosine over the candidate set.
See “Backends & cost” below for the trade-off.
Default (zero infra): any PostgreSQL 15+. No extension required.
psql -U postgres -d mydb -f luamemo/schema_bruteforce.sqlFaster path (when you can install extensions): the official
pgvector/pgvector:pg15 image, the postgresql-15-pgvector Debian/Ubuntu
package, or a managed Postgres that allows CREATE EXTENSION vector.
psql -U postgres -d mydb -f luamemo/schema.sqlThe library auto-detects which is in use — no config change.
Local-first (recommended for getting started): no service required.
embedder_local = "hash" -- pure Lua, in-processSee examples/local_hash_embedder.md.
Or one of the HTTP options:
- Ollama (local, semantic):
ollama pull nomic-embed-text— see examples/ollama_embedder.md. - OpenAI:
text-embedding-3-smallwith your API key. - Bundled Python sidecar (
examples/python_embedder/):cd examples/python_embedder && docker build -t memo-embedder . docker run -p 8000:8000 memo-embedder
local lapis = require("lapis")
local memory = require("luamemo")
local app = lapis.Application()
memory.setup({
-- Local-first: zero external services
embedder_local = "hash",
embed_dim = 384,
default_scope = "repo:my-app",
auth_fn = function(self)
return self.current_user and self.current_user.is_admin
end,
})
memory.routes.register(app, { prefix = "/api/memory" })
return appluamemo runs in any Lua 5.1+ runtime — no OpenResty or Lapis required.
The luamemo.db module detects the absence of ngx and falls back to
a direct pgmoon connection. Configure PostgreSQL via pg_* keys or the
standard PG* environment variables.
local memory = require("luamemo")
memory.setup({
embedder_local = "hash",
embed_dim = 384,
default_scope = "repo:my-app",
auth_fn = function() return true end, -- no HTTP auth context outside Lapis
-- PostgreSQL connection — ignored under OpenResty (lapis.db manages it)
pg_host = "127.0.0.1",
pg_port = 5432,
pg_database = "mydb",
pg_user = "myuser",
pg_password = "mypass",
})
-- Now use memory.write / memory.search / memory.recent directly
memory.write{
scope = "repo:my-app",
title = "First note from plain Lua",
body = "Works without a web server.",
}Alternatively, set the standard PGHOST / PGDATABASE / PGUSER /
PGPASSWORD environment variables and omit the pg_* keys entirely.
That's it. You now have:
| Method | Path | Purpose |
|---|---|---|
| POST | /api/memory/write |
Insert a memory |
| GET | /api/memory/search?q=... |
Hybrid search |
| GET | /api/memory/recent |
Recent memories |
| GET | /api/memory/:id |
Fetch one |
| POST | /api/memory/:id/update |
Update (re-embeds if changed) |
| POST | /api/memory/:id/delete |
Hard delete |
luamemo can store encrypted API keys and tokens server-side and
inject them into HTTP requests without ever exposing the raw value to the
LLM. This is the lm_secrets module, built on the execute_with_secret
design principle.
- Secrets are stored AES-256-CBC encrypted in a local JSON file on the server. No database table is required — the file is the entire store.
- The master key is never persisted on disk. It is held in memory only while the server process is running (loaded from a Docker secret, env var, or config at startup).
list_secrets/secret_listreturns names and metadata only — no values.execute_with_secret/secret_executesubstitutes{secret}in URL, headers, and body server-side, makes the HTTP request, and returns only the response body. The decrypted value never crosses the LLM context boundary.- There is no
get_secrettool — raw values cannot be retrieved through the API.
1. Generate a master key
openssl rand -hex 32 # → 64-char hex string2. Choose a writable path for the secrets file
Pick any path that is writable by the OpenResty process and that you want
to persist across restarts. The file is created automatically on the first
store() call.
/run/secrets/lm_secrets.json # Docker secret volume mount
/app/data/lm_secrets.json # App data directory (mount a volume here)
/tmp/lm_secrets.json # Ephemeral (dev / testing only)
3. Configure secrets_file and the key source
Both secrets_file and a master key must be set for the feature to activate.
If either is absent, secrets are disabled — all other luamemo features
continue to work normally.
-- Recommended (production): Docker secrets for both the key and the file path
memory.setup({
embedder_local = "hash",
auth_fn = ...,
secrets_file = "/app/data/lm_secrets.json", -- writable path; file is auto-created
master_key_path = "/run/secrets/lm_master_key", -- file containing the 64-hex-char key
})
-- Option B — environment variable for the key
memory.setup({
embedder_local = "hash",
auth_fn = ...,
secrets_file = "/app/data/lm_secrets.json",
master_key_env = "LM_MASTER_KEY", -- name of the env var
})
-- Option C — explicit key value (CI / dev only; never commit production keys)
memory.setup({
embedder_local = "hash",
auth_fn = ...,
secrets_file = "/tmp/lm_secrets.json",
master_key = "abcdef0123456789...", -- 64 hex chars
})Key resolution order: master_key_path → master_key_env → master_key.
4. Persist the file across container restarts (Docker)
Mount the directory containing the secrets file as a named volume so it survives container rebuilds:
# docker-compose.yml
services:
app:
volumes:
- lm_secrets_data:/app/data # persists lm_secrets.json
volumes:
lm_secrets_data:
⚠️ If the file is not on a persistent volume, all stored secrets are lost when the container is removed. Back up the file or use a bind-mount to a host path if you need durability without a named volume.
No migration needed — there is no database table. memo migrate output
does not include any lm_secrets DDL.
local memory = require("luamemo")
-- Store a secret (value is encrypted before writing to the JSON file)
memory.secrets.store("openai-key", "sk-...", "OpenAI API key")
-- List secrets (names and metadata only — values never returned)
local list = memory.secrets.list()
-- Execute an HTTP request with {secret} substituted server-side
local body, err = memory.secrets.execute_with_secret("openai-key", {
url = "https://api.openai.com/v1/models",
method = "GET",
headers = { Authorization = "Bearer {secret}" },
})
-- Delete a secret
memory.secrets.delete("openai-key")
-- Check if secrets are enabled (secrets_file + master key both configured)
if memory.secrets.enabled() then ... endThese are not terminal commands. You type them in the chat window of your AI assistant (Claude Desktop, Copilot Agent Mode, Cursor, Continue.dev, etc.) while the MCP server is connected. The assistant recognises them as tool calls and executes them against the running luamemo HTTP API.
Three tools are safe to call from the chat window (no raw values involved):
| Tool | Safe in chat? | Description |
|---|---|---|
secret_list |
✅ | List stored secrets (names + metadata only — no values) |
secret_delete(name) |
✅ | Permanently delete a secret |
secret_execute(name, url, ...) |
✅ | HTTP request with {secret} substituted server-side |
secret_store |
❌ | Use memo secret-store from terminal instead (see below) |
secret_store must never be called from the chat window. The value would enter the LLM context and could be logged by the AI provider. Store secrets from the terminal only (see the section below).
⚠️ Never type a secret value in the chat window. The chat is processed by the LLM (and potentially logged by the AI provider). Use the terminal instead.
Store secrets from the terminal using memo secret-store. It prompts for the
value with no echo — the key never appears on screen, in shell history, or
in the chat context:
# Prompted, no echo — safest
export MEMO_URL=https://your-app.example.com/api/memory
export MEMO_TOKEN=your-bearer-token # if auth is enabled
memo secret-store openai-key --desc "OpenAI API key"
# Secret value for "openai-key": ████████ (hidden, no echo)Or read the value from a file (e.g. a password manager export):
memo secret-store openai-key --file ~/.secrets/openai-key.txt --desc "OpenAI API key"Or call the HTTP API directly from the terminal (value stays in your terminal, never in chat):
curl -sS -X POST "$MEMO_URL/secrets" \
-H "Authorization: Bearer $MEMO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"openai-key","value":"sk-proj-...","description":"OpenAI API key"}'The value is encrypted server-side immediately. It can never be retrieved — not by you, not by the agent, not through any API. If you lose it you must store a new one.
secret_list()
Returns names, descriptions, and timestamps — no values.
secret_execute("openai-key",
url = "https://api.openai.com/v1/models",
method = "GET",
headers = { Authorization = "Bearer {secret}" })
Write {secret} anywhere in url, header values, or body. The server substitutes
the decrypted value before making the request. Only the HTTP response body is returned
to the agent — the raw key never enters the chat context.
Call OpenAI chat completions:
secret_execute("openai-key",
url = "https://api.openai.com/v1/chat/completions",
method = "POST",
headers = { Authorization = "Bearer {secret}", Content-Type = "application/json" },
body = '{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}')
Send a message via Slack webhook:
secret_execute("slack-webhook",
url = "https://hooks.slack.com/services/{secret}",
method = "POST",
headers = { Content-Type = "application/json" },
body = '{"text":"Deploy complete"}')
Query GitHub API with a personal access token:
secret_execute("github-token",
url = "https://api.github.com/repos/myorg/myrepo/issues",
headers = { Authorization = "Bearer {secret}", Accept = "application/vnd.github+json" })
Send email via SendGrid:
secret_execute("sendgrid-key",
url = "https://api.sendgrid.com/v3/mail/send",
method = "POST",
headers = { Authorization = "Bearer {secret}", Content-Type = "application/json" },
body = '{"personalizations":[{"to":[{"email":"you@example.com"}]}],"from":{"email":"noreply@example.com"},"subject":"Hello","content":[{"type":"text/plain","value":"Hi!"}]}')
secret_delete("openai-key")
Permanently deletes from the secrets file. Cannot be undone.
| Method | Path | Description |
|---|---|---|
| GET | /api/memory/secrets |
List secrets (no values) |
| POST | /api/memory/secrets |
Create / update a secret (name, value, optional description) |
| POST | /api/memory/secrets/:name/delete |
Delete a secret |
| POST | /api/memory/secrets/:name/execute |
Execute HTTP request with secret substituted |
local memory = require("luamemo")
memory.write{
scope = "repo:my-app",
kind = "decision",
title = "Use FTS+pgvector hybrid for memory",
body = "Picked hybrid over pure vector because lexical recall ...",
tags = { "architecture", "memory" },
metadata = { author = "kaio", phase = 5 },
}
local results = memory.search{
query = "how did we handle CCA half-year rule",
scope = "repo:my-app",
limit = 10,
hybrid_weights = { vector = 0.7, fts = 0.3 },
}
local one = memory.get(42)
local list = memory.recent{ scope = "repo:my-app", limit = 20 }
memory.update(42, { body = "..." })
memory.delete(42)A self-contained, server-rendered admin browser at /memory/ui (mount
prefix configurable). List, search, scope/kind filter, inline edit, and
delete with double-submit-cookie CSRF. Pure-Lua HTML rendering, zero
template-engine dependency on the host app:
memory.routes.register(app, { prefix = "/api/memory" })
memory.web.register(app, { prefix = "/memory/ui" })See examples/web_ui.md for the full QA recipe.
export MEMO_URL=https://my-app.example.com/api/memory
export MEMO_TOKEN=your-bearer-token
memo write --scope repo:my-app --kind decision \
--title "Switched embedder to Ollama" \
--body "Local-only, removes OpenAI dep, ~50 ms/call."
memo search "embedder choice"
memo recent --scope repo:my-app --limit 5
memo get 42
memo delete 42Any HTTP service that accepts:
POST /embed
Content-Type: application/json
{ "text": "string to embed" }and returns:
{ "vector": [0.012, -0.345, ...] }is a valid embedder. The vector length must match embed_dim from
setup().
Adapters in luamemo.adapters.* translate this contract to
provider-specific formats:
| Adapter | Status | Notes |
|---|---|---|
generic |
working | The contract above; for custom embedders |
ollama |
working | Local, free, semantic |
openai |
working | text-embedding-3-small / -large |
voyage |
working | Anthropic's officially recommended provider |
cohere |
working | embed-english-v3.0 / embed-multilingual-v3.0 |
anthropic |
template only | Anthropic has no embeddings API yet — use Voyage |
deepseek |
template only | DeepSeek has no embeddings API yet |
hash |
working (in-process, not HTTP) | Pure Lua, lexical only |
Two schema files ship in the repo:
| File | Backend | Postgres extension required |
|---|---|---|
luamemo/schema.sql |
pgvector |
vector (HNSW for fast ANN) |
luamemo/schema_bruteforce.sql |
bruteforce |
none |
Both define the same columns. The only differences are the type of
embedding (vector(N) vs REAL[]) and whether an HNSW index is created.
The table is opinionated but extensible via the metadata JSONB column.
The default embedding dimension is 384 (matches all-MiniLM-L6-v2 and
nomic-embed-text); change it before running the migration if you use a
different model.
luamemo ships two backends. Both are first-class — the HTTP API,
Web UI, CLI, and MCP server work identically against either.
- Storage:
embedding REAL[]column on plain Postgres. - Search: SQL pre-filters by
scope/kind/ FTS rank, returns up tobruteforce_candidate_limit(default 1000) candidate rows; Lua computes cosine and ranks them. - Pros
luarocks install luamemo+ any Postgres 15 = working install.- Same code on dev, CI, and prod.
- No extension privileges needed; works on managed Postgres that
forbids
CREATE EXTENSION.
- Cons
- Per-query cost is
O(N · D)over the candidate set, executed in the Lua VM. Comfortable up to roughly 10k–50k memories per scope at 384 dimensions on a modern CPU. - Embeddings travel over the wire from Postgres into Lua for each
search. Fine on
localhost, noticeable over a slow link. - Quality drops if the candidate cap is hit and the relevant memory
sits outside the FTS-ranked top 1000. Mitigated by always passing a
meaningful
scope(and ideally akind).
- Per-query cost is
- Storage:
embedding vector(N)column + HNSW index. - Search: native pgvector cosine ANN, FTS rank blended in SQL.
- Pros
- Sub-linear ANN; scales to millions of rows with stable latency.
- All ranking happens inside Postgres in a single query.
- Cons
- Requires the
vectorextension installed andCREATE EXTENSIONpermission. - One more dependency to keep in sync across environments.
- Requires the
Leave backend = "auto" and let the library decide at startup. Override
with backend = "pgvector" or "bruteforce" only if you want to force
the choice (e.g. lock a deployment to brute-force for portability even
when the extension is available).
The brute-force cons above have a roadmap. Each item below is intended to stay pure-Lua in the host process and add zero runtime services:
- Smarter pre-filter. Today the candidate set is FTS-ranked then capped. A future revision will combine FTS + tag-prefix + recency to push more relevant rows into the top-1000 bucket before Lua sees them.
- Pure-Lua ANN index (
HNSWorIVF) over theREAL[]column, built and maintained in Lua. Lets the brute backend keep working beyond the 50k-row crossover without taking on pgvector. - SQLite + sqlite-vec adapter. Optional second persistence layer for
single-user / desktop / MCP-only deployments where Postgres itself is
overkill. Not a replacement — a sibling backend selected the same way
via
backend = "sqlite_vec". - Embedder caching. A small in-process LRU on
embed()so repeated search-then-write loops (common in agent flows) don’t re-embed the same query / body.
None of these change the public API. Pick the simplest backend that fits
today — the upgrade path is purely a setup() switch.
Every row carries two numeric weights used at search time:
| Column | Range | Default | Purpose |
|---|---|---|---|
importance |
0..10 | 1.0 | Static multiplier on the hybrid score. |
decay_rate |
0..1 per day | 0.0 | Exponential time-decay; 0 disables it. |
The effective ranking weight applied to each candidate is:
weight = importance * exp(-decay_rate * days_since_updated)
score = (vector_weight * vec_score + fts_weight * fts_score) * weight
Defaults preserve the original behaviour: every row has weight 1.0 and ordering is pure hybrid similarity.
Write a high-importance, slowly-decaying memory:
memory.write{
title = "Production DB password rotation policy",
body = "...",
importance = 8.0, -- pin near the top for a long time
decay_rate = 0.005, -- ~half-life of 138 days
}Write a session-scratch memory that fades within days:
memory.write{
scope = "session:abc",
title = "Working hypothesis: cache invalidation off-by-one",
body = "...",
importance = 1.0,
decay_rate = 0.5, -- ~half-life of 1.4 days
}App-wide defaults are configurable via setup():
memory.setup({
-- ...
default_importance = 1.0,
default_decay_rate = 0.0,
})For debugging, pass ignore_decay = true (Lua) or ?ignore_decay=1 (HTTP)
to search to bypass the multiplier and inspect raw hybrid scores.
See examples/decay_importance.md for an end-to-end recipe.
A pure-Lua eval harness against
LongMemEval
lives in eval/. Results are on the _s (short-session)
corpus, bruteforce backend, default hybrid weights (vector=0.7, fts=0.3).
| Embedder | n | R@1 | R@5 | R@10 | R@20 | MRR |
|---|---|---|---|---|---|---|
| hash (pure Lua, in-process) | 200 | ~40% | ~60% | ~70% | ~80% | ~0.50 |
| nomic-embed-text 768d (Ollama) | 200 | 62.0% | 81.5% | 87.5% | 92.5% | 0.706 |
| bge-m3 1024d (TEI sidecar) | 500 | 85.2% | 96.0% | 97.8% | 99.4% | 0.900 |
MemPalace reports 96.6% R@5 on LongMemEval-S using a custom LLM-summarisation pipeline. The bge-m3 result above (96.0%) is 0.6 pp behind MemPalace — with no LLM summarisation, no training data, and single-stage retrieval.
The bge-m3 result requires a GPU sidecar (see eval/sidecars/tei.md)
but no other code or schema changes — just swap the embedder in setup().
See eval/results/longmemeval.md for full
phase-by-phase details, reproduce commands, and the weight-sweep analysis.
Quick start:
# zero-deps benchmark (hash embedder, no GPU needed)
PGHOST=127.0.0.1 PGDATABASE=lm_bruteforce_test \
lua5.1 eval/longmemeval_run.lua --embedder hash \
--corpus eval/data/longmemeval_s.json --n 50 \
--out eval/results/smoke.json
# GPU benchmark (requires TEI sidecars — see eval/sidecars/)
PGHOST=127.0.0.1 PGDATABASE=lm_bruteforce_test \
TEI_URL=http://127.0.0.1:8081/embed TEI_DIM=1024 \
lua5.1 eval/longmemeval_run.lua --embedder tei \
--corpus eval/data/longmemeval_s.json \
--out eval/results/my_run.jsonAI coding agents lose context constantly:
- Chat session JSON files exceed V8's
kMaxLengthand crash the editor. - Compaction summaries drop critical decisions.
- A new device or new session = total amnesia.
- Cloud-only memory tools don't survive offline work or org policies.
luamemo makes the chat transcript disposable. The agent writes
durable summaries / decisions / facts to your own Postgres, then searches
them on demand. Survives crashes, devices, and editor bugs.
MIT. See LICENSE.