]> git.giacomolli.uk - onechat/blobdiff - README.rst
Name timeouts for the operations they limit
[onechat] / README.rst
index 7912d0daa3c618ac8bc0f412ed8eeeacdcd8b624..a02b87096e711820e2decedf28ec110e416dceec 100644 (file)
@@ -3,31 +3,23 @@
 #########
 
 OneChat is one AI agent in a single chat that never ends. A daemon owns
-the agent and its files, and a terminal client and a Telegram bot talk
-to it. It is in early development. Linux is supported.
+the agent and its memory, and you talk to it from a terminal client,
+Telegram, and through your WhatsApp chats. It is in early development.
+Linux is supported.
 
-****************
- Data directory
-****************
-
-OneChat keeps its data and its token file in one directory. It uses the
-first of these:
-
-#. ``ONECHAT_DIR``, when it is set.
-#. ``$XDG_DATA_HOME/onechat``, when ``XDG_DATA_HOME`` is an absolute
-   path. A relative value is ignored.
-#. ``~/.local/share/onechat``. With no home directory, OneChat exits
-   with an error.
+*******
+ Setup
+*******
 
-OneChat creates the directory with mode 0700.
+Data directory
+==============
 
-***************
- Configuration
-***************
+OneChat keeps all its data in one directory, with mode 0700. It uses
+``ONECHAT_DIR`` when it is set, else ``$XDG_DATA_HOME/onechat``, else
+``~/.local/share/onechat``. The development shell sets ``ONECHAT_DIR``
+to ``.devenv/onechat`` in the checkout.
 
-``config.json`` in the data directory holds the settings. OneChat does
-not create it. A missing file or field takes the default, and an unknown
-field is an error.
+Optional settings go in ``config.json`` in the data directory:
 
 .. code:: json
 
@@ -36,155 +28,60 @@ field is an error.
      "compactor": {"model": "gpt-6.1-sol", "effort": "high"}
    }
 
-``endpoint``
-   ``chatgpt`` (the default) sends model calls to the ChatGPT backend
-   with the subscription login. ``api`` sends them to the OpenAI API
-   with the key in ``OPENAI_API_KEY``, which must then be set. With
-   ``chatgpt``, OneChat ignores ``OPENAI_API_KEY``.
+``endpoint`` is ``chatgpt`` (the ChatGPT subscription, the default) or
+``api`` (the OpenAI API, with ``OPENAI_API_KEY`` set). ``compactor`` is
+the model and effort of the memory summaries.
 
-``compactor``
-   The model and the reasoning effort of the summaries. The defaults are
-   ``gpt-6.1-sol`` and ``high``.
-
-*******
- Login
-*******
-
-The ``chatgpt`` endpoint needs a login with a ChatGPT account:
+Login
+=====
 
 .. code:: console
 
    onechat login
 
-The command shows a link and a code. Open the link on any device, enter
-the code, and approve the login. Device code login must be allowed in
-the ChatGPT security settings. OneChat writes the tokens to
-``auth.json`` in the data directory, with mode 0600, and refreshes them
-before they expire. The file belongs to OneChat only. Do not share it
-with the Codex CLI, because each program rotates the refresh token and
-logs the other out.
+Open the link on any device and enter the code. Device code login must
+be allowed in the ChatGPT security settings. Do not share the login with
+the Codex CLI: each program logs the other out.
 
-*******
- Agent
-*******
+Telegram
+========
 
-Start the daemon, and then the client in another terminal:
+#. Make a bot with BotFather and keep its token.
+#. Find your numeric Telegram user ID.
+#. Write both to ``telegram.json`` in the data directory, with mode
+   0600:
 
-.. code:: console
+.. code:: json
 
-   onechat daemon
-   onechat
+   {"token": "123456:ABC-token-from-BotFather", "owner": 123456789}
 
