]> git.giacomolli.uk - onechat/log
onechat
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

4 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

4 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

4 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

4 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

4 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.

4 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

4 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

4 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

4 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

4 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

4 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.

4 days agoLog in to ChatGPT and refresh the tokens
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:29:10 +0000 (14:29 -0300)]
Log in to ChatGPT and refresh the tokens

onechat login runs the device code login of the Codex client. It shows
the link and the code, polls until the user approves the code on any
device, trades the grant for tokens, reads the account ID from the auth
claim of the token, and writes auth.json in the data directory with mode
0600. The command needs no browser on the machine.

Before each call to the ChatGPT backend, the authorizer reads the expiry
from the access token. Within 60 seconds of the expiry it refreshes the
tokens, keeps a new refresh token when one comes, and replaces the file
atomically. A mutex lets one refresh run at a time. A missing file or a
failed refresh gives errLoginNeeded and leaves the file as it is. Errors
hold the HTTP status but never an answer body, so no token reaches a
log.

The endpoint in config.json selects the ChatGPT backend with this
authorizer or the OpenAI API with OPENAI_API_KEY, with no fallback from
one to the other. OneChat keeps its own token file, because the Codex
CLI rotates the same refresh token and two programs with one file log
each other out.

Issue: b14d537

4 days agoName the fields of the config test cases
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:19:36 +0000 (14:19 -0300)]
Name the fields of the config test cases

The test cases of the configuration and the data directory set their
fields by name, with one field on each line, as the go-code skill
requires. A case then shows which value is the XDG directory and which
is the home directory without a look at the struct.

A search of the rest of the code finds no other positional struct
literal. The node keys stay positional, because the skill allows a small
two-field type that acts as one value.

Issue: b14d537

4 days agoSend model calls to a Responses endpoint
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:19:03 +0000 (14:19 -0300)]
Send model calls to a Responses endpoint

One client sends every model call of an agent. The ChatGPT backend and
the OpenAI API take the same request body, so the client differs between
them only in its URL and its credentials. Each request sets store to
false and stream to true, sends the input as a list, asks for the
encrypted reasoning, and carries the fixed cache key of the agent, so
the turns and the compactions share one prompt cache. It never sends
max_output_tokens or metadata, which the ChatGPT backend refuses.

The client reads the event stream until the response completes and fails
on a failed, incomplete, or cut stream. Each call, also a failed one,
adds a line to usage.jsonl with the model, the input, cached, and output
tokens, the HTTP status, and the x-codex-* quota headers.

Issue: b14d537

4 days agoRequire field names in struct literals
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:17:52 +0000 (14:17 -0300)]
Require field names in struct literals

Struct literals use field names, with one field on each line when more
than one field is set, and table-driven test cases follow the same rule.
The old rule applied only to keyed literals, so positional test cases
with many fields on one line passed it. A small two-field type that acts
as one value, such as a node key, can still use a positional literal.

4 days agoRead the data directory and the configuration
Anderson A. Giacomolli [Fri, 6 Nov 2026 17:12:51 +0000 (14:12 -0300)]
Read the data directory and the configuration

OneChat finds its data directory in ONECHAT_DIR, then in an absolute
XDG_DATA_HOME, then in ~/.local/share/onechat, and creates it with mode
0700. config.json in that directory selects the model endpoint and the
compactor model and effort. A missing file or field takes the default,
and an unknown field or an invalid value is an error, so a typo never
becomes a silent default.

The endpoint is a name, chatgpt or api, not a URL, so the configuration
cannot send a token to another host.

Issue: b14d537

4 days agoRead messages and nodes through indexes
Anderson A. Giacomolli [Fri, 6 Nov 2026 15:39:12 +0000 (12:39 -0300)]
Read messages and nodes through indexes

The memory core no longer holds the whole log, which grows by about 200
MB a month. The log and the tree read each message and node from its day
file at the position in an index. index/main.idx holds the position of
message i in record i. index/tree.idx lists the node lines in file
order, and index/tree-L.idx holds the position of node (L, i) in record
i, with a zero record for a node that is not built. A read is one read
of the index and one of the day file.

At open the log and the tree index the lines after their last indexed
line, so a missing index is built again from the day files, and the
lines that a crash left out of an index join it. A partial record at the
end of an index is cut. The last node of the tree gets its level record
again, because a crash can stop an add between the two index writes.

The views keep the nodes of their own lines, and a running build keeps
its task. Nothing else stays in memory: after the 100,000-message
synthetic log the heap in use is about 3.4 MB.

Issue: beb9a70

4 days agoStart the builds in the order of the messages
Anderson A. Giacomolli [Fri, 6 Nov 2026 15:24:07 +0000 (12:24 -0300)]
Start the builds in the order of the messages

