]> git.giacomolli.uk - onechat/log
onechat
25 hours agoLog each change of the WhatsApp connection
Anderson A. Giacomolli [Mon, 9 Nov 2026 13:23:55 +0000 (10:23 -0300)]
Log each change of the WhatsApp connection

The WhatsApp log gets one line for each change of the connection of the
linked device: connected, a failed first connection, a lost connection,
connected again with the length of the outage, a connection that does
not come back by itself, a link, an unlink, and a logout from the phone.
The retries of the library between them log nothing, and the logger of
the library stays off.

Each line says first what happens to the WhatsApp messages, and then the
cause. An unlink clears the time of the last disconnection, so the first
connection of a new link does not count as the end of an outage.

Issue: e4acfcb

25 hours agoStamp daemon log lines only outside the journal
Anderson A. Giacomolli [Mon, 9 Nov 2026 13:19:42 +0000 (10:19 -0300)]
Stamp daemon log lines only outside the journal

The daemon makes the logger of each part in one place. Under the
journal, which sets JOURNAL_STREAM and adds its own time, the lines have
no time stamp. Otherwise every line has the standard time stamp, the
onechat lines too, so that no line has two time stamps and none has
none.

The start line names each value: the socket, the model and its effort,
the directory where the commands run, whether WhatsApp is linked, and
the number of active schedules.

Issue: e4acfcb

25 hours agoLog Telegram outages once in plain words
Anderson A. Giacomolli [Mon, 9 Nov 2026 13:16:50 +0000 (10:16 -0300)]
Log Telegram outages once in plain words

A Telegram outage logs one line at its start and one at its end, with
its length, and the retries between them log nothing. Each line says
first what the user loses or gets, such as "cannot get messages from
Telegram" or "replies go to Telegram again, after 1m25s", and then the
cause in plain words, such as "no answer in 1m10s" or "Telegram answered
502 Bad Gateway".

The errors of the Bot API client hold no method name and no Go deadline
text, so that the log line can say what the call was for. A refused
format, a dropped reply, and an ignored message use the same plain
words.

Issue: e4acfcb

25 hours agoOpen the chat when no command is given
Anderson A. Giacomolli [Mon, 9 Nov 2026 13:13:51 +0000 (10:13 -0300)]
Open the chat when no command is given

onechat with no command opens the terminal chat, as onechat chat does,
because the chat is the command of daily use once the daemon runs as a
service. A start of the daemon stays explicit, so a second daemon does
not start by accident. onechat help, --help, and -h print the commands,
and an unknown command prints the error and the same help. The install
check runs onechat help in place of the bare command.

25 hours agoSend the compaction view in block messages
Anderson A. Giacomolli [Mon, 9 Nov 2026 13:02:40 +0000 (10:02 -0300)]
Send the compaction view in block messages

A compaction sends each whole block of four view lines as its own user
message, the first one with the opening tag, and one last user message
with the rest of the lines, the closing tag, and the task. The note of a
retry is one more user message after them. The blocks count from the
first line of the view, so two compactions whose views begin with the
same lines send the same block messages. Joined, the messages are the
same bytes as the one message before.

The backend reads a cache entry only at a message end of a request. With
the view in one message, a compaction read only the shared prompt and
tools, about 1,920 of 10,000 tokens. The first request of a turn sends
its view as messages that only grow and reads 74% to 85% from the cache,
so the compactions get the same kind of split.

Issue: 72c95ad

36 hours agoShorten user service updates
Anderson A. Giacomolli [Mon, 9 Nov 2026 02:03:01 +0000 (23:03 -0300)]
Shorten user service updates

The user unit links through result-service, so each build replaces its
package without another unit link. The update command builds with that
out-link, reloads the user manager, and uses try-restart to restart only
an active service. A stopped service stays stopped.

The README gives the first link and the two update commands. The checks
include shell lint for the update script.

Issue: 99a5e07

36 hours agoDocument user service updates
Anderson A. Giacomolli [Mon, 9 Nov 2026 01:59:06 +0000 (22:59 -0300)]
Document user service updates

The README gives the update commands after a git pull. The same result
link keeps the new package available, and linking the unit again makes
the user manager load the unit from that package before the service
restarts.

Issue: 99a5e07

36 hours agoPackage a systemd user service
Anderson A. Giacomolli [Mon, 9 Nov 2026 01:51:50 +0000 (22:51 -0300)]
Package a systemd user service

The flake package supplies a user unit that runs its own daemon binary
at login, restarts failures after five seconds, and sends output to the
journal. SIGTERM starts cleanup, with two minutes before a forced stop.
The unit keeps the rights of the user and a private file creation mask.

A unit in the existing package is the smallest flake interface for this
service. It needs no extra module interface or dependency and leaves the
data directory and tool environment in a standard user drop-in.

The README covers the data copy with the daemon stopped, the shared
client data setting, the package result link, and service controls. A
flake check verifies the built unit without a running user manager.

Issue: 99a5e07

37 hours agoStart a turn when a schedule comes due
Anderson A. Giacomolli [Mon, 9 Nov 2026 00:35:08 +0000 (21:35 -0300)]
Start a turn when a schedule comes due

The daemon runs the schedules next to WhatsApp. At its time, a schedule
goes to the chat as a work message that starts with [Schedule ID] and
starts a turn of the root agent, which does the task or spawns a
subagent for long work. The message ends with the empty reply line, so a
task that finds nothing for the user gives no message.

The runner waits until the first due time, at most one minute at a time,
so it sees a suspend of the machine or a change of the clock within a
minute. A set or a remove wakes it. It logs all due schedules in due
order, flushes the log, ends each run in the file, and wakes the turn
loop once, as the WhatsApp batches do. A crash between the flush and the
file write makes a run repeat once. So a schedule that came due while
the daemon was stopped runs once at the next start, with the line that
the run is late.

Telegram does not send the work message, because the reply of the agent
carries its content. The terminal client shows it. The README describes
the schedules, the cron subset, the limit of 5 minutes, and the rules
for daylight saving time.

Issue: 60e4e4f

38 hours agoGive the root agent the schedule tools
Anderson A. Giacomolli [Mon, 9 Nov 2026 00:29:32 +0000 (21:29 -0300)]
Give the root agent the schedule tools

The root agent gets three tools. schedule_set adds a schedule with a
task text and either a local time or a cron expression, and answers with
its ID and next time. schedule_list gives each schedule with its ID,
next time, cron expression or once, and text. schedule_remove removes
one by its ID.

The descriptions tell the agent to set a schedule only when the user
asks, to write the text as an order to itself, and to turn a time such
as "tomorrow at 9" into a local time from the Now line. The cron
description gives the limit of 5 minutes between runs. The prompt text
does not change, so it stays near the reference design.

The daemon opens agent/schedules.json at its start. The toolbox of a
subagent has no schedules, so the tools are unknown there, as the
WhatsApp tools are. Compactions send the new tools too, so they keep the
shared prompt cache.

Issue: 60e4e4f

38 hours agoKeep the schedules in a file
Anderson A. Giacomolli [Mon, 9 Nov 2026 00:24:44 +0000 (21:24 -0300)]
Keep the schedules in a file

The agent gets schedules: a task with one local time or a cron
expression. agent/schedules.json holds them, and each set and remove
writes it whole with mode 0600 before it returns, so a schedule survives
a restart. The file keeps the last ID, so that an ID is never used
again. A mutex protects the schedules, because the tools change them
from a turn while the runs read them.

A one-time time is a local wall time after now. A time in the hour that
the clock repeats is its first occurrence, as in cron. The cron
expressions are checked at a set and at the start.

A run ends with Done: a one-time schedule goes, and a repeating one gets
the next time after now, so a repeating schedule that missed several
times while the daemon was stopped runs once. The message of a run names
the schedule, gives its time and the task, says when the run is more
than one minute late, and ends with the line about the empty reply, as a
WhatsApp batch does.

Issue: 60e4e4f

38 hours agoParse cron expressions and find the next time
Anderson A. Giacomolli [Mon, 9 Nov 2026 00:20:15 +0000 (21:20 -0300)]
Parse cron expressions and find the next time

The schedules of the agent repeat by a five-field cron expression, so
OneChat parses one with a small parser and no new dependency. A field is
a comma list of *, N, and N-M, where * and N-M can have a step. When
both day fields are restricted, a day matches when either one matches,
as in classic cron. The parser refuses names, macros, the characters ? L
W #, a seconds field, N/S, values out of range, and an expression that
matches no day in 8 years, each with an error that names the problem.

It also refuses an expression whose runs can be less than 5 minutes
apart, also across the end of an hour and across midnight. Each run is a
full turn, so a run every minute would use the weekly quota in hours.
The error gives the limit, so that the agent can ask the user.

The next time is the first matching wall time in the local zone. A wall
time that the clock skips runs later by the length of the skip, and a
wall time that the clock repeats runs once, at its first occurrence.

