--- type: concept title: Message history store description: The local SQLite database in src/history.rs that stores messages, chat summaries, contacts and the per-chat AI autoreply flag. tags: [sqlite, rusqlite, history, persistence, schema] verified: - by: openwiki/0.7.2 at: 2026-10-19T18:58:46.219Z sources: - id: openwiki-source-ce0c4ea8cd3b15431fa691ce resource: repo://src/history.rs generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" } --- # Message history store `src/history.rs` wraps a single `rusqlite::Connection` in the `History` struct. It is the only persistent state owned by the app itself (the WhatsApp session lives in a separate database, see [Configuration and data](../operations/configuration-and-data.md)). [`App`](../architecture/app-state.md) owns the `History` and calls it synchronously from the event loop. ## Opening - `History::open(path)` (`src/history.rs:60-69`) creates missing parent directories, opens the file and runs the schema script. - `History::open_in_memory()` (`src/history.rs:76-81`) exists only under `#[cfg(test)]`. - The schema is applied with `CREATE TABLE/INDEX IF NOT EXISTS` (`src/history.rs:259-285`); there are no migrations. ## Schema | Table | Columns | Notes | |---|---|---| | `messages` | `message_id` (PK), `chat_jid`, `sender_jid`, `sender_name`, `text`, `timestamp`, `from_me`, `is_ai` | Index `idx_messages_chat_timestamp` on `(chat_jid, timestamp)` | | `chats` | `jid` (PK), `name`, `last_timestamp` (default 0), `autoreply` (default 0) | One row per chat | | `contacts` | `jid` (PK), `name` (NOT NULL) | Names from the address book / push names | Rust types (`src/history.rs:21-48`): `StoredMessage`, `ChatSummary` (`jid`, `name`, `last_timestamp`, `autoreply`) and `Contact` (`jid`, `name`). ## Saving messages and deduplication `save_message` (`src/history.rs:100-111`) runs in one transaction: 1. `INSERT OR IGNORE` into `messages` — the WhatsApp message id is the primary key, so redelivered messages are ignored (`src/history.rs:291-312`). 2. Only if the row was new, upsert the `chats` row (`src/history.rs:319-332`): `last_timestamp` keeps the maximum seen and the name is set only when the chat has no non-empty name yet. 3. Return `true` for a new message, `false` for a duplicate. The chat name candidate is the trimmed sender push name of an **incoming** message; own messages never name a chat (`src/history.rs:338-348`). The boolean return value drives the app: duplicates do not refresh the UI or trigger the [AI autoreply](../workflows/ai-autoreply.md). ## Reading - `recent_messages(chat_jid, limit)` (`src/history.rs:117-135`): the latest `limit` messages, queried newest first (`timestamp DESC, rowid DESC`) and reversed to ascending order for display and AI context. - `list_chats()` (`src/history.rs:142-158`): all chats ordered by `last_timestamp DESC`; the name is the contact name, else the chat name, else the user part of the JID (text before `@`). - `list_contacts()` (`src/history.rs:184-203`): ordered by name, case-insensitive. - `display_name(jid)` (`src/history.rs:209-220`): same contact → chat → JID user part fallback, used for the open chat title. ## Contacts `upsert_contact` (`src/history.rs:164-178`) inserts or renames a contact and ignores blank names, so an empty push name never erases a known name. ## Autoreply flag - `set_autoreply(chat_jid, enabled)` (`src/history.rs:226-237`) upserts the `chats` row, creating it with defaults if the chat has no messages yet. - `is_autoreply(chat_jid)` (`src/history.rs:243-252`) returns the stored flag or `false` when the chat is unknown. New messages never reset the flag, because the message upsert only touches `last_timestamp` and `name`. ## Tests `src/history.rs:392-593` covers duplicate ids, limit and ordering, flag and sender-name round trips, chat ordering, name fallbacks, chat naming rules, autoreply defaults and persistence, contact ordering and blank names, and on-disk persistence with parent directory creation. See [Unit tests](../testing/unit-tests.md).