Skip to content

05 · Persistence Layer ​

Fifth installment of the architecture deep-dive: SQLite WAL + file-tree layered storage, migration system, and path self-healing mechanism. Corresponds to ARCHITECTURE.md §6.


§6.1 Why SQLite ​

DimensionChoiceRationale
Data scaleSingle user, thousands to tens of thousands of rowsNo need for Postgres / MySQL concurrency power
OperationsZero opsSingle file, no server process
TransactionsWAL modeMulti-process safe (serialized writer)
PrivacyLocal fileData never leaves the machine
Concurrencycheck_same_thread=True + thread-localIndependent connection per thread, no lock contention

§6.2 Data Layering ​

SQLite (workspace/familiars.db) ​

Structured data:

TablePurpose
agentsAgent metadata
dialoguesDialogue tree index
cron_jobsCron jobs
schedulesSchedules
todosTo-do items
remindersReminders
watch_jobsMonitor jobs
skillsSkill manifests
approvalsPermissions / approvals
rag_feedbackRAG feedback
proactive_feedbackProactive message feedback
broker_*Broker metadata (v18+)
guild_*Guild (v20+)
flow_*Flow instances (v25+)

File Tree (workspace/) ​

workspace/
├── familiars.db                    # SQLite main database
├── .bridge_token                   # Bridge auth token
├── .bridge.port                    # Bridge actual port
├── .bridge.pid                     # Daemon pidfile
├── .bridge.log                     # Daemon log (rotating)
├── .bridge.crt                     # Self-signed HTTPS certificate
├── .bridge.key                     # Certificate private key
├── .salt                           # Encryption salt (crypto.py)
├── .paths_aliases.json             # External directory aliases
├── SECRET.json                     # Encrypted credentials
├── gpu_tier.json                   # GPU tier cache
├── info.json                       # OS info
├── USER.md                         # User profile (RAG target)
├── IDENTITY.md                     # User identity
├── AGENTS.md                       # Project-level agent rules
├── tools.json                      # Tool schema merge source
├── user/                           # User-scope memory
│   ├── profile.md
│   ├── preference.md
│   ├── relation.md
│   ├── knowledge.md
│   └── event.md
├── agents/
│   └── agent_<id>/
│       ├── IDENTITY.md             # Agent identity
│       ├── dialogue_<n>/
│       │   ├── dialogue.json       # Dialogue tree (Node list)
│       │   ├── turn_<k>/
│       │   │   └── plan.json       # Tool plan
│       │   ├── step_<k>.json       # Step record
│       │   ├── dialogue/           # Dialogue-scope memory
│       │   │   ├── preference.md
│       │   │   ├── relation.md
│       │   │   ├── knowledge.md
│       │   │   ├── event.md
│       │   │   ├── state.md
│       │   │   └── directive.md
│       │   └── ephemeral/          # Ephemeral-scope memory
│       │       ├── state.md
│       │       └── directive.md
│       ├── identity/               # Identity-scope memory
│       │   ├── preference.md
│       │   ├── procedure.md
│       │   └── directive.md
│       ├── voice.wav               # Voice sample (digital human)
│       ├── client_portraits/       # Client portraits
│       └── reports/                # Reports (deprecated → manager_review skill)
├── manager/                        # Agent 0 (manager) data
├── reminders/
│   └── inbox.jsonl                 # Reminder events (SSE stream source)
├── rewriter/
│   └── prompt.md                   # Rewrite prompt
├── reports/                        # Report templates + system prompts
├── skills/                         # Skill definitions
├── tool_skills/                    # Tool skills
├── external_kb/                    # External knowledge bases
├── manager/brokers/                # Installed broker apps
│   └── <broker_id>/
│       ├── manifest.json
│       ├── cover.png
│       └── ...
└── manager/flows/                  # Committed flow templates
    └── <template_id>/
        ├── template.json
        ├── layout.json
        ├── renderers/              # Custom field renderer .vue files
        └── docs/                   # Design / development docs

§6.3 SQLite Connection Management ​

6.3.1 connect() Factory ​

python
# src/storage/db.py

