X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/ae8532ff0e9d3d676fec5f48780a62e58aa6882d..HEAD:/README.rst?ds=sidebyside diff --git a/README.rst b/README.rst index 929272c..7ca3d74 100644 --- a/README.rst +++ b/README.rst @@ -138,6 +138,40 @@ 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 ************** @@ -223,6 +257,100 @@ login. To stop it, or also remove its start at login: 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. + +.. 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 ************ @@ -315,7 +443,8 @@ device. The terminal client and Telegram take three commands: ``/whatsapp status`` Shows the link: not linked, linked and connected, or linked and - disconnected, with the times of the connection. + disconnected, with the times of the connection and the WhatsApp + routes. ``/whatsapp unlink`` Ends the link and deletes the session. When WhatsApp is disconnected, @@ -368,9 +497,10 @@ 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. +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 @@ -390,6 +520,17 @@ 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 @@ -415,21 +556,34 @@ first: 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. + onechat import -n --agent House CHAT.zip OTHER.zip + onechat import --agent House CHAT.zip OTHER.zip + +The target is the root unless ``--agent NAME`` selects an existing +direct child. A missing child is refused. The rebuild uses the target's +log, prompt, cache key, and usage records. The root keeps its history +when the target is a child. An import or plan is refused while the +target has pending agent deliveries. Start the daemon and let those +turns finish before you stop it and try again. This keeps their saved +log IDs valid. + +``-n`` makes no model call and prints the target name, chats, chat-day +batches, local dates, cutoffs, 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. +For each chat, the cutoff is the first message time of its earliest live +WhatsApp batch across the root and target logs. Only messages before +that cutoff enter the import. Live times have minute precision, so the +cutoff excludes that whole minute. A chat-day already imported into the +target is skipped. An imported day held only by the root does not skip +that day for a child. 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 @@ -438,9 +592,11 @@ 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. +installs the new one. A child's staging directory and backup stay in the +root agent directory, outside ``children/``. Its backup is +``NAME-backup-TIMESTAMP``. Keep the backup until the new memory is +checked, then start the daemon. Message IDs change to their positions in +the merged log. *********** Schedules @@ -451,7 +607,8 @@ 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. +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