Automatic L0 conversation capture is enabled by default (capture.enabled defaults to true); agents matching patterns in capture.excludeAgents are excluded from capture, recall, and pipeline scheduling entirely.[1] At the L0 stage, auto-capture.ts extracts new messages from each agent_end event, sanitizes them, and passes them to the pipeline — no remote call happens at this stage; messages are buffered locally per-session.[2] L0 capture filters out MMD context blocks injected by the offload mechanism to prevent storing compression intermediates as memories.[3]
src/core/conversation/l0-recorder.ts implements the L0 raw conversation capture layer, writing sanitized messages to ~/.openclaw/memory-tdai/conversations/YYYY-MM-DD.jsonl — one file per day, all sessions merged into it.[4] JSONL format is used with one message per line so the output is flat and easy to grep or stream; the sessionKey is stored as a field in each JSONL line, not in the filename.[4] The L0MessageRecord interface defines the flat JSONL record shape: sessionKey, sessionId, recordedAt (ISO timestamp), id, role, content, and timestamp (epoch ms).[4]
generateMessageId() in src/core/conversation/l0-recorder.ts produces short IDs of the form msg_<epoch>_<6-hex-chars> using crypto.randomBytes(3); the id field is later used by L1 for source_message_ids tracking.[4] L0 vector record IDs generated by auto-capture.ts include the session key, current timestamp, per-message index, and 3 random bytes of hex to ensure uniqueness across multiple messages in the same capture round.[5]
auto-capture.ts accepts an originalUserText parameter — the clean user prompt captured at before_prompt_build time before prependContext was injected — alongside originalUserMessageCount to locate the exact message position, preventing memory-injected user text from being written to L0.[5] After content replacement, recordConversation sanitizes all messages with sanitizeText() and additionally strips fenced code blocks from assistant replies using stripCodeBlocks() to reduce embedding noise.[4] recordConversation returns an empty array and skips file I/O when no new user/assistant messages remain after position-slicing, timestamp-cursor filtering, and noise filtering.[4] When the position slice is unavailable and all messages pass the timestamp filter with more than 8 messages present, recordConversation emits a warning about possible timestamp drift after a gateway restart.[4]
auto-capture.ts prevents duplicate L0 records from concurrent agent_end events by performing L0 recording and checkpoint cursor advance atomically inside checkpoint.captureAtomically(), which holds a file lock across the entire read-cursor → recordConversation → advance-cursor sequence.[5] When no per-session checkpoint cursor exists, pluginStartTimestamp is used as the floor cursor to prevent the first agent_end from dumping all session history into L0.[5] A checkpoint cursor marks the timestamp of the last successfully captured message in a session, acting as a lower bound so that each capture round in auto-capture.ts processes only new messages and avoids re-recording earlier turns.
auto-capture.ts supports two L0 vector indexing paths: Path A (stores with supportsDeferredEmbedding === true, e.g. SQLite) writes metadata and FTS immediately and fires a background task for embedding to avoid blocking agent_end with 2–3 s embedding calls; Path B (VDB/remote stores) embeds synchronously then upserts with the embedding in one call.[5] When using a server-side embedding service (embeddingService.getDimensions() === 0, i.e. NoopEmbeddingService), local embedding is skipped during L0 vector indexing.[5] Embedding failure for an individual L0 message is non-fatal: a warning is logged and the record is still written to the store with metadata only (no embedding vector).[5]
The bgTaskRegistry parameter (a Set<Promise<void>>) lets TdaiCore.destroy() await all in-flight background L0 embedding tasks before closing vectorStore/embeddingService, preventing use of an already-closed DB connection; the parameter is optional for backwards compatibility.[5]
The AutoCaptureResult interface exported from auto-capture.ts exposes schedulerNotified (bool), l0RecordedCount (number of messages written to L0), l0VectorsWritten (number of L0 message vectors written), and filteredMessages (messages for L1 immediate use — see L1 extraction & dedup).[5]
Sources