Status: Active | Last updated: 2026-03-11
System-level resilience design. Supersedes the tactical patterns in
genesis-v3-resilience-patterns.mdfor architectural decisions; that document remains the building-blocks reference.
Genesis runs in a container where only Python + SQLite are guaranteed local. Everything else — LLM providers, Qdrant, Ollama, external APIs — may be cloud or network-adjacent and therefore subject to outage, latency spikes, rate limits, or complete unavailability.
The resilience layer must:
- Detect partial and total failures across independent axes.
- Defer work that cannot be completed now without losing it.
- Queue embedding operations for replay on recovery.
- Track Claude Code session budgets and throttle proactively.
- Orchestrate recovery in the correct order when services return.
- Notify the user through whatever channel remains available.
The legacy DegradationLevel enum (L0–L5) collapses multiple failure dimensions
into a single ordinal. This makes it impossible to express "cloud LLMs are fine
but embeddings are down" without overloading semantics.
| Axis | Enum | States |
|---|---|---|
| Cloud LLM | CloudStatus |
NORMAL → FALLBACK → REDUCED → ESSENTIAL → OFFLINE |
| Memory | MemoryStatus |
NORMAL → FTS_ONLY → WRITE_QUEUED → DOWN |
| Embedding | EmbeddingStatus |
NORMAL → QUEUED → UNAVAILABLE |
| Claude Code | CCStatus |
NORMAL → THROTTLED → RATE_LIMITED → UNAVAILABLE |
Each axis is updated independently. The composite ResilienceState is the
product of all four axes plus a timestamp and transition history.
If the same axis transitions more than 3 times within a 15-minute window, the state machine holds the worse state for a 10-minute stabilization period. During stabilization, transitions to better states on that axis are suppressed; transitions to worse states are always accepted.
For backward compatibility, ResilienceState maps to a single
DegradationLevel via to_legacy_degradation_level():
cloud=NORMAL/FALLBACK→NORMAL/FALLBACKcloud=REDUCED→REDUCEDcloud=ESSENTIAL/OFFLINE→ESSENTIALmemory=FTS_ONLY/WRITE_QUEUED/DOWN→MEMORY_IMPAIRED(overrides if worse)embedding=QUEUED/UNAVAILABLE→LOCAL_COMPUTE_DOWN(overrides if worse)
Implementation: src/genesis/resilience/state.py
| Failure Mode | Detection | Response |
|---|---|---|
| Single provider failure | Circuit breaker opens | Route to next in chain; no user impact |
| Multiple provider failure | ≥2 breakers open | cloud=REDUCED; defer non-essential work |
| All cloud down | All providers open | cloud=OFFLINE; defer all LLM work; notify user |
| CC rate limits | 429 / budget exhaustion | cc=THROTTLED→RATE_LIMITED; defer background sessions |
| Full internet outage | All external calls fail | All axes degrade; local-only operation |
| SLM/embeddings down, heavy LLMs fine | Ollama unreachable, cloud OK | embedding=UNAVAILABLE; queue embeddings; awareness ticks skip SLM |
| Full chain exhaustion | Dead letter overflow | cloud=ESSENTIAL; critical-only operation |
A SQLite table (deferred_work_queue) stores Genesis-level work items that
cannot be completed due to degraded state.
See src/genesis/db/schema/_tables.py for DDL. Key fields:
work_type: categorizes the work (e.g.,reflection,outreach_draft,morning_report)priority: integer 10–100, lower = higher prioritystaleness_policy: one ofdrain,refresh,discard,ttlstaleness_ttl_s: seconds before TTL-policy items expire
| Policy | Behavior |
|---|---|
drain |
Always process on recovery, regardless of age |
refresh |
Discard stale item; re-derive from current state on recovery |
discard |
Drop if not processed within a reasonable window |
ttl |
Drop if staleness_ttl_s has elapsed since deferred_at |
| Name | Value | Use Case |
|---|---|---|
FOREGROUND |
10 | User-initiated work |
URGENT_OUTREACH |
20 | Blocker/alert outreach |
REFLECTION |
30 | Reflection sessions |
SCHEDULED |
40 | Scheduled tasks |
MEMORY_OPS |
50 | Memory maintenance |
OUTREACH_DRAFT |
60 | Non-urgent outreach |
MORNING_REPORT |
70 | Morning report generation |
SURPLUS |
80 | Surplus/brainstorm work |
Implementation: src/genesis/resilience/deferred_work.py
When embedding providers (Ollama, Mistral) are unavailable, content that needs
embedding is written to pending_embeddings rather than dropped.
See src/genesis/db/schema/_tables.py. Each row captures the raw content, memory type,
target collection, and tags needed to embed and upsert on recovery.
EmbeddingRecoveryWorker drains the pending queue at a configurable pace
(default 10/min) to avoid overwhelming a freshly-recovered embedding provider.
On individual item failure, the worker marks that item failed and continues.
Implementation: src/genesis/resilience/embedding_recovery.py
Claude Code sessions are rate-limited by Anthropic. Genesis must:
- Track active sessions, completions, and rate-limit responses.
- Prioritize foreground > background reflection > background tasks.
- Throttle by deferring low-priority background sessions when approaching limits.
- Back off on 429s with exponential delay.
The CCStatus axis reflects current CC availability. When RATE_LIMITED,
only foreground sessions proceed; all background work is deferred.
When a degraded axis returns to a better state, recovery follows this sequence:
- Health confirmation — Verify the service is genuinely back (not just one successful call). Require 3 consecutive successes before upgrading state.
- Staleness expiry — Run
expire_stale()on the deferred work queue to drop items whose policies say they're no longer relevant. - Embedding drain —
EmbeddingRecoveryWorker.drain_pending()processes queued embeddings at controlled pace. - Deferred work drain — Process remaining deferred work items in priority order.
- Dead-letter replay — Replay items from
dead_lettertable (existing infrastructure ingenesis.routing.dead_letter). - Catch-up tick — Fire an out-of-cycle awareness tick to re-assess signals with restored capabilities.
When Genesis enters a degraded state, it needs to notify the user even if the primary conversation channel is unavailable.
Write a JSON status file to ~/.genesis/status.json on every state transition.
Any local tool can read this. Fields: current state, transition history,
deferred work count, pending embeddings count, timestamp.
If configured, POST state transitions to a webhook URL. Fire-and-forget with
3 retries. Configuration via GENESIS_STATUS_WEBHOOK_URL env var.
The health-mcp server (Phase 8) reads resilience state and exposes it via MCP tools for the Agent Zero dashboard.
Handled by the stabilization mechanism in §2.2. The 10-minute hold prevents rapid state oscillation from triggering repeated recovery cycles.
One axis recovers while others remain degraded. Recovery orchestration runs only for the recovered axis's dependent work. Other deferred work stays queued.
If deferred_work_queue exceeds 1000 pending items, begin aggressive staleness
expiry: treat refresh and discard policies as immediate-expire. Log a
warning. This prevents unbounded queue growth during extended outages.
Deferred outreach items include a content hash. On drain, check
outreach_history for a matching hash before sending. Skip duplicates.
Morning reports have refresh staleness policy. If the outage spans the
morning window, the deferred report is discarded and a fresh one is generated
from current state on recovery.
If SQLite writes fail with SQLITE_FULL, Genesis enters a hard-degraded state.
Log the error, attempt to clear WAL checkpoint, and notify via stderr. No
deferred work can be queued — this is a last-resort scenario.
| File | Purpose |
|---|---|
src/genesis/resilience/__init__.py |
Package init, public re-exports |
src/genesis/resilience/state.py |
Composite state machine |
src/genesis/resilience/deferred_work.py |
DeferredWorkQueue class |
src/genesis/resilience/embedding_recovery.py |
EmbeddingRecoveryWorker |
src/genesis/db/crud/deferred_work.py |
Deferred work CRUD |
src/genesis/db/crud/pending_embeddings.py |
Pending embeddings CRUD |
| File | Change |
|---|---|
src/genesis/db/schema/_tables.py |
Add deferred_work_queue and pending_embeddings tables + indexes |
src/genesis/observability/types.py |
Add RESILIENCE to Subsystem enum (if needed) |
| File | Relationship |
|---|---|
src/genesis/routing/types.py |
DegradationLevel — legacy mapping target |
src/genesis/routing/dead_letter.py |
Dead letter queue — replayed during recovery |
src/genesis/memory/embeddings.py |
EmbeddingProvider — used by recovery worker |
src/genesis/memory/linker.py |
MemoryLinker — optional auto-linking on embed recovery |
src/genesis/qdrant/collections.py |
upsert_point() — vector storage target |
src/genesis/observability/events.py |
GenesisEventBus — emits resilience events |
The resilience logic lives entirely in src/genesis/resilience/. The health-mcp
server (built in Phase 8) is a consumer of resilience state:
- Reads
ResilienceStateMachine.currentfor dashboard display. - Exposes
get_resilience_stateandget_deferred_work_statsas MCP tools. - Does NOT modify resilience state — that's the responsibility of the routing layer and awareness loop.
This separation ensures resilience logic is testable without MCP infrastructure and that health-mcp remains a thin read-only view.
Cross-reference: genesis-v3-resilience-patterns.md for tactical patterns
(circuit breakers, retry policies, fallback chains) that this architecture
builds upon.
- genesis-v3-resilience-patterns.md — Tactical resilience patterns
- genesis-v3-self-healing-design.md — Self-healing server extension
- genesis-v3-survivable-architecture.md — Survivability under degradation