The queue starts the node whose context ends first: a leaf ends before
its message and a merge after its last message, and at a tie the merge
starts first. With leaves before all merges, a backlog of long messages
kept back the merges that a batch needs, so the view could not shrink.
The merges wait in a heap by the end of their context, so the next build
is found without a scan. The leaf window of 8 unbuilt leaves and the
limit of 8 builds stay as they are.

Issue: beb9a70

4 days agoRemove two timing gaps from the memory tests
Anderson A. Giacomolli [Fri, 6 Nov 2026 15:19:24 +0000 (12:19 -0300)]
Remove two timing gaps from the memory tests

The build limit test opens the gate of the fake summarizer only after
all 8 builds run, so its peak count no longer depends on goroutine
timing. The synthetic log test measures each view as the append did,
with the newest line as the pending mark, because the leaf of the new
message can be built before the check reads the view.

Issue: beb9a70

4 days agoReplay a synthetic log through the core
Anderson A. Giacomolli [Fri, 6 Nov 2026 15:06:45 +0000 (12:06 -0300)]
Replay a synthetic log through the core

The exit check of the memory core runs a synthetic log of 100,000
messages through the core with the fake summarizer. The log mixes short
user lines, replies, notes, reports, tool calls, and tool output, and
one output in a hundred is over 30 KB. The core has no reader of the
optchat log, because that belongs to the history import.

After each message the test checks that the chat view is at most 128,000
bytes and the compaction view at most 32,000, and that a batch merges
the chat view to 64,000 bytes or less. It checks each rendered line: a
built line holds its node, a long message has a summary and not its
text, and an unbuilt line holds only the pending mark. A restart in the
middle and at the end loads the same views and view.json byte for byte.

Issue: beb9a70

4 days agoWrite each line to the OS without an fsync
Anderson A. Giacomolli [Fri, 6 Nov 2026 15:04:10 +0000 (12:04 -0300)]
Write each line to the OS without an fsync

Each log line and each node goes to the operating system with one write
and no buffer before the call returns, so a crash of the process loses
nothing. view.json stays atomic through a temporary file and a rename.
An fsync on each write cost about 96 ms per message on a typical disk,
which made a long replay take hours. Durability against a power loss is
a later group commit, with one background sync that makes all earlier
writes durable.

Issue: beb9a70

4 days agoBuild the nodes from a queue in the background
Anderson A. Giacomolli [Fri, 6 Nov 2026 14:44:12 +0000 (11:44 -0300)]
Build the nodes from a queue in the background

The core keeps the nodes that are ready for a model build in a queue and
never scans the tree for work. A leaf joins the queue when its message
is too long to be its own node. A merge joins when both of its children
are built and their texts do not fit in one node. A parent that fits is
built at once, up the tree.

At most 8 builds run at once. A leaf starts once fewer than 8 leaves
before it are unbuilt. Leaves start before merges, oldest first, because
a turn waits for the leaves of all earlier messages. A failed build
starts again at the next message. Each build gets its compaction view
and its input, the source of a leaf or the two child lines of a merge.

The model stays behind a small Summarizer interface, and the tests use a
fake one. The start reads the log and the tree once to fill the queue
again, and builds the free nodes that a crash left out.

Issue: beb9a70

4 days agoAdd the compaction view
Anderson A. Giacomolli [Fri, 6 Nov 2026 14:39:32 +0000 (11:39 -0300)]
Add the compaction view

A compaction reads the chat view merged further, at the limits of 16 to
32 KB, so that it has context to resolve references without the whole
chat. The compaction view takes each new line of the chat. It merges in
one batch from above 32 KB down to 16 KB, and again when the chat view
merges: then it first takes each merge of the chat view, so it stays the
chat view merged further. Compactions share its prefix and so read each
other's view from the prompt cache. view.json saves it with the chat
view, and a restart loads both as they are.

The context of a build ends at its node: before the message of a leaf,
or after the last message of a merge. It stops at the first line that is
not built, so a compaction never sees a pending mark or a part of a
message.

Issue: beb9a70

4 days agoAdd the view and save it across restarts
Anderson A. Giacomolli [Fri, 6 Nov 2026 14:36:37 +0000 (11:36 -0300)]
Add the view and save it across restarts

The view is a list of tree nodes that covers the whole chat, oldest
first. Each new message appends its leaf as a line. Once the node text
of the view goes above 128,000 bytes, one batch merges the most due
sibling pairs until it is at most 64,000 bytes. Due is (T - last) / 2^l,
measured from the last message of the pair, and the oldest pair wins a
tie. A batch merges only pairs whose parent is built, and it continues
at each new message until it gets to 64,000 bytes. With the length of
Taelin's push list as the budget, the merges match the push for t = 0 to
20,000.

