Back to atlas

Pattern · Capture

Zero-LLM Capture

Durably record memory-worthy events without putting a model call on the synchronous write path.

Intent

Make capture cheap, deterministic, and available even when an extraction model is slow, unavailable, rate-limited, or unnecessary.

The problem

If every hook, message, or tool result must pass through an LLM before it can be stored, memory inherits provider latency and failure. A transient outage can erase a session from memory, and high-volume agent traces can make capture prohibitively expensive.

Skipping all processing has its own cost: raw events are noisy, private, and hard to retrieve. The pattern is therefore not “never use an LLM.” It is “do not require one to preserve the event.”

Which means the name overstates it, and the more standard term for the shape is asynchronous enrichment or a decoupled write path. The reason this page keeps a narrower name is that the standard terms describe a scheduling choice — do the expensive work later — while the load-bearing property here is an availability one: the durable write must be able to complete when no model is reachable at all. A system with async enrichment that still needs one model call before anything is persisted has adopted the scheduling and not the guarantee, which is the failure this page exists to prevent. Read the name as "zero LLM on the path that must not fail".

The pattern

Write a deterministic event envelope first:

flowchart TD
    A["Hook, message,<br/>tool result"] --> B["Validate + redact"]
    B --> C["Durable scoped event"]
    C --> D["Lexical /<br/>metadata index"]
    C --> E["Optional async<br/>extraction"]
    E --> F["Summary, fact,<br/>graph, embedding"]
    C --> G["Raw fallback<br/>recall"]
    F --> H["Derived recall"]
    D --> H
    G --> H

The synchronous path may compute IDs, hashes, timestamps, scope, file paths, and a lexical index, but it makes no model call. Expensive compression, embedding, graph extraction, and consolidation run later and remain optional.

The raw event is useful before enrichment and remains a fallback afterward. Derived records reference the event ID, and callers can observe whether enrichment is pending, failed, or complete.

Why it works

  • Capture latency and availability no longer depend on a model provider.
  • Provider outages become delayed enrichment rather than lost memory.
  • Low-value events can remain raw instead of incurring extraction cost.
  • New extractors can reprocess the retained corpus.
  • Deterministic lexical and metadata search provides a useful baseline.

Tradeoffs

Raw capture can retain secrets, huge tool outputs, and irrelevant noise. Redaction, size limits, retention, and explicit scope still belong on the synchronous path. Lexical-only recall misses paraphrases, while deferred enrichment creates a freshness window. A zero-LLM capture claim is misleading if the system cannot retrieve or inspect the event until later processing finishes.

Cost to adopt

Build: a synchronous write path with no model dependency, and an enrichment stage that runs later against the same records.

Forces elsewhere: you now store material that has not been structured yet, so retrieval must handle both enriched and raw records, and the enrichment lag becomes user-visible — something said is not yet recallable in the enriched form.

Ongoing: two representations of the same memory drift unless enrichment is idempotent and re-runnable.

Skip it if capture volume is low and the model call is reliable. The pattern buys resilience, and resilience you do not need is complexity.

Seen in the atlas

OpenClaw captures with no model call at all, and spends its effort on a problem model-based capture never has to face: 567 lines in memory-capture-sanitization.ts stripping its own message envelope — media notes, ⟦openclaw:ctx⟧ markers, reply headers, sender prefixes, timestamps — with looksLikeEnvelopeSludge() rejecting whatever is still mostly wrapper.

Redis Agent Memory Server shows the scheduling version: messages land in TTL-scoped working memory immediately, and should_extract_session_thread plus schedule_trailing_extraction defer the model call behind a trailing-edge debounce. The message is durable before anything expensive happens.

Magic Context applies the same ordering inside a single write: promoteSessionFactsDurable persists synchronously and embedPromotedFacts runs as a best-effort async pass. Durable first, enriched after.

Holographic is the minimal version — six regexes over user turns (I prefer|like|use, we decided/agreed/chose) storing the raw matching message. It also demonstrates the cost: what is stored is conversational prose rather than a normalized claim, which then degrades the contradiction detection built on top of it.

Moltis exports sanitized session transcripts into its Markdown corpus; GenericAgent archives raw sessions to an L4 layer on a 12-hour cron; agentmemory keeps a synthetic observation path on the hot loop; Claude-Mem queues hook events durably before its observer runs; and engram remains the small no-extraction baseline.

