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