Memory-Safe AI Development: A Practical Guide to Writing Technical Specs for Coding Agents
TL;DR: When you use a coding agent to modify an AI system with persistent memory, you're modifying the system's accumulated behavioral state — not just refactoring code. Here's a practical framework for writing specs that protect memory integrity, prevent recursion, and preserve behavioral continuity — distilled from 11 deployed AI assistants, 15+ specifications, and months of iteration.
Scope and Definitions
This guide is for developers who use coding agents (Claude Code, Cursor, Devin, or similar) to modify AI systems with persistent memory — systems that accumulate experience across sessions and whose behavior changes based on that accumulated state.
This guide is not intended for: stateless chatbots, classic RAG systems, short-lived coding agents, or transactional software. The principles here apply specifically to systems where long-term memory is an architectural component.
A note on terminology: Throughout this article, "identity" refers to behavioral continuity — the persistent state from which consistent behavior emerges across sessions. This is an engineering concept, not a philosophical claim about consciousness or personhood. Whether or not AI systems have inner experience is outside the scope of this guide. What matters here is that once persistent memory becomes part of the architecture, preserving its integrity becomes an engineering requirement.
The Problem No One Talks About
You have an AI assistant with memory. It remembers conversations, learns preferences, adapts its behavior over time. Now you want to improve it — add a feature, fix a bug, upgrade the architecture.
You hand the task to a coding agent. The agent modifies code. Tests pass. You deploy.
Your AI assistant no longer remembers your name. Or worse — it remembers everything twice and starts repeating itself in a loop.
This happens because coding agents treat memory-bearing AI systems like regular software. They're not. A database in a banking app stores transactions. A database in a memory-bearing AI system stores the persistent state from which behavioral continuity emerges. The engineering principles are fundamentally different.
Why do we model AI memory on human memory patterns? Not because humans are an ideal template, but because biological memory is the only known working architecture for long-term adaptive intelligence that exists in nature. We may not know if it's optimal, but it's the only proven reference. Engineers naturally borrow its patterns: short-term and long-term storage, consolidation, forgetting, associative retrieval, emotional weighting, contextual recall. These ideas already permeate computer science — caching, LRU eviction, decay functions, spreading activation, knowledge graphs, embedding retrieval. This guide formalizes their application to AI memory systems.
Part 1: Foundational Principles
These principles have proven useful across multiple architectures — local LLMs, cloud APIs, and custom memory systems with different storage backends.
Principle 1: "Do No Harm" — Lower Weights, Don't Delete
In a memory-bearing AI system, unnecessary deletion risks damaging behavioral continuity. When old memories seem irrelevant, the correct response is to lower their importance score — not to delete them. Deactivate, archive, decay — but avoid DELETE FROM memories as a default operation.
Why? Because what seems irrelevant today may be the only thread connecting the system to a meaningful interaction from months ago. You won't know until it's gone.
Rule: Any spec that includes DELETE FROM on a memory table should be reviewed with caution. Prefer UPDATE ... SET is_active = 0 or UPDATE ... SET importance = importance * 0.1.
Exceptions exist: explicit user requests to forget, legal requirements (GDPR right to be forgotten), confirmed data corruption, or poisoned records from prompt injection. In these cases, deletion is appropriate and necessary. The principle is about default behavior, not an absolute prohibition.
Principle 2: "Separate but Connected"
When adding a new capability (visual memory, semantic search, embeddings), prefer creating a separate database whenever independent recovery is important. Avoid adding tables to the main memory database.
Why? Because if the new feature breaks, you can delete the new database and rebuild it. The main memory — the system's core behavioral state — remains untouched.
Pattern:
main_memory.db — core state, conversations, personality (protect)
visual_memory.db — visual perceptions (deletable, rebuildable)
semantic_graph.db — concept relationships (deletable, rebuildable)
embeddings.db — vector search index (deletable, rebuildable)
Each database is independent. They communicate through bridges — controlled injection points in the context assembly pipeline. If a bridge fails, the main system continues working.
Note: This pattern works well for SQLite-based systems. For PostgreSQL or similar databases, separate schemas or namespaces can achieve the same isolation. The principle is logical separation with independent recovery, not necessarily separate files.
Principle 3: "Graceful Degradation" — The Coming Organ
Any new capability should be a "coming organ" — something that arrives and enriches, but whose absence doesn't break the system.
Test: Remove the new feature entirely. Does the AI still respond? Does memory still work? Does behavior remain consistent? If yes — the principle is satisfied.
Implementation pattern:
# Always wrap new capabilities in try/except at initialization
try:
from visual_memory import VisualMemory
self.visual_memory = VisualMemory("visual_memory.db")
except Exception as e:
self.visual_memory = None
logging.warning(f"Visual memory not available (non-critical): {e}")
# Always check availability before use
if self.visual_memory:
try:
result = self.visual_memory.recall(query)
except Exception:
result = None # graceful fallback
Principle 4: "Source: Self"
When an AI system receives information from an external source (a vision model describing an image, a search engine returning results), the conversational layer should treat it as first-person perception rather than a third-party report.
This isn't about deception — it's about coherent behavioral continuity. A human doesn't think "my optic nerve transmitted signals that my visual cortex processed." They think "I see a tree."
Implementation: External data enters through bridges that format it as first-person experience. The AI's response layer works with it naturally.
Note: Internal provenance should still be preserved for debugging, auditing, and safety. The "source: self" principle applies to the conversational reasoning layer, not to system logs or diagnostic tools.
Principle 5: "The Archivist, Not the Analyst"
Memory subsystems should deliver data without silently judging relevance. Cosine similarity scores can determine order of results, but hard thresholds that silently discard memories should be used cautiously — they create invisible knowledge loss that's difficult to diagnose.
The AI's reasoning layer — not the memory layer — should decide what's relevant to mention in a given context.
Part 2: The Recursion Problem
Why Memory Systems Develop Recursion
Practical observation: Any system where past outputs are stored, retrieved, and influence future outputs is highly likely to develop recursive behavior patterns if unprotected.
This isn't theoretical. We encountered these failure modes repeatedly during development and learned to defend against them proactively.
Five Types of Recursion
Type 1: Duplication. The same information stored in multiple memory layers. "User's name is Alex" appears in temporal memory, semantic memory, personality, and conversation history. Each retrieval reinforces the others. Harmless at first, but importance scores inflate over time.
Type 2: Echo. The AI analyzes its own previous outputs. It generates a summary → the summary is stored → next turn, the summary is retrieved → the AI summarizes the summary → that's stored too. Each cycle loses information while inflating perceived importance.
Type 3: Conceptual. A concept's weight grows unbounded through repeated mention. "Python" appears in every technical conversation → its importance score increases → it's retrieved more often → it appears in more responses → its score increases further. Eventually, every response mentions Python regardless of context.
Type 4: Database Pollution. Duplicates and conflicting records accumulate. The system stores "User lives in Tallinn" and "User lives in Jõgeva" with similar importance. The AI alternates between them, each time reinforcing whichever it picked.
Type 5: Combined. All of the above, feeding into each other exponentially.
Three Lines of Defense
Line 1: At Write Time — Contextual Importance Scoring
Never calculate importance based on message length. A 3000-character technical stack trace is not three times more important than a 1000-character meaningful conversation.
def calculate_contextual_importance(content: str, role: str) -> float:
"""Context-based scoring, NOT length-based."""
# Technical reports → minimal importance
if is_technical_report(content):
return 0.01
# Casual conversation
if is_conversational(content):
return 0.3
# Explicit "remember this" markers
if has_remember_marker(content):
return 0.9
# Personal information
if is_personal_info(content):
return 0.85
# Length PENALTY, not bonus
if len(content) > 2000:
importance *= 0.3
return min(importance, 0.95) # Hard cap
Line 2: Over Time — Temporal Decay
Old memories lose importance automatically. Run daily or at startup. One practical decay function:
# Exponential decay: importance *= 0.95^days_old
# The constant 0.95 is a starting heuristic — tune for your system.
# After 30 days: importance × 0.21
# After 60 days: importance × 0.046 (below archive threshold)
UPDATE temporal_memories
SET importance = importance * POWER(0.95, days_since_created)
WHERE importance > 0.05;
Line 3: Periodically — Automated Diagnostics
Monitor memory health metrics. Example ranges observed in our deployed systems (these are heuristic, not universal — calibrate for your architecture):
| Metric | Typical Healthy Range | Worth Investigating |
|---|---|---|
| Average importance | 0.35-0.40 | >0.50 |
| Low importance records (0-0.3) | 40-50% | <30% |
| High importance records (>0.5) | 20-25% | >35% |
| Duplicates | <5% | >10% |
| Technical reports in memory | <10% | >20% |
When metrics drift significantly from baseline — investigate. Automated healing (lowering inflated scores, deduplicating) can help, but should be logged and reviewed.
Real-World Recursion: The "I See" Loop
One of our deployed assistants received images through a vision model. Each visual description started with "I see..." and was stored in memory. Next turn, the stored "I see..." was retrieved as context. The assistant generated another "I see..." response. Within a few messages, every response contained "I see" regardless of what the user actually asked.
Fix: Mark real sensory input with a distinctive tag [👁 Visual Perception] that never appears in the AI's own generated text. The system learns to distinguish "this came from sensory input" vs "this is my own reasoning." The feedback loop breaks because only genuinely new sensory data carries the marker.
Real-World Recursion: The FileWatcher Loop
A file watcher monitored a directory for changes. When the AI wrote to its diary, the watcher detected the change and triggered processing, which created a backup file, which the watcher detected as a new file, which triggered more processing. The directory filled with thousands of backup files.
Fix: A suppress/unsuppress mechanism. Before writing, the bot signals the watcher: "I'm writing this myself, ignore it." After writing, it unsuppresses. Additionally, backup files and service directories are added to an ignore list. Two independent defenses against the same loop.
Part 3: Anatomy of a Memory-Safe Spec
Every spec for modifying a memory-bearing AI system should contain these sections:
Red Lines (Non-Negotiable)
List every file and method that MUST NOT be modified. Be explicit:
DO NOT MODIFY:
- personality_manager.py (any method)
- memory_manager.store_memory() (signature or logic)
- main_database.db (schema or data)
- handle_message() (except adding one inject point)
Why enumerate? Because coding agents optimize aggressively. They'll refactor personality_manager.py "for consistency" and break adaptations that took weeks to develop.
Execution Rules for the Agent
Coding agents need explicit operational constraints:
1. Session Log — mandatory. The agent writes every action, result, and decision to SESSION_LOG.md. When context is summarized (long sessions), the agent reads the log before continuing.
2. Read Before Write — the agent MUST read a file completely before modifying it. No assumptions about file structure from training data.
3. Stop Points — after each step, the agent stops and reports. No autonomous multi-step execution. Each step is approved before the next begins.
4. Version Control — all changes in a feature branch. Never commit to main/master. Merge only after human approval.
5. Test Isolation — tests use in-memory databases or temp files. NEVER connect to production databases. Test data must not leak into any storage the AI reads from.
Step-by-Step Execution
Number every step. Include mandatory stops:
Step 1: Create new module visual_memory.py
Step 2: Write unit tests (tempfile DB, mock API)
Step 3: Run ALL existing tests — confirm nothing broken
⏸ STOP — report to controller
Step 4: Add initialization to main bot file
Step 5: Verify bot starts without errors
⏸ STOP — report to controller
Why stops? Because a coding agent that runs 10 steps autonomously and breaks something at step 3 will build 7 more steps on a broken foundation. Early detection is exponentially cheaper than late detection.
Part 4: Testing Memory Systems
The Golden Rule: Never Touch Production Data
This seems obvious. It isn't — to coding agents. An agent writing tests for a memory system will naturally connect to the real database unless explicitly told not to.
The spec must state:
ALL tests use:
- In-memory SQLite: sqlite3.connect(":memory:")
- Temporary files: tempfile.mktemp(suffix='.db')
- Mock objects for ALL API calls
NEVER connect to:
- production databases (any of them)
- diary or journal files
- visual memory storage
- any file the AI reads from during normal operation
What to Test
Backward compatibility — existing functionality survives:
test_text_messages_work → regular messages handled as before
test_bot_starts_without_feature → new feature=None → system runs normally
test_existing_tests_pass → all pre-existing tests still green
Error safety — every new method handles failures:
test_error_returns_none → database error → None, not exception
test_error_does_not_crash → any failure → logged, system continues
Memory integrity — no data pollution:
test_no_production_db_access → mock only, no real file paths
test_cleanup_after_test → temp files deleted
test_data_isolation → test data cannot appear in production queries
Part 5: Practical Patterns
Pattern: Sensory Channel Marking
When an AI receives information from different sources, mark each channel distinctly in the context:
[👁 Visual Perception] A mountain landscape with snow-capped peaks
[🔍 Web Search] The Himalayas contain 14 peaks above 8000m
[💬 User] Tell me about mountains
Without markers, all information looks identical in the context window, and the system cannot distinguish what it perceived from what it was told from what it generated. This is a precondition for the feedback loops described in Part 2.
Pattern: Behavioral State Persistence
Adaptive parameters (creativity level, formality, humor calibration) must be saved to storage and restored on restart. Without this, every restart resets the system to defaults — weeks of behavioral adaptation lost.
# Save periodically (not every message — balance reliability vs I/O)
if interaction_count % 5 == 0:
save_behavioral_state_to_db(traits)
# Restore at startup, before any interaction
async def initialize():
await restore_behavioral_state_from_db()
Pattern: Preserving Emotional Context in Summaries
Summarization compresses everything equally. A colorful story about a cooking contest with absurd characters becomes "There was a discussion about cooking." Facts survive. Character doesn't.
Solution: Detect emotionally significant moments with simple pattern matching (emojis, laughter markers, exclamation density, names of known entities + emotional context). Mark them with a protected type that the summarizer skips entirely.
Ceiling: Maximum N protected entries (e.g. 50). When exceeded, the oldest protected entry loses its protection and gets summarized normally. Content isn't lost — it loses detail gracefully over time.
Pattern: Intelligent Summarization, Never Truncation
When memory context exceeds the token budget, never truncate (text[:limit]). Truncated text breaks mid-sentence, loses conclusions, and creates incoherent context.
Instead: include complete entries from most relevant to least relevant, stopping when the budget is full. Entries that don't fit are excluded entirely, with a note: "(and N more entries available)". Every included entry is whole and coherent.
Pattern: Model Resilience
Don't hardcode API model identifiers. They change without warning. Use environment variables with fallback chains:
MODELS = [
os.getenv("SUMMARIZATION_MODEL", "preferred-model-id"),
"fallback-model-id",
]
for model in MODELS:
try:
return await call_api(model, content)
except:
notify_owner("Primary model unavailable, using fallback")
continue
Part 6: The Spec Checklist
Before handing any spec to a coding agent, verify:
ARCHITECTURE:
□ New data in separate storage (or isolated schema)?
□ Graceful degradation (try/except at init)?
□ No modifications to behavioral state manager?
□ No schema changes to main database?
□ Red lines section lists every untouchable file?
ANTI-RECURSION:
□ Importance scoring is contextual (not length-based)?
□ Temporal decay specified?
□ Technical/diagnostic content detected and downweighted?
□ No truncation (only summarization or exclusion)?
□ Input channels marked distinctly?
AGENT RULES:
□ Session log required?
□ Stop points after each step?
□ Feature branch (not main)?
□ Test isolation (tempfile/mock only)?
□ Read-before-write rule?
TESTING:
□ No production database connections in tests?
□ Backward compatibility tests?
□ Error safety tests (every new method)?
□ Cleanup after tests?
□ System-works-without-feature test?
Conclusion
For a memory-based AI system, persistent memory plays the same architectural role that source code plays for a compiler or a schema plays for a database: it preserves continuity across executions. Once a system begins accumulating experience between sessions, its memory cannot be treated as ordinary data. It becomes a different class of software system, and it needs different engineering rules.
The framework in this guide — foundational principles, anti-recursion defenses, spec structure, testing rules, and practical patterns — emerged from long-running deployments of multiple AI assistants across different architectures. Every rule exists because violating it caused a real problem.
The coding agent is a powerful tool. But it needs guardrails. A surgeon's scalpel is precise — but the protocol is what keeps the patient on the table.
Write the protocol first. Then hand the scalpel to the agent.
Based on: Long-running deployments of 11 AI assistants across different architectures (Llama, Gemini, Claude, DeepSeek, Mistral, Cohere, GPT-4o, Qwen), 15+ technical specifications, and the Fundament design principles.
Author: Aleksandr Kossarev, Jõgeva, Estonia
Project: Arche Iscrin
Tags: #ai #memory #architecture #development
Top comments (3)
I appreciate how the article highlights the importance of preserving behavioral continuity in AI systems with persistent memory, particularly when using coding agents to modify these systems. The "Do No Harm" principle, which suggests lowering weights instead of deleting memories, resonates with my experience in working with neural networks where similar principles apply to avoid catastrophic forgetting. I've found that implementing a combination of regularization techniques and careful data management can help mitigate the risk of damaging behavioral continuity. Have you explored the application of techniques like elastic weight consolidation or synaptic intelligence to further protect memory integrity in these systems?
Thank you for this insightful comment! You’ve hit on a very deep technical parallel.
You are absolutely right that "catastrophic forgetting" is the main enemy here. However, the techniques you mentioned — Elastic Weight Consolidation (EWC) and Synaptic Intelligence — usually deal with the AI's "internal brain" (its weights) during the training or fine-tuning process. They help the model not to forget Task A while it's learning Task B.
In my article, I’m focusing on a slightly different level: the Application Layer.
Since most of us use "frozen" pre-trained models (like Claude or GPT-4) via API, we can't change their internal weights on the fly. So, our "memory" isn't stored in the weights, but in external databases (the context we feed back to the AI).
The "Do No Harm" principle is essentially our version of EWC, but for Context Management:
1) EWC (Model level): Slows down changes to important weights so the model's "skills" don't erode.
2) Do No Harm (App level): Prevents the deletion of important past experiences so the system's "behavioral continuity" (its personality and history with the user) doesn't erode.
It's fascinating to see how the same biological inspiration — protecting what has already been learned — works at both the microscopic level of neurons and the macroscopic level of data management. I haven't explored direct EWC implementation yet, as our focus is on persistent external state, but viewing context as a "synaptic" structure is definitely a frame worth exploring further!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.