--- 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] sources: - id: openwiki-source-ce0c4ea8cd3b15431fa691ce resource: repo://src/history.rs generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" } verified: - by: openwiki/0.7.2 at: 2026-10-19T19:53:56.480Z --- # 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:285-311`); 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:23-53`): `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:317-338`). 2. Only if the row was new, upsert the `chats` row (`src/history.rs:345-358`): `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:364-374`). 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 (the open chat view) and for the last-message lookup of the startup catch-up. - `messages_since(chat_jid, since, limit)` (`src/history.rs:143-161`): the messages with `timestamp >= since` (inclusive), capped to the `limit` most recent ones (`timestamp DESC, rowid DESC LIMIT ?3`) and reversed to ascending order. The [AI autoreply](../workflows/ai-autoreply.md) uses it to load the context from local midnight of yesterday, at most `CONTEXT_MESSAGES` messages. - `list_chats()` (`src/history.rs:168-184`): 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:210-229`): ordered by name, case-insensitive. - `display_name(jid)` (`src/history.rs:235-246`): same contact → chat → JID user part fallback, used for the open chat title. ## Contacts `upsert_contact` (`src/history.rs:190-204`) 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:252-263`) upserts the `chats` row, creating it with defaults if the chat has no messages yet. - `is_autoreply(chat_jid)` (`src/history.rs:269-278`) 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:418-663` covers duplicate ids, limit and ordering, `messages_since` (boundary inclusion, limit keeping the most recent, ascending order), 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).