-``onechat`` with no command is the same as ``onechat chat``. ``onechat
-help`` lists the commands.
-
-The daemon owns the root agent in ``agent/`` in the data directory and
-listens on the Unix socket ``onechat.sock`` there. In the client, each
-line is a message to the agent, the line ``/stop`` interrupts the
-running turn, the line ``/stop NAME`` stops the subagent NAME, and
-Ctrl-D ends the client. On terminal output, the client shows ``> ``
-after each event while idle and clears it before the next event. An
-event can interrupt a line while the user types. The client shows the
-replies, the tool calls, and the first lines of each tool result as they
-happen. Replies and subagent reports render Markdown with styles and
-wrapped tables only when the output is a terminal, and stay unchanged in
-a pipe or file. It also shows each message that the user sends from
-another client, with the client as a mark, such as ``[telegram] text``.
-When a subagent starts, the client shows ``[NAME] started``, and it
-shows the whole report of the subagent, which starts with ``[NAME]``,
-but not the tool calls of the subagent. The daemon is the only process
-that uses the token file while it runs, so ``onechat login`` and
-``onechat probe`` refuse to start until it stops.
-
-The agent runs commands with bash as the user in its working directory,
-with no sandbox. It has the rights of the user, also on the token file.
-
-When the user asks for it, the agent starts subagents for long tasks.
-Each subagent has a name, such as ``GreenOtter``, and runs in the
-background while the agent answers other messages. It gets the view of
-the agent and its task, and it runs its tools as the agent does, in
-``agent/subagents/NAME/work``. A task in a repository gives the path of
-the repository. The steps of a subagent go to its own log in
-``agent/subagents/NAME``, not to the chat, and its final reply comes
-back to the agent as one message that starts with ``[NAME]``. The agent
-can send a subagent more messages: a running subagent reads them between
-its tool calls, and a finished one goes on from its log, and its reply
-comes back in the same way. When the user stops a subagent, the chat
-gets a note, and the report of the subagent is logged, but it starts no
-turn. Each model call of a subagent has the name in the ``agent`` field
-of ``agent/usage.jsonl``. ``agent/work.json`` holds the names of the
-running subagents. A subagent that runs when the daemon stops goes on at
-the next start: the chat gets the note that the daemon restarted, and
-the subagent gets the same note and goes on from its log.
-
-``agent/config.json`` holds the settings of the agent. The first start
-writes it with the defaults and a random ID. Do not change the ID: the
-prompt cache key comes from it.
+Start the daemon and write to the bot in a private chat. It answers only
+you.
 
-.. code:: json
+WhatsApp
+========
 
-   {
-     "name": "OneChat",
-     "id": "0123456789abcdef0123456789abcdef",
-     "model": "gpt-6.1-sol",
-     "effort": "medium",
-     "workdir": "/home/user"
-   }
+Link your WhatsApp account as a linked device from the terminal client
+or Telegram:
+
+``/whatsapp pair PHONE``
+   Gives a pairing code. On the phone, open Linked devices, Link a
+   device, Link with phone number instead, and enter the code.
 
