]> git.giacomolli.uk - onechat/blob - README.rst
Start a turn when a schedule comes due
[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. The terminal client and Telegram take three commands:
181
182 ``/whatsapp pair PHONE``
183    Links the account of PHONE, the number with its country code, such as
184    ``+15550001111``. The daemon answers with the steps and then with a
185    pairing code in its own message: in WhatsApp on the phone, open
186    Linked devices, tap Link a device, then tap Link with phone number
187    instead, and enter the code. The code expires in about 2 minutes, and
188    the same command gives a new code. When the link succeeds, the daemon
189    answers with the linked number. Linked devices on the phone shows the
190    device as OneChat. The name applies at the pairing, so an existing
191    link keeps its old name until ``/whatsapp unlink`` and a new
192    ``/whatsapp pair PHONE``.
193
194 ``/whatsapp status``
195    Shows the link: not linked, linked and connected, or linked and
196    disconnected, with the times of the connection.
197
198 ``/whatsapp unlink``
199    Ends the link and deletes the session. When WhatsApp is disconnected,
200    the phone does not get the logout, so remove the device in Linked
201    devices on the phone.
202
203 The answers go only to the client that asked, and the commands do not go
204 to the agent, so the agent never gets the phone number or the code. The
205 chat gets only the notes that WhatsApp is linked or unlinked.
206
207 The session is ``whatsapp/session.db`` in the data directory, with mode
208 0600 in a directory with mode 0700. It gives full access to the WhatsApp
209 account, so the daemon refuses the file when its mode is not 0600. The
210 shell of the agent runs as the user, so it can read the session. If the
211 session leaks, remove the linked device in WhatsApp on the phone, and
212 the session stops working. Without the file, WhatsApp is not linked.
213
214 The daemon reads the text messages of the private chats and the groups,
215 from other people and from the user. It does not read media, reactions,
216 edits, deletions, view-once and disappearing messages, or the history
217 from before the link. The messages of one chat form one batch until the
218 chat is quiet for 20 seconds, for 2 minutes or 50 messages at the most.
219 Each batch goes to the chat of the agent as one work message that starts
220 with ``[WhatsApp]``, with the name and the ID of the chat, and the time,
221 the sender, and the text of each message. The messages of the user show
222 as ``You``. ``whatsapp/state.json``, with mode 0600, keeps the batches
223 that are not in the chat yet, so they survive a restart, and the records
224 of the sends. A crash just after a batch goes to the chat can make the
225 batch repeat once.
226
227 A batch of a private chat with a message of another person starts a
228 turn, and so does a batch of a group with a message that mentions the
229 user or replies to a message of the user. The other batches go to the
230 chat with no turn, and the agent sees them at its next turn. A batch
231 that starts a turn ends with a line that tells the agent to end its turn
232 with an empty reply when nothing in the batch needs the user. No client
233 shows an empty reply, so such a batch gives no message in Telegram. The
234 terminal client shows each batch, and Telegram shows none.
235
236 The phone number of the user never goes to the chat: a mention of the
237 user shows as ``@you``, the number shows as ``OWN``, and the chat of the
238 user with itself is not read. But the IDs of the other chats go to the
239 chat and to the model, because the agent needs them to send. The ID of a
240 private chat holds the phone number of the contact, such as
241 ``15550002222@s.whatsapp.net``, and the ID of a group can hold the
242 number of the person who made it.
243
244 While the daemon is stopped or disconnected, the WhatsApp servers keep
245 the messages for the linked device and send them at the next connection.
246 They keep them only for a limited time, so a long stop can lose
247 messages. These messages form batches by their original times, as live
248 messages do. The daemon waits for the end of this catch-up, or one
249 minute at the most, and then gives all its batches to the agent in one
250 turn, not one turn for each chat. The batches of ``whatsapp/state.json``
251 at the start wait for the first catch-up in the same way.
252
253 The agent finds a chat with the tool ``whatsapp_find``: it gives the
254 contacts and the groups whose name holds a text, with no regard to case
255 and accents, with their IDs. The names of the contacts come from the
256 session, and the names of the groups from WhatsApp, kept in memory for
257 one hour. The names arrive from the phone some minutes after the link,
258 and until then the tool says that they are not ready. The agent sends a
259 text message with the tool ``whatsapp_send``, only when the user asks
260 for it. Each send has an ID that the agent gives: a repeated call with
261 the same ID gives the answer of the first call and sends nothing. The
262 state file keeps the last 1000 send records. When the daemon stops
263 during a send, the result is not known, and the tool tells the agent to
264 ask the user to look at the chat in WhatsApp. Without a link, both tools
265 tell the agent to ask the user to run ``/whatsapp pair PHONE``. Only the
266 agent has the WhatsApp tools, not its subagents.
267
268 Each batch of a group goes to the chat, also with no turn, and the
269 memory tree summarizes it with model calls, which use the quota.
270
271 When the session holds a linked device, the daemon connects at its
272 start, and after a network failure it connects again by itself. When a
273 disconnection lasts 10 minutes, all clients get one notice, and one more
274 when the connection comes back. When WhatsApp ends the connection for
275 good, for example because another program uses the same session, all
276 clients get a notice at once. When the user removes the device in the
277 phone, the daemon deletes the session and tells all clients one time.
278
279 ***********
280  Schedules
281 ***********
282
283 The agent sets reminders and timed tasks when the user asks for them,
284 such as "remind me tomorrow at 9" or "every weekday at 8, tell me what
285 my WhatsApp groups talked about". A schedule has a text, the task for
286 the agent, and either one time or a cron expression. The agent also
287 lists the schedules with their next times, and removes one when the user
288 asks. Only the agent has the schedule tools, not its subagents.
289
290 A cron expression has five fields: minute, hour, day of month, month,
291 and day of week, where 0 and 7 are Sunday. A field is a comma list of
292 ``*``, ``N``, and ``N-M``, and ``*`` and ``N-M`` can have a step ``/S``,
293 as in ``0 8 * * 1-5`` or ``*/15 9-17 * * *``. When both day fields start
294 with something other than ``*``, a day matches when either field
295 matches. Names such as ``MON``, macros such as ``@daily``, the
296 characters ``? L W #``, and ``N/S`` are refused. The runs must be at
297 least 5 minutes apart, because each run is a full turn that uses the
298 quota, so ``*/5 * * * *`` is the shortest schedule.
299
300 The times use the time zone of the machine. A time that the clock skips
301 at the start of daylight saving time runs later by the length of the
302 skip. A time that the clock repeats at its end runs once, at its first
303 occurrence.
304
305 At its time, a schedule goes to the chat as a work message that starts
306 with ``[Schedule ID]``, with its time and its text, and it starts a
307 turn. The message ends with a line that tells the agent to end its turn
308 with an empty reply when nothing needs the user. The terminal client
309 shows the message, and Telegram shows only the reply. After its run, a
310 one-time schedule goes, and a repeating schedule gets its next time.
311
312 ``agent/schedules.json``, with mode 0600, holds the schedules. Each
313 change writes it before the tool answers, so the schedules survive a
314 restart. A schedule that came due while the daemon was stopped runs once
315 at the next start, with its original time and a line that says that the
316 run is late. A repeating schedule that missed several times runs once. A
317 crash just after a run goes to the chat can make the run repeat once.
318
319 *******
320  Probe
321 *******
322
323 ``onechat probe LOG COUNT [BUILDS]`` measures the compactor on a log. It
324 appends the first COUNT messages of LOG, one JSON object ``{"kind",
325 "text"}`` on each line, to a new chat in ``probe/`` in the data
326 directory, and builds its tree with real model calls. The calls use the
327 quota of the account. The run stops at the first failed call, after 660
328 calls, or when the used percent of a quota window grows by more than 15
329 points. At the end it reports the calls per message, the cached share of
330 the input tokens, the retries, the line sizes, and the quota at the
331 first and the last call. BUILDS, from 1 to 8, limits the model calls at
332 once in the run.
333
334 A test writes a generated log of readable made-up messages:
335
336 .. code:: console
337
338    ONECHAT_PROBE_LOG=probe.jsonl go test -run TestProbeLogHasTheSyntheticMix
339
340 *************
341  Development
342 *************
343
344 Nix supplies the Go toolchain and all tools. Enter the development shell
345 with direnv:
346
347 .. code:: console
348
349    direnv allow
350
351 Or enter it directly:
352
353 .. code:: console
354
355    nix develop
356
357 The shell sets ``ONECHAT_DIR`` to ``.devenv/onechat`` in the checkout,
358 unless it is already set. Development data goes there, and Git ignores
359 ``.devenv``.
360
361 Build and run:
362
363 .. code:: console
364
365    nix build
366    ./result/bin/onechat
367
368 Format and check all files:
369
370 .. code:: console
371
372    bin/format
373    bin/check
374    nix flake check
375
376 ``bin/check`` and ``nix build`` run a short replay of the synthetic log.
377 ``nix flake check`` also runs the full replay of 100,000 messages.