CyberCLI

← All engines · Storage

LifecycleStore

stable

v1.0.0 · last updated 2026-06-06 · source: cybercli.lifecycle.store.LifecycleStore

§1

What it does

SQLite-WAL store that holds every Signal, Case, TriageDecision, and ActionItem — the operator-visible state the dashboard reads from and the engine writes to with sqlite-busy retries that survive concurrent ingest + dashboard contention.

§2

Why you care

The LifecycleStore is the engine that turns 'what's happening right now' into queryable, durable state. Every signal on /signals, every case on /cases, every action on /actions — they all come from this store. The audit log is the cryptographic receipt; the lifecycle store is the operator's live working memory.

Other AI security products either lock you to their cloud database (Sentinel, Charlotte AI) — your operational state is theirs to lose — or ship with a hand-rolled JSON-file store that breaks under any real load (some Wazuh-derived stacks). CyberCLI's store is plain SQLite-WAL, sits on your engine box, gets backed up by `cybercli backup`, and survives the kinds of concurrent-write storms a real install produces.

The store is also what makes the dashboard's live tail safe. Multiple browser tabs polling the same endpoints, multiple ingest workers writing in parallel, the AckLoop ticking, the engine restarting — all of it lands on the same SQLite file with WAL-mode and sqlite-busy retry wrappers. Operators don't see 'database is locked' errors; the engine swallows the transient contention and retries.

§3

How it works

On construction the store opens `lifecycle.sqlite3` under the data dir with `PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000`. WAL gives concurrent reads + one writer without serializing the readers; synchronous=NORMAL is a deliberate durability-vs-throughput call (operators who want fsync-on-commit set `PRAGMA synchronous=FULL` via the config override).

Every write method (`upsert_signal`, `upsert_case`, `record_triage_decision`, `upsert_action_item`) wraps the SQL call in a `_retry_on_busy()` helper that catches `sqlite3.OperationalError: database is locked` and retries with exponential backoff up to 3 attempts. A 4th failure raises `LifecycleStoreError` which the caller surfaces as a 503 (engine retry-able) rather than a silent drop.

The schema is the SQLite source-of-truth for the dashboard's REST API. `/v1/signals` reads from the `signals` table, `/v1/cases` from `cases`, `/v1/triage` from `triage_decisions`, `/v1/actions` from `action_items`. The dashboard never reads from the audit log — the audit chain is for forensic replay, the lifecycle store is for present-tense operator state.

Backup is a one-shot copy. `cybercli backup` runs `sqlite3 lifecycle.sqlite3 ".backup target.sqlite3"` which produces a consistent snapshot even under concurrent writes (SQLite's online backup API handles the page-by-page copy). Restore is the inverse — overwrite the running file under a stop-engine guard. Both are CLI verbs, both are dashboard buttons.

§4

Configure & observe

CLI

$ cybercli doctor

Spot-checks `lifecycle.sqlite3` exists, is WAL-mode, is readable, and reports the file size (a 'is this growing as expected' check).

$ cybercli backup

Take a consistent online backup of the lifecycle store (and the audit chain). Writes to a timestamped tarball under the backup dir.

$ cybercli restore

Restore from a backup tarball. Stops the engine, overwrites the lifecycle.sqlite3 + audit.db, restarts. Logs the restore as an audit row before stopping.

Dashboard surfaces

/signals

Reads the signals table. Every row in the live tail comes from one row in this store.

/cases

Reads the cases table with a join to signals + triage_decisions for the per-case timeline.

/actions

Reads action_items table. The bridge's QUEUED_FOR_APPROVAL outcomes land here; operator approve/reject writes back here.

Audit row shape

Outcomes emitted
signal_recordedcase_openedcase_closedtriage_decision_recordedaction_item_state_changed
Parameters block

Each row carries parameters._id and parameters.previous_state + parameters.new_state for state transitions. The lifecycle store does NOT own the entity payloads — those live in the audit log's payload block + the evidence lake — so the lifecycle audit rows are intentionally thin.

§5

Status

Stable — the schema has been frozen since 2026-04-22 with two no-op migrations since then (adding indexes). The store survives the 1000-signal/sec stress test in `tests/perf/test_lifecycle_concurrency.py` with no busy-retry overflow.

Known limit: no built-in cross-host replication. Multi-host HA for the lifecycle store ships with v1.5 + the Sovereign federation engine; until then a single SQLite file is the source of truth, and `cybercli backup` is the disaster-recovery story.

Known limit: long-lived cases accumulate ever-growing triage_decisions joins. A case open for 6 months with daily triage decisions has ~180 join rows; query latency stays under 10ms but the table grows linearly. Triage-decision pruning rules ship in v1.x.

How this engine talks to the rest

Upstream · feeds this engine
Downstream · this engine feeds