+device. The terminal client and Telegram take three commands:
+
+``/whatsapp pair PHONE``
+ Links the account of PHONE, the number with its country code, such as
+ ``+15550001111``. The daemon answers with the steps and then with a
+ pairing code in its own message: in WhatsApp on the phone, open
+ Linked devices, tap Link a device, then tap Link with phone number
+ instead, and enter the code. The code expires in about 2 minutes, and
+ the same command gives a new code. When the link succeeds, the daemon
+ answers with the linked number. Linked devices on the phone shows the
+ device as OneChat. The name applies at the pairing, so an existing
+ link keeps its old name until ``/whatsapp unlink`` and a new
+ ``/whatsapp pair PHONE``.
+
+``/whatsapp status``
+ Shows the link: not linked, linked and connected, or linked and
+ disconnected, with the times of the connection.
+
+``/whatsapp unlink``
+ Ends the link and deletes the session. When WhatsApp is disconnected,
+ the phone does not get the logout, so remove the device in Linked
+ devices on the phone.
+
+The answers go only to the client that asked, and the commands do not go
+to the agent, so the agent never gets the phone number or the code. The
+chat gets only the notes that WhatsApp is linked or unlinked.
+
+The session is ``whatsapp/session.db`` in the data directory, with mode
+0600 in a directory with mode 0700. It gives full access to the WhatsApp
+account, so the daemon refuses the file when its mode is not 0600. The
+shell of the agent runs as the user, so it can read the session. If the
+session leaks, remove the linked device in WhatsApp on the phone, and
+the session stops working. Without the file, WhatsApp is not linked.
+
+The daemon reads the text messages of the private chats and the groups,
+from other people and from the user. It does not read media, reactions,
+edits, deletions, view-once and disappearing messages, or the history
+from before the link. The messages of one chat form one batch until the
+chat is quiet for 20 seconds, for 2 minutes or 50 messages at the most.
+Each batch goes to the chat of the agent as one work message that starts
+with ``[WhatsApp]``, with the name and the ID of the chat, and the time,
+the sender, and the text of each message. The messages of the user show
+as ``You``. ``whatsapp/state.json``, with mode 0600, keeps the batches
+that are not in the chat yet, so they survive a restart, and the records
+of the sends. A crash just after a batch goes to the chat can make the
+batch repeat once.
+
+A batch of a private chat with a message of another person starts a
+turn, and so does a batch of a group with a message that mentions the
+user or replies to a message of the user. The other batches go to the
+chat with no turn, and the agent sees them at its next turn. A batch
+that starts a turn ends with a line that tells the agent to end its turn
+with an empty reply when nothing in the batch needs the user. No client
+shows an empty reply, so such a batch gives no message in Telegram. The
+terminal client shows each batch, and Telegram shows none.
+
+The phone number of the user never goes to the chat: a mention of the
+user shows as ``@you``, the number shows as ``OWN``, and the chat of the
+user with itself is not read. But the IDs of the other chats go to the
+chat and to the model, because the agent needs them to send. The ID of a
+private chat holds the phone number of the contact, such as
+``15550002222@s.whatsapp.net``, and the ID of a group can hold the
+number of the person who made it.
+
+While the daemon is stopped or disconnected, the WhatsApp servers keep
+the messages for the linked device and send them at the next connection.
+They keep them only for a limited time, so a long stop can lose
+messages. These messages form batches by their original times, as live
+messages do. The daemon waits for the end of this catch-up, or one
+minute at the most, and then gives all its batches to the agent in one
+turn, not one turn for each chat. The batches of ``whatsapp/state.json``
+at the start wait for the first catch-up in the same way.
+
+The agent finds a chat with the tool ``whatsapp_find``: it gives the
+contacts and the groups whose name holds a text, with no regard to case
+and accents, with their IDs. The names of the contacts come from the
+session, and the names of the groups from WhatsApp, kept in memory for
+one hour. The names arrive from the phone some minutes after the link,
+and until then the tool says that they are not ready. The agent sends a
+text message with the tool ``whatsapp_send``, only when the user asks
+for it. Each send has an ID that the agent gives: a repeated call with
+the same ID gives the answer of the first call and sends nothing. The
+state file keeps the last 1000 send records. When the daemon stops
+during a send, the result is not known, and the tool tells the agent to
+ask the user to look at the chat in WhatsApp. Without a link, both tools
+tell the agent to ask the user to run ``/whatsapp pair PHONE``. Only the
+agent has the WhatsApp tools, not its subagents.
+
+Each batch of a group goes to the chat, also with no turn, and the
+memory tree summarizes it with model calls, which use the quota.
+
+When the session holds a linked device, the daemon connects at its
+start, and after a network failure it connects again by itself. When a
+disconnection lasts 10 minutes, all clients get one notice, and one more
+when the connection comes back. When WhatsApp ends the connection for
+good, for example because another program uses the same session, all
+clients get a notice at once. When the user removes the device in the
+phone, the daemon deletes the session and tells all clients one time.
+
+***********
+ Schedules
+***********
+
+The agent sets reminders and timed tasks when the user asks for them,
+such as "remind me tomorrow at 9" or "every weekday at 8, tell me what
+my WhatsApp groups talked about". A schedule has a text, the task for
+the agent, and either one time or a cron expression. The agent also
+lists the schedules with their next times, and removes one when the user
+asks. Only the agent has the schedule tools, not its subagents.
+
+A cron expression has five fields: minute, hour, day of month, month,
+and day of week, where 0 and 7 are Sunday. A field is a comma list of
+``*``, ``N``, and ``N-M``, and ``*`` and ``N-M`` can have a step ``/S``,
+as in ``0 8 * * 1-5`` or ``*/15 9-17 * * *``. When both day fields start
+with something other than ``*``, a day matches when either field
+matches. Names such as ``MON``, macros such as ``@daily``, the
+characters ``? L W #``, and ``N/S`` are refused. The runs must be at
+least 5 minutes apart, because each run is a full turn that uses the
+quota, so ``*/5 * * * *`` is the shortest schedule.
+
+The times use the time zone of the machine. A time that the clock skips
+at the start of daylight saving time runs later by the length of the
+skip. A time that the clock repeats at its end runs once, at its first
+occurrence.
+
+At its time, a schedule goes to the chat as a work message that starts
+with ``[Schedule ID]``, with its time and its text, and it starts a
+turn. The message ends with a line that tells the agent to end its turn
+with an empty reply when nothing needs the user. The terminal client
+shows the message, and Telegram shows only the reply. After its run, a
+one-time schedule goes, and a repeating schedule gets its next time.
+
+``agent/schedules.json``, with mode 0600, holds the schedules. Each
+change writes it before the tool answers, so the schedules survive a
+restart. A schedule that came due while the daemon was stopped runs once
+at the next start, with its original time and a line that says that the
+run is late. A repeating schedule that missed several times runs once. A
+crash just after a run goes to the chat can make the run repeat once.