AI Memory — Long-term Agent Memory
- Area: Intelligence
- Path:
services/ai/memory - Kind: Self-hosted episodic memory + semantic recall (per user/tenant)
- Status: v0.0.3 —
#007kdb store swap shipped 2026-06-22. ProductionEpisodeStore(internal/store/kdb_store.go) persists metadata over kdb-pgwire (SQL) + embeddingsANN in kdb-vector;[store] backend="kdb". Homologated against real Postgres 17 (faithful pgwire) + go-vector InMemory on dev-linux-id. Previous:#008re-scoped + shipped 2026-05-11.ServiceEmbedderspeaks the real `servicesaiembedv1embedtextHTTP API;[recall.backend] = "service"enables semantic recall;InlineEmbedder` remains as the deterministic test fallback.
Role in the stack
memory is the foundation for persistent agents. Without a memory service, every product reinvents continuity (Kode CURRENT.md, Kortex indexing, agent ad-hoc state) and lose context across sessions. This sector fills that gap.
It is the Koder analog of Mem0MemGPTLetta and Anthropic's Memory Tool — self-hosted, tenant-isolated, integrated with services/ai/embed for semantic recall and infra/data/kdb (pgwire SQL for metadata + kdb-vector for the ANN index) for storage.
Features (v1 target)
- Episodic write/read API
- Semantic recall (top-K + score)
- Tenant isolation (handler-enforced)
- Forgetting policies: TTL, decay, explicit erase, GDPR sweep
- Pluggable embedding backend (inline ONNX or
services/ai/embed)
Primary couplings
| Consumer | Relationship |
|---|---|
services/ai/agents |
Reads pre-prompt; writes post-tool-call |
services/ai/kode |
Replaces ad-hoc session continuity files |
services/ai/runtime |
Optional middleware read-through |
services/ai/embed |
Vector backbone |
infra/data/kdb (sdk/go-pgwire) |
Episode metadata / row store (pgwire SQL) |
infra/data/kdb (sdk/go-vector) |
Embedding + ANN index |
RFC and bootstrap
- RFC:
memory-RFC-001-foundations.kmd— accepted 2026-05-09 - Bootstrap ticket:
services/ai/backlog/done/129-memory-bootstrap.md - Implementation tickets:
done/:001(OpenAPI),002(skeleton),003(tenant scope + audit),004(embed integration),005(forgettingTTLdecayeraseGDPR),006(real JWT viaengines/sdk/go/auth.JWKSValidator),008(re-scoped —ServiceEmbedderHTTP client toservices/ai/embedshipped 2026-05-11; BGE local ONNX moved to#012),009(decay auto-prune + quarantine),010(bench harness inline+memory)done/:007(kdb store swap —KDBStoreover kdb-pgwire + kdb-vector, shipped 2026-06-22; design corrected from the non-existent "kdb-doc" to kdb-next pgwire per RFC-001)pending/:011(bench at 100K against kdb+BGE — now unblocked:007+008done),012(BGE local ONNX embedder — split from#008, blocked onservices/ai/embed#009ONNX runtime),013(kdb store slice 2 — live kdb-gateway vector e2e + hyperscale TTL/decay SQL pushdown + per-request tenant)
Recent changes
- 2026-06-24 (memory#014 — DEPLOYED; real-embedding recall loop CLOSED end-to-end) —
koder-memoryis running in the fleet (LXCembed@10.0.1.146:18080, systemd, co-located withservices/ai/embed):[store] backend="memory"(in-process) +[recall] backend="service"→ the real embed service + Koder ID JWT auth. Proof: stored 3 pt-BR episodes, recalledq="encontro para discutir verba de publicidade"→ top-1 = the "orçamento de marketing" episode, score 0.7399, semantically above the unrelated catdatabase episodes (threshold-filtered). The whole AImemory product now works end-to-end on real multilingual embeddings (/k-arch-chosen service path: memory→embed HTTP, not in-process). Deploy-as-code inbackend/deploy/. Production gaps (memory#014): embed-token refresh (the ServiceEmbedder uses a static token that expires), persistent[store]="kdb"(needs a deployed kdb-gateway; the store is already bench-validated #007/#013), and the service-account-vs-user identity model. Caps the embed arc (#017→#021) — real embeddings produced, served, authed, cached, and consumed by memory. - 2026-06-24 (kdb#808 Slice 2 — vector + meta cross-request cache; latency target CRUSHED) — Slice 1 proved adjacency wasn't the bottleneck; the surviving per-node
read_vector+is_tombstonedsled reads were. Slice 2 caches both across requests. Design (Quality > Speed, DRY): generalised the #059 slab-LRU intoByteLru<K,V>+ aWeighabletrait — three caches (adjacency / vector / meta) on one proven machine; the 15 existing cache tests pass verbatim.PersistentHnswIndexgainedvec_cache+meta_cacheArcs (warm via Slice 1's registry);read_vectoris L1→L2→KV,is_tombstonedis metacache→KV; write-through invalidation on insertdeletewipe. Re-benched: 2K p50 109 → 6.7 ms (~16×); 100K acceptance p50 19.6 ms (5× under) / p99 45.0 ms (11× under) — RFC target (p50<100msp99<500ms) MET at both 2K and 100K, closing kdb#808 + memory#011. Throughput 9.7 → 133.8 qs (2K) / 47.6 qs (100K); populate 15 → 73 eps (2K), 28 ep/s at 100K scale. Homologated dev-linux-kdb: 91 kdb-vector lib tests (3 new cache + `slice2nodecachescoherentafterwarm`) + the gateway coherence regressions. #807 (filtered-HNSW) is now an optional compounding follow-up, not a blocker. - 2026-06-24 (kdb#808 Slice 1 — warm-handle registry; perf reframe) — The kdb-gateway vector service was stateless per RPC, rebuilding a fresh
PersistentHnswIndex(and a fresh empty adjacency cache) every call, so the #059 cache never survived a request. Slice 1 adds a gateway-residentIndexRegistrythat keeps one handle warm per index identity (built once under a write lock → exactly one sharedArc<HnswCache>per key, so the existing write-through invalidation stays coherent). Homologated on dev-linux-kdb: 3 unit tests + a 2-case coherence regression (tests/regression/vector_warm_cache_coherence_808.rs— post-warm insertsdeletes must still be observed). Re-benched: p50 109 → 105.7 ms (only ≈3 %) — decisive finding that adjacency was NOT the bottleneck: at ef=128, ~256 per-noderead_vector+is_tombstonedsled readsquery survive. Real latency levers are kdb#808 Slice 2 (cross-request vector+meta cache, enabled by this registry) + #807 (filtered-HNSW). Also found the bench SQL side can't run on kdb-pgwire (ANY($1::text[])encode gap → kdb#809); uses real PG17. - 2026-06-23 (#011 bench harness — kdb store benched; perf finding) —
bench/main.gogained--store=memory|kdb+--embedder=inline|service+make bench-100k. Benching the real kdb-vector gateway (RELEASE, RTT 0.058 ms) at 2K gave p50 109 ms (✗ the < 100 ms RFC target). Finding: the #007 store swap is functionally correct + storage-scalable but latency-bound by per-request sled node reads —PersistentHnswIndex::searchrebuilds itsVectorCacheper call (no cross-request cache), amplified by the #806 filter over-fetch (ef=128). The 100K acceptance run isblocked-bykdb#808 (in-memory index/node cache) + kdb#807 (filtered-HNSW). Baseline inperf-baseline.md. - 2026-06-23 (#013 criterion 3 — per-request tenant; #013 COMPLETE) — One memory instance now serves many tenants. The authenticated tenant flows
auth.Identity.TenantID→ aservermiddleware (tenantContextMW) →store.ContextWithTenant(ctx)→KDBStore.tenantFor(ctx), scoping every SQL query (request path + sweeper scans) and falling back to the configured[store] tenantfor non-request callers.Episode.Tenantis populated on read;ListTenants+ theTenantListercapability let the TTLdecay sweepers iterate every tenant (no leak). The store imports no auth (the composition root bridges, avoiding an auth→handler→store cycle). Integration-tested vs real PG17 (cross-tenant GetSearch/Delete isolation; ListTenants). #013 complete (all 3 criteria). Next: #011 bench @100K. - 2026-06-23 (#013 criterion 2 — TTL/decay SQL pushdown) — The TTL + decay sweepers now scan only candidates instead of pulling every tenant row each pass.
KDBStoregainedListExpired(WHERE expires_at < now) andListDecayCandidates(flagged ORGREATEST(last_accessed, created_at) < cutoffOR overridden-user — provably complete; the sweeper re-evaluates each exactly). Thepolicysweepers fast-path onto the optionalExpiredListerDecayCandidateListerseams (MemoryStore keeps the full-scan fallback). Removes the only app-side full-table scan (hyperscale-first). Integration-tested vs real PG17 (77). Remaining #013: per-request tenant threading. - 2026-06-22 (#013 live kdb-gateway vector e2e —
/k-go, slice 2 partial) — Ran theinternal/storeintegration suite against a live kdb-gateway (vector gRPC10.0.1.227:50061) for the first time, metadata→real PG17. The test is parameterized (MEMORY_KDB_TEST_VECTOR_GATEWAY→vector.Open, else InMemory). The live run surfaced + fixed a real gateway bug (kdb#806):vector_service.rs::nearestapplied the metadata filter after the top-k truncation, so a usertenant-scopedSearchreturned fewer than k results when the globally-nearest vectors belonged to other users — recall silently degraded for every metadata-scoped vector search. Fix = oversample (k*10, min 128) before filtering, then truncate; Rust regression test + registry entry; HNSW predicate pushdown for highly-selective filters tracked askdb#807. memory#013 e2e now *5 green*against the real gateway. Remaining #013: hyperscale TTL/decay SQL pushdown + per-request tenant. - 2026-06-22 (#007 kdb store swap —
/k-go memory, slice 1) — Shipped the productionEpisodeStorebacked by kdb-next (RFC-001 unified data plane).internal/store/kdb_store.gosplits each episode: metadatacontentmutable lifecycle fields + point-getdelete-by-user → kdb-pgwire (`sdkgo-pgwire, pgx/v5) SQL tablememory_episodes; embeddings + ANN → **kdb-vector** (sdkgo-vector), joined by ULID↔fnv64a(ULID)`. Design correction: the ticket named "kdb-doc" (no such component) — the canonical row store is kdb-next over pgwire. The vector store is ANN-only, which is why the two-backend split is required (it can't serve GetDeleteAllscan). AddedListAllso the TTL+decay sweepers keep working;[store] backend="kdb"config;buildStorewiring (vector.Open+Migrateon boot); `cmdkoder-memory-migrateone-shot loader. **Homologated on dev-linux-id (Go 1.25, PG17.10):** build+vet+unit green; integration suite green against **real Postgres 17 + go-vector InMemory** (round-trip+bump, user-scoping, search ranking/threshold/topK + no-recall-bump, delete idempotency, DeleteAll, ListAll). Found+fixed a latent compile break insdk/go-pgwire(BulkLoader.source *pgx.CopyFromRows— a func used as a type in pgx v5; forensickdb#805). Residuals: live kdb-gateway vector e2e + hyperscale TTL pushdown →#013; bench @100K →#011` (unblocked). - 2026-05-11 (#008 re-scope + ship) —
ServiceEmbedderrewritten from a 3-line stub returningErrEmbedServiceUnavailableto a full HTTP client ofservices/ai/embed/v1/embed/text(~165 lines). Options pattern:WithEmbedderBearer,WithEmbedderIntent,WithEmbedderModel,WithEmbedderTimeout. Dim verification on every response prevents silent contract drift.[recall.backend] = "service"now consumes the new config:embed_url,embed_token,embed_model,embed_dim,embed_timeout_ms. 10 tests SE1-SE10 (happy, 503, 401, bearer, dim mismatch, count mismatch, empty endpoint, empty inputs, model override, timeout). InlineEmbedder retained as test/bench fallback. Closes the AI cross-sector loop: memory now joins cache#003semantic + embed#007HTTP cache as a consumer of an AI sibling sector. Original BGE-local-ONNX path split off as#012(blocked-by embed#009).
Self-hosted-first analysis (5 gates)
| Gate | Status | Notes |
|---|---|---|
| G1 Feature parity | partial | Episodic + semantic + 4 forgetting policies (TTLdecayeraseGDPR) + decay auto-prune w quarantine; reflection not yet |
| G2 Performance | partial | Inline+memory @ 10 K: p50 5.3 ms, p99 10.5 ms (≈10× under target); kdb store shipped (007 done); 100 K + kdb-vector + BGE bench pending 011 |
| G3 Stability | pending | Pre-MVP |
| G4 Capability | partial | Episodic + semantic + forgetting (incl. auto-prune); no fancy reflection yet |
| G5 Critical-path readiness | pending | Pre-MVP; agents/kode can adopt once v1 ships |
Performance baseline
Latest bench numbers (and per-metric trend) live in registries/perf-baseline.md. Re-run via make bench-full from services/ai/memory/backend/.