Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Memory architecture

What

Castellan memory is the coordination substrate plus durable episode artifacts — it is not a vector store, not a conversation archive, and not RAG. There is no embedding index in the default path; “recall” means querying structured episode/checkpoint/observation records, and “remember” means persisting a typed observation, not chunking text for similarity search.

Why

Hot field state (blackboard + pheromones) must stay fast and ephemeral; warm episodes must remain replayable for evolve. Lexicon: Conventions.

If you’re deciding whether to enable SQLite, wiring castellan_recall/castellan_remember from an MCP client, or figuring out where a run’s state actually lives on disk, this page is the map.

Note: Compared to vector-RAG systems, Castellan memory is environment-mediated — typed slots and decaying zone signals, not embedding similarity. See Coordination model.

Anatomy: hot / warm / cold layers

LayerWhat lives hereWhereLifetime
HotBlackboard, PheromoneField, scheduler queueIn-process memoryOne run
WarmEpisodes, traces, checkpoints, topology archive.castellan/ — JSON by default, or SQLiteDurable, local disk
ColdTurso Sync (libSQL replica)Multi-machine shareDurable, remote — off by default, opt-in
flowchart TB
    subgraph hot["Hot"]
        BB[Blackboard]
        FIELD[PheromoneField]
        SCHED[Scheduler]
    end

    subgraph warm["Warm"]
        EP[episodes]
        TR[traces]
        TA[topology archive]
    end

    RUN[castellan run] --> SCHED
    SCHED --> FIELD
    RUN --> BB
    RUN --> EP
    RUN --> TR
    EVOLVE[castellan evolve] --> EP
    EVOLVE --> TA
    MCP[castellan mcp] --> warm
    REPLAY[castellan replay] --> EP

    classDef runtime stroke:#0969da,stroke-width:2px
    classDef substrate stroke:#5b6b8c,stroke-width:2px
    classDef evolve stroke:#d97706,stroke-width:2px

    class RUN,REPLAY runtime
    class BB,FIELD,SCHED,MCP substrate
    class EVOLVE,EP,TR,TA evolve

On-disk layout

ArtifactPathWritten by
Episodes.castellan/episodes/<goal-id>.jsoncastellan run (episode end)
Traces.castellan/traces/<goal-id>.jsonlcastellan run (per-tick)
Checkpoints.castellan/checkpoints/castellan run (for --resume)
Topology archive.castellan/topology_archive.jsoncastellan evolve, and castellan run --write-back
Proposals.castellan/proposals/castellan evolve --dry-run
SQLite DB (optional).castellan/castellan.db (configurable via [memory] path)castellan run/evolve when backend = "sqlite"

How operators use it

1. Default path — nothing to configure. Every run writes episode JSON automatically:

castellan run --goal "reach target" --plugin gridworld
castellan replay --episode .castellan/episodes/<id>.json
castellan memory status

2. Read live substrate state from an editor/MCP client while a run is active:

castellan_read_blackboard
castellan_deposit_signal

3. Switch to the SQLite backend when you want queryable aggregates instead of scanning JSON files. Add to castellan.toml:

[memory]
backend = "sqlite"
path = ".castellan/castellan.db"
mirror_json = true
auto_import = true

mirror_json (default true) keeps writing .castellan/episodes/*.json alongside SQL so tooling that reads plain JSON keeps working. auto_import (default true) imports any existing JSON corpus the first time SQLite opens.

4. Query durable memory from MCP tools once SQLite is enabled:

castellan_recall
castellan_remember
castellan_query_field_history

5. Confirm which backend is active and where the DB lives:

castellan memory status

6. Mid-episode durable checkpoints: persist scheduler state during a run so castellan run --resume survives process loss — not just episode-end snapshots.

[memory]
checkpoint_interval_ticks = 10   # 0 = episode-end only (default, backward-compatible)

Checkpoints land in .castellan/checkpoints/ and SQLite field_history when backend = "sqlite". The engine fires on_checkpoint every N ticks; CLI wires this to MemoryStore::save_checkpoint_json. Integration tests: durable_checkpoint.rs, durable_resume.rs.

Recipes

When you’re prototyping a plugin, stay on the JSON default — episodes are human-readable, diffable, and git-friendly for fixture corpora (see tests/fixtures/evolve-episodes/).

When your episode corpus grows past a few hundred runs and castellan evolve gets slow, switch to backend = "sqlite" so evolve reads SQL aggregates instead of re-parsing every JSON file.

When you need memory to survive across machines (e.g. a shared team archive), configure [memory.sync] with a Turso/libSQL URL and set sync_on_episode_end = true — tokens are referenced by env var name (turso_token_env), never stored inline in castellan.toml. On each castellan run / MCP session open, Castellan pulls newer remote episodes into the local SQLite file; after each episode ends it pushes that episode’s rows when sync_on_episode_end = true.

Check sync health:

castellan doctor --json | jq '.stages[] | select(.name=="memory_sync")'

The memory_sync stage reports reachability, last pull/push timestamps, and the last error (if any). State is stored in .castellan/sync_state.json (no tokens).

When you want a clean corpus for a benchmark run, cp a known-good fixture set into .castellan/episodes/ before castellan evolve rather than hand-editing the live archive.

What never goes in the database

DataWhy
Live pheromone ticksIn-process decay semantics only make sense mid-run; snapshots go in, not the tick stream
Conversation transcriptsAnti-coordination design — see Coordination model
Vector embeddingsOut of scope for v1 — no similarity search in the default path

Failure paths / troubleshooting

SymptomLikely causeFix
castellan evolve says “no episodes found”Wrong --workspace, or episodes never written (run failed before episode_end)Confirm .castellan/episodes/ has files; pass --workspace .castellan/episodes explicitly
SQLite backend configured but episodes still look emptyauto_import ran before any JSON existed, or path points to a fresh DBRe-run castellan doctor --json to confirm the resolved DB path; check mirror_json output
castellan_recall returns nothingBackend is still json (default) — castellan_recall is a SQLite-backed toolSet [memory] backend = "sqlite" in castellan.toml
Turso sync silently not happeningsync.turso_url unset, sync_on_episode_end false, or [memory] backend not sqliteSet turso_url, sync_on_episode_end = true, backend = "sqlite"; export token env from turso_token_env; castellan doctor memory_sync stage shows last error
Two commands disagree on episode countOne reads JSON, one reads SQLite, and mirror_json was toggled mid-projectKeep mirror_json = true, or fully migrate and stop reading the JSON tree

Engineering detail

Full SQL schema, the MemoryStore trait, and sync policy internals: MEMORY_ARCHITECTURE.md (maintainer spec, not a book page).

See also