-``workdir`` is an absolute path, and its default is the home directory.
-``agent/instructions.md`` holds the user's instructions: who they are,
-how their files are organized, and how they want work done. The prompt
-ends with them, and a change applies at the next call. The agent can
-propose a change to the file, and it makes the change only after the
-user agrees.
-
-A permanent child agent lives in ``agent/children/NAME/`` with the same
-files as the root. The root's ``agent_create(name, responsibility)``
-tool creates and starts it with the root's model, effort, and working
-directory, and a separate random ID. A name describes the area, such as
-``House`` or ``Calendar``. It is one word of 2 to 32 ASCII letters,
-starts with a capital letter, and cannot name the root, an existing
-child, or a subagent. The daemon opens all children at startup. A
-missing or invalid child config stops startup. Only one level of
-children is supported. Remove a child by hand while the daemon is
-stopped. The root's turn state lists each child and its responsibility.
-The root passes area messages from mixed chats with sender, date, and
-words, and asks the user before it sends a child's draft to WhatsApp.
-
-Each child has its own memory, model client, usage file, subagents,
-schedules, and turn loop. Its prompt limits work to its area, asks for
-only reports that need the parent or the user, and permits an empty
-reply. Children have no WhatsApp tools and draft replies instead of
-sending them. Clients address only the root. A child's saved schedules
-and subagents can resume at startup. The ready log line lists the child
-names.
-
-A message from a parent to its own child enters the child's log as
-``user`` and wakes it. A child's final reply reaches the parent as
-``work`` with the prefix ``[NAME] `` and wakes it. An empty reply sends
-nothing. A busy recipient takes the message between tool calls or in its
-next turn. ``agent/messages.json``, with mode 0600, keeps messages until
-their recipient finishes its turn. A daemon stop preserves queued
-messages and reports for the next start without a second log entry. An
-interrupted turn can repeat its model calls and tools after restart.
-Backups lock this queue with the agent files, including children made
-during the daemon run. The root uses ``tell`` for both children and
-subagents. ``zoom("Name")`` gives a child's view or a subagent's whole
-log. ``spawn`` starts short-lived subagents, not permanent children.
-
-**************
- User service
-**************
-
-The Nix package includes ``share/systemd/user/onechat.service``. It runs
-``onechat daemon`` from that package, starts at login when enabled, and
-restarts 5 seconds after a failure. A stop sends SIGTERM to the daemon
-and allows up to 120 seconds for cleanup. Its output goes to the user
-journal. The service has the rights of the user, as the terminal daemon
-does.
-
-Build a package with a separate result link. Keep this link so that Nix
-does not collect the package while the service uses it:
+``/whatsapp status``
+   Shows the link and the routes of chats to child agents.
+
+``/whatsapp unlink``
+   Ends the link.
+
+The session file gives full access to the account. If it leaks, remove
+the device in Linked devices on the phone.
+
+User service
+============
+
+Run the daemon as a systemd user service. Keep the ``result-service``
+link, so that Nix keeps the package:
 
 .. code:: console
 
@@ -192,112 +89,41 @@ does not collect the package while the service uses it:
    systemctl --user link --force "$PWD/result-service/share/systemd/user/onechat.service"
    systemctl --user edit onechat.service
 
-In the editor, set the data directory with this drop-in. Replace
-``DATA_DIR`` with an absolute path to the data directory. Set the same
-``ONECHAT_DIR`` for each terminal client. Without a drop-in, the data
-directory rules above apply to the environment of the user manager, not
-to the development shell.
+In the editor, set the data directory, and the tools that the agent may
+use in its shell:
 
 .. code:: ini
 
    [Service]
    Environment="ONECHAT_DIR=DATA_DIR"
+   Environment="PATH=TOOLS_PATH"
 
-The service does not read the environment of an open terminal. If the
-agent needs tools from a shell, set ``Environment="PATH=TOOLS_PATH"`` in
-the same drop-in, with ``TOOLS_PATH`` replaced by the complete search
-path, including bash. For the API endpoint, use
-``EnvironmentFile=ENV_FILE``, with ``ENV_FILE`` replaced by an absolute
-path to a mode 0600 file that sets ``OPENAI_API_KEY``. Keep secrets out
-of the unit and Nix sources.
-
-Use the existing data directory, or copy the development data before the
-first service start. For a copy, first stop the terminal daemon with
-Ctrl-C and wait for it to exit. Replace ``DATA_DIR`` below with a new,
-empty destination. If the development shell uses a custom
-``ONECHAT_DIR``, replace ``.devenv/onechat`` with that source. Copy the
-whole directory so that ``auth.json``, ``telegram.json``,
-``config.json``, ``agent/``, and ``whatsapp/`` stay together, including
-the WhatsApp session and its database files. The copy preserves the file
-modes and the source as a backup. Do not copy or use the same data from
-two running daemons.
-
-.. code:: console
-
-   install -d -m 0700 DATA_DIR
-   cp -a .devenv/onechat/. DATA_DIR/
-
-After the terminal daemon exits and the data is ready, enable and start
-the service:
+Then start it, and let it run while you are logged out:
 
 .. code:: console
 
    systemctl --user daemon-reload
    systemctl --user enable --now onechat.service
