]> git.giacomolli.uk - onechat/blob - README.rst
Document user service updates
[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 User service
134 **************
135
136 The Nix package includes ``share/systemd/user/onechat.service``. It runs
137 ``onechat daemon`` from that package, starts at login when enabled, and
138 restarts 5 seconds after a failure. A stop sends SIGTERM to the daemon
139 and allows up to 120 seconds for cleanup. Its output goes to the user
140 journal. The service has the rights of the user, as the terminal daemon
141 does.
142
143 Build a package with a separate result link. Keep this link so that Nix
144 does not collect the package while the service uses it:
145
146 .. code:: console
147
148 nix build --out-link result-service
149 systemctl --user link --force "$(realpath result-service/share/systemd/user/onechat.service)"
150 systemctl --user edit onechat.service
151
152 In the editor, set the data directory with this drop-in. Replace
153 ``DATA_DIR`` with an absolute path to the data directory. Set the same
154 ``ONECHAT_DIR`` for each terminal client. Without a drop-in, the data
155 directory rules above apply to the environment of the user manager, not
156 to the development shell.
157
158 .. code:: ini
159
160 [Service]
161 Environment="ONECHAT_DIR=DATA_DIR"
162
163 The service does not read the environment of an open terminal. If the
164 agent needs tools from a shell, set ``Environment="PATH=TOOLS_PATH"`` in
165 the same drop-in, with ``TOOLS_PATH`` replaced by the complete search
166 path, including bash. For the API endpoint, use
167 ``EnvironmentFile=ENV_FILE``, with ``ENV_FILE`` replaced by an absolute
168 path to a mode 0600 file that sets ``OPENAI_API_KEY``. Keep secrets out
169 of the unit and Nix sources.
170
171 Use the existing data directory, or copy the development data before the
172 first service start. For a copy, first stop the terminal daemon with
173 Ctrl-C and wait for it to exit. Replace ``DATA_DIR`` below with a new,
174 empty destination. If the development shell uses a custom
175 ``ONECHAT_DIR``, replace ``.devenv/onechat`` with that source. Copy the
176 whole directory so that ``auth.json``, ``telegram.json``,
177 ``config.json``, ``agent/``, and ``whatsapp/`` stay together, including
178 the WhatsApp session and its database files. The copy preserves the file
179 modes and the source as a backup. Do not copy or use the same data from
180 two running daemons.
181
182 .. code:: console
183
184 install -d -m 0700 DATA_DIR
185 cp -a .devenv/onechat/. DATA_DIR/
186
187 After the terminal daemon exits and the data is ready, enable and start
188 the service:
189
190 .. code:: console
191
192 systemctl --user daemon-reload
193 systemctl --user enable --now onechat.service
194 systemctl --user status onechat.service
195 ONECHAT_DIR=DATA_DIR ./result-service/bin/onechat chat
196 journalctl --user -u onechat.service -f
197
198 After ``git pull``, build with the same result link, link the unit
199 again, reload the user manager, and restart the service:
200
201 .. code:: console
202
203 nix build --out-link result-service
204 systemctl --user link --force "$(realpath result-service/share/systemd/user/onechat.service)"
205 systemctl --user daemon-reload
206 systemctl --user restart onechat.service
207
208 The service starts with the user manager at login, not at boot without a
209 login. To stop it, or also remove its start at login:
210
211 .. code:: console
212
213 systemctl --user stop onechat.service
214 systemctl --user disable onechat.service
215
216 **********
217 Telegram
218 **********
219
220 The daemon also connects the agent to a Telegram bot when
221 ``telegram.json`` exists in the data directory. It polls the Bot API for
222 new messages, so it needs no open port.
223
224 #. Make a bot with BotFather and keep its token.
225
226 #. Find your numeric Telegram user ID.
227
228 #. Write the token and the ID to ``telegram.json``, and give the file
229 mode 0600, because the token is a secret. The daemon refuses the file
230 with any other mode.
231
232 #. Start the daemon, open a private chat with the bot, and write.
233
234 .. code:: json
235
236 {"token": "123456:ABC-token-from-BotFather", "owner": 123456789}
237
238 The bot reads only the private chat of the owner and ignores all other
239 updates. Each text message goes to the agent as a user message, the line
240 ``/stop`` interrupts the running turn, and the line ``/stop NAME`` stops
241 the subagent NAME. The bot sends the replies in parts of at most 4096
242 characters, with their Markdown as Telegram formats: bold, italic,
243 strikethrough, code, code blocks, links, and headings as bold text. When
244 Telegram refuses the formats of a part, the bot sends the part again as
245 plain text. Each error goes as a short message in plain text. It shows
246 the typing action while a turn runs. It does not send the tool calls.
247 For a subagent it sends only ``[NAME] started`` and ``[NAME] finished``,
248 ``[NAME] stopped``, or ``[NAME] failed``, because the reply of the agent
249 carries the content of the report. It sends each message that the user
250 types in the terminal client, with the mark ``[terminal]``.
251
252 ``agent/telegram-offset.json`` holds the ID of the next update. The bot
253 moves it past an update only after the message is durable in the log, so
254 a restart of the daemon does not lose a message. A crash just after the
255 write of a message can make the message repeat once. A network failure
256 or a rate limit makes the bot wait and try again, and does not stop the
257 daemon.
258
259 **********
260 WhatsApp
261 **********
262
263 The daemon connects to the WhatsApp account of the user as a linked
264 device. The terminal client and Telegram take three commands:
265
266 ``/whatsapp pair PHONE``
267 Links the account of PHONE, the number with its country code, such as
268 ``+15550001111``. The daemon answers with the steps and then with a
269 pairing code in its own message: in WhatsApp on the phone, open
270 Linked devices, tap Link a device, then tap Link with phone number
271 instead, and enter the code. The code expires in about 2 minutes, and
272 the same command gives a new code. When the link succeeds, the daemon
273 answers with the linked number. Linked devices on the phone shows the
274 device as OneChat. The name applies at the pairing, so an existing
275 link keeps its old name until ``/whatsapp unlink`` and a new
276 ``/whatsapp pair PHONE``.
277
278 ``/whatsapp status``
279 Shows the link: not linked, linked and connected, or linked and
280 disconnected, with the times of the connection.
281
282 ``/whatsapp unlink``
283 Ends the link and deletes the session. When WhatsApp is disconnected,
284 the phone does not get the logout, so remove the device in Linked
285 devices on the phone.
286
287 The answers go only to the client that asked, and the commands do not go
288 to the agent, so the agent never gets the phone number or the code. The
289 chat gets only the notes that WhatsApp is linked or unlinked.
290
291 The session is ``whatsapp/session.db`` in the data directory, with mode
292 0600 in a directory with mode 0700. It gives full access to the WhatsApp
293 account, so the daemon refuses the file when its mode is not 0600. The
294 shell of the agent runs as the user, so it can read the session. If the
295 session leaks, remove the linked device in WhatsApp on the phone, and
296 the session stops working. Without the file, WhatsApp is not linked.
297
298 The daemon reads the text messages of the private chats and the groups,
299 from other people and from the user. It does not read media, reactions,
300 edits, deletions, view-once and disappearing messages, or the history
301 from before the link. The messages of one chat form one batch until the
302 chat is quiet for 20 seconds, for 2 minutes or 50 messages at the most.
303 Each batch goes to the chat of the agent as one work message that starts
304 with ``[WhatsApp]``, with the name and the ID of the chat, and the time,
305 the sender, and the text of each message. The messages of the user show
306 as ``You``. ``whatsapp/state.json``, with mode 0600, keeps the batches
307 that are not in the chat yet, so they survive a restart, and the records
308 of the sends. A crash just after a batch goes to the chat can make the
309 batch repeat once.
310
311 A batch of a private chat with a message of another person starts a
312 turn, and so does a batch of a group with a message that mentions the
313 user or replies to a message of the user. The other batches go to the
314 chat with no turn, and the agent sees them at its next turn. A batch
315 that starts a turn ends with a line that tells the agent to end its turn
316 with an empty reply when nothing in the batch needs the user. No client
317 shows an empty reply, so such a batch gives no message in Telegram. The
318 terminal client shows each batch, and Telegram shows none.
319
320 The phone number of the user never goes to the chat: a mention of the
321 user shows as ``@you``, the number shows as ``OWN``, and the chat of the
322 user with itself is not read. But the IDs of the other chats go to the
323 chat and to the model, because the agent needs them to send. The ID of a
324 private chat holds the phone number of the contact, such as
325 ``15550002222@s.whatsapp.net``, and the ID of a group can hold the
326 number of the person who made it.
327
328 While the daemon is stopped or disconnected, the WhatsApp servers keep
329 the messages for the linked device and send them at the next connection.
330 They keep them only for a limited time, so a long stop can lose
331 messages. These messages form batches by their original times, as live
332 messages do. The daemon waits for the end of this catch-up, or one
333 minute at the most, and then gives all its batches to the agent in one
334 turn, not one turn for each chat. The batches of ``whatsapp/state.json``
335 at the start wait for the first catch-up in the same way.
336
337 The agent finds a chat with the tool ``whatsapp_find``: it gives the
338 contacts and the groups whose name holds a text, with no regard to case
339 and accents, with their IDs. The names of the contacts come from the
340 session, and the names of the groups from WhatsApp, kept in memory for
341 one hour. The names arrive from the phone some minutes after the link,
342 and until then the tool says that they are not ready. The agent sends a
343 text message with the tool ``whatsapp_send``, only when the user asks
344 for it. Each send has an ID that the agent gives: a repeated call with
345 the same ID gives the answer of the first call and sends nothing. The
346 state file keeps the last 1000 send records. When the daemon stops
347 during a send, the result is not known, and the tool tells the agent to
348 ask the user to look at the chat in WhatsApp. Without a link, both tools
349 tell the agent to ask the user to run ``/whatsapp pair PHONE``. Only the
350 agent has the WhatsApp tools, not its subagents.
351
352 Each batch of a group goes to the chat, also with no turn, and the
353 memory tree summarizes it with model calls, which use the quota.
354
355 When the session holds a linked device, the daemon connects at its
356 start, and after a network failure it connects again by itself. When a
357 disconnection lasts 10 minutes, all clients get one notice, and one more
358 when the connection comes back. When WhatsApp ends the connection for
359 good, for example because another program uses the same session, all
360 clients get a notice at once. When the user removes the device in the
361 phone, the daemon deletes the session and tells all clients one time.
362
363 ***********
364 Schedules
365 ***********
366
367 The agent sets reminders and timed tasks when the user asks for them,
368 such as "remind me tomorrow at 9" or "every weekday at 8, tell me what
369 my WhatsApp groups talked about". A schedule has a text, the task for
370 the agent, and either one time or a cron expression. The agent also
371 lists the schedules with their next times, and removes one when the user
372 asks. Only the agent has the schedule tools, not its subagents.
373
374 A cron expression has five fields: minute, hour, day of month, month,
375 and day of week, where 0 and 7 are Sunday. A field is a comma list of
376 ``*``, ``N``, and ``N-M``, and ``*`` and ``N-M`` can have a step ``/S``,
377 as in ``0 8 * * 1-5`` or ``*/15 9-17 * * *``. When both day fields start
378 with something other than ``*``, a day matches when either field
379 matches. Names such as ``MON``, macros such as ``@daily``, the
380 characters ``? L W #``, and ``N/S`` are refused. The runs must be at
381 least 5 minutes apart, because each run is a full turn that uses the
382 quota, so ``*/5 * * * *`` is the shortest schedule.
383
384 The times use the time zone of the machine. A time that the clock skips
385 at the start of daylight saving time runs later by the length of the
386 skip. A time that the clock repeats at its end runs once, at its first
387 occurrence.
388
389 At its time, a schedule goes to the chat as a work message that starts
390 with ``[Schedule ID]``, with its time and its text, and it starts a
391 turn. The message ends with a line that tells the agent to end its turn
392 with an empty reply when nothing needs the user. The terminal client
393 shows the message, and Telegram shows only the reply. After its run, a
394 one-time schedule goes, and a repeating schedule gets its next time.
395
396 ``agent/schedules.json``, with mode 0600, holds the schedules. Each
397 change writes it before the tool answers, so the schedules survive a
398 restart. A schedule that came due while the daemon was stopped runs once
399 at the next start, with its original time and a line that says that the
400 run is late. A repeating schedule that missed several times runs once. A
401 crash just after a run goes to the chat can make the run repeat once.
402
403 *******
404 Probe
405 *******
406
407 ``onechat probe LOG COUNT [BUILDS]`` measures the compactor on a log. It
408 appends the first COUNT messages of LOG, one JSON object ``{"kind",
409 "text"}`` on each line, to a new chat in ``probe/`` in the data
410 directory, and builds its tree with real model calls. The calls use the
411 quota of the account. The run stops at the first failed call, after 660
412 calls, or when the used percent of a quota window grows by more than 15
413 points. At the end it reports the calls per message, the cached share of
414 the input tokens, the retries, the line sizes, and the quota at the
415 first and the last call. BUILDS, from 1 to 8, limits the model calls at
416 once in the run.
417
418 A test writes a generated log of readable made-up messages:
419
420 .. code:: console
421
422 ONECHAT_PROBE_LOG=probe.jsonl go test -run TestProbeLogHasTheSyntheticMix
423
424 *************
425 Development
426 *************
427
428 Nix supplies the Go toolchain and all tools. Enter the development shell
429 with direnv:
430
431 .. code:: console
432
433 direnv allow
434
435 Or enter it directly:
436
437 .. code:: console
438
439 nix develop
440
441 The shell sets ``ONECHAT_DIR`` to ``.devenv/onechat`` in the checkout,
442 unless it is already set. Development data goes there, and Git ignores
443 ``.devenv``.
444
445 Build and run:
446
447 .. code:: console
448
449 nix build
450 ./result/bin/onechat
451
452 Format and check all files:
453
454 .. code:: console
455
456 bin/format
457 bin/check
458 nix flake check
459
460 ``bin/check`` and ``nix build`` run a short replay of the synthetic log.
461 ``nix flake check`` also runs the full replay of 100,000 messages.