]> git.giacomolli.uk - onechat/blob - README.rst
Show the WhatsApp link with /whatsapp status
[onechat] / README.rst
1 #########
2 OneChat
3 #########
4
5 OneChat is one AI agent in a single chat that never ends. A daemon owns
6 the agent and its files, and a terminal client and a Telegram bot talk
7 to it. It is in early development. Linux is supported.
8
9 ****************
10 Data directory
11 ****************
12
13 OneChat keeps its data and its token file in one directory. It uses the
14 first of these:
15
16 #. ``ONECHAT_DIR``, when it is set.
17 #. ``$XDG_DATA_HOME/onechat``, when ``XDG_DATA_HOME`` is an absolute
18 path. A relative value is ignored.
19 #. ``~/.local/share/onechat``. With no home directory, OneChat exits
20 with an error.
21
22 OneChat creates the directory with mode 0700.
23
24 ***************
25 Configuration
26 ***************
27
28 ``config.json`` in the data directory holds the settings. OneChat does
29 not create it. A missing file or field takes the default, and an unknown
30 field is an error.
31
32 .. code:: json
33
34 {
35 "endpoint": "chatgpt",
36 "compactor": {"model": "gpt-6.1-sol", "effort": "high"}
37 }
38
39 ``endpoint``
40 ``chatgpt`` (the default) sends model calls to the ChatGPT backend
41 with the subscription login. ``api`` sends them to the OpenAI API
42 with the key in ``OPENAI_API_KEY``, which must then be set. With
43 ``chatgpt``, OneChat ignores ``OPENAI_API_KEY``.
44
45 ``compactor``
46 The model and the reasoning effort of the summaries. The defaults are
47 ``gpt-6.1-sol`` and ``high``.
48
49 *******
50 Login
51 *******
52
53 The ``chatgpt`` endpoint needs a login with a ChatGPT account:
54
55 .. code:: console
56
57 onechat login
58
59 The command shows a link and a code. Open the link on any device, enter
60 the code, and approve the login. Device code login must be allowed in
61 the ChatGPT security settings. OneChat writes the tokens to
62 ``auth.json`` in the data directory, with mode 0600, and refreshes them
63 before they expire. The file belongs to OneChat only. Do not share it
64 with the Codex CLI, because each program rotates the refresh token and
65 logs the other out.
66
67 *******
68 Agent
69 *******
70
71 Start the daemon, and then the client in another terminal:
72
73 .. code:: console
74
75 onechat daemon
76 onechat chat
77
78 The daemon owns the root agent in ``agent/`` in the data directory and
79 listens on the Unix socket ``onechat.sock`` there. In the client, each
80 line is a message to the agent, the line ``/stop`` interrupts the
81 running turn, the line ``/stop NAME`` stops the subagent NAME, and
82 Ctrl-D ends the client. The client shows the replies, the tool calls,
83 and the first lines of each tool result as they happen. It also shows
84 each message that the user sends from another client, with the client as
85 a mark, such as ``[telegram] text``. When a subagent starts, the client
86 shows ``[NAME] started``, and it shows the whole report of the subagent,
87 which starts with ``[NAME]``, but not the tool calls of the subagent.
88 The daemon is the only process that uses the token file while it runs,
89 so ``onechat login`` and ``onechat probe`` refuse to start until it
90 stops.
91
92 The agent runs commands with bash as the user in its working directory,
93 with no sandbox. It has the rights of the user, also on the token file.
94
95 When the user asks for it, the agent starts subagents for long tasks.
96 Each subagent has a name, such as ``GreenOtter``, and runs in the
97 background while the agent answers other messages. It gets the view of
98 the agent and its task, and it runs its tools as the agent does, in
99 ``agent/subagents/NAME/work``. A task in a repository gives the path of
100 the repository. The steps of a subagent go to its own log in
101 ``agent/subagents/NAME``, not to the chat, and its final reply comes
102 back to the agent as one message that starts with ``[NAME]``. The agent
103 can send a subagent more messages: a running subagent reads them between
104 its tool calls, and a finished one goes on from its log, and its reply
105 comes back in the same way. When the user stops a subagent, the chat
106 gets a note, and the report of the subagent is logged, but it starts no
107 turn. Each model call of a subagent has the name in the ``agent`` field
108 of ``agent/usage.jsonl``. ``agent/work.json`` holds the names of the
109 running subagents. A subagent that runs when the daemon stops goes on at
110 the next start: the chat gets the note that the daemon restarted, and
111 the subagent gets the same note and goes on from its log.
112
113 ``agent/config.json`` holds the settings of the agent. The first start
114 writes it with the defaults and a random ID. Do not change the ID: the
115 prompt cache key comes from it.
116
117 .. code:: json
118
119 {
120 "name": "OneChat",
121 "id": "0123456789abcdef0123456789abcdef",
122 "model": "gpt-6.1-sol",
123 "effort": "medium",
124 "workdir": "/home/user"
125 }
126
127 ``workdir`` is an absolute path, and its default is the home directory.
128 ``agent/instructions.md`` holds the user's instructions: who they are,
129 how their files are organized, and how they want work done. The prompt
130 ends with them, and a change applies at the next call.
131
132 **********
133 Telegram
134 **********
135
136 The daemon also connects the agent to a Telegram bot when
137 ``telegram.json`` exists in the data directory. It polls the Bot API for
138 new messages, so it needs no open port.
139
140 #. Make a bot with BotFather and keep its token.
141
142 #. Find your numeric Telegram user ID.
143
144 #. Write the token and the ID to ``telegram.json``, and give the file
145 mode 0600, because the token is a secret. The daemon refuses the file
146 with any other mode.
147
148 #. Start the daemon, open a private chat with the bot, and write.
149
150 .. code:: json
151
152 {"token": "123456:ABC-token-from-BotFather", "owner": 123456789}
153
154 The bot reads only the private chat of the owner and ignores all other
155 updates. Each text message goes to the agent as a user message, the line
156 ``/stop`` interrupts the running turn, and the line ``/stop NAME`` stops
157 the subagent NAME. The bot sends the replies in parts of at most 4096
158 characters, with their Markdown as Telegram formats: bold, italic,
159 strikethrough, code, code blocks, links, and headings as bold text. When
160 Telegram refuses the formats of a part, the bot sends the part again as
161 plain text. Each error goes as a short message in plain text. It shows
162 the typing action while a turn runs. It does not send the tool calls.
163 For a subagent it sends only ``[NAME] started`` and ``[NAME] finished``,
164 ``[NAME] stopped``, or ``[NAME] failed``, because the reply of the agent
165 carries the content of the report. It sends each message that the user
166 types in the terminal client, with the mark ``[terminal]``.
167
168 ``agent/telegram-offset.json`` holds the ID of the next update. The bot
169 moves it past an update only after the message is durable in the log, so
170 a restart of the daemon does not lose a message. A crash just after the
171 write of a message can make the message repeat once. A network failure
172 or a rate limit makes the bot wait and try again, and does not stop the
173 daemon.
174
175 **********
176 WhatsApp
177 **********
178
179 The daemon connects to the WhatsApp account of the user as a linked
180 device. In the terminal client or in Telegram, the line ``/whatsapp
181 status`` shows the link: not linked, linked and connected, or linked and
182 disconnected, with the times of the connection. The answer goes only to
183 the client that asked, and the line does not go to the agent.
184
185 The session is ``whatsapp/session.db`` in the data directory. It gives
186 full access to the WhatsApp account, so the daemon refuses the file when
187 its mode is not 0600. The shell of the agent runs as the user, so it can
188 read the session. If the session leaks, remove the linked device in
189 WhatsApp on the phone, and the session stops working. Without the file,
190 WhatsApp is not linked. When the session holds a linked device, the
191 daemon connects at its start, and after a network failure it connects
192 again by itself.
193
194 *******
195 Probe
196 *******
197
198 ``onechat probe LOG COUNT [BUILDS]`` measures the compactor on a log. It
199 appends the first COUNT messages of LOG, one JSON object ``{"kind",
200 "text"}`` on each line, to a new chat in ``probe/`` in the data
201 directory, and builds its tree with real model calls. The calls use the
202 quota of the account. The run stops at the first failed call, after 660
203 calls, or when the used percent of a quota window grows by more than 15
204 points. At the end it reports the calls per message, the cached share of
205 the input tokens, the retries, the line sizes, and the quota at the
206 first and the last call. BUILDS, from 1 to 8, limits the model calls at
207 once in the run.
208
209 A test writes a generated log of readable made-up messages:
210
211 .. code:: console
212
213 ONECHAT_PROBE_LOG=probe.jsonl go test -run TestProbeLogHasTheSyntheticMix
214
215 *************
216 Development
217 *************
218
219 Nix supplies the Go toolchain and all tools. Enter the development shell
220 with direnv:
221
222 .. code:: console
223
224 direnv allow
225
226 Or enter it directly:
227
228 .. code:: console
229
230 nix develop
231
232 The shell sets ``ONECHAT_DIR`` to ``.devenv/onechat`` in the checkout,
233 unless it is already set. Development data goes there, and Git ignores
234 ``.devenv``.
235
236 Build and run:
237
238 .. code:: console
239
240 nix build
241 ./result/bin/onechat
242
243 Format and check all files:
244
245 .. code:: console
246
247 bin/format
248 bin/check
249 nix flake check
250
251 ``bin/check`` and ``nix build`` run a short replay of the synthetic log.
252 ``nix flake check`` also runs the full replay of 100,000 messages.