-   systemctl --user status onechat.service
-   ONECHAT_DIR=DATA_DIR ./result-service/bin/onechat
-   journalctl --user -u onechat.service -f
+   loginctl enable-linger "$USER"
 
-After a pull, update the package and unit with:
+After a ``git pull``, ``bin/service-update`` builds and restarts it.
+Never run two daemons on the same data.
 
-.. code:: console
-
-   git pull
-   bin/service-update
-
-The script builds with the same ``result-service`` link, reloads the
-user manager, and restarts the service only if it is active. The unit
-link follows ``result-service``, so no new link is needed.
-
-The service starts with the user manager at login, not at boot without a
-login. To stop it, or also remove its start at login:
+Backups
+=======
 
 .. code:: console
 
-   systemctl --user stop onechat.service
-   systemctl --user disable onechat.service
-
-********************
- Backup and restore
-********************
-
-Back up the running daemon to one mode 0600 ``.tar.gz`` archive. The
-command prints its path and size in bytes. With no daemon, it fails.
+   onechat backup BACKUP_DIR/onechat.tar.gz
+   ONECHAT_DIR=EMPTY_DIR onechat restore BACKUP_DIR/onechat.tar.gz
 
-.. code:: console
+A backup runs while the daemon runs. Restore needs a stopped daemon and
+an empty directory. **An archive holds your logins, the bot token, and
+the WhatsApp link**, so keep it private.
 
-   onechat backup BACKUP_FILE.tar.gz
-   ONECHAT_DIR=EMPTY_DATA_DIR onechat restore BACKUP_FILE.tar.gz
-
-The daemon flushes memory and holds its write locks during the agent
-copy. It also flushes and locks the child agents and all subagent logs.
-It copies the WhatsApp session with SQLite ``VACUUM INTO``, not as raw
-database files. It then compresses the copy and installs the archive
-through a temporary file and a rename. Use a backup path outside the
-data directory.
-
-The archive includes configuration, instructions, the log, tree,
-indexes, views, layout, schedules, subagents, children, Telegram offset,
-tokens, and WhatsApp state and session. It excludes sockets, lock files,
-``probe/``, and the ``agent-backup-*`` import copies. Links and special
-files fail, rather than enter the archive. **The archive holds
-secrets**, including login tokens, the Telegram bot token, and the
-WhatsApp link. Keep it private. There is no encryption or remote copy.
-
-Restore needs a stopped daemon and an empty destination. It checks the
-token lock and validates the archive before it installs files, with
-their modes. It rejects unsafe paths, links, and special files. A failed
-restore can leave ``auth.lock`` in the destination. Use another empty
-directory for a retry. Do not run the source and restored WhatsApp links
-at the same time. Check the restored log and view before you use it in
-place of the source.
-
-For a daily local archive with the last 14 kept, save this script as
-``BACKUP_SCRIPT``. Replace ``BACKUP_DIR`` with a private archive
-directory outside the data directory. Replace ``ONECHAT_BINARY`` with
-the absolute path through the stable ``result-service/bin/onechat``
-package link. Make the archive directory with mode 0700 before you
-enable the timer.
+For a daily backup with the last 14 kept, save this script, with
+``BACKUP_DIR`` replaced by a private directory outside the data
+directory:
 
 .. code:: sh
 
@@ -305,384 +131,118 @@ enable the timer.
    set -eu
    umask 077
    cd BACKUP_DIR
-   ONECHAT_BINARY backup "onechat-$(date +%F).tar.gz"
-   find . -maxdepth 1 -type f -name 'onechat-????-??-??.tar.gz' -printf '%f\n' |
-     sort -r | tail -n +15 |
-     while IFS= read -r file; do
-       rm -- "$file"
-     done
-
-Save these units as ``onechat-backup.service`` and
-``onechat-backup.timer`` in the systemd user unit directory. Replace
-``DATA_DIR`` and ``BACKUP_SCRIPT`` with absolute paths. The service uses
-the daemon unit from User service above.
-
-.. code:: ini
+   onechat backup "onechat-$(date +%F).tar.gz"
+   ls -1 onechat-????-??-??.tar.gz | sort -r | tail -n +15 | xargs -r rm --
 
