Implementation-oriented reference for the LightGrep two-file memory system.
┌──────────────────────────────────────────────────────────────┐
│ Every LLM call: │
│ ContextBuilder injects MEMORY.md into system prompt │
│ SessionManager returns messages[last_consolidated:] │
├──────────────────────────────────────────────────────────────┤
│ When prompt tokens > context_window_tokens: │
│ MemoryConsolidator.maybe_consolidate_by_tokens() │
│ → pick boundary (user-turn edge) │
│ → MemoryStore.consolidate(chunk) → LLM save_memory tool │
│ → append HISTORY.md + overwrite MEMORY.md │
│ → advance last_consolidated; repeat if still over target │
└──────────────────────────────────────────────────────────────┘
Components: MemoryStore (read/write markdown files, run LLM consolidation), MemoryConsolidator (token estimation, boundary selection, multi-round loop, per-session async locking), ContextBuilder (system prompt injection), SessionManager (JSONL persistence, last_consolidated tracking).
Session:
key(string): session identifiermessages(list[dict]): append-only; each hasrole,content,timestamp, optionaltool_calls/tool_call_id/namelast_consolidated(int): pointer to first unconsolidated message index- Persisted as JSONL (first line = metadata record, then one line per message)
MEMORY.md: Full markdown file (~few thousand tokens max). Sections: User Information, Preferences, Project Context, Important Notes. Complete replacement on each consolidation.
HISTORY.md: Append-only markdown. Each entry: [YYYY-MM-DD HH:MM] paragraph.... Designed for grep searchability with timestamps and keywords.
save_memory tool output (from LLM):
history_entry(string): timestamped paragraph for HISTORY.mdmemory_update(string): complete new MEMORY.md content
- Backend: Plain filesystem — two
.mdfiles per workspace, JSONL for session messages. - Indexing: None. MEMORY.md is loaded fully into system prompt. HISTORY.md is searched by grep.
- No vector store, no embeddings, no database.
- MEMORY.md (automatic): ContextBuilder reads file, injects as
# Memory\n## Long-term Memory\n{content}in system prompt. Every LLM call gets full facts. - HISTORY.md (on-demand): Agent uses shell grep via exec tool. Always-on memory skill teaches the agent grep patterns (
grep -i "keyword" HISTORY.md). - No ranking, no filtering — facts are always present; history search is keyword-based.
Trigger: estimated_prompt_tokens > context_window_tokens (checked pre- and post- LLM call).
Consolidation loop (up to 5 rounds):
- Estimate tokens. If under full window, skip. Target: half the window.
pick_consolidation_boundary(): find a user-turn edge that would shed enough tokens. Never splits assistant/tool-call groups.- Extract chunk:
messages[last_consolidated:boundary]. - LLM consolidation call: system="You are a memory consolidation agent", user=current MEMORY.md + formatted chunk. Forced
tool_choiceforsave_memory(fallback toautoif provider rejects forced). - Parse
save_memoryargs → appendhistory_entryto HISTORY.md → writememory_updateto MEMORY.md if changed. - Advance
last_consolidated. Re-estimate. Loop.
Direct agent writes: The agent can also edit MEMORY.md directly using file tools for immediate fact capture (e.g., "I prefer dark mode" → agent writes it immediately without waiting for consolidation).
Failure handling: After 3 consecutive LLM failures, _raw_archive() dumps formatted messages to HISTORY.md with [RAW] prefix. Counter resets. Data never lost.
- No TTL / compaction / archival: MEMORY.md is bounded by the LLM's judgment (it decides what to keep). HISTORY.md grows without bound (append-only).
- Implicit dedup: The LLM sees existing MEMORY.md + new conversation and outputs the merged result. Redundant facts are naturally deduplicated.
- Implicit conflict resolution: When new facts contradict old ones, the LLM updates the relevant section. No explicit contradiction detection.
- Session-level locking:
WeakValueDictionaryofasyncio.Lockprevents concurrent consolidation of the same session.
- Boundary selection: Always cuts at user-turn boundaries to avoid orphaned tool results or split assistant responses.
- Token estimation chain: Try provider's native counter first → fallback to tiktoken
cl100k_base. Counts content, tool call JSON, tool_call_id, and name fields. - Prompt cache friendliness: Messages are append-only;
last_consolidatedadvances but never mutates the list. Historical messages retain their positions for prefix caching. - Message formatting for consolidation:
[{timestamp[:16]}] {ROLE} [tools: {tool1, tool2}]: {content}— compact but preserves tool-call context. - Provider-agnostic: Works with any LLM provider supporting tool calling. Forced
tool_choicewith graceful fallback.
Reusable:
- Two-file fact/event separation pattern (any agent needing always-available facts + searchable history).
- Token-pressure-driven consolidation with multi-round loop (any system managing context window).
- Full-replacement memory via LLM (when memory size is manageable and explicit dedup is overkill).
- Raw archive fallback (resilience pattern for any LLM pipeline).
- Append-only session with cursor (prompt cache optimization).
Tightly coupled:
- Requires the agent to have file-system access and exec tools for grep-based history search.
- MEMORY.md must fit in system prompt; doesn't scale to large fact corpora.
Best fit: Lightweight single-user agents needing persistent memory with minimal infrastructure. Systems where simplicity and debuggability outweigh retrieval sophistication.