history-store.md 4.0 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] verified:

  • by: openwiki/0.7.2 at: 2026-10-19T18:58:46.219Z sources:
  • id: openwiki-source-ce0c4ea8cd resource: repo://src/history.rs generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" } ---

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:259-285); 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:21-48): 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:291-312).
  2. Only if the row was new, upsert the 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.
  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:338-348).

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

Contacts

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.

Autoreply flag

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

Tests

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.