Issue: 60e4e4f

39 hours agoShow the linked device as OneChat on the phone
Anderson A. Giacomolli [Sun, 8 Nov 2026 22:21:11 +0000 (19:21 -0300)]
Show the linked device as OneChat on the phone

The daemon sets the device properties of the library once before any
client exists, because they are global values of the library. The
properties have the OS name OneChat and the desktop platform type.
Without a known platform type, the phone ignores the name and shows the
device as "Other device".

The display name of the phone code pairing stays "Chrome (Linux)": the
server accepts only the form "Browser (OS)" with a common browser and
OS, and refuses the pairing otherwise. A comment says so.

The name applies at the pairing, so an existing link keeps its old name
until the user unlinks and pairs again.

40 hours agoMove whatsapp_send to its own file
Anderson A. Giacomolli [Sun, 8 Nov 2026 21:40:53 +0000 (18:40 -0300)]
Move whatsapp_send to its own file

The send of whatsapp_send is now the function whatsappSend in
whatsapp_send.go, with its check of the arguments and its constants. It
owns no state: it gets a WhatsAppLink for the device, the client, the
connection, and the own IDs, the WhatsAppGroups for the name of a group
in the answer, and the state file for the send records. WhatsApp.Send
stays the entry point of the tool and gives the current link. The tests
of the tool move to whatsapp_send_test.go. The behavior does not change.

Issue: 6f69ca1

40 hours agoMove whatsapp_find to its own file
Anderson A. Giacomolli [Sun, 8 Nov 2026 21:39:43 +0000 (18:39 -0300)]
Move whatsapp_find to its own file

The search of whatsapp_find is now the function whatsappFind in
whatsapp_find.go, with its matches, its fold of the names, and its
answer. It owns no state: it gets a WhatsAppLink for the device, the
client, and the own IDs, and the WhatsAppGroups for the names of the
groups. WhatsApp.Find stays the entry point of the tool and gives the
current link. The tests of the tool move to whatsapp_find_test.go. The
behavior does not change.

Issue: 6f69ca1

40 hours agoGive the WhatsApp batches their own type
Anderson A. Giacomolli [Sun, 8 Nov 2026 21:38:18 +0000 (18:38 -0300)]
Give the WhatsApp batches their own type

WhatsAppInbox in whatsapp_inbox.go puts the incoming text messages into
batches, logs the closed batches, and holds the catch-up after each
connection. It owns the wake and failures channels of Run, the limit of
the catch-up, and, under its own mutex, the state of the catch-up and
its timer. The conversion of an event to a batch message goes with it.

WhatsApp keeps the session and the connection. Its event handler gives
each message to the inbox with a WhatsAppLink, and starts and ends the
catch-up there. The state file stays a field of WhatsApp too, because it
also holds the send records, and it has a mutex of its own. The tests of
the batches and the catch-up move to whatsapp_inbox_test.go, and the
helpers that more test files use move to whatsapp_fake_helper_test.go.
The behavior does not change.

Issue: 6f69ca1

40 hours agoGive the WhatsApp group names their own type
Anderson A. Giacomolli [Sun, 8 Nov 2026 21:34:05 +0000 (18:34 -0300)]
Give the WhatsApp group names their own type

WhatsAppGroups in whatsapp_groups.go holds the names of the groups, the
time of the last read of the groups from WhatsApp, and the time of the
last read that succeeded, under its own mutex. The batches, find, and
send use it, and the event handler of WhatsApp sets the names of the
group events in it.

WhatsAppLink is a copy of the link of the device: whether it is linked,
the device, the client, whether it is connected, and the own IDs.
WhatsApp keeps these fields under its mutex and gives a copy to each
call of the groups, so the groups read with the current client and never
keep it. No type calls another type while it holds its mutex. The
behavior does not change.

Issue: 6f69ca1

41 hours agoConvert WhatsApp events with pure functions
Anderson A. Giacomolli [Sun, 8 Nov 2026 21:32:21 +0000 (18:32 -0300)]
Convert WhatsApp events with pure functions

The conversion of a message event to a batch message and the removal of
the own IDs from a text are now the functions whatsappMessage,
whatsappText, and whatsappRedact. They get the device and the own IDs as
arguments and read no state of WhatsApp, so the batches, find, and send
can use them after the split of WhatsApp by responsibility. The method
receive copies the device and the own IDs under the mutex, as message
did, and the method redact calls whatsappRedact. The behavior does not
change.

Issue: 6f69ca1

44 hours agoKeep the connection of a new WhatsApp link
Anderson A. Giacomolli [Sun, 8 Nov 2026 17:50:26 +0000 (14:50 -0300)]
Keep the connection of a new WhatsApp link

After a pairing, the linked device never connected again. The pairing
client connected with the context of the pairing, and the library keeps
the context of ConnectContext for its keep-alive loop, its event handler
loop, and its reconnections. After PairSuccess the server ends the
stream and the library connects again with that context, but the end of
the pairing canceled it, so the reconnection stopped. The session got no
prekeys, no Signal sessions, and no contacts, and no message came.

The pairing client now connects with the context of Run, which lives
until the daemon stops, as the connection at the start of the daemon
does. The context of the pairing still limits the QR channel, the wait
for the code, and the wait for the link, and a pairing that does not
link disconnects its client.

The fake client keeps the context of each connection, and the pairing
test checks that the context stays alive after the link. The test fails
without the fix.

Issue: 6f69ca1

46 hours agoReflow the comments of promptTools and terminal
Anderson A. Giacomolli [Sun, 8 Nov 2026 15:59:07 +0000 (12:59 -0300)]
Reflow the comments of promptTools and terminal

The WhatsApp changes left two doc comments with uneven lines: the
comment of promptTools had a line of 100 characters, and the comment of
terminal had a line that ended early. Both are reflowed with bin/reflow,
with no change of their words.

Issue: 6f69ca1

47 hours agoSend WhatsApp messages with an ID
Anderson A. Giacomolli [Sun, 8 Nov 2026 15:26:50 +0000 (12:26 -0300)]
Send WhatsApp messages with an ID

The agent gets the tool whatsapp_send, which sends a text message to a
private chat or a group by its ID, only when the user asks for it. Each
call has an ID that the agent gives. The state file keeps a record of
each send by its ID, so a repeated call gives the answer of the first
call and sends nothing, and the same ID with another chat or text is an
error. The record "sending" is durable before the send, so a stop of the
daemon during a send gives an answer that the result is unknown and that
the agent must ask the user, and never a second send. A disconnected
WhatsApp sends nothing and makes no record.

The state file keeps the last 1000 send records with the batches. Its
type is now WhatsAppState in whatsapp_state.go, because it holds more
than the batches, and whatsapp_batch.go keeps the batch, its text, and
its turn rule.

The send tool takes only the ID of a private chat or a group, and not
the chat of the user with itself, and the own number in an error shows
as OWN. Only the root agent has the tool, and its new schema makes the
prompt cache miss once.

Issue: 6f69ca1

47 hours agoMake two WhatsApp tests independent of the load
Anderson A. Giacomolli [Sun, 8 Nov 2026 15:25:24 +0000 (12:25 -0300)]
Make two WhatsApp tests independent of the load

The test of a private batch gave its two messages with 50 ms of real
quiet time between them, so on a loaded machine the first message closed
its batch alone and the test failed. The messages now come during a
catch-up, which holds the batch open until its end, so the test no
longer depends on the speed of the machine.

The test of a long disconnection waits 300 ms before the notice and 400
ms for a notice that must not come, so that a slow machine does not give
a notice between two events of the test.

Issue: 6f69ca1

47 hours agoFind WhatsApp contacts and groups
Anderson A. Giacomolli [Sun, 8 Nov 2026 15:09:02 +0000 (12:09 -0300)]
Find WhatsApp contacts and groups

The agent gets the tool whatsapp_find, which gives the contacts and
groups of the user whose name holds a text, with no regard to case and
accents, as lines "ID | name | contact or group", at most 20 and the
number of the others. The send tool of the next step takes these IDs.

The contacts come from the session database of the library, with the
full, first, business, and push names, and with no copy. The groups come
from WhatsApp and stay in memory for one hour; the batches use the same
names. When the groups cannot be read and none are known, the answer
says so. Before the names arrive from the phone, the tool says that they
are not ready, and without a link it tells the agent to ask the user to
run /whatsapp pair PHONE.

Only the root agent has the tool: the toolbox of a subagent has no
WhatsApp, so it is an unknown tool there. The new tool changes the tool
list, so the prompt cache misses once. The match uses golang.org/x/text,
which the library needs already, so the vendor hash changes only for its
packages.

Issue: 6f69ca1

47 hours agoWait for the WhatsApp catch-up before a turn
Anderson A. Giacomolli [Sun, 8 Nov 2026 15:03:17 +0000 (12:03 -0300)]
Wait for the WhatsApp catch-up before a turn

