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