history-store.md 5.8 KB


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 and the global autoreply setting, and supports local chat deletion. tags: [sqlite, rusqlite, history, persistence, schema] sources:

  • id: openwiki-source-ce0c4ea8cd resource: repo://src/history.rs generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" } verified:
  • by: openwiki/0.7.2 at: 2026-10-20T20:27:06.515Z ---

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). App owns the History and calls it synchronously from the event loop.

Opening

  • History::open(path) (src/history.rs:65-74) creates missing parent directories, opens the file and runs the schema script.
  • History::open_in_memory() (src/history.rs:80-86) exists only under #[cfg(test)].
  • The schema is applied with CREATE TABLE/INDEX IF NOT EXISTS (schema_sql, src/history.rs:344-374); there are no migrations, so an existing history.db simply gains the new settings table on next start.

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
settings key (PK), value (INTEGER NOT NULL) App-wide flags; currently only global_autoreply

Rust types (src/history.rs:26-53): StoredMessage, ChatSummary (jid, name, last_timestamp, autoreply) and Contact (jid, name).

Saving messages and deduplication

save_message (src/history.rs:105-116) 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:380-402).
  2. Only if the row was new, upsert the chats row (upsert_chat_for_message, src/history.rs:408-421): 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 (chat_name_candidate, src/history.rs:427-437).

The boolean return value drives the app: duplicates do not refresh the UI or trigger the AI autoreply.

Reading

  • recent_messages(chat_jid, limit) (src/history.rs:122-140): 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:148-166): 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:173-189): 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:215-234): ordered by name, case-insensitive.
  • display_name(jid) (src/history.rs:240-251): same contact → chat → JID user part fallback, used for the open chat title.

Contacts

upsert_contact (src/history.rs:195-209) inserts or renames a contact and ignores blank names, so an empty push name never erases a known name.

Autoreply flags

  • set_autoreply(chat_jid, enabled) (src/history.rs:257-268) upserts the chats row, creating it with defaults if the chat has no messages yet.
  • is_autoreply(chat_jid) (src/history.rs:274-283) returns the stored flag or false when the chat is unknown.
  • set_global_autoreply(enabled) / is_global_autoreply() (src/history.rs:289-315) 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.

Deleting a chat

delete_chat(chat_jid) (src/history.rs:322-337) removes, in one transaction, all messages of the chat 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).

Tests

src/history.rs:481-763 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, 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.