The memory core logs each message, builds the nodes that need no model
call, appends to the view, and writes view.json atomically after each
change. view.json holds the lines and the batch state, and the start
loads it as it is. The core never builds the view again from the log: a
chat with messages and no view.json does not open. The start only
appends the messages that a crash left out of the view.

A line whose node is not built renders as a pending mark, so the view
never holds a part of an unsummarized message.

Issue: beb9a70

4 days agoName the reply kind for its role
Anderson A. Giacomolli [Fri, 6 Nov 2026 14:33:01 +0000 (11:33 -0300)]
Name the reply kind for its role

The agent's own replies have the kind reply. Every agent in the tree
uses the same kinds, so a kind cannot carry the name of one agent.

Issue: beb9a70

4 days agoAdd the summary tree nodes
Anderson A. Giacomolli [Fri, 6 Nov 2026 14:32:30 +0000 (11:32 -0300)]
Add the summary tree nodes

The tree stores each built node (l, i) as one JSON line, {l, i, text,
size}, in tree/YYYY-MM-DD.jsonl, and loads all lines again at start.
Node (l, i) covers the 2^l messages from i*2^l on. The tree refuses a
node that is already built and a parent without both children, so each
node is built once and the tree never holds a gap.

A source that fits in 512 bytes is its own node with no model call. A
short message becomes the leaf "kind: text" word for word, and two short
siblings join with a newline into their parent.

Issue: beb9a70

4 days agoAdd the append-only message log
Anderson A. Giacomolli [Fri, 6 Nov 2026 14:30:56 +0000 (11:30 -0300)]
Add the append-only message log

The log of one chat stores each message as one JSON line, {i, kind,
text, size, date}, in main/YYYY-MM-DD.jsonl for the local day of the
write. Each append goes to the disk before it returns, and the log loads
all lines again at start. An exclusive lock on the chat makes sure that
only one process writes it. After a failed write the log refuses more
writes, because the end of the file is not known.

The kinds are user, unii, tool, echo, work, and note. Size is the UTF-8
byte length of the text. Tests use testify, so the Nix package gets a
vendor hash.

Issue: beb9a70

4 days agoKeep development data in .devenv
Anderson A. Giacomolli [Fri, 6 Nov 2026 14:26:51 +0000 (11:26 -0300)]
Keep development data in .devenv

The development shell sets ONECHAT_DIR to .devenv/onechat at the root of
the checkout, unless the variable is already set. Development data, such
as a copy of an old chat log for a replay, stays inside the checkout,
away from a running service, and Git ignores it. Each worktree has its
own .devenv, so parallel work does not share data.

4 days agoTrack issues with git-bug
Anderson A. Giacomolli [Fri, 6 Nov 2026 14:00:38 +0000 (11:00 -0300)]
Track issues with git-bug

Issues live in git-bug, inside the Git repository, so they work offline
and travel with the code through git bug push and pull. They are not
files in the checkout, so they stay out of diffs, formatters, and the
rule that files describe only the current state.

The development shell supplies git-bug. The issues skill tells agents
how to read and write issues, and warns that the -F option drops every
line that starts with "#". Commit messages now refer to an issue with an
Issue trailer that holds its git-bug ID, because there are no issue URLs
to link.

git-bug has no links between issues, so relations are Parent and
Depends-on trailers at the end of the body, as in a commit message. git
interpret-trailers can read them, and a search for an issue ID finds the
issues that refer to it.

4 days agoAdd Nix-built Go program and agent rules
Anderson A. Giacomolli [Fri, 6 Nov 2026 12:50:06 +0000 (09:50 -0300)]
Add Nix-built Go program and agent rules

OneChat is a Go program that prints its name. A Nix flake pins the Go
toolchain and every tool, builds the program with nix build, and runs
the binary in an install check. The development shell supplies the
formatters, the linters, and a pinned Vim, and it turns on the commit
message hook.

AGENTS.md holds the rules that apply to every task, such as the Nix
environment, the current-state-only rule, and the rule against revealing
infrastructure. The skills in .agents/skills hold the procedures for Go
code, documentation and comments, checks, and commits, so an agent loads
each one only for the task that needs it.

bin/format and bin/check cover every Go, Nix, RST, and Markdown file
that Git knows about. bin/reflow wraps comment paragraphs the same way
on every machine because it does not use a personal Vim configuration.
CLAUDE.md and .claude/skills are links to the same files, so Claude Code
reads one copy of the rules and skills.