After each connection, the WhatsApp servers send the messages that came
while the daemon was away, and then the event OfflineSyncCompleted.
Until that event, or one minute at the most, the daemon keeps the closed
batches in the state file and logs none. At the end it closes all
batches, logs them in order, and wakes the turn loop once with the
batches that need the user, so the whole catch-up gives one turn and not
one turn for each chat. The batches that the state file holds at the
start wait for the first catch-up in the same way.

TurnLoop.Work now only logs and notifies, and the new TurnLoop.Wake adds
several logged messages to the waiting messages at once. A report of a
subagent uses both, and the WhatsApp batches of one delivery join one
turn through one Wake.

Issue: 6f69ca1

47 hours agoGive the WhatsApp batches to the agent
Anderson A. Giacomolli [Sun, 8 Nov 2026 14:57:43 +0000 (11:57 -0300)]
Give the WhatsApp batches to the agent

The daemon reads the text messages of the private chats and the groups
and gives them to the agent in batches. The messages of one chat form
one batch while they are at most 20 seconds apart, for 2 minutes or 50
messages at the most, and a batch also closes after 20 quiet seconds in
real time. The rule uses the WhatsApp times of the messages, so the
catch-up after a stop gives the same batches as live messages.

Each batch goes to the log as one work message that starts with
[WhatsApp], with the chat, its ID for the later send tool, and the time,
sender, and text of each message. It takes the path of the subagent
reports: TurnLoop.Work logs it and, for a batch that needs the user,
wakes the loop, so a batch that comes during a turn joins its next
request. A batch starts a turn when it is private with a message of
another person, or from a group with a message that mentions the user or
replies to the user. Such a batch ends with a line that lets the agent
end the turn with an empty reply. The chain already drops an empty
reply, and Telegram drops the batches, so a batch that does not need the
user gives no Telegram message.

whatsapp/state.json keeps the batches that are not in the log. The
library acknowledges a message only after the file holds it, and a batch
leaves the file only after the log is durable, so a crash loses nothing
and can repeat a batch once. A failed write stops the daemon.

The own number never goes to the log: the messages of the user show as
You, a mention of the user as @you, the number as OWN, and the chat of
the user with itself is not read. The batches do not take media,
reactions, edits, view-once and disappearing messages, or old history.

Issue: 6f69ca1

47 hours agoLink WhatsApp with a pairing code
Anderson A. Giacomolli [Sun, 8 Nov 2026 14:44:00 +0000 (11:44 -0300)]
Link WhatsApp with a pairing code

The line /whatsapp pair PHONE in the terminal client or in Telegram
links the WhatsApp account of PHONE as a linked device. The daemon
answers with the steps and then with the pairing code in its own
message, so that it is easy to copy, and later with the linked number,
an expired code, or the failure. A new pairing ends the one before.
/whatsapp unlink logs the device out and deletes the session, and tells
the user to remove the device in the phone when the logout cannot reach
WhatsApp.

The phone number and the code go only to the client that asked. The log
of the agent gets only the notes that WhatsApp is linked or unlinked,
and the errors in the daemon log show the own number and the own LID as
OWN.

When the user removes the device in the phone, the daemon deletes the
session, notes it, and tells all clients one time. A disconnection that
lasts 10 minutes gives all clients one notice, not one for each retry of
the library, and one more when the connection comes back. A
disconnection that the library does not retry gives a notice at once.

The pair and unlink commands run one at a time in the WhatsApp goroutine
of the daemon, which ends a running pairing at the stop of the daemon.

Issue: 6f69ca1

47 hours agoShow the WhatsApp link with /whatsapp status
Anderson A. Giacomolli [Sun, 8 Nov 2026 14:36:14 +0000 (11:36 -0300)]
Show the WhatsApp link with /whatsapp status

The daemon opens the WhatsApp session in whatsapp/session.db and keeps a
linked device connected, so the next steps can pair, read, and send. The
line /whatsapp status in the terminal client or in Telegram tells
whether WhatsApp is not linked, linked and connected, or linked and
disconnected, with the times of the connection.

The clients handle a /whatsapp line before it reaches the agent, and the
daemon answers only the client that asked, with a notice event that the
terminal client now shows. So the lines of the later pairing, which hold
the phone number and the code, never reach the log, the model, or the
other client.

The session gives full access to the account, so the daemon refuses a
session file whose mode is not 0600, and the library logs nothing,
because its log lines hold the own phone number. The daemon opens the
session only after it holds the lock of the data directory. The library
and its SQLite driver, modernc.org/sqlite, are pure Go.

Issue: 6f69ca1

2 days agoGive each subagent its own prompt cache key
Anderson A. Giacomolli [Sun, 8 Nov 2026 14:13:11 +0000 (11:13 -0300)]
Give each subagent its own prompt cache key

A call of a subagent sends the cache key of the agent with "-" and the
name of the subagent, in the body and in the session_id header. With the
key of the agent, the second request of a subagent in a live check read
0 of 4,435 input tokens from the prompt cache, although it starts with
the whole first request, while the agent made calls with the same key at
the same time. With its own key, the second request of a subagent in the
same check read 4,608 of 4,761 tokens from the cache. The calls of the
agent keep the key of the agent.

Issue: b1ac698

2 days agoResume the subagents that a restart cut
Anderson A. Giacomolli [Sun, 8 Nov 2026 14:03:18 +0000 (11:03 -0300)]
Resume the subagents that a restart cut

A subagent that runs when the daemon stops goes on at the next start.
work.json in the agent directory holds the names of the running
subagents: each start and each end of a run writes it whole, but the
stop of the daemon does not, so it names the runs that the stop cut. The
logs alone cannot tell this, because a stop can cut a run just after its
final reply.

At the start, the chat gets the note "The daemon restarted, cutting
NAMES; nothing was lost: go on.", and each cut subagent gets the same
text as a told message, so it goes on from its log with the view of now.
The note starts no turn, but the reports of the subagents do. A subagent
that cannot go on gets a failed report, and work.json then no longer
names it, so the next start does not try it again. A failed write of
work.json stops the agent, as a failed log write does.

Issue: b1ac698

2 days agoShow the subagents in the clients
Anderson A. Giacomolli [Sun, 8 Nov 2026 13:57:31 +0000 (10:57 -0300)]
Show the subagents in the clients

When a subagent starts a run, the terminal client and Telegram show
"[NAME] started". The terminal shows the whole report, which starts with
the name. Telegram shows only "[NAME] finished", "[NAME] stopped", or
"[NAME] failed", because the reply of the agent in the turn of the
report carries its content, and the phone then gets one message for each
result. Neither client shows the tool calls of a subagent.

The work event of a report gets the name of the subagent in its source
and the end of the run in a new status field, so that a client needs no
parse of the report text.

Issue: b1ac698

2 days agoStop a subagent with /stop NAME
Anderson A. Giacomolli [Sun, 8 Nov 2026 13:53:29 +0000 (10:53 -0300)]
Stop a subagent with /stop NAME

The line "/stop NAME" in the terminal client and in Telegram stops the
running subagent NAME: its model call ends, and a running command is
killed with its process group. The chat gets the note "Stopped by the
user: NAME.", and then the report of the subagent with the reply
"stopped", which is logged but starts no turn, so the agent does not act
on a subagent that the user stopped. A name that does not run gives an
answer to the client. The bare "/stop" still interrupts only the turn of
the agent.

The note is logged under the mutex of the runs, which the run takes
before its report, so the note always comes before the report.

Issue: b1ac698

2 days agoSend messages to subagents with tell
Anderson A. Giacomolli [Sun, 8 Nov 2026 13:48:37 +0000 (10:48 -0300)]
Send messages to subagents with tell

The agent gets the tell tool: tell(name, message) logs the message as a
user message of the subagent. A running subagent reads it between its
tool calls, and when it comes after a final answer, the run goes on with
one more request chain that reads the log again. The report of the run
then holds the final answer of each chain. A finished subagent goes on
from its log with a new run, with the view of now and the effort of the
note at the start of its log, and its reply comes back as a new report.

The log holds every message of a tell, so a told subagent needs no state
outside its log, and a restart can resume it in the same way.

Issue: b1ac698

2 days agoRun subagents in the background with spawn
Anderson A. Giacomolli [Sun, 8 Nov 2026 13:33:12 +0000 (10:33 -0300)]
Run subagents in the background with spawn

The agent gets the spawn tool: spawn(tasks, effort) starts one subagent
for each task and returns their names at once. It waits until the view
has no pending summary, and gives that view to each subagent. Each
subagent runs in its own goroutine with its own request chain, its own
system prompt with the user's instructions at the end, and the tools
zoom, date, shell, read, write, and edit, in subagents/NAME/work. It
cannot start a subagent. Each request starts with the view, the state,
and the whole log of the subagent as "i|kind: text" lines, and the log
starts with the note "subagent" and the task.

