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