-   [Unit]
-   Description=Back up OneChat data
-   Requires=onechat.service
-   After=onechat.service
+and run it from a ``Type=oneshot`` user service with the same
+``ONECHAT_DIR`` and an ``OnCalendar=daily`` timer.
 
-   [Service]
-   Type=oneshot
-   Environment="ONECHAT_DIR=DATA_DIR"
-   UMask=0077
-   ExecStart=/bin/sh BACKUP_SCRIPT
-
-.. code:: ini
-
-   [Unit]
-   Description=Back up OneChat each day
-
-   [Timer]
-   OnCalendar=daily
-   Persistent=true
-
-   [Install]
-   WantedBy=timers.target
-
-Enable the timer with the user manager. A failed backup leaves the
-existing archives in place and skips removal of old archives.
+*********
+ Talking
+*********
 
 .. code:: console
 
-   systemctl --user daemon-reload
-   systemctl --user enable --now onechat-backup.timer
-   systemctl --user list-timers onechat-backup.timer
-
-************
- Daemon log
-************
-
-The daemon writes short events with the part prefix and ordered
-``key=value`` attributes. An error cause comes last. Normal events use
-info, failures that can be retried or lose nothing use warning, and
-failures that lose data or stop work use error.
-
-With ``JOURNAL_STREAM`` set, each line carries a syslog priority without
-a time stamp or level word. The user journal can show only warnings and
-errors:
-
-.. code:: console
-
-   journalctl --user -u onechat -p warning
+   onechat
 
-The summary warning names its node. A schedule run has ``late`` only
-when it ran late:
+``onechat`` connects to the running daemon. Each line is a message, and
+replies show with Markdown styles and tables. These lines are commands:
 
-.. code:: text
+``/stop``
+   Interrupts the running turn.
 
-   <4>onechat: summary failed level=0 index=0: fake failure
-   <6>onechat: schedule ran id=1
-   <6>onechat: schedule ran id=2 late=1m0s
+``/stop NAME``
+   Stops the subagent NAME.
 
-Outside the journal, each line starts with the time stamp and level.
+``/image PATH [caption]``
+   Sends a JPEG, PNG, or WebP image. Quote a path with spaces.
 
-**********
- Telegram
-**********
+``/whatsapp ...``
+   The WhatsApp commands above.
 
-The daemon also connects the agent to a Telegram bot when
-``telegram.json`` exists in the data directory. It polls the Bot API for
-new messages, so it needs no open port.
+In Telegram, write as in the client, also with photos and image files.
+``onechat help`` lists all commands.
 
-#. Make a bot with BotFather and keep its token.
+*********************
+ What the agent does
+*********************
 
-#. Find your numeric Telegram user ID.
+Memory
+   The agent remembers the whole chat as summaries that it can open
+   again, so it never starts over. ``agent/instructions.md`` holds your
+   lasting preferences. The agent can propose a change to it and makes
+   the change only after you agree.
 
-#. Write the token and the ID to ``telegram.json``, and give the file
-   mode 0600, because the token is a secret. The daemon refuses the file
-   with any other mode.
+Tools
+   The agent runs shell commands as you, in its working directory, with
+   no sandbox.
 
-#. Start the daemon, open a private chat with the bot, and write.
+Subagents
+   For long tasks it starts subagents in the background, such as
+   ``GreenOtter``, and gets their reports as messages.
 
-.. code:: json
+Child agents
+   A child is a permanent agent for one area of your life, such as
+   ``House``, with its own memory. The root creates it, talks to it, and
+   hears from it only what needs you. Children never send WhatsApp
+   messages; they draft replies for you to approve.
 
