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:
ce0c4ea8cd
resource: repo://src/history.rs
generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
verified: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).
App owns the History and calls it
synchronously from the event loop.
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)].CREATE TABLE/INDEX IF NOT EXISTS
(src/history.rs:285-311); there are no migrations.| 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).
save_message (src/history.rs:100-111) runs in one transaction:
INSERT OR IGNORE into messages — the WhatsApp message id is the
primary key, so redelivered messages are ignored (src/history.rs:317-338).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.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.
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 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.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.
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.
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.