At the end of a run, its final answer, or "failed: ERROR" after a failed
call such as a full context, goes to the agent as one work message:
"[Name]", the reply, and the zoom that gives its whole chat. The report
joins a running turn between its tool calls, or starts a new turn. A
report of a stopped subagent is logged but starts no turn. When the loop
ends at the stop of the daemon, the runs end with no report, and the
loop waits for them. The prompt gets the lines on subagents.

The efforts of spawn are low, medium, high, and xhigh: the efforts of
the reference design that the Responses API also takes.

Issue: b1ac698

2 days agoStore the subagents and read them with zoom
Anderson A. Giacomolli [Sun, 8 Nov 2026 13:23:35 +0000 (10:23 -0300)]
Store the subagents and read them with zoom

Each subagent has a directory in subagents/ of the agent directory,
named for it, with its own message log. A name is an adjective and a
noun from two word lists, each capitalized and joined, such as
GreenOtter, and a new subagent draws again when the directory of its
name exists. Only such a name can be a directory, so a name never leaves
subagents/.

The zoom tool gets a nullable name, and zoom("Name") gives the whole log
of that subagent, each message as "i|kind: text". The prompt gets the
tool line for it. The log of a name opens once and stays open until the
daemon stops, because its lock allows one open log, and its mutex keeps
the run of the subagent and a zoom of the agent apart.

Issue: b1ac698

2 days agoWait until the view has no pending summary
Anderson A. Giacomolli [Sun, 8 Nov 2026 13:18:02 +0000 (10:18 -0300)]
Wait until the view has no pending summary

Memory.Settle returns when every line of the chat view is built, so a
subagent that starts gets a view with no pending summary, and the view
holds every message so far. A pending line with no running build belongs
to a failed build, which waits for the next message, so Settle returns
an error in place of a wait that never ends. It also ends with its
context, so an interrupted turn does not wait.

Issue: b1ac698

2 days agoShare the request chain of a turn
Anderson A. Giacomolli [Sun, 8 Nov 2026 13:12:00 +0000 (10:12 -0300)]
Share the request chain of a turn

The request chain moves out of the turn loop into Chain: it sends a
request, logs the replies, runs the tool calls, and sends their results
back until a response has no tool call. A turn builds a chain with the
prompt, the tools, and the input of the turn, so its requests do not
change. The chain also returns the replies of the final answer, because
a subagent needs that text for its report and runs the same chain with
its own prompt, toolbox, and log.

Issue: b1ac698

2 days agoRecord the subagent name in the usage records
Anderson A. Giacomolli [Sun, 8 Nov 2026 12:50:31 +0000 (09:50 -0300)]
Record the subagent name in the usage records

A model request names the subagent that makes it, and the usage record
of the call holds that name in the agent field. A call of the agent
itself has no such field. The name goes only to the usage file, never to
the endpoint, so the usage records show the cost of each subagent in the
shared usage file of the agent.

Issue: b1ac698

2 days agoRun a short replay outside the flake check
Anderson A. Giacomolli [Sun, 8 Nov 2026 12:23:29 +0000 (09:23 -0300)]
Run a short replay outside the flake check

The replay of the synthetic log runs 5,000 messages with -short and
100,000 messages without it. bin/check and the test run of nix build
pass -short, so bin/check takes about 20 seconds instead of more than
two minutes under the race detector. A new flake check runs only
TestReplaySyntheticLog without -short, so nix flake check still runs the
full replay and does not run the other tests a second time.

The test uses testing.Short, because -short is the standard Go flag to
make long tests shorter, and go test and buildGoModule pass it without
new code. An environment variable would need its own name and parsing
for the same result. The short replay gives the chat view 26 batches,
and the test still requires at least one batch.

Issue: e4109b7

2 days agoEscape tags in rich replies as HTML entities
Anderson A. Giacomolli [Sat, 7 Nov 2026 19:38:29 +0000 (16:38 -0300)]
Escape tags in rich replies as HTML entities

The bot writes each < of a reply outside inline code and fenced code
blocks as &lt; and each & as &amp; before it sends the reply with
sendRichMessage. Rich Markdown does not read a backslash before < as an
escape: it shows the backslash and still reads the tag as HTML. It reads
HTML entities, so &lt; shows as <. The escape of & makes an entity that
the model writes in prose, such as &lt;, show as written.

The entity fallback reads no HTML, so a rich part that Telegram refuses
goes to the fallback with the escape reversed, and the user sees the
text as the model wrote it. The reverse is exact, because after the
escape each & outside code starts an entity of the escape. The escape
and its reverse find the code of a text in the same way, so the reverse
also works on each part of the split.

The split still counts the escaped text against the rich limit, and the
fallback splits the text without the escape at its own limit. When the
split cuts a line longer than the limit, it cuts before an & in the last
four bytes, so that it does not cut an entity.

Issue: d75e312

2 days agoMove shared test helpers into helper files
Anderson A. Giacomolli [Sat, 7 Nov 2026 18:14:59 +0000 (15:14 -0300)]
Move shared test helpers into helper files

Each helper that more than one test file uses moves out of the test file
that holds it into a file without tests, named for what it holds: the
fake Responses endpoint, the fake ChatGPT login service, the fake Bot
API, the terminal test client, the daemon start with a bot, and the tree
node helpers. A helper that only one test file uses stays in that file.
A reader then finds a shared helper from the file name, and a test file
holds only the tests and the helpers of its own feature.

The code of the helpers does not change.

Issue: 947bddb

2 days agoRename the test files of shared helpers
Anderson A. Giacomolli [Sat, 7 Nov 2026 18:09:44 +0000 (15:09 -0300)]
Rename the test files of shared helpers

The test files that hold no tests and only helpers for other test files
get names that end in _helper_test.go and say what they hold: the sync
buffer, the clock, and the summarizer. A reader then finds a shared
helper from the file name and does not look for tests in these files.
The content of the files does not change.

Issue: 947bddb

2 days agoEscape tags in the Markdown of rich replies
Anderson A. Giacomolli [Sat, 7 Nov 2026 17:59:33 +0000 (14:59 -0300)]
Escape tags in the Markdown of rich replies

The bot puts a backslash before each < of a reply outside inline code
and fenced code blocks before it sends the reply with sendRichMessage.
Rich Markdown reads a tag in prose, such as <details>, as raw HTML: the
tag name is lost and the rest of the paragraph is in a collapsed
section. With the escape, Telegram shows the tag as written, and a reply
cannot carry raw HTML.

Inline code and code blocks keep their text, also a code block that a
split opens again, because the escape runs before the split. The split
thus counts the escaped text against the length limit. A < that a
backslash already makes plain stays as it is. The pipe, the dollar sign,
and the equals sign are not escaped: the tables use the pipe, and the
live check showed no problem with the dollar sign.

The entity fallback reads the backslash as an escape, so it shows the
same text as before. Other events go as plain text and do not change.

Issue: d75e312

2 days agoName the test files that hold only shared helpers
Anderson A. Giacomolli [Sat, 7 Nov 2026 17:57:37 +0000 (14:57 -0300)]
Name the test files that hold only shared helpers

A helper that one test file uses stays in that file. A helper that more
than one test file uses goes into a file without tests, whose name says
what it holds and ends with _helper_test.go. The name then shows that
the file has no tests, and a file named for one type does not seem to
test a feature of that name.

2 days agoFix two comments of the Telegram API and the view
Anderson A. Giacomolli [Sat, 7 Nov 2026 17:36:56 +0000 (14:36 -0300)]
Fix two comments of the Telegram API and the view

The doc comment of TelegramAPI names its sendTimeout field, which limits
each send call, and not SendTimeout, which is no name in the code.

The doc comment of the test helper viewPush describes what the push does
to the rollback state list, and it does not name a file of the reference
design.

Issue: 947bddb

2 days agoName and order the memory core code
Anderson A. Giacomolli [Sat, 7 Nov 2026 17:10:33 +0000 (14:10 -0300)]
Name and order the memory core code

The names of the memory core that only one of its parts uses start with
that part: the day helpers of the journal, the record decoder of the
tree, the message kinds and check of the log, and the view file of the
memory. The types of the core start with a capital letter: Journal,
Position, Records, Syncer, and NodeKey.

The fake summarizer of the tests is in summarizer_test.go, because most
tests use it. The test helpers of the tree, the log, the syncer, and the
replay test start with their part. The declarations of the core files
follow the order of the go-code skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the agent and config code
Anderson A. Giacomolli [Sat, 7 Nov 2026 17:06:14 +0000 (14:06 -0300)]
Name and order the agent and config code

The names that only the agent or only the config uses start with agent
or config: the instructions file of the agent and the endpoint values of
config.json. Config and ModelConfig stay generic, because the daemon,
the probe, the model client, and the compactor read them.