-   {"token": "123456:ABC-token-from-BotFather", "owner": 123456789}
+WhatsApp
+   The agent reads your chats in batches and wakes for a private message
+   or a mention of you. It finds chats and sends messages only when you
+   ask. It proposes to route a chat that is only about one area to the
+   child of that area, and sets the route after you agree.
 
-The bot reads only the private chat of the owner and ignores all other
-updates. Each text message goes to the agent as a user message, the line
-``/stop`` interrupts the running turn, and the line ``/stop NAME`` stops
-the subagent NAME. The bot sends the replies in parts of at most 4096
-characters, with their Markdown as Telegram formats: bold, italic,
-strikethrough, code, code blocks, links, and headings as bold text. When
-Telegram refuses the formats of a part, the bot sends the part again as
-plain text. Each error goes as a short message in plain text. It shows
-the typing action while a turn runs. It does not send the tool calls.
-For a subagent it sends only ``[NAME] started`` and ``[NAME] finished``,
-``[NAME] stopped``, or ``[NAME] failed``, because the reply of the agent
-carries the content of the report. It sends each message that the user
-types in the terminal client, with the mark ``[terminal]``.
-
-``agent/telegram-offset.json`` holds the ID of the next update. The bot
-moves it past an update only after the message is durable in the log, so
-a restart of the daemon does not lose a message. A crash just after the
-write of a message can make the message repeat once. A network failure
-or a rate limit makes the bot wait and try again, and does not stop the
-daemon.
-
-**********
- WhatsApp
-**********
-
-The daemon connects to the WhatsApp account of the user as a linked
-device. The terminal client and Telegram take three commands:
+Images
+   Send a photo in Telegram, WhatsApp, or with ``/image`` to the agent.
+   Its memory keeps what the image shows, and it can look at an older
+   image again.
 
-``/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``.
+Schedules
+   Ask for reminders and timed tasks, such as "every weekday at 8, tell
+   me what my groups talked about". Each run uses the quota, so runs are
+   at least 5 minutes apart.
 
-``/whatsapp status``
-   Shows the link: not linked, linked and connected, or linked and
-   disconnected, with the times of the connection and the WhatsApp
-   routes.
-
-``/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 unrouted batches to the root
-in one turn, not one turn for each chat. Routed batches wake their
-child. 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.
-
-The root manages source routes with ``route(source, agent)``,
-``unroute(source)``, and ``routes()``, only after the user agrees. A
-source is ``whatsapp:CHAT-ID``, with the ID from ``whatsapp_find`` or a
-``[WhatsApp]`` message, and the agent must be its child. The root keeps
-the map in ``agent/routes.json`` with mode 0600. Children and subagents
-have no route tools. There is no command to change routes. Each routed
-batch enters only its child's log and wakes it, including a group batch
-with no mention. The durable agent queue prevents a second log entry
-after a restart. An unrouted source or a missing child uses the root. A
-queued batch whose child is removed while stopped goes to the root.
-
-When the session holds a linked device, the daemon connects at its
-start. If the first connection fails, it tries again after 2 seconds,
-then doubles the wait up to 5 minutes. The inbox and commands stay
-active during each wait. An unlink or a logout stops these retries.
-After a network failure of an open connection, the library 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.
-
-*************************
- Import WhatsApp history
-*************************
-
-Stop the daemon and finish all subagents before an import. Export each
-chat from WhatsApp as a ZIP with ``_chat.txt``. Keep the chat name in
-the ZIP name, such as ``WhatsApp Chat - Example.zip``. Read the plan
-first:
+Import
+   Old WhatsApp chats can join the memory in date order. Stop the
+   daemon, export the chat as a ZIP, and run the plan before the import.
+   The import rebuilds the target's memory with model calls:
 
 .. code:: console
 
