Architecture Overview
September 17, 2026 · View on GitHub
Version: 1.5.x
Statewave is an open-source memory runtime for AI agents. It compiles raw events into ranked, token-bounded context bundles with full provenance — so your AI stops forgetting across sessions. Self-hosted on Postgres, no vendor lock-in.
Core loop
RECORD → COMPILE → CONTEXT → GOVERN
- Record — immutable episodes capture raw interaction truth
- Compile — pluggable compilers (heuristic or LLM) derive typed memories with provenance, embeddings, and conflict resolution
- Context — assembly service builds ranked, token-bounded, deterministic context bundles using temporal reasoning and semantic similarity, gated by the policy layer (memory
sensitivity_labels✕ caller identity → allow / deny / redact) before ranking - Govern — provenance inspection, delete-by-subject, authentication, rate limiting, webhook notifications, immutable state-assembly receipts per call (content-hash integrity, ULID-addressable), per-tenant configuration for emission / retention / enforce mode
Where do episodes come from? Either the SDKs (Python / TypeScript) or the Connectors — modular packages for GitHub, Markdown/ADRs, MCP-compatible agents, and more. Connectors normalize source events into the same episode shape Statewave records natively.
Component architecture
┌──────────────────────────────────────────────────────────────┐
│ FastAPI Server │
│ │
│ ┌─ Middleware Stack (execution order) ─────────────────────┐ │
│ │ CORS → RequestID → Auth → RateLimit → Tenant │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Episodes │ │ Memories │ │ Context │ │ Subjects │ │
│ │ Route │ │ Route │ │ Route │ │ Route │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
│ │ │ │ │ │
│ ┌────┴──────────────┴──────────────┴──────────────┴───────┐ │
│ │ Service Layer │ │
│ │ Compilers (heuristic | LLM) · Embeddings (stub|LiteLLM)│ │
│ │ ContextAssembler (ranked, semantic, temporal) │ │
│ │ ConflictResolver · Webhooks │ │
│ └────┬──────────────┬──────────────┬──────────────────────┘ │
│ │ │ │ │
│ ┌────┴──────────────┴──────────────┴──────────────────────┐ │
│ │ Repository / DB Layer │ │
│ │ episodes · memories · semantic search (pgvector) │ │
│ └──────────────────────┬──────────────────────────────────┘ │
└──────────────────────────┼───────────────────────────────────┘
│
┌──────┴──────┐
│ PostgreSQL │
│ + pgvector │
└─────────────┘
Key design decisions
- Thin routes, strong services — API handlers delegate to service functions
- Raw truth first — episodes are append-only, never mutated
- Compiled memory second — memories are derived, with provenance to source episodes
- Provenance everywhere — every memory links back to source episodes via
source_episode_ids - Idempotent compilation — recompiling sequentially is safe; only uncompiled episodes are processed. Concurrent compiles of one subject can currently duplicate memories (statewave#417)
- Token-bounded context — context bundles respect configurable token budgets
- Ranked retrieval — composite scoring: kind priority × recency × relevance × temporal validity
- Semantic search — pgvector cosine similarity with graceful fallback to text search
- Structured errors — consistent
{error: {code, message, details, request_id}}everywhere - Local-first —
docker compose up+pip installgets you running - Framework-neutral — no AI framework coupling in core
Middleware stack
Execution order (outermost to innermost):
- CORS — cross-origin headers
- RequestID — generate/propagate
X-Request-ID, bind to structlog - Auth — validate
X-API-Key(skipped when no key configured) - RateLimit — per-IP sliding window (skipped when RPM = 0)
- Tenant — extract
X-Tenant-ID, scope all queries to tenant (app-layer isolation)
Compilation pipeline
Uncompiled Episodes → Compiler → Raw Memories → Embedding → Conflict Resolution → Commit
- Compilers:
HeuristicCompiler(regex/pattern, no external deps) andLLMCompiler(any provider via LiteLLM, runs in thread pool to avoid blocking) - Embeddings:
StubEmbeddingProvider(deterministic hash vectors for dev/test) andLiteLLMEmbeddingProvider(real semantic vectors via LiteLLM — supports OpenAI, Azure, Cohere, Voyage, Bedrock, Ollama, and any other LiteLLM-compatible embedding model) - Conflict resolution: Jaccard similarity within same (subject, kind) groups; older memory superseded with
valid_toset - All steps execute in a single database transaction
Context scoring model
| Signal | Range | Source |
|---|---|---|
| Kind priority | 3–10 | profile_fact=10, procedure=8, episode_summary=5, raw_episode=3 |
| Recency | 0–5 | Linear scale: most recent = max |
| Task relevance | 0–5 (text) or 0–8 (semantic) | Word overlap or cosine similarity |
| Temporal validity | -4 to +3 | Currently valid = +3, expired = -4 |
In addition to the core signals above, support-agent workloads apply session, urgency, and repeat-issue adjustments. Scoring is deterministic and not user-configurable today — see Ranking & Retrieval for the full signal list and rationale.
Data model
| Entity | Description | Key fields |
|---|---|---|
| Episode | Immutable raw event | id, subject_id, source, type, payload, metadata, provenance, created_at, last_compiled_at |
| Memory | Derived typed memory | id, subject_id, kind, content, summary, confidence, valid_from, valid_to, source_episode_ids, status, embedding, sensitivity_labels |
| ContextBundle | Runtime output | subject_id, task, facts, episodes, procedures, provenance, assembled_context, token_estimate |
| Receipt | Immutable audit artifact for a /v1/context or /v1/handoff call | id (ULID), tenant_id, subject_id, mode, selected_entries, bundle_hash, policy (filters_applied, filters_skipped), parent_receipt_id, created_at |
| PolicyBundle | Content-hashed, immutable rule set governing memory access | id, tenant_id (nullable for global), bundle_hash, rules (predicates + actions), source_yaml, created_at |
| TenantConfig | Per-tenant operator knobs | tenant_id, receipts (always | on_request | never), receipt_retention_days, policy_mode (log_only | enforce), require_caller_identity, version |
Version history
Ordered newest first. See roadmap.md for the canonical list of shipped items per release.
v1.5 — Timeline paging & recent-first windows
- Timeline pagination —
GET /v1/timelinetakeslimit(1–200, default 100) andoffsetper collection and returnsepisodes_has_more/memories_has_more, computed by over-fetching one row instead of a separate count (statewave#362). newest_first— page from the most recent end: the window is selected from the newest rows,offsetcounts back from the newest row, and rows within a page stay in ascending chronological order (statewave#363). Ordering is a total order (timestamps, then row id) in both directions, so paging never skips or repeats rows that share a timestamp.- Resolutions pagination —
GET /v1/resolutionstakeslimit(default 50) andoffsetoverupdated_atdesc (statewave#361). - Runtime dependencies declared —
numpy(conflict/cluster admin endpoints) andhttpx(webhook delivery) were previously only transitive; pgvector 0.5.0 dropped numpy, leaving two admin endpoints failing in the published image. Both are declared now and an AST test guards the class (statewave#327). - Non-breaking — no schema migrations; the new query parameters default to the previous behaviour. SDKs: Python 1.5.0 and TypeScript 1.5.0 expose the paging surface.
v1.4 — Tenant scoping & operator resilience
- Tenant-scoped lookups — memory provenance,
/v1/contextsource-episode fetch, and admin bulk delete now key by(subject_id, tenant_id); global rows usetenant_id IS NULLexplicitly. Closes cross-tenant leakage windows that the single-tenant repository helpers left open on shared infra. - Admin ops surface —
POST /admin/jobs/reset-stuckrecovers orphaned running compile jobs;DELETE /admin/jobs/{id}closes the gap left by immortal terminal jobs; empty-subject stats return 200 instead of 404 so zero-data subjects don't break dashboards;DELETE /subjects/{id}succeeds when a subject has no episodes or memories. - Bootstrap resilience — support-docs rebuild now runs an async compile with build-then-swap, gated on the compile job's
memories_createdrather than the lagging subjects-list count. A failed rebuild no longer empties the live pack — the exact silent-fail mode called out in the operator runbook. - Non-breaking — no schema migrations; existing deployments upgrade in place.
search_modeadded to/v1/memories/search(opt-in),subject.deletedwebhook guarded against no-op fires, purge-event coverage extended.
v1.3 — hybrid retrieval & compile pipeline
- Hybrid retrieval —
/v1/memories/searchblends pgvector cosine similarity with Postgres BM25 (ts_rank_cdover a generatedcontent_tsvector), on by default; passhybrid=falsefor the pure-semantic path. An optional entity-boost lane and an LLM reranker are available, both off by default. - Compile pipeline — near-duplicate dedup before reconcile, context-aware reconcile with assistant-fact extraction, technical-spec/metric extraction, and full-conversation windowed compile with chunked reconcile improve recall on multi-hop and long-context retrieval.
- LLM layer — provider-aware API-key routing for mixed-vendor deployments, token usage + model returned from
/v1/llm/complete, and model-aware reasoning-effort / temperature handling. - Non-breaking — existing deployments are unaffected; migrations 0027/0028 add the BM25 tsvector column and the per-subject entity store.
v1.2 — admin API, dynamic settings & security hardening
- Admin API expansion — dynamic settings endpoint lets operators hot-reload topology-agnostic configuration without a restart; new production-readiness endpoint surfaces deployment health for orchestrators and load-balancer probes.
- Security hardening — SSRF blocked on webhook URL probe; DNS-rebinding TOCTOU closed so a probe result can no longer be hijacked between validation and use; LIKE metacharacters escaped in admin search and prefix filters to prevent wildcard injection.
- Admin UI inspection surface — retrieval simulator, activity timeline, memory provenance graph, and six new inspection endpoints back the operator console's inspector views.
- Starlette 1.3 compatibility — route-registration tests updated to handle
_IncludedRouterwrappers introduced in Starlette 1.3 / FastAPI 0.137.
v1.1 — idempotent ingest & reasoning-model compilation
- Idempotent episode ingest (#240) —
POST /v1/episodeshonours a client-suppliedidempotency_key: re-ingesting an episode with the same key (re-running a connector seed, retrying a request) returns the existing episode instead of inserting a duplicate. Backed by a partial unique index on(tenant_id, subject_id, idempotency_key)(migration 0025,NULLS NOT DISTINCT); keyless episodes are never de-duplicated. Both SDKs expose the optional key oncreate_episode/createEpisode. - Reasoning-model compilation (#240) — the LLM compiler's output budget is now a configurable ceiling (
STATEWAVE_LITELLM_COMPILE_MAX_TOKENS, default 16000) floored per call, so reasoning models (which spend budget on hidden reasoning tokens before the JSON) no longer truncate into invalid output. The readiness ping tolerates the same output-limit signal. - NUL-byte sanitization (#240) — compiled memory text strips NUL (U+0000), which Postgres text columns reject, so a single stray byte in generated content can no longer fail a whole compile batch.
v1.0 — first stable public developer release
- Stable
/v1API contract — the/v1/*surface and the v0.9 governance layer (HMAC-signed receipts, receipt-driven replay, sensitivity labels + declarative policy, opt-in detector-suggested labels, per-region residency) are now stable for developer use under a self-hosted model. Backward-compatible additions only from here; carried-forward limitations stay documented in why-statewave.md. - Both SDKs to v1.0.0 —
statewave(PyPI) and@statewavedev/sdk(npm) cut their first stable releases alongside the server, typed surfaces matching the REST contract, semver-stable from 1.0.0 forward. - Python SDK governance helpers (#176) —
list_suggested_labels()/promote_suggested_labels()wrap the v0.9 suggested-label review surface (sync + async, typed result models). - Public version-discovery endpoint (#178) — unauthenticated
GET /v1/versionreports the running server version. session_idoncreate_episode(#174) — both SDKs forward the optional session pin on the wire.- Webhook delivery stats + tenant scoping — optional tenant filter on event-status queries and per-tenant delivery statistics; permanent 4xx deliveries dead-letter instead of retrying.
v0.9 — Replay, Signing, & Auto-Labeling
- Scheduled retention-purge worker (#156) — hourly worker reads
tenant_configs.config.receipt_retention_daysand tombstones expired receipts. Soft-delete only; rows persist for forensic lookup. Closes the loop on the retention surface v0.8 reserved. Migration 0020. - HMAC signing for receipts (#157) —
hmac-sha256-canonical-v1over the canonical body. Operator-provided keys viaSTATEWAVE_RECEIPT_SIGNING_KEYS, never persisted to DB. Per-tenant active key viatenant_configs.config.receipt_signing_key_id.GET /v1/receipts/{id}/verifyreturns{valid, key_id, algorithm, reason}with constant-time compare. Pre-v0.9 receipts verify cleanly asno_signature. Migration 0021. - Compiler heuristic auto-labeling (#158) — opt-in
STATEWAVE_AUTO_LABELING_ENABLED. Detectors stamp advisorysuggested_labels, strictly separate from authoritativesensitivity_labels. First wave:pii.email,pii.phone,financial.card(Luhn),secret.token. Migration 0022 (GIN-indexed). - Receipt-driven replay (#159) — every v0.9+ receipt embeds the active bundle's YAML (
policy_snapshot).POST /v1/receipts/{id}/replayre-runs against current memories with the original policy and returns a structural diff envelope. Modeas_of_replay; child receipts link to the parent. Semantic: current code + original policy. Migration 0023. - Operator promote endpoint + admin UI (#160) —
POST /admin/memories/{id}/promote-labelsis review-only, with audit-trail entries onmemory.metadata.label_promotions. Admin app/suggested-labelspage + receipt-detail replay button rendering the diff envelope inline. - Per-tenant data residency (#161) — per-region deployment + metadata-pinned tenants.
STATEWAVE_REGION+tenant_configs.config.region. Hard application-layer enforcement on/v1/AND/admin/(total isolation). HTTP 403residency.mismatchon conflict. Receipts stampregionfor end-to-end audit.
v0.8 — Governance & Audit
- State-assembly receipts (#49) —
/v1/contextand/v1/handoffcan emit an immutable, ULID-addressable receipt of which memories + episodes shaped the bundle, with a SHA-256 hash of the bytes delivered. Strict-superset schema with amodediscriminator (retrievalships;as_of_replay/eval_runreserved for v0.9). Emission gated by env kill-switch → per-tenant config (always | on_request | never) → per-request flag. Read API in both SDKs and the admin app. - Sensitivity labels + per-memory policy bindings (#50) —
memories.sensitivity_labels TEXT[]+ GIN index, set viaPATCH /v1/memories/{id}/labels. Declarative YAML/JSON policy bundles (content-hashed, immutable) with six predicates anddeny/redactactions; first-match-wins, default-allow. Per-tenantpolicy_mode: log_only | enforcefor safe rollout —log_onlyrecords decisions into receipts without filtering. - Caller identity —
caller_id/caller_typeon/v1/contextand/v1/handofffeed the policy evaluator; tenant configrequire_caller_identity: true401s anonymous calls. - Per-tenant configuration endpoint —
GET / PATCH /admin/tenants/{tenant_id}/configfor receipts emission, retention, policy mode, and caller-identity gate. PATCH-shape merge, enum/bound validation at the API boundary, optimistic concurrency viaexpected_version. - Cross-tenant policy bundle uniqueness (#79) —
policy_bundleskeyed on(tenant_id, bundle_hash) NULLS NOT DISTINCT; two tenants installing identical YAML produce independently-resolvable rows. - Connector ecosystem — modular packages for GitHub, Markdown/ADRs, MCP, Slack, Discord, Zendesk, Intercom, Freshdesk, Notion, Gmail, n8n, Zapier. Tier 2 push receivers (Slack DM/MPIM, Freshdesk/Zendesk/Intercom webhooks, Gmail Pub/Sub) and Tier 3 operator productization (TOML config, hosted runner, persistent state adapters, auth-gated Prometheus
/metrics, Docker/Compose/Helm/Fly/Railway recipes) shipped viastatewave-connectors.
v0.7 — Operator & Cloud Experience
- Single LiteLLM adapter —
server/services/llm.pyis the only module that imports LiteLLM; compilers, embeddings, and the readiness check route through it. Provider swaps are config-only viaSTATEWAVE_LITELLM_*. AST-based isolation test enforces the boundary. - Native pgvector retrieval —
memories.embeddingmigrated tovector(1536)with an HNSW index;search_memories_by_embeddinguses the<=>cosine-distance operator. Eliminates the in-Python cosine compute that floored/v1/contextat ~1.5s. /v1/contextcandidate-pool union — semantic-search rows enter the per-kind candidate pool alongside recency rows, so semantically-relevant memories outside the recency window can rank.- Two-layer query embedding cache — in-process LRU+TTL (L1) + Postgres-backed
query_embedding_cache(L2), shared across all backend instances. Repeat queries hit sub-second regardless of which instance handles them. - Deep readiness checks —
/readyzverifies DB, job queue, and LLM provider reachability with typed errors and per-check latency. - Migration safety — preflight script, startup schema guard,
/ops/migrationsendpoint, runbook. - Admin dashboard (read-only) — system health, jobs, webhooks, counts, health distribution.
- Usage metering — episodes/month, compiles/month, per-tenant.
v0.6 — Support-Agent Superiority
- Session-aware context assembly (active session boosted, resolved deprioritized)
- Resolution tracking (open/resolved/unresolved per session)
- Handoff context packs (structured escalation briefs with health + SLA)
- Customer health scoring (0–100, explainable factors)
- Repeat-issue detection (prior resolution surfacing)
- Proactive health alerts (webhooks on state transitions)
- SLA tracking (response time, resolution time, breach flags)
- Proof layer: 3 eval suites (56 assertions), support-workflow benchmark (8 criteria, open harness)
v0.5 — Reliability & Trust
- True multi-tenant isolation (tenant_id on all tables, query-scoped)
- Distributed rate limiting (Postgres-backed)
- Backup/restore tooling (subject-level export/import)
- Durable async compilation (Postgres-backed job queue)
- Reliable webhook delivery — persistent queue, exponential backoff, dead-letter
- SDK retry with backoff — automatic 429/5xx retry with jitter
- Admin introspection endpoints (jobs, webhooks, tenant audit)
- Compilation status API
v0.4 — Adoption Readiness
- Batch episode ingestion (up to 100 per request)
- OpenTelemetry tracing (optional)
- Deployment guide (Docker, Fly.io, Railway)
- SDK publish readiness, getting-started guide
- Support-agent benchmark and "Why Statewave" comparison doc
v0.3.5 stabilization
- Fixed middleware execution order (auth before rate limit)
- Compile + conflict resolution in single transaction
- Request validation (string lengths, bounded limits)
- LLM compiler runs in ThreadPoolExecutor (non-blocking)
- SDKs support auth, tenant headers, and semantic search
v0.3 additions
- LLM-backed memory compiler (any provider via LiteLLM, thread-pooled)
- Embedding generation (LiteLLM + stub providers)
- Semantic search via pgvector cosine similarity with fallback
- Temporal reasoning in context assembly
- Memory conflict resolution (Jaccard similarity, auto-supersede)
- Webhooks (episode.created, memories.compiled, subject.deleted)
- API key authentication, rate limiting (per-IP)
- Multi-tenant header extraction
v0.2 additions
- Pluggable
BaseCompilerprotocol - Ranked retrieval with composite scoring
- Request ID middleware, structured errors, health endpoints
- SDKs with typed exceptions (Python + TypeScript)