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 folder mounts (folder) + internal shared_file organizer state |
user_profiles |
Per-user profile fields (display_name, timezone, channels, worker overrides, disabled_skills, disabled_modules, email_addresses, trusted_email_senders) |
briefing_configs |
Briefing schedule + delivery (cron, conversation_token, output, enabled flag). Content lives in the per-user briefings module DB, not here |
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) |
web_user_tokens |
Retained user-scoped Nextcloud OAuth pairs for post-as-user Talk mirroring; encrypted with the web-only ISTOTA_WEB_TOKEN_KEY (distinct salt + table from secrets, so "who can decrypt" stays greppable) |
task_logs |
Structured task-level observability |
task_steers |
Mid-run steering notes for a running task (!steer): per-task monotonic seq, pending / consumed / dropped, plus who steered and from which surface |
istota_kv |
Per-user key-value store for script runtime state |
shared_kv |
Cross-user namespaced JSON store. Reads are open; writes are admin-only and fail closed. Substrate for curated shared briefing content |
_migration_state |
Markers for one-time data migrations, so each runs exactly once |
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 |
trusted_email_senders |
Per-user fnmatch allowlist for the email trust gate |
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, plus the standing per-room model / effort default applied by record_inbound |
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 |
message_stars |
Per-user starred messages (Talk has no per-message star API, so this is web-only) |
message_deletions |
Hard-delete ledger with its own stream cursor, so a reconnecting client learns what vanished while it was away. Pruned at 30 days |
Scheduling¶
| Table | Purpose |
|---|---|
scheduled_jobs |
Cron job definitions (synced from CRON.md) |
briefing_configs |
Briefing schedule + delivery per user |
briefing_state |
Last-run timestamps per briefing per user |
shared_block_configs |
Admin-managed definitions of module-owned shared briefing blocks (cron, render mode, trust flag, sources JSON). Seeded once from config, DB-authoritative thereafter |
briefing_shared_block_state |
Last-run timestamps for shared-block generation (global, not 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.
Watch the names: money/db.py creates its own monarch_synced_transactions, csv_imported_transactions, and kv_store in the per-user money DB. The framework copies listed above are the ones _process_deferred_tracking writes; the money-DB copies are internal to the module.
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, plus embed_url (an inline video player) and file_url (an attachment such as a PDF) |
entry_images |
Repeat-image index (entry_id, image_key, seen_ts) backing the reader's reblog-image suppression |
schema_meta |
Schema version, the global default poll interval, and feeds_settings.image_dedupe_window_days |
A poll updates an entry it has already seen rather than discarding it, so a provider fix or a richer render reaches entries already on file instead of applying only to new ones. User state (status, starred, starred_at) and fetched_at are never overwritten — fetched_at is the first sighting, which keeps "recently added" ordering and the image-dedupe look-back stable — and a field is only overwritten by a non-empty value, so a sparser re-fetch cannot blank a title. The "N new entries" count still means newly inserted; refreshes are counted separately.
Location (per-user location.db)¶
Location tables live in a per-user location.db, 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 |
schema_meta |
Schema version |
The two Nominatim caches (geocode_cache, reverse_geocode_cache) remain in the framework istota.db for cross-user dedup.
Health (per-user health.db)¶
| Table | Purpose |
|---|---|
stats |
Body stat time series (metric, value, unit, date, source) |
panels |
Bloodwork panels |
biomarkers |
Individual results linked to a panel |
biomarker_explainers |
Cached explainer text per (name, direction) |
biomarker_refs |
Bundled canonical reference ranges and aliases |
encounters |
Visits, procedures, screenings, hospitalizations |
diagnoses |
Conditions with status (active, resolved, chronic) |
diagnosis_encounters |
Which appointments a condition was seen at (many-to-many; real FKs, cascading) |
immunizations |
Vaccine administration records |
immunization_refs |
Bundled canonical vaccine list and schedules |
documents |
Stored paperwork, one row per blob, deduped by content hash |
document_links |
Polymorphic join: which records a document evidences |
health_settings |
Profile (DOB, height, sex) and unit display preferences |
schema_meta |
Schema version |
Only the .db is local; document and panel bytes stay in the workspace on the mount.
Briefings (per-user briefings.db)¶
Blocks, their sources, and the archive of rendered results live in a per-user briefings.db. Schedule and delivery stay framework-owned in briefing_configs.
| Table | Purpose |
|---|---|
briefing_blocks |
The blocks a user's briefing is assembled from, in order |
briefing_block_sources |
Per-block source rows (kind + config) |
briefing_items |
Items gathered for a block |
briefing_item_state |
Per-item seen/dismissed state for next-run dedup |
briefing_archive |
Rendered briefing results |
schema_meta |
Schema version |
Archived results are pruned by [briefings] archive_retention_days on insert, and individually deletable from the web reader. |
Module DB storage¶
The framework istota.db and all five per-user module DBs (feeds, health, location, money, briefings) run WAL on local disk, at Config.module_db_path(user_id, module) — by default {db_path.parent}/modules/{user}/{module}.db. Only the .db files are local; user-facing workspace files (health uploads, money ledgers, feeds exports) stay on the Nextcloud mount. Module DBs were moved off the mount because WAL's mmap'd -shm file SIGBUSes on the rclone FUSE mount, which had forced them onto journal_mode=DELETE and left them with no reader/writer concurrency. python -m istota.db_relocate is the one-time idempotent migrator; db_backup snapshots the now-local DBs back to dated directories on the mount for off-host durability, and db_restore copies them back.
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.