Back to Theory
Tutorial10 min read · June 16, 2026

Building a Coding Assistant with Persistent Memory Using Feather DB

A Claude-powered coding assistant that remembers your project architecture, past decisions, and debugging sessions — with context_chain surfacing the full decision trail, not just the nearest match.

F
Feather DB
Engineering

What a memory-backed coding assistant looks like

A standard coding assistant knows only what's in the current context window. You explain the project architecture every session, re-paste the same files, repeat the same constraints. A memory-backed assistant knows your project. It remembers that you chose PostgreSQL over MongoDB in January because of ACID requirements, that you've had three separate debugging sessions around the ORM's connection pool, and that the architecture decision to use event sourcing was made during a specific refactor.

This tutorial builds that assistant using Feather DB and the Anthropic Python SDK.

Step 1: Project context ingestion

Start by ingesting your project's static context: README, architecture docs, and past decision records. These get high importance and long half_life — architecture decisions are relevant for months.

import os
import feather_db as fdb
from anthropic import Anthropic

# Initialize Feather DB
db = fdb.DB.open("project_memory.feather", dim=768)
client = Anthropic()

def embed(text: str) -> list:
    """Embed text using your preferred provider."""
    # Using Voyage AI via feather-serve, or directly:
    import voyageai
    vo = voyageai.Client()
    result = vo.embed([text], model="voyage-3")
    return result.embeddings[0]

def ingest_document(path: str, doc_type: str, importance: float = 1.0):
    """Chunk and ingest a document file."""
    with open(path) as f:
        content = f.read()

    # Simple paragraph-level chunking
    chunks = [c.strip() for c in content.split("\n\n") if len(c.strip()) > 50]

    mems = []
    for i, chunk in enumerate(chunks):
        vec = embed(chunk)
        mem = db.add(vec, text=chunk,
                     namespace="project-myapp",
                     entity=doc_type)
        mem.meta.set_attribute("source", path)
        mem.meta.set_attribute("chunk_index", i)
        mem.meta.set_attribute("importance", importance)
        mems.append(mem)
    return mems

# Ingest project context — high importance, searched often so stickiness builds
arch_mems = ingest_document("ARCHITECTURE.md", "architecture", importance=1.8)
readme_mems = ingest_document("README.md", "overview", importance=1.2)

print(f"Ingested {len(arch_mems) + len(readme_mems)} context chunks")

Step 2: Conversation memory with write-back

Each assistant turn generates new knowledge: decisions made, code patterns discussed, bugs discovered. Write these back to the memory store.

def save_conversation_memory(user_msg: str, assistant_msg: str,
                              memory_type: str = "conversation"):
    """Distill and save a conversation turn to memory."""
    # Distill the key takeaway from the exchange
    distill_prompt = f"""Extract the key technical fact, decision, or insight from this exchange.
Write one clear sentence. No preamble.

User: {user_msg}
Assistant: {assistant_msg[:500]}"""

    distilled = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=100,
        messages=[{"role": "user", "content": distill_prompt}]
    ).content[0].text.strip()

    if len(distilled) > 20:  # skip trivial exchanges
        vec = embed(distilled)
        mem = db.add(vec, text=distilled,
                     namespace="project-myapp",
                     entity="decisions" if memory_type == "decision" else "conversations")
        mem.meta.set_attribute("type", memory_type)
        mem.meta.set_attribute("importance", 1.5 if memory_type == "decision" else 1.0)
        return mem
    return None

Step 3: Linking related memories

Decisions are connected to their rationale and resulting code changes. Use Feather DB's edge types to wire these relationships.

def record_decision(decision_text: str, rationale_text: str,
                    related_code_desc: str = None):
    """Record an architecture decision with its rationale and code impact."""
    vec_d = embed(decision_text)
    vec_r = embed(rationale_text)

    # Add the decision
    decision_mem = db.add(vec_d, text=decision_text,
                          namespace="project-myapp",
                          entity="decisions")
    decision_mem.meta.set_attribute("type", "architecture_decision")
    decision_mem.meta.set_attribute("importance", 2.0)

    # Add the rationale
    rationale_mem = db.add(vec_r, text=rationale_text,
                           namespace="project-myapp",
                           entity="decisions")
    rationale_mem.meta.set_attribute("type", "rationale")

    # Link: decision leads_to rationale
    db.add_edge(decision_mem.id, rationale_mem.id, edge_type="leads_to")

    if related_code_desc:
        vec_c = embed(related_code_desc)
        code_mem = db.add(vec_c, text=related_code_desc,
                          namespace="project-myapp",
                          entity="code-changes")
        # Decision causes the code change
        db.add_edge(decision_mem.id, code_mem.id, edge_type="causes")

    return decision_mem

