-
Notifications
You must be signed in to change notification settings - Fork 0
Memory
🇬🇧 English · 🇮🇹 Italiano
Generated from docs/memory.md — edit that file in the repository, not this page.
Leggi in 🇮🇹 Italiano
Educational Premise: Large Language Models are stateless functions: every API call begins with a blank slate. To build an agent capable of learning across sessions without suffering from context window bloat or relying on heavy, opaque vector databases, we need a clean mental model and a deterministic storage architecture.
This guide explores the design of TSUKA's persistent memory system: the core concepts, the architectural trade-offs, the underlying algorithms, and the practical lessons learned from real-world engineering mistakes.
Before diving into algorithms, we must distinguish between the three distinct levels of state in an agentic harness:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. Turn History (RAM) │
│ • Scope: Current conversational turn │
│ • Lifetime: Ephemeral (lost on restart, pruned when context window fills)│
│ • Purpose: Immediate ReAct loop messages (user input, tool calls, logs) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 2. Run Blackboard (AsyncLocalStorage) │
│ • Scope: Active multi-agent workflow (/team or /goal) │
│ • Lifetime: Single workflow run (destroyed upon completion) │
│ • Purpose: Shared scratchpad for agents to post and read working notes │
├─────────────────────────────────────────────────────────────────────────────┤
│ 3. Long-Term Persistent Memory (memory/memory.json) │
│ • Scope: Cross-session, cross-workspace, shared by all agents │
│ • Lifetime: Permanent (persisted on disk, governed by score eviction) │
│ • Purpose: Architectural choices, conventions, and hard-won lessons │
└─────────────────────────────────────────────────────────────────────────────┘
A frequent beginner trap is treating the chat prompt as a dumping ground for all past execution history.
- The failure mode: Small and local models (<30B) quickly experience attention dilution — when thousands of tokens of old logs flood the prompt, the model loses track of immediate instructions and hallucinates tool parameters.
- The architectural rule: Ephemeral logs stay in turn RAM; collaborative task scratchpads stay in the run Blackboard; only durable, curated insights graduate into Long-Term Memory.
Memory systems in AI are not a binary choice. They exist on a ladder of trade-offs, where higher rungs add semantic capabilities at the cost of infrastructure complexity, latency, and determinism:
Rung 6: Temporal Knowledge Graphs (Zep, Mem0) ── Heavy infrastructure, graph engines
Rung 5: Autonomous Self-Editing Memory (Letta) ── Continuous LLM-in-the-loop curation
Rung 4: Vector Embeddings & Semantic RAG ── Requires embedding models & vector stores
────────────────────────────────────────────────────────────────────────────────────────────────
Rung 3: Lexical Ranking + Half-Life Decay (TSUKA) ◄── ZERO dependencies, 100% deterministic & local
────────────────────────────────────────────────────────────────────────────────────────────────
Rung 2: Rolling Context Summaries ── Loses precise details, prompt-expensive
Rung 1: Raw Chat History Buffer ── Explodes context budget immediately
| Feature / Metric | Vector / Semantic Retrieval (Rung 4) | Lexical + Half-Life Decay (TSUKA - Rung 3) |
|---|---|---|
| External Dependencies | Requires embedding model + vector DB binaries |
Zero (Pure TypeScript + node:fs) |
| Latency & Overhead | 50–500ms per embedding call, extra GPU/CPU RAM | 0ms, instantaneous CPU string scoring |
| Determinism & Debugging | Opaque float vectors, non-deterministic ranking | Plain text JSON file (memory/memory.json), grep-able |
| Offline / Local-First | Can fail if embedding server/model crashes | Fully self-contained, 100% offline |
| Accepted Trade-off | Captures paraphrasing ("car" matches "automobile") | Matches exact stems and prefixes ("build", "builder") |
🔑 Key Insight: In coding and engineering harnesses, queries typically look for specific file paths, exact error strings, technology names, and concrete rules rather than poetic synonyms. Lexical BM25 ranking paired with morphological stemming covers ~90% of real-world needs with zero external moving parts.
Not all knowledge has the same shelf life. An execution error from 10 minutes ago is obsolete once resolved, but an architectural standard ("Always use UTF-8 without BOM in PowerShell") must survive indefinitely.
TSUKA organizes memory into 4 distinct durability tiers:
▲ ┌───────────────────────────────┐
│ │ LEZIONE (Lesson) │ Weight: 3 | Half-life: 30 days
│ │ "Never disable TLS in prod" │ (Hard-won rules, safety conventions)
│ ├───────────────────────────────┤
│ │ DECISIONE (Decision) │ Weight: 2 | Half-life: 7 days
│ │ "We use Vitest, not Jest" │ (Architectural and tech stack choices)
│ ├───────────────────────────────┤
DURABILITY │ FATTO (Fact) │ Weight: 1 | Half-life: 48 hours
│ │ "Config is at src/config.ts" │ (System state, environment snapshots)
│ ├───────────────────────────────┤
│ │ RUN (Run Note) │ Weight: 0 | Half-life: 2 hours
│ │ "Build failed on step 2" │ (Transient logs, evicted first)
▼ └───────────────────────────────┘
-
Run Quota Protection: When memory reaches capacity (
maxFacts = 200), transientrunnotes are capped at a maximum of 30% of total storage during an eviction pass, preventing a burst of workflow logs from ever starving durable lessons.
┌──────────────────────────────┐
│ 1. Writing & Deduplication │ Normalizes content, auto-tags, merges hits
└──────────────┬───────────────┘
│
┌──────────────▼───────────────┐
│ 2. Searching & Retrieval │ BM25 lexical ranking + morphological stemming
└──────────────┬───────────────┘
│
┌──────────────▼───────────────┐
│ 3. Aging & Eviction │ Exponential half-life decay; recall refreshes age
└──────────────┬───────────────┘
│
┌──────────────▼───────────────┐
│ 4. Prompt Injection │ Task-Aware (BM25) vs General (Retention Score)
└──────────────────────────────┘
When an agent calls save_memory:
-
Atomic File Safety: Writing directly to
memory.jsonrisks file corruption if the user halts the process mid-write. TSUKA writes to a temporary sibling file (memory.json.tmp) and performs an atomic filesystem rename. If corrupted bytes are ever encountered, they are preserved asmemory.json.corrupt-<timestamp>before starting fresh. -
Normalized Deduplication: Before inserting a new fact, the system creates a normalized key:
key = `${scope} ${content.trim().replace(/\s+/g, ' ').toLowerCase()}`
-
Smart Merge: If a duplicate key already exists:
- It upgrades the durability
kindif the new one is higher (e.g.fatto$\to$ decisione). - It increments the existing fact's
hitscount (a fact re-discovered multiple times is a fact that matters). - It updates timestamps and merges keyword tags.
- It upgrades the durability
- Auto-Tagging: If tags are omitted, the engine automatically extracts up to 5 significant keywords from the content, ignoring common stop words.
When agents search memory with recall_memory(query):
Tokens are normalized to their base linguistic root (e.g. "running" "runn", "processi" "process"). This allows queries in English and Italian to match inflected words naturally.
Instead of a naive substring count, BM25 provides intuitive, mathematically sound ranking:
-
Term Rarity (IDF): Common words carry little weight; rare, discriminative words (e.g.
"OAuth","deadlock") dominate the score. - Term Frequency Saturation: Repeating a keyword 10 times in a single note does not multiply its score tenfold. BM25 applies logarithmic saturation, preventing keyword stuffing.
- Document Length Normalization: A concise 20-word note matching the keyword scores higher than a 500-word block where the keyword appeared once by accident.
When memory exceeds capacity (maxFacts = 200), the store must evict the lowest-scoring non-pinned entry:
EVICTION SCORE FORMULA
Score = (Kind_Weight × 100) + (Time_Decay × 10) + Recency_TieBreak + Hits_Bonus
▲ ▲
│ │
Dominant factor: Exponential erosion
Lezione always beats based on Kind's half-life
Run notes (2h, 48h, 7d, 30d)
- When a memory is returned by
recall_memory(query), the engine touches it:-
hitsis incremented by 1. -
lastUsedtimestamp is refreshed to now.
-
- Result: Facts that agents actively reference stay permanently fresh and survive eviction indefinitely. Facts that are never used naturally decay and get purged over time.
-
Pinned Facts (
pinned: true): Pinned facts are permanently exempt from decay and eviction.
How does long-term memory reach the agent when building a prompt?
Is a specific task or user objective available?
│
├── YES ──► Task-Aware Injection (formatRelevant)
│ Uses BM25 search against the task text to inject only contextually relevant facts.
│ (Crucial: This injection does NOT touch hits, preventing artificial popularity).
│
└── NO ──► Retention-Ranked Injection (formatForPrompt)
Injects the top globally important durable memories (Lessons & Decisions).
Each memory is formatted with scannable badges that small models parse effortlessly:
- [2026-08-15][LESSON] (security_auditor) Never disable TLS verification in production scripts.
- [2026-08-16][DECISION] (architect) All custom tools must return structured JSON strings.
Agents interact with persistent memory through 4 native tools:
Saves a durable fact or lesson into the knowledge base.
{
"content": "Windows PowerShell requires explicit UTF-8 encoding when piping non-ASCII characters.",
"summary": "PowerShell UTF-8 encoding rule",
"kind": "lesson"
}Searches memory using BM25 keyword matching and updates the fact's freshness.
{
"query": "PowerShell encoding pipe"
}Modifies or enriches an existing memory entry.
{
"id": "mem_j8x19",
"content": "Updated rule: PowerShell 7 uses UTF-8 natively; Windows PowerShell 5.1 needs chcp 65001.",
"kind": "lesson"
}Permanently removes an obsolete or incorrect memory entry by ID.
{
"id": "mem_j8x19"
}Following Directive 8 (Modularity by Design), TSUKA decouples memory storage and retrieval behind an explicit contract:
// src/core/memory/types.ts
export interface MemoryBackend {
load(): Promise<void>;
save(): Promise<void>;
addFact(fact: Omit<MemoryFact, 'id' | 'createdAt' | 'lastUsed' | 'hits'>): MemoryFact;
updateFact(id: string, patch: Partial<MemoryFact>): boolean;
forgetFact(id: string): boolean;
search(query: string, scope?: string, options?: SearchOptions): ScoredFact[];
formatForPrompt(maxChars?: number, scope?: string): string;
formatRelevant(taskText: string, maxChars?: number, scope?: string): string;
// ...
}-
Default Implementation (
JsonMemoryBackend): Zero external dependencies, pure TypeScript JSON file storage structured across focused modules:-
codec.ts: normalization, automatic summary derivation from known patterns (goals/traces/subagents), fact validation, deduplication by key/title, badge formatting, and prompt rendering with character budgeting (promptMaxChars). -
storage.ts: atomic persistence via sibling.tmpfile andrenameSync, orphan file cleanup at boot, and automatic corruption recovery backup (.corrupt-<timestamp>). -
bm25.ts: pure lexical tokenization, stop-word filtering, and BM25 relevance scoring. -
retention.ts: category weights (pinned,lesson,decision,fact,run), half-life decay, and deterministic eviction victim selection.
-
-
Pluggable Registry: Alternative backends (e.g. SQLite with FTS5, vector databases, remote cloud stores) can be registered via
registerMemoryBackend(name, factory)and selected dynamically throughmemoryBackendintsuka.config.jsonor theTSUKA_MEMORY_BACKENDenvironment variable. -
Unified Facade: Consumers throughout the harness access memory via the standard
MemoryStoresingleton facade, maintaining 100% backward compatibility.
Building this harness provided several concrete lessons on what not to do with agent memory:
-
What went wrong: In early versions, every tool output and summary was saved to
memory.json. - The consequence: Memory rapidly clogged with transient file dumps and error logs. Crucial architectural conventions were evicted within hours.
- The fix: Memory must be curated. Only explicit lessons, architectural decisions, and stable facts belong in persistent storage.
-
What went wrong: Every time a fact was injected into a prompt, its
hitswas incremented and itslastUsedwas updated. - The consequence: The first 10 facts saved in a project became permanently immortal because they were injected into every prompt, preventing newer, more relevant facts from surviving.
-
The fix: Prompt assembly uses
touch: false. Only deliberate agent searches (recall_memory) count as real usage.
-
What went wrong: Agents attempted to save entire source code files via
save_memory. - The consequence: Hit the 500-character safety limit and consumed context token capacity.
- The fix: The workspace filesystem is the single source of truth for code; persistent memory is strictly for meta-knowledge, rules, and conventions.
TSUKA v0.8.1 · Repository · Issues · MIT License