Operations Runbook¶
Applies to: CPersona 2.5.x. This page is the canonical operations reference: backup, degradation detection, recall tuning, CJK guidance, and corpus indexing patterns. The behavior facts it relies on are contracts — see Behavior Contracts.
Backup and restore¶
The database is a single SQLite file (CPERSONA_DB_PATH) running in WAL
mode. A plain cp of a live WAL database can straddle a checkpoint and
produce a corrupt copy — do not script one.
Recommended, in order:
- Online physical backup (first choice — safe while the server runs):
sqlite3 "$CPERSONA_DB_PATH" ".backup 'cpersona-backup.db'"
# or
sqlite3 "$CPERSONA_DB_PATH" "VACUUM INTO 'cpersona-backup.db'"
Both produce a consistent snapshot under concurrent writes.
-
Offline copy: stop the server, then copy the
.dbtogether with its-waland-shmsiblings. -
Logical backup (recommended as a low-frequency complement):
export_memorieswrites JSONL that is independent of the schema version, andimport_memoriesis idempotent (duplicates are skipped) — so restore drills are safe to rehearse. Monthly is a reasonable cadence.
The .db is not the whole instance. State lives outside it that none of
the backup forms above touch:
<CPERSONA_DB_PATH>.calibration.json— per-agent vector thresholds, gate state, scoring version, and the per-agentset_recall_precisionbetas. Restore without it and the measurements repair themselves: with the defaultCPERSONA_CALIBRATE_ON_MODEL_CHANGE=true, a startup with no sidecar recalibrates rather than falling back, anddeep_checkreportsnever_calibratedif it did not. The betas do not. They are policy inputs, not measurements — nothing re-derives a preference you stated — so an operator who tuned precision loses that tuning silently and the recalibration measures every gate at the default beta. Copy the sidecar beside the database, or re-applyset_recall_precisionafter a restore.~/.cpersona/operating-context.toml(orCPERSONA_OPERATING_CONTEXT_PATH) — the operator instructions served to every client.- The ACL file, if
CPERSONA_ACL_FILEis set — restoring without it changes who may connect. - The external vector index, under
CPERSONA_VECTOR_SEARCH_MODE=remotewithCPERSONA_STORE_BLOB=false. In that configuration the embeddings exist only in the remote index, so restoring the database and all three files above still comes back with no vector arm.check_healthreports it asno_local_vector_fallback; back the index up with the same schedule, or keepSTORE_BLOB=trueso the.dbstays self-sufficient.
Keep the live database outside cloud-sync folders (Dropbox, Drive, etc.): sync clients interact badly with SQLite WAL files. Sync the backups instead.
Detecting a dead embedding server¶
Vector search is the strongest retrieval layer, and it degrades silently at the network boundary if nothing watches for it. Three detection surfaces exist, and the calling agent should be instructed to watch the first:
advisoryon recall responses (v2.4.33+, the primary surface). A state machine observes real embedding failures, and degraded recalls carryadvisory = {degraded, severity, reason, evidence, runbook, advisory_scope}— "the vector layer is down; serving FTS + keyword only". Instruct your agent to surface this field to the user and follow itsrunbook(usually: start or repoint the embedding server, then recall again). A shortened runbook means "you were already told";advisory_scopesays who "you" was —sessionwhen the process is yours alone,processon the HTTP transport, where the state is the whole server's. On a shared server a fault repeats its full runbook rather than assume your session saw it (bug-251). Opt out withCPERSONA_DEGRADED_ADVISORY=falseonly for a deliberate keyword-only deployment. Design: DEGRADED_ADVISORY_DESIGN.embeddedon store responses. Every write reports whether its embedding was persisted. Writes made while the server is down come backembedded: false— those rows are repairable (next item).check_health(agent_id, fix=true)detects rows stored with NULL embeddings and re-embeds them once the server is back. Caveat: when the embedding endpoint is unreachable at check time, the dimension check is skipped, not failed — there is currently no dedicated red check for "endpoint unreachable" itself, so do not read a greencheck_healthas proof the embedding server is up; theadvisorysurface is what watches liveness.
Tuning recall¶
The knobs, in the order you should reach for them:
set_recall_precision(agent_id, precision)— the main (and under the default fusion modes, effectively the only) policy knob. It moves the operating point β of the fused quality gate:strict=2.0(fewer contaminants, more misses),balanced=1.0(default),lenient=0.5(fewer misses, more contaminants). Takes effect immediately; no restart.calibrate_threshold(agent_id)— re-derives the gate positions from the corpus (no labels needed). Called with anagent_id(and the fused gate enabled), it calibrates both the vector threshold and the fused gate; called without one, it calibrates the vector threshold only. The value it prints is the vector-side threshold — under fusion modes the component actually cutting results is the fused gate. Re-run it after the corpus changes substantially, after a bulk (re)build, and after changing the embedding model.CPERSONA_AUTOCUT_MIN_RESULTS— autocut fires on similarity-scale signals: under confidence scoring, or on a homogeneous raw-cosine list (which is whatcascadeproduces, confidence on or off). It is deliberately inert underrsf/rrf(contract §6), so under the default configuration this knob does nothing — but it is the fusion mode that decides that, not the confidence flag.CPERSONA_FUSED_GATE_ENABLED=false— last resort. Without the gate, filtering falls back to the pool-size heuristic (_adaptive_min_score), which still rejects weak matches — it is a coarser gate, not an open door. What you give up is the operating point measured for this corpus.
Choosing a precision: the trade is asymmetric per use case. For
index-style corpora (recall feeds an AI that can discard irrelevant rows), a
miss is worse than a contaminant — run lenient for a few days and step
back to balanced only if contamination becomes a real cost. For
contamination-sensitive contexts (recall injected into a small window),
prefer balanced/strict.
When not to rely on recall¶
Probabilistic retrieval can lose. Facts whose absence from context causes
harm — the current top-priority decision, standing safety rules — belong in
a deterministically injected surface (CLAUDE.md, system prompt, an
index file your agent always loads), not primarily in memory. A useful
split: what should fire without being asked goes in deterministic
injection; what should be findable when asked goes in memory. Two
corollaries:
- Update decisions by overwrite, not append. When a decision changes,
update_memorythe old row (it re-embeds automatically). The most reliable way to stop a stale decision from winning recall is for it not to exist in the search space. lock_memoryprotects against loss, not against losing a ranking (contract §9).
Japanese and CJK corpora¶
- Set
CPERSONA_RECALL_MODE=rsf. FTS5 tokenizes CJK poorly; rsf keeps the keyword channel's score magnitude in the merge, which is the signal that compensates. No further CJK-specific settings exist. - Known characteristics of
jina-v5-nano— the model this project runs, and the one the reference embedding server downloads by default — on Japanese, confirmed by long-term production use: strong when at least one proper-noun / identifier anchor overlaps between query and memory; weak on pure concept matches with no shared vocabulary. Phrasing queries with a concrete anchor term ("keyword anchoring") is a correct adaptation to the current model, not a workaround to feel bad about. - The model slot is replaceable (
CEmbeddingprovider). If you swap the embedding model: full re-embed of the corpus, thencalibrate_threshold. The server auto-recalibrates on a dimension change, but a same-dimension model swap needs the manual recalibration.
Corpus indexing and sync patterns¶
CPersona's primary design center is memory that accrues from conversation. Using it as a search index over canonical Markdown files works, but two facts shape the correct pattern: CPersona is a passive server (no file watching — ingestion is always caller-driven), and dedup is skip, not upsert (contract §5).
Pattern A — rebuild the index as a disposable projection (recommended first).
- Give the index a dedicated
agent_id(e.g.md-index). Do not mix index chunks into a conversational agent's memory — it also keeps the episode boundary machinery out of the picture. - When the source documents change:
delete_agent_data(agent_id="md-index")→ re-storeevery chunk. - Re-run
calibrate_thresholdafter each rebuild (and re-applyset_recall_precisionif you use it):delete_agent_datadiscards that agent's calibration state along with its rows. If the rebuild is a nightly batch, the recalibration is part of the batch. - Cost: re-embedding a few thousand chunks on CPU is minutes to tens of
minutes (environment-dependent). What you buy: no diff logic, and an index
that matches the source once the rebuild finishes. It does not match
during one:
delete_agent_datacommits before the re-storebegins, so a recall issued inside that window sees a partial index or none at all. Run rebuilds when nothing is querying, or build under a secondagent_idand switch readers over when it is complete.
Pattern B — differential updates with an external content-hash ledger.
storeeach chunk with a stablemsg_id(e.g.path#heading) and the source document insource.id— thesource_idrecall argument (prefix-match) then doubles as a per-document filter.- Keep a caller-side ledger of
key → content hash; process only chunks whose hash changed. - Changed chunks must go through
update_memory(ordelete_memory+store) — re-storing changed content under the samemsg_idis silently skipped. Unchanged chunks may be re-submitted blindly; exact-match dedup guarantees that is harmless.
Prefer A until the corpus is large enough that rebuild time actually hurts: it has no ledger to corrupt and no drift mode.
Scale¶
Growth needs no archival or thinning routine. The vector layer scans a
recency window (CPERSONA_MAX_MEMORIES, default 10,000 — raise it via env
for large corpora; contract §4),
FTS and keyword channels reach the full history, and old rows sink through
windows and decay rather than being deleted.
Maintenance cadence¶
- Monthly:
check_health(agent_id, fix=true)— deterministic integrity checks + auto-repair;deep_check(agent_id, fix=true)for the semantic pass; a logicalexport_memoriesbackup. - After corpus upheaval (bulk import, rebuild, model change):
calibrate_threshold(agent_id). - Version upgrades: the schema migrates itself forward; calibration is fingerprinted to the scoring function and embedding dimension, and the server recalibrates at first boot when either changes (v2.5.2+).