Database
Istota uses SQLite with WAL mode for concurrent access. All operations live in db.py. The schema is defined in schema.sql.
Tables
Core
| Table |
Purpose |
tasks |
Task queue with full lifecycle: id, status, source_type, user_id, prompt, conversation_token, talk_delivery_token, priority, attempts, last_heartbeat (worker-liveness ping for stuck-task reclaim, ISSUE-112), execution trace, model/effort overrides, plus skill / skill_args for skill-task dispatch |
user_resources |
Per-user resource permissions (calendar, folder, todo_file, email_folder, ledger, etc.) |
user_profiles |
Per-user profile fields (display_name, timezone, channels, worker overrides, disabled_skills, disabled_modules, email_addresses, trusted_email_senders) |
briefing_configs |
DB-stored briefing configurations (cron, components, conversation_token, enabled flag) |
secrets |
Per-user encrypted credentials (Fernet over scrypt-derived ISTOTA_SECRET_KEY) |
google_oauth_tokens |
Google OAuth access/refresh token pairs (Fernet-encrypted at rest) |
task_logs |
Structured task-level observability |
istota_kv |
Key-value store for script runtime state |
Messaging
| Table |
Purpose |
talk_poll_state |
Last message ID per Talk conversation |
talk_messages |
Poller-fed message cache for conversation context |
processed_emails |
Email dedup with RFC 5322 thread tracking |
sent_emails |
Outbound email tracking for emissary thread matching |
task_events |
Task-event-streaming log: id, task_id, seq, kind, payload (JSON), created_at, UNIQUE(task_id, seq). One persisted, typed event stream per task feeding Talk / SSE / log / push consumers. seq is monotonic per task (writer-assigned, resumed across retries via get_max_task_event_seq); rows are deleted only by cleanup_old_tasks (retention) |
Web chat (per-user rooms)
| Table |
Purpose |
web_chat_rooms |
One row per web chat room: id, user_id, token (channel id), name, archived, created_at, updated_at. One room = one conversation_token, each with its own CHANNEL.md |
web_chat_messages |
Bot-delivered (unsolicited) room messages — alerts / logs / notifications routed to the web surface via WebTransport.deliver: id, user_id, token, role, title, text, created_at. Distinct from task-backed turns; merged into room history by time |
Rooms (unified Talk/web)
The unified Talk/web room-sync model (defined in schema.sql) supersedes the de-facto tasks-as-history store with a surface-neutral room + message model.
| Table |
Purpose |
rooms |
Canonical room registry keyed on conversation_token; origin (talk|web), display name, archived flag |
room_bindings |
One row per (room, surface) exposing a room; maps canonical token to each surface's ref |
messages |
Canonical transcript (role user|assistant|system, task_id, origin_surface, external_ids mirror ledger) |
room_members |
Per-user membership of a shared room; web visibility resolves through this, not the single-owner rooms.user_id |
room_dismissals |
Per-user "hide this room" tombstone, cleared by the user's own next inbound |
room_read_state |
Per-surface, per-user read cursors driving unread badges |
trusted_email_senders |
Per-user fnmatch allowlist for the email trust gate |
Scheduling
| Table |
Purpose |
scheduled_jobs |
Cron job definitions (synced from CRON.md) |
briefing_configs |
DB-stored briefing configurations |
briefing_state |
Last-run timestamps per briefing per user |
istota_file_tasks |
Tasks sourced from TASKS.md files (content-hash identity) |
Memory
| Table |
Purpose |
sleep_cycle_state |
Per-user nightly memory extraction state |
channel_sleep_cycle_state |
Per-channel memory extraction state |
memory_chunks |
Text chunks for hybrid search; carries valid_from / valid_until episode-window columns (ISSUE-109) so a chunk whose episode has closed self-suppresses from recall |
memory_chunks_fts |
FTS5 virtual table (trigger-synced from memory_chunks) |
knowledge_facts |
Temporal subject/predicate/object triples (freeform predicates, fuzzy dedup); valid_from / valid_until bound a fact's currency |
knowledge_facts_audit |
Append-only audit trail of KG fact add/invalidate/delete ops |
user_skills_fingerprint |
Skills version tracking for "what's new" |
Monitoring
| Table |
Purpose |
heartbeat_state |
Per-check monitoring state (timestamps, consecutive errors) |
reminder_state |
Shuffle queue for briefing reminders |
Tracking
| Table |
Purpose |
monarch_synced_transactions |
Monarch Money sync dedup |
csv_imported_transactions |
CSV import dedup |
Invoice timing tables (invoice_schedule_state, invoice_overdue_notified) live in the per-user money DB (money/db.py), not the framework istota.db.
Feeds (per-user feeds.db)
| Table |
Purpose |
feed_categories |
User-defined feed categories |
feeds |
Subscribed RSS/Atom/Tumblr/Are.na sources + per-feed poll state |
feed_entries |
Aggregated feed content + read/star state |
schema_meta |
Schema version + global default poll interval |
Location (per-user location.db)
Location tables live in per-user {workspace}/location/data/location.db files, not in the framework DB. The module package at src/istota/location/ provides resolve_for_user(user_id, config).
| Table |
Purpose |
location_pings |
Raw GPS data from Overland webhook |
places |
Named geofences |
visits |
Detected place visits |
location_state |
Per-user location tracking state |
dismissed_clusters |
Clusters the user chose not to save as places |
The two Nominatim caches (geocode_cache, reverse_geocode_cache) remain in the framework istota.db for cross-user dedup.
Key operations
Task lifecycle
create_task(conn, prompt, user_id, source_type="cli", ...) # -> task_id
claim_task(conn, worker_id, user_id=None) # -> Task | None
update_task_status(conn, task_id, status, result=None, ...) # completed/failed
set_task_pending_retry(conn, task_id, error, delay_minutes) # exponential backoff
set_task_confirmation(conn, task_id, confirmation_prompt) # -> pending_confirmation
cancel_task(conn, task_id) # sets cancel_requested
Conversation history
get_conversation_history(conn, token, exclude_task_id=None, limit=10)
# Returns: list[ConversationMessage(id, prompt, result, created_at, actions_taken)]
Cleanup
expire_stale_confirmations(conn, timeout_minutes) # -> list of expired tasks
fail_ancient_pending_tasks(conn, fail_hours) # -> list of failed tasks
cleanup_old_tasks(conn, retention_days) # -> count deleted
Single source of truth for Task columns
Every Task-returning helper (claim_task, get_task, get_pending_confirmation*, get_reply_parent_task, get_stale_pending_tasks, get_completed_*_since) routes its SELECT / RETURNING clause through a single _TASK_COLUMNS constant. Adding a column means editing one place; missing columns now raise IndexError rather than silently returning None.
WAL mode
SQLite WAL mode allows concurrent reads from multiple threads (talk poller, workers, CLI) while the scheduler thread writes. Each worker creates fresh DB connections per call.
Schema initialization
The schema is applied via schema.sql. The CLI command istota init creates the database and applies the schema.