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:
ce0c4ea8cd
resource: repo://src/history.rs
generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" }
---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:259-285); 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:21-48): 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:291-312).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.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.
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.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.
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.
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.