######### OneChat ######### 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. **************** 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. OneChat creates the directory with mode 0700. *************** Configuration *************** ``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. .. code:: json { "endpoint": "chatgpt", "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``. ``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: .. 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. ******* Agent ******* Start the daemon, and then the client in another terminal: .. code:: console onechat daemon onechat chat 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. The client shows the replies, the tool calls, and the first lines of each tool result as they happen. 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. .. code:: json { "name": "OneChat", "id": "0123456789abcdef0123456789abcdef", "model": "gpt-6.1-sol", "effort": "medium", "workdir": "/home/user" } ``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. ********** Telegram ********** 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. #. Make a bot with BotFather and keep its token. #. Find your numeric Telegram user ID. #. 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. #. Start the daemon, open a private chat with the bot, and write. .. code:: json {"token": "123456:ABC-token-from-BotFather", "owner": 123456789} 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: ``/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. ``/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. 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. 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. ******* Probe ******* ``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. A test writes a generated log of readable made-up messages: .. code:: console ONECHAT_PROBE_LOG=probe.jsonl go test -run TestProbeLogHasTheSyntheticMix ************* 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: .. code:: console nix build ./result/bin/onechat Format and check all files: .. code:: console 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.