The data directory is in datadir.go, with its tests in datadir_test.go,
because the login, the terminal client, the daemon, and the probe use
it, and it is not a part of config.json. The declarations of these files
follow the order of the go-code skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the compactor code
Anderson A. Giacomolli [Sat, 7 Nov 2026 17:02:56 +0000 (14:02 -0300)]
Name and order the compactor code

Every package-level name that only the compactor uses starts with
compactor: its limit of tries, the ruler, the task text, the note on a
long line, and the helpers that cut a line and pick the shortest one.
Names such as ruler and shortest read as helpers of the whole program,
but only the compactor uses them.

The declarations of the compactor files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the tool code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:59:39 +0000 (13:59 -0300)]
Name and order the tool code

Every package-level name of the tools starts with tool: the Toolbox, its
interfaces ToolLog and ToolChat, the argument types, the head and tail
buffer, and the clip helpers. The interfaces chatLog and chatReader read
as parts of the chat, but only the tools use them.

The declarations of tools.go follow the order of the go-code skill. The
behavior does not change.

Issue: 947bddb

2 days agoName the tool schemas of the prompt
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:56:02 +0000 (13:56 -0300)]
Name the tool schemas of the prompt

The function tools that every model call sends are promptTools, and
their schema helpers start with prompt too. The turns and the
compactions send the same prompt and tools, so the tools are a part of
the prompt, and agentTools read as the tools of an agent next to the
agent config. The parts of the prompt text, such as the task sentence
and the clause on the instructions, start with prompt as well.

The declarations of prompt.go already follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the probe code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:52:39 +0000 (13:52 -0300)]
Name and order the probe code

Every package-level name of the probe starts with probe: its run
ProbeRun, the summarizer ProbeSlotSummarizer that limits the builds at
once, the reader of its log and of the usage file, and the parts of its
report. Names such as ratio, timeText, and modelLines read as helpers of
the whole program, but only the probe uses them.

The declarations of the probe files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the build queue code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:48:54 +0000 (13:48 -0300)]
Name and order the build queue code

The queue of the node builds is BuildQueue in build_queue.go, and its
other names start with build: buildMax and buildLeafWindow are its
limits, and BuildMergeHeap holds its merges. The Telegram bot has a
queue of events too, so a bare queue does not tell which one it is.

The declarations of the queue file follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the view code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:45:37 +0000 (13:45 -0300)]
Name and order the view code

Every package-level name of the view starts with view: its size limits,
the pending mark, the saved state ViewState, and the interface
ViewNodeReader. The layout of the view messages in the turn requests is
in view_layout.go, as ViewLayout. The limits chatHigh and compactionHigh
read as limits of the chat and of the compactor, but they are limits of
the two views.

The declarations of the view files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the turn loop code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:42:01 +0000 (13:42 -0300)]
Name and order the turn loop code

Every package-level name that only the turn loop uses starts with turn:
the loop is TurnLoop, its function call items are TurnFunctionCall and
TurnFunctionCallOutput, and its error of a log write is errTurnLogWrite.
Event stays generic, because the daemon, the terminal client, and the
Telegram bot read the events too.

The declarations of the turn files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the daemon and socket code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:38:35 +0000 (13:38 -0300)]
Name and order the daemon and socket code

Every package-level name that only the daemon uses starts with daemon:
its hub of socket clients is DaemonHub, a connected client is
DaemonClient, and the loop that serves the agent is daemonServe. So a
client of the socket does not read as the model client or the Bot API
client.

What the daemon and the terminal client share is in socket.go: the
socket file, the size limits, the request type SocketRequest, and the
check of the socket path. The file locks that the log, the daemon, the
login, and the probe share are in lock.go, with their test in
lock_test.go. The declarations of these files follow the order of the
go-code skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the terminal client code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:34:32 +0000 (13:34 -0300)]
Name and order the terminal client code

Every package-level name of the terminal client starts with terminal,
and its file is terminal.go, with its tests in terminal_test.go. The
client runs the command "onechat chat", but in the code the chat is the
chat of the agent, with its log, tree, and views, so the prefix names
the client by its kind.

The stop command is in stop.go and the test buffer in buffer_test.go,
because the terminal client and the Telegram bot use them both. The
declarations of the terminal files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the ChatGPT login code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:29:06 +0000 (13:29 -0300)]
Name and order the ChatGPT login code

Every package-level name of the ChatGPT login starts with chatgpt, and
its file is chatgpt.go. The login has an issuer, a client ID, a token
file, and a poll interval, and the prefix keeps these names apart from
the names of the Telegram bot and the model client. The client ID
constant does not name another program.

The declarations of the login files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the model client code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:25:00 +0000 (13:25 -0300)]
Name and order the model client code

Every package-level name of the client that sends the model calls starts
with model, and its file is model.go. The client talks to the ChatGPT
backend or to the OpenAI API, so the name tells its role and not one of
the endpoints. The interface of the turn loop that the client satisfies
is TurnClient, so it does not take the name of the client. The fake
output items of the tests are in model_test.go, next to the other fakes
of the endpoint.

The declarations of the model files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

2 days agoName and order the Telegram bot code
Anderson A. Giacomolli [Sat, 7 Nov 2026 16:20:24 +0000 (13:20 -0300)]
Name and order the Telegram bot code

Every package-level name of the Telegram bot starts with telegram, and
its files start with the same name: the Bot API client is in
telegram_api.go and the Markdown conversion in telegram_markdown.go. So
the names of the bot do not read as names of the model client or of the
login, which also have a client, an API, and a poll. Type names start
with a capital letter, so that a variable can have the name of its type.

The declarations of the Telegram files follow the order of the go-code
skill: entry points, then each type with its methods, then the utility
functions, each group in the order of first use. The behavior does not
change.

Issue: 947bddb

2 days agoCapitalize type names so variables can use them
Anderson A. Giacomolli [Sat, 7 Nov 2026 15:58:50 +0000 (12:58 -0300)]
Capitalize type names so variables can use them

The go-code skill now asks for type names with a capital letter and for
all other package-level names in lower case. A lower-case type name
clashes with an import, such as a log type with the log package, and
with local variables that have the name of their type, such as a memory
variable of a memory type. A capital letter keeps both names free.

2 days agoSend the Telegram replies as rich messages
Anderson A. Giacomolli [Sat, 7 Nov 2026 15:30:51 +0000 (12:30 -0300)]
Send the Telegram replies as rich messages

The bot sends each model reply with sendRichMessage and the Markdown of
the reply unchanged, so Telegram renders the tables and the other blocks
itself. Message entities have no tables, so a table showed as lines of
pipes. A reply over the limit of a rich message goes as more than one
rich message, split between blocks with splitRich.

When the Bot API answers ok false with HTTP 400 or 404, the reply was
not sent, so the bot sends the same Markdown once with the entity path:
markdownEntities, splitEntities, and sendMessage. The fallback logs one
line with no text of the reply. Every other failure gives no fallback: a
network error, HTTP 429, and HTTP 5xx wait and try the rich message
again, and any other failure drops the reply with one log line. The send
timeout applies to each sendRichMessage call.

The notices of the bot, the errors, and the messages from the terminal
stay plain text with sendMessage. The log and the terminal client do not
change.

Issue: fec23f9

2 days agoName package-level code for its feature
Anderson A. Giacomolli [Sat, 7 Nov 2026 15:24:35 +0000 (12:24 -0300)]
Name package-level code for its feature

The go-code skill now asks for a feature prefix on each package-level
name that belongs to one feature or integration, and on the names of its
files. A name stays generic only when more than one feature uses it. All
the code is in one main package, so every name is visible in every file,
and generic names such as a client, a request, or a poll interval of one
service read as if they belong to all of them.

The skill also asks for unexported names, because an exported name has
no effect in a main package, and for initialisms in one case.

2 days agoSplit long replies between Markdown blocks
Anderson A. Giacomolli [Sat, 7 Nov 2026 15:23:09 +0000 (12:23 -0300)]
Split long replies between Markdown blocks

splitRich cuts the Markdown of a reply into parts of at most 32768
bytes, the limit of the Markdown of one rich message. It cuts after a
blank line outside a code block, so a table, a code block, and a
paragraph stay whole, and each part renders as the reply would. When no
such cut fits, it cuts after a line break outside a code block, and then
inside a code block, which it closes at the end of the part and opens
again with the same fence line in the next part. A single line over the
limit is cut at the limit, never inside a character.

The Bot API allows 32768 characters of rendered text and does not say if
a character is a byte. The bytes of the Markdown are at least the
characters of the text, so a part is never over the limit. The bot does
not use splitRich yet.

Issue: fec23f9

2 days agoCall sendRichMessage of the Bot API
Anderson A. Giacomolli [Sat, 7 Nov 2026 15:18:08 +0000 (12:18 -0300)]
Call sendRichMessage of the Bot API