-   onechat import -n CHAT.zip OTHER.zip
-   onechat import CHAT.zip OTHER.zip
-
-``-n`` makes no model call and prints the chats, chat-day batches, local
-dates, cutoff, text bytes, merged message count, and estimated calls.
-The import keeps multiline text, system lines, and omitted-media
-markers. It maps the session's profile name to ``You``. Without that
-name, it keeps the senders as written and says so in the plan. An exact,
-unambiguous local contact or batch name supplies the chat ID when known.
-No network lookup is made.
-
-Each chat and local day becomes one work message marked ``(imported)``.
-Only messages before the first message of the earliest live WhatsApp
-batch enter the import. Live times have minute precision, so the cutoff
-excludes that whole minute. A chat-day already imported is skipped.
-Overlapping chat-days in the supplied ZIPs are refused. Review the
-exports before import: their text goes to the model as chat history.
-
-The import merges the full log in date order and builds its tree with
-real compactor calls, the agent prompt, and the agent cache key. It uses
-a limit of 1,500 calls and the probe's 15-quota-point limit, and prints
-the same usage report. Its estimate is about 1.3 calls per merged
-message, not a cost guarantee. A stopped or failed run leaves the agent
-unchanged. A new run starts over. Success keeps the other agent files
-and renames the old directory to ``agent-backup-TIMESTAMP`` before it
-installs the new one. Keep the backup until the new memory is checked,
-then start the daemon. Message IDs change to their positions in the
-merged log.
-
-***********
- 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. The root and its children have the schedule tools, not their
-subagents. Each schedule records its owner and wakes only that agent.
-
-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.
-
-*******
- Probe
-*******
+   onechat import -n [--agent NAME] CHAT.zip
+   onechat import [--agent NAME] CHAT.zip
 
-``onechat probe LOG COUNT [BUILDS]`` measures the compactor on a log. It
-appends the first COUNT messages of LOG, one JSON object ``{"kind",
-"text"}`` on each line, to a new chat in ``probe/`` in the data
-directory, and builds its tree with real model calls. The calls use the
-quota of the account. The run stops at the first failed call, after 660
-calls, or when the used percent of a quota window grows by more than 15
-points. At the end it reports the calls per message, the cached share of
-the input tokens, the retries, the line sizes, and the quota at the
-first and the last call. BUILDS, from 1 to 8, limits the model calls at
-once in the run.
+*****
+ Log
+*****
 
-A test writes a generated log of readable made-up messages:
+The daemon writes short events with ``key=value`` attributes to the user
+journal, at three levels: info, warning for a failure that is retried or
+loses nothing, and error for a failure that loses data or stops work.
 
 .. code:: console
 
-   ONECHAT_PROBE_LOG=probe.jsonl go test -run TestProbeLogHasTheSyntheticMix
+   journalctl --user -u onechat -f
+   journalctl --user -u onechat -p warning
 
 *************
  Development
 *************
 
 Nix supplies the Go toolchain and all tools. Enter the development shell
-with direnv:
-
-.. code:: console
-
-   direnv allow
-
-Or enter it directly:
-
-.. code:: console
-
-   nix develop
-
-The shell sets ``ONECHAT_DIR`` to ``.devenv/onechat`` in the checkout,
-unless it is already set. Development data goes there, and Git ignores
-``.devenv``.
-
-Build and run:
+with ``direnv allow`` or ``nix develop``. Then:
 
 .. code:: console
 
    nix build
-   ./result/bin/onechat
-
-``onechat version`` prints ``yyyy-mm-dd-gitshort`` for a Nix build, adds
-``-dirty`` for a dirty tree, and prints ``dev`` for a direct Go build.
-
-Format and check all files:
-
-.. code:: console
-
+   ./result/bin/onechat version
    bin/format
    bin/check
    nix flake check
 
-``bin/check`` and ``nix build`` run a short replay of the synthetic log.
-``nix flake check`` also runs the full replay of 100,000 messages.
+``onechat version`` prints ``yyyy-mm-dd-gitshort`` for a Nix build and
+``dev`` for a direct Go build. ``bin/check`` runs the linters and a
+short replay of a synthetic log, and ``nix flake check`` also runs the
+full replay of 100,000 messages. ``onechat probe LOG COUNT`` measures
+the memory summaries on a log with real model calls.