type: workflow title: AI auto-reply workflow description: End-to-end trace of the per-chat AI autoreply — enabling it, the ReplyGate rules, building the AI context, the vision path with its courtesy fallback, and sending the reply. tags: [ai, autoreply, workflow, vision, reply-gate] verified:
9fec9d1af4
resource: repo://src/app.rs043e6252ba
resource: repo://src/autoreply.rs
generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" }
---The autoreply answers incoming WhatsApp messages with an AI-generated text,
per chat and only when the user turned it on. Decision rules and context
building are pure functions in src/autoreply.rs (no I/O,
src/autoreply.rs:1-4); orchestration lives in
App; the HTTP call is done by the
AI client.
WaEvent::Message
-> History::save_message (new only)
-> App::maybe_autoreply
ReplyGate::should_reply? --no--> stop
ReplyGate::record_reply
History::recent_messages(20) -> build_turns
image + vision model? -> vision request (fallback: text request)
else -> text request
-> AppEvent::AiReply
-> autoreply still enabled? --no--> discard
-> WaHandle::send_text -> AppEvent::Sent -> stored with is_ai = true
Ctrl+A toggles the flag of the open chat (toggle_autoreply,
src/app.rs:477-491). Turning it off is always allowed; turning it on
requires a configured AI (API key and system_prompt, see
Configuration) and a private
chat (can_enable_autoreply, src/app.rs:743-751). The flag is persisted in
the history store (set_autoreply,
default off).
A chat is private when its JID server is s.whatsapp.net or lid; groups,
broadcasts, newsletters and malformed JIDs are not
(is_private_chat, src/autoreply.rs:48-58).
Only messages that History::save_message reports as new reach
maybe_autoreply (src/app.rs:251-284), so duplicates and history-sync
re-deliveries of stored messages never trigger. maybe_autoreply needs an
AI client and a system prompt, refreshes the own JIDs if unknown and reads
the chat flag.
ReplyGate::should_reply(message, enabled, now)
(src/autoreply.rs:130-137) returns true only when all hold:
| Rule | Detail |
|---|---|
| Enabled | per-chat flag is on |
| Sender allowed | not sent by the user — except human-written messages in the self-chat ("Messaggi a te stesso"); AI messages never trigger (src/autoreply.rs:115-117) |
| Private chat | is_private_chat |
| No backlog | timestamp >= started_at, the process start time |
| Has content | text not blank and not [media]/[messaggio non supportato]; voice notes and images do trigger |
| Cooldown | at least COOLDOWN_SECONDS = 30 s since the last reply in that chat (src/autoreply.rs:155-159) |
The self-chat is detected by comparing the chat JID with the account's own
PN/LID JIDs, ignoring a :N device suffix (src/autoreply.rs:94-107,
src/autoreply.rs:168-178).
record_reply is called before the AI request is spawned, so a second
message arriving during the request cannot cause a double reply
(src/autoreply.rs:145-147).
build_turns(system_prompt, messages, context)
(src/autoreply.rs:268-280):
System turn with the configured prompt;CONTEXT_MESSAGES = 20 messages (src/autoreply.rs:15),
skipping blank texts; the last one is the triggering message;turn_role, src/autoreply.rs:187-190): in normal chats own
messages are Assistant; in the self-chat, where every message is
from_me, only is_ai messages are Assistant;User turns are rewritten (src/autoreply.rs:229-254):
Didascalia: ... (src/autoreply.rs:209-219);TurnContext { self_chat, vision } (src/autoreply.rs:193-199) carries
the two switches.
The image is kept only when the triggering message is a live image and a
vision model is configured (vision_applies). In that case spawn_reply
(src/app.rs:293-316) builds two turn lists — with vision: true and,
as fallback, vision: false — and runs one background task that:
complete_with_image with the vision model;complete using the fallback turns
(the courtesy "cannot see it" reply).If both fail, the error combines both messages
(<vision error>; Fallback cortesia fallito: <error>,
src/app.rs:653-676). Without a usable image, a plain text request is
spawned.
AppEvent::AiReply is handled by send_ai_reply_if_enabled
(src/app.rs:338-359), which re-reads the chat flag: if autoreply was
turned off while the request was running, the reply is discarded with
status Risposta AI scartata: autoreply disattivato. Otherwise the text is
sent with spawn_send(..., is_ai = true); on success handle_sent
(src/app.rs:365-389) stores it as an own message with is_ai = true,
which keeps it from re-triggering the self-chat. Errors appear in the status
line (Errore AI: ..., Invio fallito: ...).
The rules and turn building are covered by 41 tests in
src/autoreply.rs:282+; enable rules and vision applicability by
src/app.rs:762+. See Unit tests.