The Bot API client sends Markdown as a rich message with
sendRichMessage, which Telegram renders itself, tables included. The
call has the same send timeout as sendMessage, so a stalled connection
fails with a network error that the sender can try again.

A botError now tells if the answer is a Bot API answer with ok false,
and rejected tells if such an answer has HTTP 400 or 404. Then the call
did nothing, so the sender can send the same content another way with no
risk to repeat it. An error page of a proxy with HTTP 404 is not a Bot
API answer, so it is not a refusal.

Issue: fec23f9

2 days agoSend the replies to Telegram with their formats
Anderson A. Giacomolli [Sat, 7 Nov 2026 14:43:10 +0000 (11:43 -0300)]
Send the replies to Telegram with their formats

The bot converts the Markdown of each model reply to text and message
entities, and sends them with the entities field of sendMessage, with no
parse mode. The phone then shows bold text, code and code blocks
formatted, not their marks. A reply longer than 4096 UTF-16 units is
split as before, and an entity that crosses a cut becomes one entity in
each part, with offsets from the start of its part.

When Telegram refuses a part with entities with HTTP 400, the bot sends
the same text again once with no entities and logs one line without the
text, so a bad entity does not lose a reply. Other failures wait and try
again as before.

Only the replies get entities. The notices of the bot get their own
event kind, so they stay plain text with the errors and the messages
typed in the terminal. A reply with no text left after the conversion
goes as its Markdown. The log and the terminal client do not change.

Issue: 3309f9b

2 days agoConvert reply Markdown to Telegram entities
Anderson A. Giacomolli [Sat, 7 Nov 2026 14:37:49 +0000 (11:37 -0300)]
Convert reply Markdown to Telegram entities

A converter on the standard library turns the Markdown of a model reply
into plain text and Telegram message entities. It knows bold, italic,
strikethrough, inline code, fenced code blocks with an optional
language, links, and ATX headings, which become bold. Lists, quotes and
tables stay as their text. With entities, the text needs no escaping, so
a mark that does not close only stays as text, and it cannot make
Telegram refuse the message.

Inline marks do not cross a line break, so one open mark cannot change
the rest of a reply. Underscores inside a word stay as text, so
snake_case names do not turn italic, and an asterisk that a space
follows, as in a list item, opens nothing. Code holds no entity. The
offsets and lengths count UTF-16 code units, as Telegram does.

Issue: 3309f9b

3 days agoLimit the time of each Telegram send call
Anderson A. Giacomolli [Sat, 7 Nov 2026 13:51:13 +0000 (10:51 -0300)]
Limit the time of each Telegram send call

Each sendMessage and sendChatAction call gets its own limit of 30 s,
through a child context of the call. Before, only getUpdates had a
limit, so a connection that stopped without a close, such as after a
network change, held the sender until TCP gave up, and no reply, error
or typing action reached the chat in that time.

A call that times out fails with the same network error as the other
network failures, so the sender waits and tries again, and the error
text holds no token and no URL. The HTTP client keeps no timeout of its
own, because the long poll of getUpdates needs a longer limit. A stop of
the daemon still ends a call in progress at once, because the limit is a
child of the daemon context.

Issue: a143bd8

3 days agoTalk to the agent through a Telegram bot
Anderson A. Giacomolli [Sat, 7 Nov 2026 13:26:55 +0000 (10:26 -0300)]
Talk to the agent through a Telegram bot

When telegram.json exists in the data directory, the daemon runs a
Telegram bot next to the socket clients. The file holds the bot token
and the user ID of the owner, and the daemon refuses it unless its mode
is 0600, because the token is a secret. The bot uses getUpdates long
polling only, so it needs no webhook and no open port.

The bot reads only the text messages of the owner in the private chat
and ignores every other update without a log of its content. Each text
goes to the agent as a user message from the source telegram, and /stop
interrupts the turn. A message that is not text gets the answer that
only text is supported. The chat gets the replies as plain text in parts
of at most 4096 UTF-16 units, the errors as short messages, and the
messages typed in the terminal with a mark. It shows the typing action
while a turn runs. Tool calls stay out of the chat, so the phone stays
readable and the bot stays far from the rate limit.

agent/telegram-offset.json holds the next update ID. It moves past an
update only after the message is flushed to the log, so a restart loses
no message. A crash between the flush and the offset write repeats that
message once, which the user accepts. A failed Bot API call waits and
tries again, as long as HTTP 429 asks or from one second up to a minute,
and never stops the agent. A failed write of a message or of the offset
stops the daemon, because the bot could then lose or repeat messages.

Issue: a143bd8

3 days agoCall the Telegram Bot API
Anderson A. Giacomolli [Sat, 7 Nov 2026 13:18:47 +0000 (10:18 -0300)]
Call the Telegram Bot API

A small client of the Bot API, on the standard library only, waits for
new messages with getUpdates long polling, sends plain text with no
parse mode, and shows the typing action. It tells a failure that a later
try can fix, such as a network error, HTTP 429 with its wait, or a
server error, from one that it cannot.

The bot token is a part of each URL, and a net/http error holds the URL.
So no error of the client wraps a net/http error: it keeps only the text
of the cause, with the token removed. A test checks the error text of
each kind of failure for the token.

Issue: a143bd8

3 days agoShow the user messages of the other clients
Anderson A. Giacomolli [Sat, 7 Nov 2026 13:12:48 +0000 (10:12 -0300)]
Show the user messages of the other clients

Each user message event names its source client. The socket clients
submit as terminal, and the terminal client shows a user message from
any other source with the source as a mark, such as "[telegram] text".
With more than one client on one agent, each client then shows the full
conversation, so a reply always comes with its question. The log does
not keep the source.

Issue: a143bd8

3 days agoExit with status 0 when a signal stops the daemon
Anderson A. Giacomolli [Sat, 7 Nov 2026 02:53:53 +0000 (23:53 -0300)]
Exit with status 0 when a signal stops the daemon

A signal is the normal stop of the daemon and of the terminal client, so
both exit with status 0 and print no error after Ctrl-C or SIGTERM. The
other commands still report a canceled context, because a signal there
stops an operation before it is done.

Issue: 5daf23e

3 days agoCheck the socket path before the daemon writes
Anderson A. Giacomolli [Sat, 7 Nov 2026 02:53:53 +0000 (23:53 -0300)]
Check the socket path before the daemon writes

The daemon checks the length of the socket path first, before it reads
the config, takes the token lock, or opens the agent. With a data
directory whose path is too long, it gives the error and leaves the
directory empty, so a failed start leaves no agent or lock file behind.

Issue: 5daf23e

3 days agoServe the agent to a terminal client
Anderson A. Giacomolli [Sat, 7 Nov 2026 02:07:05 +0000 (23:07 -0300)]
Serve the agent to a terminal client

onechat daemon owns the root agent in agent/ in the data directory: its
config, chat, compactor, and loop. It listens on the Unix socket
onechat.sock in the data directory, with mode 0600, and sends each event
to every client as a JSON line: ready with the agent's name on connect,
busy and idle around each turn, each logged message, and errors. A
client sends JSON lines too: send with a message, or interrupt. A slow
client is dropped, so it never stops the agent.

onechat chat is a line-based client without a terminal library. Each
input line is a message, the line /stop interrupts the turn, and the end
of the input ends the client. It shows the replies, the tool calls, and
the first 10 lines of each result as they happen.

The daemon, login, and probe each hold a lock on auth.lock in the data
directory while they run, so only one process reads and refreshes the
tokens. Two processes that refresh one token file rotate the refresh
token of each other and log each other out. The client never reads the
token file.

Issue: 5daf23e

3 days agoRemove the warm-up request after the turns
Anderson A. Giacomolli [Sat, 7 Nov 2026 01:54:20 +0000 (22:54 -0300)]
Remove the warm-up request after the turns

The first request of a turn reads the view of the turn before from the
prompt cache without a warm-up. In two live runs of 6 turns on a view of
about 31,000 tokens, turns 2 to 6 read 99.1% to 99.4% of their input
from the cache without the warm-up, and 99.3% to 99.6% with it. One call
per turn for about 0.1 points is not worth it, so the loop sends no
warm-up and the client does not stop a stream early.

The view messages that only grow stay. With the whole view in one user
message, turn 2 of a third run read nothing from the cache, and turns 3
to 6 read 99.2% to 99.5%. With the grow-only messages, each of the 10
later turns hit.

Issue: 5daf23e

3 days agoLay out turn requests for the prompt cache
Anderson A. Giacomolli [Fri, 6 Nov 2026 23:27:35 +0000 (20:27 -0300)]
Lay out turn requests for the prompt cache

The ChatGPT backend writes a cache entry only at the end of the latest
message of a request, and a later request reads it only at one of its
own message ends. The first request of a turn now puts the stable parts
first and the per-turn state last: the prompt and the tools, then the
view as user messages, then a message with the end of the view and the
date, then each new message.

