type: concept title: Message history store description: The local SQLite database in src/history.rs that stores messages, chat summaries, contacts, the per-chat AI autoreply flag, the global autoreply setting and the AI descriptions of images, and supports local chat deletion. tags: [sqlite, rusqlite, history, persistence, schema] sources:
ce0c4ea8cd
resource: repo://src/history.rs
generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
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:67-76) creates missing parent
directories, opens the file and runs the schema script.History::open_in_memory() (src/history.rs:82-88) exists only under
#[cfg(test)].CREATE TABLE/INDEX IF NOT EXISTS
(schema_sql, src/history.rs:397-436); there are no migrations, so an
existing history.db simply gains new tables (such as settings and
image_descriptions) on next start.| 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 |
settings |
key (PK), value (INTEGER NOT NULL) |
App-wide flags; currently only global_autoreply |
image_descriptions |
message_id (PK), chat_jid, description |
AI description of an image message, written once per image |
Rust types (src/history.rs:28-55): StoredMessage, ChatSummary
(jid, name, last_timestamp, autoreply) and Contact (jid, name).
save_message (src/history.rs:107-118) 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:438-463).chats row
(upsert_chat_for_message, src/history.rs:465-483): 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 (chat_name_candidate,
src/history.rs:485-499).
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:124-142): 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:150-168): 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:175-191): 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:217-236): ordered by name,
case-insensitive.display_name(jid) (src/history.rs:242-253): same contact → chat → JID
user part fallback, used for the open chat title.upsert_contact (src/history.rs:197-211) 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:259-270) upserts the
chats row, creating it with defaults if the chat has no messages yet.is_autoreply(chat_jid) (src/history.rs:276-285) returns the stored
flag or false when the chat is unknown.set_global_autoreply(enabled) / is_global_autoreply()
(src/history.rs:291-317) upsert and read the global_autoreply row of
settings; the default is false when it was never stored. The global
flag is independent of the per-chat flags: toggling it never changes a
chats.autoreply value.New messages never reset the per-chat flag, because the message upsert only
touches last_timestamp and name.
The AI autoreply asks the vision model to describe each image once and stores the text, so later replies reuse it instead of downloading and describing the image again:
message_exists(message_id) (src/history.rs:319-332) tells whether a
message is still stored; the app checks it before saving a description, so
a chat deleted while its images were being described gets no orphan rows.save_image_description(message_id, chat_jid, description)
(src/history.rs:334-349) upserts the row, replacing a previous
description of the same message.image_descriptions(chat_jid) (src/history.rs:351-367) returns the
chat's descriptions keyed by message id; the app uses it to skip images
already described and to turn image messages into text when building the
AI context.delete_chat(chat_jid) (src/history.rs:369-394) removes, in one
transaction, all messages of the chat, their image descriptions and its
chats row (and therefore its per-chat autoreply flag). The contact row, other chats and the WhatsApp
session are untouched; deleting an unknown chat is not an error, and a later
message simply recreates the chat. The app calls it after a double Delete
press in the chat list (see Application state).
src/history.rs:543 onward 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, per-chat autoreply defaults and persistence,
the global autoreply default, toggle and independence from chat flags,
message_exists, image descriptions (per chat, replaced, deleted with the
chat), delete_chat (messages and row gone, contact and other chats kept), contact
ordering and blank names, and on-disk persistence with parent directory
creation. See Unit tests.