# Example: recording the DB choice
record_decision(
    "Chose PostgreSQL over MongoDB for the primary datastore.",
    "ACID guarantees required for financial transaction records. MongoDB's eventual consistency model was incompatible with audit requirements.",
    "Migrated user_transactions table to use pg via psycopg2 with connection pooling."
)

Step 4: Querying with context_chain

context_chain() combines ANN search with BFS graph traversal. For a query about a decision, it returns the decision plus its rationale, related code changes, and superseded alternatives — the full context trail.

def get_relevant_context(query: str, k: int = 8) -> str:
    """Retrieve relevant memories + their connected context."""
    vec = embed(query)

    # context_chain: ANN to find seeds, then BFS to follow edges
    chain = db.context_chain(
        vec,
        k=k,
        namespace="project-myapp",
        max_depth=2,   # traverse up to 2 hops from each seed
        half_life=90   # architecture context stays relevant for months
    )

    if not chain:
        return "No relevant context found."

    context_parts = []
    for mem in chain:
        context_parts.append(f"- [{mem.meta.get_attribute('type', 'memory')}] {mem.text}")

    return "\n".join(context_parts)

Step 5: The assistant loop

def coding_assistant(user_input: str, conversation_history: list) -> str:
    """Main assistant loop with memory retrieval and write-back."""
    # Retrieve relevant context from memory
    context = get_relevant_context(user_input)

    # Build the system prompt with retrieved context
    system = f"""You are a coding assistant with memory of this project.

Relevant context from project memory:
{context}

Use this context to give accurate, consistent answers that align with past decisions.
If a past decision is relevant, reference it explicitly."""

    # Add user message to history
    conversation_history.append({"role": "user", "content": user_input})

    # Call Claude
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=2000,
        system=system,
        messages=conversation_history
    )
    assistant_response = response.content[0].text

    # Add assistant response to history
    conversation_history.append({"role": "assistant", "content": assistant_response})

    # Write-back: save the distilled insight from this turn
    # Detect if a decision was made
    is_decision = any(kw in user_input.lower() or kw in assistant_response.lower()
                      for kw in ["decided", "chose", "architecture", "tradeoff", "going with"])
    save_conversation_memory(user_input, assistant_response,
                              memory_type="decision" if is_decision else "conversation")

    return assistant_response

# Run the assistant
history = []
while True:
    user_input = input("You: ").strip()
    if user_input.lower() in ["exit", "quit"]:
        break
    response = coding_assistant(user_input, history)
    print(f"Assistant: {response}\n")

Step 6: Decay tuning for code contexts

Different types of code knowledge decay at different rates. Tune half_life per entity type:

HALF_LIVES = {
    "architecture": 365,   # architecture decisions stay relevant for a year+
    "decisions": 180,      # explicit decisions: 6 months
    "conversations": 30,   # general Q&A: 30 days
    "debugging": 14,       # debugging sessions: 2 weeks (bugs get fixed)
    "code-changes": 60,    # code change context: 2 months
}

def search_by_entity(query: str, entity: str, k: int = 5):
    vec = embed(query)
    half_life = HALF_LIVES.get(entity, 30)
    return db.search(vec, k=k,
                     namespace="project-myapp",
                     entity=entity,
                     half_life=half_life)

The result is an assistant that becomes more useful the longer you use it. Architecture decisions made six months ago surface when relevant. Debugging sessions from last week appear when you hit a similar issue. The conversation history isn't a flat log — it's a weighted, decaying knowledge graph that understands what mattered and for how long.

Install: pip install feather-db anthropic voyageai · GitHub: github.com/feather-store/feather