The view messages only grow between batches. layout.json in the agent's
directory keeps the line count and a hash of each view message sent. The
next request sends the same messages while their lines give the same
text, and the new lines follow as one new message. At the first message
that differs, such as after a batch merge, the rest of the view becomes
one new message.

After the turns, when the builds are done and no message waits, a
warm-up request sends only the prompt, the tools, and the view messages.
It ends at the view, where the next turn has a message end, so that turn
can read the view from the cache. The warm-up stops reading at its first
output item, after the input is read, to save output tokens, and its
output is never logged or used. Each usage record names the request of a
turn: first, next, or warmup.

Issue: 5daf23e

3 days agoRun the turns of an agent in a loop
Anderson A. Giacomolli [Fri, 6 Nov 2026 23:19:46 +0000 (20:19 -0300)]
Run the turns of an agent in a loop

A user message goes to the log when it comes and wakes the loop. A turn
waits until every build is done, so the view holds the summaries of the
messages before it. Its first request holds the prompt, the six tools,
and one user message with the view, the date and time of the turn, and
each new message whole. The view leaves out the leaf lines of the new
messages, but it keeps every other line, also the replies that came
after a message that waited.

The loop runs the tool calls of each response and sends their results
back until a response has no tool call. Each later request begins with
the whole previous input, then the output items with the encrypted
reasoning and without their ids, because the calls do not store them,
then the tool results. So each request of a turn begins with all of the
one before it. User messages that come during a turn join the next
request after the tool results, and a message that comes during the
final request starts a new turn. Replies, tool calls, and results go to
the log as they happen, and the reasoning never does.

An interrupt cancels the turn and kills a running tool. A failed call
ends the turn without a retry, and its messages stay in the log, so a
lasting failure does not repeat. Only a failed log write stops the loop.

The prompt gets a paragraph on persistent mode: a reply with no tool
call ends the turn, a question in the middle of a task does not end the
task, and "continue" resumes the latest unfinished task.

Issue: 5daf23e

3 days agoRun the tools of an agent and log each call
Anderson A. Giacomolli [Fri, 6 Nov 2026 22:12:48 +0000 (19:12 -0300)]
Run the tools of an agent and log each call

The agent gets four more tools after zoom and date: shell, read, write,
and edit. Turns and compactions send all six, so they keep one cached
prefix. shell runs a command with bash as the user in the working
directory of the agent, with no input and no sandbox, in its own process
group, so an interrupt kills the command and its children. read, write,
and edit take a path relative to the working directory or an absolute
one, and edit replaces a text that occurs exactly once.

Each call goes to the log as a tool message, the log is flushed, and
only then does the tool run, so the log never misses an action outside
the agent. The result goes to the log as an echo message. An interrupt
before the flush logs that the tool did not run. A tool error is the
output, so the model sees it.

Each output keeps its first and last 15,000 characters, 30,000 in all,
with a mark of the cut bytes between them. shell and read keep only the
ends while they read, so a long output does not fill the memory.

Issue: 5daf23e

3 days agoGive each agent a config and user instructions
Anderson A. Giacomolli [Fri, 6 Nov 2026 22:05:03 +0000 (19:05 -0300)]
Give each agent a config and user instructions

Each agent has its own directory with a config.json: the name, a random
ID, the model, the effort, and the working directory of its commands.
The first open writes the file with the defaults (OneChat, gpt-6.1-sol
at medium effort, the home directory) and a new 128-bit ID, and no later
open changes the ID. The prompt cache key comes from the ID, so a rename
keeps the prompt cache and two agents with one name do not share a key.
An unknown field is an error, and the ID must be there.

The user's instructions are in instructions.md in the agent's directory.
The prompt puts them at its end and gets back the clause of the
reference design that points to them. Without instructions the prompt
has neither, so it describes only what exists. The agent reads the file
on each call, and the compactor takes the prompt from the agent at each
build, so turns and compactions keep one cached prompt.

Issue: 5daf23e

3 days agoMake the chat writes durable by group commit
Anderson A. Giacomolli [Fri, 6 Nov 2026 21:57:56 +0000 (18:57 -0300)]
Make the chat writes durable by group commit

Each write of a chat still goes to the operating system at once, so a
crash of the process loses nothing. After each write, a background
syncfs of the file system of the chat starts unless one runs already,
and the next one starts when it ends. One sync makes every earlier write
durable, so a power loss loses at most the writes after the start of the
last finished sync, and there is never more than one sync at a time.
Memory.Flush returns when the writes before it are durable, so a caller
can make a tool call durable before the tool runs. A failed sync stays
and fails each later flush, because the kernel can drop the data of a
failed write back.

A crash can stop a write in the middle of a line. The open now cuts a
part of a line at the end of the last day file, whose write never
returned, and still refuses one in an earlier file.

golang.org/x/sys gives syncfs on every Linux architecture. The frozen
syscall package has no syncfs number on amd64.

A test kills a writer child process three times during writes of small
and large messages and finds every acknowledged message after each open.

Issue: 5daf23e

3 days agoKeep a built leaf when the view lags the tree
Anderson A. Giacomolli [Fri, 6 Nov 2026 21:53:34 +0000 (18:53 -0300)]
Keep a built leaf when the view lags the tree

A chat opens again after a crash between the build of a free leaf and
the save of the view. The catch-up adds the messages that the view does
not cover, and it now keeps a leaf that the tree holds already instead
of a second build of it, which the tree refuses as a duplicate. The
parent of such a leaf is settled at the start, with the other built
nodes.

A process that is killed during writes then opens again, so the log
loses no message that it acknowledged.

Issue: 5daf23e

3 days agoRevert the block parts of the compaction view
Anderson A. Giacomolli [Fri, 6 Nov 2026 20:26:53 +0000 (17:26 -0300)]
Revert the block parts of the compaction view

This reverts commits 4b9b226 and 56129a5. A compaction sends its view
again as one input part, followed by the task and, on a retry, the note.

The ChatGPT backend writes a cache entry only at the end of the latest
message of a request, and a later request reads it only at one of its
own message ends. Parts inside a message are not lookup boundaries, so
the blocks gave no cache hit. The entry of a compaction ends after its
task, which no later compaction begins with, so first tries miss with
any split of the view. A warm-up call per prefix would write a usable
entry, but it costs more than the misses, so the first-try misses stay.

Issue: 8c5fd2a

3 days agoSend the end of the view in one part with the task
Anderson A. Giacomolli [Fri, 6 Nov 2026 20:04:29 +0000 (17:04 -0300)]
Send the end of the view in one part with the task

The first try of a compaction sends the opening tag and each whole block
of four view lines as parts of their own, and then one last part with
the rest of the lines, the closing tag, and the task. The note of a
retry is the part after that. Joined, the parts are the same bytes as
before.

A pair check showed that a compaction whose view shares whole blocks
with an earlier one, but has its own rest part, reads nothing from the
cache, and that an exact repeat does. A rule that fits all results so
far: the ChatGPT backend stores a cache entry for a request without its
last part, and a later request reads it only if it begins with that
whole entry. With the end of the view in the last part, the entry of a
compaction ends at its last whole block, and a later compaction with a
longer view begins with it.

Issue: 8c5fd2a

3 days agoSend the compaction view in blocks of four lines
Anderson A. Giacomolli [Fri, 6 Nov 2026 19:55:18 +0000 (16:55 -0300)]
Send the compaction view in blocks of four lines

A compaction sends its view as separate input parts of the user
message: the opening tag, one part for each whole block of four lines,
and the rest of the lines with the closing tag. The task and the note
of a retry follow as parts of their own. Joined, the parts are the same
bytes as the single view part before, so the model reads the same text.

The views of two compactions end at different lines when builds run at
once, so before this change no two first tries sent the same view part.
With blocks, they send the same parts up to the last whole block that
both views have. This tests the hypothesis that the ChatGPT backend
reuses a cache entry only up to the end of an input part that both
requests have. The explicit cache breakpoints of the reference
implementation are not used, because the ChatGPT endpoint rejects both
prompt_cache_breakpoint and prompt_cache_options with gpt-6.1-sol.

Issue: 8c5fd2a

3 days agoLimit the probe calls at once and sort by start
Anderson A. Giacomolli [Fri, 6 Nov 2026 19:21:48 +0000 (16:21 -0300)]
Limit the probe calls at once and sort by start

onechat probe takes an optional BUILDS argument from 1 to 8, which
limits the builds that call the model at once in that run. A summarizer
with that many slots wraps the compactor, so maxBuilds in the core stays
8. A lower limit lowers the rate of calls on one cache key, which tests
whether the rate is the cause of the low cached share. The report adds
the calls per minute.

