history-store.md 4.5 KB


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-ce0c4ea8cd 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). App 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.

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 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.