def connect(db_path: Optional[str] = None, *, run_migrations: bool = True) -> sqlite3.Connection:
    """Return a configured SQLite connection. Cached per-thread per-path."""
    path = db_path or get_default_db_path()
    cache: dict = getattr(_thread_local, "conns", None) or {}
    if path in cache:
        return cache[path]

    conn = sqlite3.connect(
        path,
        isolation_level=None,     # autocommit; manual BEGIN IMMEDIATE
        timeout=30.0,             # wait up to 30s for writer lock
        check_same_thread=True,   # independent connection per thread
    )
    conn.row_factory = sqlite3.Row
    _configure(conn)
    if run_migrations:
        _run_migrations(conn)

    cache[path] = conn
    _thread_local.conns = cache
    return conn

6.3.2 PRAGMA Configuration ​

python
def _configure(conn: sqlite3.Connection) -> None:
    cur = conn.cursor()
    cur.execute("PRAGMA journal_mode = WAL")        # Write-Ahead Logging
    cur.execute("PRAGMA synchronous = NORMAL")       # Balance performance / safety
    cur.execute("PRAGMA foreign_keys = ON")          # Foreign key constraints
    cur.execute("PRAGMA busy_timeout = 5000")        # Busy wait 5s
    cur.execute("PRAGMA temp_store = MEMORY")        # Temp tables in memory

6.3.3 DB Path Resolution ​

python
def get_default_db_path() -> str:
    explicit = os.getenv("FAMILIARS_DB_PATH")
    if explicit:
        return explicit
    workspace = os.getenv("WORKSPACE_ROUTE") or os.getcwd()
    return str(Path(workspace) / "workspace" / "familiars.db")

FAMILIARS_DB_PATH=:memory: is commonly used for tests.

§6.4 Migration System ​

6.4.1 Migration Runner ​

python
# src/storage/db.py
_MIGRATIONS_DIR = Path(__file__).parent / "migrations"
_MIGRATION_NAME_RE = re.compile(r"^v(\d+)__.+\.(sql|py)$")

def _run_migrations(conn: sqlite3.Connection) -> None:
    """Execute pending migrations in version order."""

6.4.2 Migration Manifest (v1–v50) ​

VersionFileTypeDescription
v1v1__init.sqlSQLInitial schema
v2v2__cron_jobs.sqlSQLCron jobs table
v3v3__rag_feedback.sqlSQLRAG feedback
v4v4__memory_category.sqlSQLMemory categories
v5v5__schedules.sqlSQLSchedules table
v6v6__todos.sqlSQLTo-do table
v7v7__cron_runs.sqlSQLCron run history
v8v8__schedules_status.sqlSQLSchedule status
v9v9__reminders.sqlSQLReminders table
v10v10__proactive_feedback.sqlSQLProactive message feedback
v11v11__node_origin.sqlSQLNode origin field
v12v12__watch_jobs.sqlSQLMonitor jobs
v13v13__recurrence.sqlSQLRecurring schedules
v14v14__watch_jobs_metadata.sqlSQLMonitor job metadata
v15v15__agent_turn_counter.sqlSQLAgent turn counter
v16v16__schedule_dedup_and_unique.pyPythonSchedule dedup + unique constraints
v17v17__cron_last_fired_and_feedback.sqlSQLCron last-fired + feedback
v18v18__broker.sqlSQLBroker layer
v19v19__broker_workshop.sqlSQLBroker workshop
v20v20__broker_local_package_dir.pyPythonBroker local package directory
v20v20__guild.sqlSQLGuild system
v21v21__broker_identity_assets.sqlSQLBroker identity assets
v21v21__guild_task_type.sqlSQLGuild task types
v22v22__broker_messages.sqlSQLBroker messages
v23v23__broker_member_caps.sqlSQLBroker member capabilities
v24v24__broker_approvals.sqlSQLBroker approvals
v25v25__flow_core.pyPythonFlow core tables
v26v26__flow_trace_index.sqlSQLFlow trace index
v27v27__guild_medal_images.sqlSQLGuild medal images
v28v28__broker_author_crl.sqlSQLBroker author CRL (certificate revocation)
v29v29__broker_trusted_authors.sqlSQLBroker trusted authors
v30v30__broker_room_federation.sqlSQLBroker room federation
v31v31__broker_thread_federation.sqlSQLBroker thread federation
v32v32__broker_message_federation.sqlSQLBroker message federation
v33v33__guild_task_federation.sqlSQLGuild task federation
v34v34__guild_medal_federation.sqlSQLGuild medal federation
v35v35__guild_ledger_federation.sqlSQLGuild ledger federation
v36v36__broker_user_consents.sqlSQLBroker user consents
v37v37__guild_scope_approvals.sqlSQLGuild scope approvals
v38v38__guild_task_lifecycle.sqlSQLGuild task lifecycle
v39v39__guild_merge_requests.sqlSQLGuild merge requests
v40v40__task_edit_history.sqlSQLTask edit history
v41v41__guild_modification_requests.sqlSQLGuild modification requests
v41v41__message_reactions.pyPythonMessage reactions
v42v42__guild_experience_reports.sqlSQLGuild experience reports
v42v42__guild_modification_votes.sqlSQLGuild modification votes
v43v43__guild_task_recall_votes.sqlSQLGuild task recall votes
v44v44__guild_scope_members.sqlSQLGuild scope members
v45v45__task_message_channels.sqlSQLTask message channels
v46v46__guild_task_tags.sqlSQLGuild task tags
v47v47__demo_accounts.sqlSQLDemo accounts
v48v48__flow_pause.sqlSQLFlow pause
v49v49__user_auth.sqlSQLUser authentication
v50v50__broker_role_requests.sqlSQLBroker role requests