The probe reads the usage records in the order of the start of their
calls. The file holds them in the order of their end, so with calls at
once the quota stop compared the first record written with the last one,
not the earliest call with the latest, and the report showed the same
records as the first and the last call.

Issue: b14d537

3 days agoMake gpt-6.1-sol the default compactor model
Anderson A. Giacomolli [Fri, 6 Nov 2026 19:15:57 +0000 (16:15 -0300)]
Make gpt-6.1-sol the default compactor model

The compactor uses gpt-6.1-sol at high effort unless config.json names
another model. In the probe run it needed 1.10 calls per model line, and
2 of 388 lines went over 512 bytes, where gpt-6-luna needed 3.3 calls
per line and kept half of its lines over the limit after five tries. It
also credited quoted tool text to the tool, where gpt-6-luna credited it
to the user. 425 calls for 300 messages moved the weekly quota window by
about one point.

Issue: b14d537

3 days agoQuote long text in issues as a blockquote
Anderson A. Giacomolli [Fri, 6 Nov 2026 19:01:02 +0000 (16:01 -0300)]
Quote long text in issues as a blockquote

A long quote in an issue goes in a blockquote below its paragraph or
list. mdformat cannot wrap inline code, so a long code span stays on one
line of more than 72 characters, and the terminal UI breaks it in the
middle of the text. A blockquote inside a list item makes the whole list
loose, so the quote goes after the list.

3 days agoRun the probe builds in parallel to a call limit
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:56:54 +0000 (15:56 -0300)]
Run the probe builds in parallel to a call limit

The probe waits for the builds only before a user message, as a turn
does, so up to eight builds run at once while the other messages come
in. During a wait it checks the stop rules each second, so a stop does
not wait for a long backlog. After a stop it waits for the calls in
flight, so no call that was sent is cancelled.

The client takes a call limit and refuses each call over it before it
sends anything, so the probe stops at 660 calls also with eight builds
at once. The report adds the calls per model line, the time per call,
and how many calls ran at once.

Issue: b14d537

3 days agoFormat issue bodies and comments with mdformat
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:54:56 +0000 (15:54 -0300)]
Format issue bodies and comments with mdformat

Issue bodies and comments go through mdformat with the repository
configuration, so they wrap and number lists as every other Markdown
file does. mdformat joins trailer lines into one line, so the text is
formatted first and the trailers are added after it.

3 days agoRecord open questions and reasons in issues
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:48:10 +0000 (15:48 -0300)]
Record open questions and reasons in issues

A question to the user that is still open at the end of a session goes
on the issue as a comment, with the options and a recommendation, and
the answer follows as a comment. Otherwise the question is lost when the
session ends, and a new agent cannot see it. Each decision comment gives
its reason, so that a later agent does not undo the decision.

3 days agoRecord the node, reply size, and time of each call
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:43:32 +0000 (15:43 -0300)]
Record the node, reply size, and time of each call

Each line of usage.jsonl also gives the length of the call in seconds,
the size of the text of the reply in bytes, and, for a compaction, the
node [l, i] that it builds. With these fields the records show the size
of each try of a node, the time per call, and how many calls ran at
once, also when eight builds run in parallel and the order of the calls
no longer tells which node a call belongs to.

The compactor names the node on its requests. The node goes only to the
usage file and never to the endpoint.

Issue: b14d537

3 days agoSend the cache key in the session_id header
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:36:58 +0000 (15:36 -0300)]
Send the cache key in the session_id header

Each call to the ChatGPT backend sends the cache key of the agent in the
session_id header too, and keeps prompt_cache_key in the body. With the
key in the body only, the probe got no cached token, also on five tries
of one node with the same prefix. The same request sent twice with the
header got 1,664 of 1,756 input tokens from the cache on the second
call. Calls to the OpenAI API do not send the header.

Issue: b14d537

3 days agoLeave the turn state out of the usage records
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:33:04 +0000 (15:33 -0300)]
Leave the turn state out of the usage records

The ChatGPT backend sends x-codex-turn-state on each response: an opaque
state of the backend of about 1 KB, not quota. The usage records keep
every other x-codex-* header, so each line holds the plan and the
windows without the noise.

Issue: b14d537

3 days agoMeasure the compactor with a probe run
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:16:49 +0000 (15:16 -0300)]
Measure the compactor with a probe run

onechat probe LOG COUNT appends the first COUNT messages of a log to a
new chat in probe/ in the data directory and builds its tree with real
model calls, as the live check of the compactor. After each message it
waits for the builds, as a turn does. It reports the calls per message,
the cached share of the input tokens, the retries, the sizes of the
model lines, and the quota at the first and the last call.

The run guards the quota of the account: it stops at the first failed
call, so a wrong setting cannot repeat a failing call at each message,
and it stops when the used percent of any quota window grows by more
than 15 points since the first call.

A test generates the probe log: 1,000 readable made-up messages about a
small bakery order system, with the mix of kinds and sizes of the
synthetic log of the core. It writes the log to a file when
ONECHAT_PROBE_LOG names one, and needs no network.

Issue: b14d537

3 days agoMark the line limit with ASCII only
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:12:33 +0000 (15:12 -0300)]
Mark the line limit with ASCII only

The note for a line that is too long ends its cut with "| <- LIMIT"
instead of a Unicode arrow, as the guide requires ASCII in every file
except Unicode test data.

Issue: b14d537

3 days agoWrite only ASCII characters
Anderson A. Giacomolli [Fri, 6 Nov 2026 18:00:15 +0000 (15:00 -0300)]
Write only ASCII characters

Files, commit messages, and issues use only ASCII characters, with <-
and -> for arrows and plain dashes and quotes. Decorative characters
such as a Unicode arrow in a model task text are hard to type and search
for, and they add nothing that ASCII cannot say. Test data that checks
Unicode handling is the only exception.

3 days agoBuild the summary nodes with a model
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:56:02 +0000 (14:56 -0300)]
Build the summary nodes with a model

The compactor is the Summarizer of the memory core. Each build is a call
like a turn, with the same system prompt, tools, and cache key, so it
reads the prompt cache of the turns and never calls a tool. The input is
one user message with the compaction view up to the node and the task:
the leaf or merge text of the reference design with the 512-dash ruler.

A line over 512 bytes gets another try with the same view and task plus
only the newest note, which gives its size and its cut at the limit. The
model's earlier reply and reasoning never go back, so each try has the
same cached prefix and about the same size. After at most five tries the
shortest line is the node. A call that fails before any line fails the
build, and the core tries it again at the next message. The model and
effort come from the compactor settings of config.json.

Issue: b14d537

3 days agoRefer to the design without names
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:51:07 +0000 (14:51 -0300)]
Refer to the design without names

Code, comments, and tests refer to the reference design and its rollback
push list by their function, not by the names of their author or their
system. The view comment, the push-order test, and the prompt comment
use "the reference design".

The prompt test keeps its check that the prompt holds no old agent name.
It builds the word from parts, so the source does not spell it.

Issue: b14d537

3 days agoHold the agent prompt and tools in one place
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:43:12 +0000 (14:43 -0300)]
Hold the agent prompt and tools in one place

The system prompt of the UniiChat design and the zoom and date tools
live in prompt.go, so the turns and the compactions send the same bytes
and share one prompt cache. The prompt is the spec text with the agent's
name in place of Unii and the kind reply in place of unii. It holds no
date and no state.

The prompt describes only what OneChat can do, so it leaves out the spec
text on images, the name form of zoom, subagents, background tasks, the
user's computers, and the user's instructions. Each part returns with
the issue that adds its feature. The tool schemas are strict, with every
property required, and encode to the same bytes on each call.

Issue: b14d537

3 days agoRead the time directly in the model client
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:38:59 +0000 (14:38 -0300)]
Read the time directly in the model client

The model client and the ChatGPT authorizer read the time with time.Now
instead of a clock field. No caller set the field, so it was an
injection with no user. A test that needs fake time can run in a
testing/synctest bubble, where the time package uses a fake clock.

Issue: b14d537

3 days agoMove shared helpers to files of their own
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:35:21 +0000 (14:35 -0300)]
Move shared helpers to files of their own

writeFileAtomic moves from memory.go to file.go with no change, because
the token file of the login uses it too, and a helper in the file of its
first user hides the dependency of the other. The test clock moves from
log_test.go to clock_test.go for the same reason: the memory, tree, and
replay tests use it only as a clock.

A search of every cross-file use finds no other helper with an unrelated
user. The other shared declarations are parts of the memory core or of
the model client that compose for one purpose.

Issue: b14d537

3 days agoMove shared code to a file of its own
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:29:18 +0000 (14:29 -0300)]
Move shared code to a file of its own

Code that gets a user in another file for an unrelated purpose moves to
a file named for its function in the same change. The file rules covered
only new code, and the rule against unrelated changes made an agent
leave a shared helper in the file of its first user, where it hid a
dependency of the new caller.