+********************
+ 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.
+
+.. code:: console
+
+ 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.
+
+.. code:: sh
+
+ #!/bin/sh
+ 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
+
+ [Unit]
+ Description=Back up OneChat data
+ Requires=onechat.service
+ After=onechat.service
+
+ [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.
+
+.. 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
+
+The summary warning names its node. A schedule run has ``late`` only
+when it ran late:
+
+.. code:: text
+
+ <4>onechat: summary failed level=0 index=0: fake failure
+ <6>onechat: schedule ran id=1
+ <6>onechat: schedule ran id=2 late=1m0s
+
+Outside the journal, each line starts with the time stamp and level.
+