* docs: add comprehensive subdirectory CLAUDE.md files and update root The repo has grown significantly. This adds module-level CLAUDE.md files for the five most complex subsystems, and updates the root CLAUDE.md to reflect the actual current state of the codebase. New files: - src/agent/CLAUDE.md — full module map (19 files), session/thread/turn model, agentic loop flow, compaction strategies with correct thresholds, scheduler invariants, self-repair details, complete submission command reference table - src/channels/web/CLAUDE.md — complete API route table (50+ endpoints), SSE event type reference, auth/rate limiting gotchas, connection limits, CORS headers, step-by-step endpoint addition guide - src/db/CLAUDE.md — dual-backend build commands, sub-trait structure (7 sub-traits, ~67 methods), SQL dialect differences, boolean/timestamp gotchas, complete schema table, in-memory test helper, shared handle pattern - src/llm/CLAUDE.md — corrected LlmProvider trait signatures, provider chain decorator order, NEAR AI dual-auth and session renewal details, circuit breaker thresholds, previously undocumented smart_routing.rs and recording.rs - tests/e2e/CLAUDE.md — conftest fixtures and async scoping, environment injected into the binary, mock_llm canned responses, writing guide with correct asyncio usage, gotchas section Root CLAUDE.md updates: - Added E2E test setup and integration test commands - Documented ~15 undocumented modules: cli/, registry/, hooks/, tunnel/, observability/, webhook_server.rs, cost_guard.rs, job_monitor.rs, etc. - Corrected libSQL backend path (libsql/ directory, 8 sub-modules) - Updated Database trait method count (~67, split across 7 sub-traits) - Fixed stale references: config.rs → config/channels.rs, main.rs → app.rs - Added Hook, Observer, Tunnel traits to extensibility section - Added tunnel and observability env vars to Configuration section - Removed resolved TODO (webhook trigger is now shipped) - Added Module Specifications entries for all 5 new CLAUDE.md files Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * docs: address PR review comments and reduce CLAUDE.md size - Fix 7-sub-trait count (was 6) and ~78 async methods (was ~60/~67) in both CLAUDE.md and src/db/CLAUDE.md - Add missing types.rs to secrets/ file tree (CLAUDE.md) - Add missing tls.rs to src/db/CLAUDE.md Files table - Fix method counts: ConversationStore 12, JobStore 13, RoutineStore 15 - Add Windows venv activation note to E2E setup commands - Collapse agent/, web/, llm/, db/ file trees to one-liners (detail lives in their respective CLAUDE.md files) - Replace verbose Database and LLM Providers sections with summaries linking to src/db/CLAUDE.md and src/llm/CLAUDE.md - Root CLAUDE.md: 43,868 → 35,270 chars (fixes >40k perf warning) [skip-regression-check] Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]>
9.1 KiB
Database Module
Dual-backend persistence layer. All new persistence features must support both backends.
Quick Reference
# Default build (PostgreSQL)
cargo build
# libSQL/Turso build
cargo build --no-default-features --features libsql
# Both backends
cargo build --features "postgres,libsql"
# Test each backend in isolation
cargo check # postgres (default)
cargo check --no-default-features --features libsql # libsql only
cargo check --all-features # both
Files
| File | Role |
|---|---|
mod.rs |
Database supertrait + 7 sub-traits (~78 async methods total) — add new ops here first |
postgres.rs |
PostgreSQL backend — delegates to Store + Repository in history/ |
libsql/mod.rs |
libSQL/Turso backend struct, connection helpers, row parsing utilities |
libsql/conversations.rs |
ConversationStore impl |
libsql/jobs.rs |
JobStore impl |
libsql/sandbox.rs |
SandboxStore impl |
libsql/routines.rs |
RoutineStore impl |
libsql/settings.rs |
SettingsStore impl |
libsql/tool_failures.rs |
ToolFailureStore impl |
libsql/workspace.rs |
WorkspaceStore impl (FTS5 + vector search) |
libsql_migrations.rs |
Consolidated libSQL schema (CREATE IF NOT EXISTS, no ALTER TABLE) |
tls.rs |
TLS connector factory for PostgreSQL (rustls + system root certs) |
PostgreSQL schema: migrations/V1__initial.sql through V9__flexible_embedding_dimension.sql (managed by refinery). V1 is the base schema; later migrations add tables, columns, and rename claude_code_events → job_events.
Trait Structure
The Database supertrait is composed of seven sub-traits. Leaf consumers can depend on the narrowest sub-trait they need rather than the full Database:
| Sub-trait | Methods | Covers |
|---|---|---|
ConversationStore |
12 | Conversations, messages |
JobStore |
13 | Agent jobs, actions, LLM calls, estimation |
SandboxStore |
13 | Sandbox jobs, job events |
RoutineStore |
15 | Routines, routine runs |
ToolFailureStore |
4 | Self-repair tracking |
SettingsStore |
8 | Per-user key-value settings |
WorkspaceStore |
13 | Memory documents, chunks, hybrid search |
Database adds run_migrations() and combines all sub-traits.
Adding a New Persistence Operation
- Decide which sub-trait the method belongs to, or create a new sub-trait
- Add the async method signature to that sub-trait in
mod.rs - Implement in
postgres.rs(delegate toStoreorRepository) - Implement in
libsql/<module>.rs(SQLite-dialect SQL, useself.connect().await?per operation) - Add migration if needed:
- PostgreSQL: new
migrations/VN__description.sql - libSQL: add
CREATE TABLE IF NOT EXISTStolibsql_migrations.rs
- PostgreSQL: new
SQL Dialect Differences
| Feature | PostgreSQL | libSQL |
|---|---|---|
| UUIDs | UUID type |
TEXT |
| Timestamps | TIMESTAMPTZ |
TEXT (ISO-8601 RFC 3339 with ms precision) |
| JSON | JSONB |
TEXT |
| Numeric/Decimal | NUMERIC |
TEXT (preserves rust_decimal precision) |
| Arrays | TEXT[] |
TEXT (JSON-encoded array) |
| Booleans | BOOLEAN |
INTEGER (0/1) |
| Vector embeddings | VECTOR (any dim, V9 removed fixed 1536) |
F32_BLOB(1536) via libsql_vector_idx |
| Full-text search | tsvector + ts_rank_cd |
FTS5 virtual table + sync triggers |
| JSON path update | jsonb_set(col, '{key}', val) |
json_patch(col, '{"key": val}') |
| PL/pgSQL | Functions | Triggers (no stored procs in SQLite) |
| Connection model | deadpool-postgres connection pool |
New connection per operation (self.connect()) |
| Concurrency | Pool-based, fully concurrent | WAL mode + 5 s busy timeout; write serialized |
| Auto-timestamp | DEFAULT NOW() |
DEFAULT (datetime('now')) |
| Timestamp parsing | Native type | Multi-format fallback in parse_timestamp() |
JSON merge patch gotcha: libSQL uses RFC 7396 JSON Merge Patch (json_patch) for metadata updates. This replaces top-level keys entirely — it cannot do partial nested updates. PostgreSQL uses jsonb_set which is path-targeted. Don't rely on partial nested metadata updates if you need libSQL compat.
Boolean storage: libSQL stores booleans as integers. When reading, use get_i64(row, idx) != 0; when writing, pass 1i64/0i64. Never pass a Rust bool directly.
Timestamp write format: Always write timestamps with fmt_ts(dt) (RFC 3339, millisecond precision). Read with get_ts() / get_opt_ts() which handle legacy naive formats too.
Vector dimension: PostgreSQL V9 migration changed the column to unbounded vector (removing the HNSW index). libSQL still uses F32_BLOB(1536) — if you use a different-dimension embedding model, the libSQL schema needs updating too.
Connection per operation: LibSqlBackend::connect() creates a fresh connection for every operation, sets PRAGMA busy_timeout = 5000, and closes it when the Connection is dropped. This is intentional — the libSQL SDK does not offer a pool. Avoid holding connections open across await points.
Schema: Key Tables
Core:
conversations— multi-channel conversation trackingconversation_messages— individual messages within a conversationagent_jobs— job metadata and statusjob_actions— event-sourced tool executionsjob_events— sandbox job streaming events (renamed fromclaude_code_eventsin V7)dynamic_tools— agent-built toolsllm_calls— cost/token trackingestimation_snapshots— learning datarepair_attempts— self-repair action log (not exposed via Database trait yet)
Workspace/Memory:
memory_documents— flexible path-based filesmemory_chunks— chunked content with FTS + vector indexesmemory_chunks_fts— FTS5 virtual table (libSQL) /tsvectorcolumn (PostgreSQL)heartbeat_state— periodic execution tracking
Security/Extensions:
secrets— AES-256-GCM encrypted credentialswasm_tools— installed WASM tool binariestool_capabilities— per-tool HTTP allowlist, secret access, rate limitsleak_detection_patterns— secret regex patterns (seed data in both backends)leak_detection_events— audit log of detected leakssecret_usage_log— per-request credential injection audit trailtool_rate_limit_state— sliding window rate limit counters
Other:
routines,routine_runs— scheduled/reactive executionsettings— per-user key-valuetool_failures— broken tool tracking for self-repair_migrations— libSQL-only internal migration version tracking
libSQL Current Limitations
- Secrets store — still requires
PostgresSecretsStore;LibSqlSecretsStoreexists but is not plumbed through the main startup path - Settings reload —
Config::from_dbskipped (requiresStore) - No incremental migrations — schema is idempotent CREATE IF NOT EXISTS; no ALTER TABLE support; column additions require a new versioned approach
- No encryption at rest — only secrets (API tokens) are AES-256-GCM encrypted; all other data is plaintext SQLite
- Hybrid search — both FTS5 and vector search (
libsql_vector_idx) are implemented; however, the vector index is fixed atF32_BLOB(1536)while PostgreSQL switched to unboundedvectorin V9 - Write serialization — WAL mode allows concurrent readers but only one writer at a time; busy timeout is 5 s, which may cause timeouts under high write concurrency
Running Locally with libSQL
# Use local SQLite file (default)
DATABASE_BACKEND=libsql LIBSQL_PATH=~/.ironclaw/test.db cargo run
# Use Turso cloud (embedded replica syncs local file to cloud)
DATABASE_BACKEND=libsql LIBSQL_URL=libsql://xxx.turso.io LIBSQL_AUTH_TOKEN=xxx cargo run
# In-memory (tests only — data is lost when the process exits)
# Use LibSqlBackend::new_memory() directly in test code
Testing the libSQL Backend
Use LibSqlBackend::new_memory() in unit tests — no files, no cleanup required:
#[tokio::test]
async fn test_my_feature() {
let backend = LibSqlBackend::new_memory().await.unwrap();
backend.run_migrations().await.unwrap();
// backend implements Database — call any trait method
}
For concurrency tests that require multiple connections sharing state, use LibSqlBackend::new_local(&tmp_path) with a tempfile::tempdir(). In-memory databases do not share state between connections.
Sharing the libSQL Database Handle
LibSqlBackend::shared_db() returns an Arc<LibSqlDatabase> for passing to satellite stores (e.g., LibSqlSecretsStore, LibSqlWasmToolStore) that need their own connections per-operation but should share the same underlying database file. These stores call .connect() on the shared handle themselves. This is the correct pattern — do not pass a live Connection to satellite stores.
Pattern: Fix the Pattern, Not the Instance
When fixing a bug in one backend's SQL, always grep for the same pattern in the other backend. A fix to postgres.rs that doesn't also fix the libSQL module (e.g., libsql/jobs.rs) is half a fix. The same applies to satellite types like LibSqlSecretsStore or LibSqlWasmToolStore.