Memory System
The memory layer in Agenvoy has three tiers for conversation memory plus a cross-session error memory tier.
| Tier | Backed by | Scope |
|---|---|---|
| 1. Context window (24 messages + summary) | history.json + summary.json |
session |
| 2. Semantic search (recent) | ToriiDB DBSessionHist (vector) |
session |
| 3. Full-text archive (all history) | SQLite FTS5 via go-sqlkit | session |
| Error memory | ToriiDB error_memory (90d TTL) |
cross-session |
Three-tier conversation memory
1. Context window (limits.max_history_messages, default 24)
Each session keeps the most recent N messages in full and feeds them into the LLM context window. Anything older remains in history.json but is not sent to the LLM.
A rolling summary (summary.json) condenses older conversations and is injected into the system prompt at the top of each turn so older context survives beyond the N-message window.
Incremental cursor: summary.meta.json (per-session) holds last_message_time (format YYYY-MM-DD HH:MM:SS, extracted from the timestamp in message content). On each summary.Generate invocation:
filterAfterTime(histories, cursor)keeps onlyt > cursormessages- Each chunk runs one
generatePassLLM call (the system prompt already includes{{.Summary}}=old summary, so merge happens during generation — no separatemergePass, avoiding 2x cost) - On success, cursor advances to that chunk's max timestamp +
SaveSummarytriggers the mtime gate generatePassfailure →return(don't bill subsequent chunks; next cron tick retries)
2. Semantic search — ToriiDB (recent conversations)
The search_chat_history tool with mode=semantic runs vector similarity search via ToriiDB db.VSearch. Each hit triggers a context window expansion: 2 entries before + 1 entry after.
ToriiDB entries are cleaned during history.json compaction — entries older than the compact cutoff are removed, keeping ToriiDB focused on recent conversations. Older data lives in SQLite (tier 3).
3. Full-text archive — SQLite FTS5 (all history)
Every message written to history.json is dual-written to SQLite (~/.config/agenvoy/.store/history.db) via go-sqlkit. SQLite always holds the complete conversation history, even after history.json is compacted.
The search_chat_history tool with mode=keyword runs FTS5 full-text search on the SQLite archive + ToriiDB substring match on recent entries, combining results.
Compaction: when history.json exceeds max_history_bytes (default 5 MiB), the oldest messages are trimmed to 80% on a complete user+assistant pair boundary. The cutoff timestamp is recorded in SQLite session_meta.start_at so that keyword search excludes entries already present in history.json (avoiding duplicates). ToriiDB entries older than the cutoff are also removed.
Backfill: on first encounter (SQLite has no data for a session but history.json has content), the entire existing history is backfilled into SQLite.
Timestamps: stored as UTC unix nanoseconds. Timestamps in message content are parsed via time.ParseInLocation (local timezone) and converted to UTC for storage. Search queries use time.Now().UnixNano() (already UTC).
In-run compaction
Beyond the stored tiers, internal/agents/exec/compact keeps a single long run inside the model's window. Each step is a separate stage, applied in order and only as far as needed:
| Stage | Trigger | Effect |
|---|---|---|
CheckThreshold(model) |
Every iteration | Token budget for the current model — 80 % of its context window (1 M for Gemini and recent GPT, 500 K for grok-4.5, 256 K for other GPT/Grok, 200 K for Claude, 128 K default) |
ToolHistory |
Budget exceeded | Summarize the oldest tool-call exchanges into text |
ExtractOldHistories |
Still over budget | Fold older conversation turns into the rolling summary |
TrimFallback |
Provider returns a context-length error | Drop the oldest exchange and resend |
RawToolFallback |
Model switch mid-run | Convert tool history to plain text so the new model can read it |
Manual compaction runs from the TUI (/memory compact) or POST /v1/session/:id/compact, which returns 202 Accepted and compacts in the background.
Search routing
mode parameter |
Source | Use case |
|---|---|---|
semantic (default) |
ToriiDB VSearch | "What did we discuss about X?" — meaning-based |
keyword |
SQLite FTS5 (archive) + ToriiDB substring (recent) | "Find messages containing 'sandbox'" — exact text |
Cross-session error memory
Tool failures, resolution paths, and abandoned strategies persist across sessions in error_memory with 90-day TTL. On hit (either via keyword Contains or db.VSearch), the entry's TTL is refreshed via db.Expire.
When the same tool name fails in a future session, toolCall.go automatically queries error_memory and injects relevant entries as hints into the next assistant turn:
| Outcome on record | Hint behavior |
|---|---|
resolved |
Agent must apply the recorded resolution |
failed / abandoned |
Agent must avoid the recorded strategy |
Storage layout
| Store | Content | Lifecycle |
|---|---|---|
history.json |
Recent messages (hot, LLM reads every turn) | Auto-compacted at 5 MiB |
ToriiDB DBSessionHist |
Recent messages with embeddings | Cleaned on compact (entries < cutoff removed) |
SQLite messages |
All messages ever written (dual-write) | Cleared on reset / remove-session |
SQLite session_meta |
start_at — compact cutoff timestamp |
Cleared on reset / remove-session |
summary.json |
Rolling summary blob | Survives reset |
ToriiDB error_memory |
Tool error records with resolution metadata | 90d TTL (refresh on hit) |
Reset / remove behavior
| Operation | history.json |
ToriiDB DBSessionHist |
SQLite (messages + meta) | summary.json |
|---|---|---|---|---|
| Compact (auto) | Trimmed to 80% | Entries < cutoff removed | Untouched (already has all data) | Untouched |
Reset (/memory reset) |
Deleted | Cleared | Cleared | Preserved |
| Remove session | Directory deleted | Cleared | Cleared | Directory deleted |
Migration note
Sessions and error memory used to live under per-session JSON files. Since ToriiDB v0.5.0 they are inside the embedded store. Do not reintroduce JSON paths.
[!NOTE] This document was auto-generated by Claude after reading the full source code.