Daimon is the variant worth studying if you have already decided you need an LLM. Its extraction is a model call and cannot be anything else — but every mechanism that guards that call is stdlib code: quote verification by string match, outcome grounding by lexicon, redaction by regex, carry and dedup by term overlap, code anchors by ast.dump hashing, external checks by a gh subprocess under a 0.8-second budget.

Two of its zero-LLM passes go further and add memory the model did not produce. pin_imperatives scans user turns for hard imperatives — must, never, don't, always, forbidden — and force-pins any the model paraphrased away, on the reasoning that a "never" softened into summary prose leaves nothing to verify later. And the opt-in scar harvester drafts negative-knowledge candidates from a session by regex, dropping any hit with no real file path in its own span, on the stated principle that a scar system dies from noise rather than from a missed lesson. Both are cheap, both are auditable, and neither can hallucinate.

The recurring hazard is capturing your own output. Five systems independently built guards against it: OpenClaw's envelope sanitizer, Holographic excluding compaction handoff summaries that were being stored as facts on every context rollover, nanobot filtering its own cron: and dream: sessions, Moltis sanitizing before export, and CowAgent's distillation rules. If you capture without a model, capture cheaply enough that everything flows in — which means something must decide what does not.

Helm is the smallest instance and demonstrates a failure this page had not recorded: the problem is not only what you capture without a model, it is what you key it on. One regex on the reply path — remember that, note that, for the record, fyi — lifts the following span into a durable fact with no model call and no latency on the turn. The key is 'note-' + Date.now().toString(36).

Because every capture mints a new key, three mechanisms that exist in the same file never fire: the unique index over (kind, key) never matches, supersession can never trigger, and the evidence counter never increments. Saying "remember that I prefer X" twice with different values leaves two live, equally-confident, mutually contradictory facts. Worse, those rows are uncorroborated by construction, and the nightly pass prunes uncorroborated rows below a confidence floor — so the one thing the user stated explicitly is on the fastest path to silent deletion.

A model-based extractor gets keying for free by being asked for a subject. A zero-LLM path has to choose one deterministically, and a timestamp is not a choice — it is the absence of one. Normalizing the captured span into a slug, or routing the write through an existing key when one matches, is the work this pattern skips at its peril.

CSM is the pattern held at a scale nothing else here approaches — 46 tables and 55,000 lines in which the only outbound call is an embedding request — and it gets the keying right where Helm got it wrong: a partial unique index on pending candidates over (candidate_type, dedup_key), a unique index on (session_id, metadata->>'messageId') for transcript rows with a unique-violation handler that returns the existing row rather than failing the capture, and an md5(compressed) index on distilled summaries.

Its failure is the one after keying: naming. The operational ledger declares twenty-six event types and its single writer is a switch on the tool name that can produce seven, so decision, blocker_identified, verification_evidence and goal_achieved are schema no code path emits, and everything unrecognised becomes note with the first 200 characters of tool output as its summary. The repository's own committed front page shows the result: 5,111 events, 49 sessions, and a current-state projection reporting no goal, no phase and no blockers. Determinism buys you a capture path that cannot hallucinate; it does not decide what is worth capturing, and a classifier with a catch-all bucket will record volume in place of state. If your event vocabulary is richer than your emitters, shrink the vocabulary or write the emitters — leaving the gap open produces a store that looks well-designed and answers nothing.

Implementation checklist

  • Assign a stable event ID before acknowledging capture.
  • Validate scope, actor, timestamp, and source deterministically.
  • Redact private blocks and cap oversized payloads before persistence.
  • Make raw events searchable through metadata, exact keys, or FTS.
  • Record enrichment state and processor version separately.
  • Link every derived record back to the event.
  • Keep the enrichment queue idempotent and replayable.
  • Define retention for raw evidence independently of derived memory.

Tests to require

  • Capture succeeds with every model provider disabled.
  • A crash immediately after acknowledgement does not lose the event.
  • Duplicate hooks produce one stable event or an explicit duplicate relation.
  • Private and oversized payload policy runs before the durable write.
  • Raw recall works while enrichment is pending or failed.
  • Replaying enrichment does not duplicate derived memory.
  • Deleting an event handles queued and derived artifacts safely.