Python migrations: v16, v20, v25, v41 are .py files — they allow complex data transformations (deduplication, index rebuilds, etc.). The runner extracts the version number from the v(\d+)__ prefix; if both .sql and .py files exist with the same name, .py takes priority.

§6.5 Path Self-Healing ​

6.5.1 The Problem ​

Absolute paths stored in dialogue trees become stale in these scenarios:

  1. Repo root drift — user moves the repo folder
  2. External directory aliases — user renames a data folder

If DialogueManager._load_existing_dialogues runs before path repair, the LLM's next turn reads stale paths as context, causing infinite ls loops or writing files to ghost locations.

6.5.2 The Solution ​

src/bridge/app.py calls align_all_paths() at import time, before route mounting:

python
# src/base/paths_align.py
def align_all_paths() -> dict:
    """Handle two kinds of path drift.
    
    Returns {"files_touched": int, "total_hits": int, "aliases_applied": [...]}
    """

Mechanism 1: Repo root drift (auto-detected)

Compares each dialogue.path prefix against the current _repo_root():

  • Mismatch → automatically rewrites dialogue.json + turn_*/plan.json + step_*.json
  • Recorded in workspace/.paths_aliases.json

Mechanism 2: External directory aliases (manually registered)

bash
python -m src.tools.paths_migrate <old> <new> --record
  • Run --dry-run first to preview
  • --record writes to .paths_aliases.json
  • Replayed on every startup
  • Each rewritten file is backed up as <path>.bak_<ts>

Coverage:

  • dialogue.json (node content + metadata.tool_calls args / results + attachments)
  • turn_*/plan.json + step_*.json

Never rewritten:

  • .bridge.log, audit.log — preserved as the authentic historical record

Idempotent: 0-hit files are skipped; stable boots have zero overhead.

§6.6 Encrypted Storage ​

6.6.1 SECRET.json ​

Credentials are encrypted and stored in workspace/SECRET.json. The encryption chain:

hostname + username + salt_bytes
    → HKDF (SHA-256, info="familiars-secrets-v1")
    → 32-byte Fernet key
    → AES-128-CBC + HMAC-SHA256
python
# src/base/crypto.py
_SALT_FILENAME = ".salt"
_KEY_INFO = b"familiars-secrets-v1"

def _derive_fernet_key() -> bytes:
    salt = _load_or_create_salt()   # os.urandom(32)
    identity = f"{platform.node()}|{os.getenv('USER', ...)}"
    hkdf = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=_KEY_INFO)
    raw_key = hkdf.derive(identity.encode("utf-8"))
    return base64.urlsafe_b64encode(raw_key)

Security model:

  • Salt is randomly generated, not shared → without machine identity + salt, the key cannot be reconstructed
  • The salt file alone is useless without machine identity
  • Machine identity alone is useless without salt
  • The HKDF info string "familiars-secrets-v1" is a versioned crypto primitive → changing it invalidates all existing stored secrets

Next: 06 - Proactive Decision & Gifts →

Familiars · 多 Agent AI 桌面应用