perf: async fact extraction for remember() and remember_document() #19
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
remember_document()withmode='full'takes ~3.4s for a tiny 3-turn session. A realistic LongMemEval session (~10-15 turns, ~1000 words) takes ~8-15s. With 53 sessions per benchmark item and 500 items, full mode ingestion would take ~74 hours — completely impractical.The bottleneck is
extract_facts()— a synchronous Haiku API call that runs on every chunk before returning. The caller blocks waiting for fact extraction before the next chunk can be stored.Timing breakdown (v5.1.1-beta, localhost)
Proposed solution — async fact extraction
Decouple ingestion from enrichment:
remember()andremember_document()store chunks immediately (embed + write) and return keys — same speed asforce=Trueextract_facts()+ contradiction check asynchronouslyImpact on benchmark
With async enrichment, full mode ingestion time drops from ~74 hours to ~2 hours for 500 LongMemEval items — making the v5-full benchmark run feasible.
Benchmark evidence
Note: the 9hr figure assumes 250 chunks/item × 0.25s. A batch ingest endpoint (separate issue) would reduce this further.
Implementation notes
OmniMem already uses Valkey + Apalis for job queuing in the Mycelium architecture. The same pattern applies here — new memories could be pushed onto an enrichment queue immediately after storage, with the RSS worker pattern reused for the enrichment consumer.
Related issues
force=Trueshould be raw write (partially addresses this for benchmark use)remember_document()implementation (now in v5)Updated timing with real LongMemEval data
Still not feasible but better than the 74hr estimate (which assumed 10-15 turns per session — real sessions average 2 turns).
The blocker remains the synchronous Haiku call per chunk. Even at 2 turns per session,
extract_facts()is running synchronously before the next session can be stored.Also noted
The v5
remember_document()tool schema does not expose amodeparameter —modeis only onremember(). This meansremember_document()currently always runs in whatever theINGEST_MODEenv var is set to. For the benchmark to control full vs raw mode viaremember_document(), either:modeparameter toremember_document()(consistent withremember())INGEST_MODEenv var switching between runsBoth are valid — just needs documenting.
Benchmark runner architecture for async enrichment
The benchmark runner needs to split into three phases to work correctly with async fact extraction:
Phase 1 — Ingest (fast)
Phase 2 — Wait for enrichment queue to drain
Phase 3 — Query (after enrichment complete)
New MCP tools needed to support this
queue_status()— returns{"pending": int, "processing": int, "completed": int}. Benchmark polls this between Phase 2 and Phase 3 to know when enrichment is complete before querying.remember_document()already returnsdoc_id— cleanup can be done bydoc_idrather than by project scan, making it reliable.Why the manifest matters
Saving the ingest manifest to disk means Phase 1 and Phase 3 can run at different times — you can ingest overnight, let enrichment run, then query the next day. Supports
--resumecleanly.Proposed implementation: ENRICHMENT_BATCH_MODE
Rather than per-memory async enrichment via a queue, a simpler approach is
ENRICHMENT_BATCH_MODE:How it works
Benchmark runner flow with batch mode
Timing estimate with batch mode
This makes the v5-full benchmark feasible in a single overnight run.
queue_status() tool still needed
The runner needs to poll
queue_status()between Phase 2 and Phase 3 to know when enrichment is complete and querying will return enriched results.Update: ENRICHMENT_BATCH_MODE partially addresses this
ENRICHMENT_BATCH_MODE=truehas been implemented and solves the bulk ingestion performance problem. During the LongMemEval benchmark:remember_document()returns immediately, enrichment queuedThis makes bulk benchmark ingestion feasible (~3.6 hours vs ~32 hours synchronous).
What remains open
ENRICHMENT_BATCH_MODEis an opt-in env var for bulk use cases. Regular daily use ofremember()still hits the synchronous Haiku call (~1.2s) on every write:The remaining work in this issue is making async enrichment the default behaviour for all
remember()calls, not just whenENRICHMENT_BATCH_MODEis explicitly set. This would:remember()calls from ~1.7s to ~0.25sENRICHMENT_BATCH_MODEThe queue infrastructure is already in place — this is now a matter of making it the default path rather than opt-in.
Closing — shipped across v5.2.0/v5.2.1.
remember()andremember_document()in full mode store content raw (embed + write, fast) and return immediately withenrichment: "queued"; a backgroundEnrichmentWorkerdaemon thread pops jobs from a Valkey-backed queue (queue:enrich, survives restarts) and writes extracted facts as linked memories.ENRICHMENT_BATCH_MODE=truecollapses a whole document into one Haiku call, andqueue_status()lets benchmark runners poll until the queue drains.Two fixes landed since that are worth knowing about: v5.3.0 restored the near-duplicate guard that the async migration had dropped (re-remembering similar content was silently piling up duplicate facts), and v5.3.1 rerouted extracted facts to the knowledge namespace with inherited timestamps (#20).