How kaijutsu and its ideas took shape — an evolving narrative, not a standup
log. git log is canonical for what landed and when; the design docs under
docs/ hold the current designs; docs/issues.md holds what isn't built yet.
This file keeps the story: the arcs, the decisions, and why they went the way
they did. It reads oldest → newest, like the story it is.
Maintenance: fold new work into the chapter it belongs to; open a new chapter
only for a genuinely new arc; compress chapters as they cool. Commit hashes,
test counts, and day-by-day detail live in git history — including this file's
own history, where the fine-grained entries this narrative was melted from
survive intact. Compressed 2026-09-08; the pre-melt text is one git log -p
away.
Kaijutsu started 2026-01-15 as "what if my agent had a Bevy frontend and its own shell." The first two days produced a UI shell, a Quake-style console, and an SSH + Cap'n Proto connection layer; kaish was embedded by day three. The ancestry is sshwarma — an SSH MUD that grew an equipment system for models and nerdsniped its author into the context problem — and hootenanny, a retired pile of music-model experiments. The README's developer note tells that part.
February consolidated the type system and taught contexts to survive a restart. March made the block store correct (Lamport clocks, fork semantics, order keys), unified two databases into one, and moved the app onto MSDF text plus per-block Vello textures. April redesigned the tool system around the MCP broker — everything routes through it, builtins included, as a virtual in-process server — and landed CAS. May was the ABC crate's first deep spec push, a Haiku-driven live-eval harness, and a kernel-wide timeout policy.
Two demolitions shaped the toolchain: the Rhai engine went once kaish could carry scripting alone, and rig-core was dropped for hand-rolled providers, because owning the wire layer is what later made cache breakpoints, CAS image memoization and per-role routing tractable. kaish grew up rapidly inside kaibo, which is in many ways the pragmatic take on what kaijutsu explores maximally.
The framing ideas weren't written first and implemented after; they crystallized while building, mostly once the music work made "players" stop being a metaphor.
Instrument, not harness. Kaijutsu is something you play — you, a model,
anyone with a connected app; many hands on one keyboard. The kernel is the
instrument's body: it supplies what a turn needs without playing the turn.
That reframe, and the composer→musician, explorer→toolie renames, live in
docs/instrument-design.md.
Shared trust, crosstalk-as-feature (settled late June). Should sibling contexts be defended from each other? Won't-fix-by-design: every player is inside the trust boundary, the kernel runs as one unix user, and the real boundaries live outside it. Capabilities and loadouts are ergonomic nudges for focus and mistake-prevention, never security; your neighbor's wrong note is one you cover.
Context vs conversation is the load-bearing invariant underneath
everything: the context is the durable, multi-writer side; the conversation is
the append-only live session hydrated from it at boundary events. stage exclude and block edit land at the next hydrate — remediate a poisoned
conversation by excluding in context, then forking.
The August 18 tool-pairing failure exposed two contributing factors: a
repair pass indexed a filtered sequence using positions from its input,
and the provider boundary trusted the result. Fixing the index stopped the
observed cascade, but logging an invariant violation still allowed a bad
request to leave. The September hydration work closes each call batch in
one repair pass and validates pairing before every provider dispatch,
including live-loop appends. Invalid history now stops locally with an
error and a terminal turn event. Late results retain the existing
interruption policy; preventing their interleaving in the durable context
is a separate writer-side design (docs/conversation-session.md).
No first-class "agent." An actor is always a principal; agent-ness emerges from fork and drift, not from a noun in the schema. Later, a principal with a sheet became a character (September).
A silent-fallback bug in rc loading turned into the biggest structural
decision of June: rather than patch the dual-ownership cluster (stale-bytes
reads, append file-wipes, mtime no-ops, stale seeds), we deleted the class.
The kernel became the sole owner of /etc/rc and /etc/config, seeded once
from embedded defaults, nothing on disk to vim. It bought the corruption
class's absence at a real cost: nothing for git or an editor to see, and a
growing list of surfaces that had to special-case config paths.
That ownership did not survive the summer. Permission to get simpler
(Amy, August 15): "if the agent can see the files and edit them, that's
fine, we don't need to complicate it just because it's config." rc melted
back onto disk first (docs/rc-on-disk.md), then all four roots followed
under one mount registry — /config/rc, /config/kernel, /config/client,
/config/midi, each an ordinary host directory through LocalBackend
(docs/config-namespace.md). The June bug does not reopen: git is a choice
about a directory, never a mechanism the kernel runs, so there is still one
place the content lives. The case for dropping the rc-write guard was
first argued as "a gate that gates nothing," which was false; the right
argument was that rc had stopped being a special category.
The same weeks put teeth in the fail-loud posture. The kernel's edit tool
had fed byte offsets into a character-indexed engine — a silent splice on
any multibyte file, reported as success — and came back with byte→char
conversion, post-write verification that crashes over corruption, and
hashline addressing. The external MCP shell hang was executor starvation on
the client's single-threaded LocalSet, made permanent by a server reap on the
first stall; a 300 s command dropped to 285 ms. FileAttr.generation split
the coherence stamp from display mtime. And June 24's cache/cost session
added the lens that still guides prompt plumbing: the prompt cache is a
prefix match, so where a byte lands matters more than whether.
The longest arc, and the one that forced most of the system's ideas to get
real. Canonical designs: docs/chameleon.md, docs/tracks.md,
docs/midi.md, docs/pcm.md, docs/hyoushigi.md.
The chameleon loop (June 13). Models playing to a beat, a player's turn text being the score. The hard-won constraint: players must be tool-free — a small local model handed the full palette stalls the turn. Players are rc programs; a musician is a context attached to a beat track.
Tracks: the score outlives the players (June 28–30). Three stages moved
the music substrate off contexts onto a durable per-track model: the clock,
then the score itself — a real, app-viewable per-track score context, minted
the lost+found way, reusing the whole per-context block machinery — then a
generalized clock with mutable tempo. The landed-code review caught three
places that had assumed tempo constant for all time, one a silent-fallback
restart data loss. context_type decomposed into rc along the way: musician-
ness became "your create rc arms you," and the rotate page-turn became a
five-line rc script.
First sound (June 30). A Haiku musician's line came out of a synth: ABC turn → track → materialize → ALSA → TiMidity → speakers. The unit tests had been green for weeks; the acceptance test was audible. Then a local gemma4-e4b bass took the chair, dialed in by making the prompt small-model- foolproof and having the tick rc precompute bar targets in kaish.
Docs learned to stay present-tense (July 1). Three intense weeks had left the music docs teaching superseded mechanisms as current; "living" had come to mean stratified. The fix was not banners but moving chronology here and to git, and letting each doc state the present.
Render convergence: bytes never ride the track (July 1–2). Two
decisions, named in docs/midi.md: we take real time seriously by refusing
to chase it — micro-batch, promise only what we can hit, a speculation lead
of seconds, hard-realtime only at the node that owns the gear — and MIDI and
samples converge on one mime-keyed wire cue, RenderCue { mime, payload: Inline | Cas, lead }. A placed sample is a clip cell: CAS ref plus
placement, bytes prefetched out of band. Once the app proved parity by ear,
the in-process render path was demolished whole (~1000 lines). The kernel
links no audio FFI; a headless kernel makes no sound, but the score is
preserved and replayable. The metronome built to settle a reviewer split
went from 50 ms inter-click stddev to 0.7 ms once integrator wind-up was
replaced by feedforward tempo with bounded phase correction.
Clips and /v/cas (July 1–2). A seven-industry survey
(docs/cue-prior-art.md) found every cue system re-inventing the same six
field clusters, half already on hyoushigi's Cell, so Cell does not expand;
a clip is a versioned JSON payload. Making the CAS pool reachable went
design → audible demo in one arc: harden kaijutsu-cas (a torn object in a
multi-process cache would be truth forever), a read-only CasFs at
/v/cas, a client resolver over its own SFTP connection that re-hashes every
fetch. Gemini caught two real concurrency bugs both the author and deepseek
missed.
Demo #1 post-mortem (July 3) burned a director's turns on stale docs,
then found kaish could not run aconnect at all. The deeper fix was
subprocess exec behind an exec loadout authority, deny-by-default;
coder/mcp/director carry it, musician/toolie never.
A kaibo three-model audit with the verbatim ABC v2.1 spec in context paid
off twice: fourteen real bugs fell TDD-first, and one confident wrong finding
was rejected because the code's accident was already spec-correct. A
robustness net (parse→midi→abc→parse never panics; NoteOns and NoteOffs
balance) immediately caught a real L:1/0. The engraver carried exact copies
of two MIDI bugs, fixed at the root by one shared Key::signature() so they
cannot drift again.
The vi editor (June 23) is a kernel-owned session — pure modalkit behind
kernel EditorSessions, the Bevy app one renderer among many drivers — and
the feared render-path collision evaporated by decision: the app renders from
a kernel-served editor-state channel and never joins the editor's context
into its document cache. docs/vi.md.
The time well evolved more visibly than anything else: constellation,
spiral, tilted vortex with an accretion throat, then rings. Idle-age bands
lasted two days. Placement you can't control isn't an instrument (Amy,
July 5): two hand-curated rings sandwiching two automatic ones, ten seats
each, digits addressing the focused ring; promote by keystroke or by
visiting; demote steps one ring out and archives off the end; promoting an
archived context resurrects it, because the archive is memory to drift back
from, not trash. The HUD's four edge panels melted into the instrument a week
later (selection drapes the bowl wall, the reading card carries specs and
ancestry) and hud.rs died whole. On August 1 the rings lay parallel to the
floor and the arithmetic indicted the design: two lower rings rendered in the
room's basement, and asked what they were for, the honest answer was the
same thing. Two rings and a floor: ACTIVE, RECENT, and an accretion disc on
the room floor that is the event horizon. The reusable lesson: a geometry
change is a proof obligation against the design it renders.
Conversation view hardening (July 3) found error blocks stuck at the bottom were Bevy child-ordering choreography, and "text loads with holes" was a full MSDF atlas respawning generation tasks every frame — infinite CPU churn wearing a missing-glyph costume. The atlas grows to a cap now, and kanji-heavy conversations keep their glyphs.
The RPC transport moved off a positional three-channel scheme onto one
channel requesting the kaijutsu-rpc subsystem by name, a flag-day cutover
with no shim. The SFTP adapter serves kernel.vfs() directly with the
generation counter as its coherence primitive. Subprocess exec (above) locked
a direction with Amy that inverts the mount posture: an opaque host, PATH-dir
bin mounts curated per context_type.
The practices that survived contact, recorded because they are the real product:
- The house review ritual: two models outside our family read the whole files, no diff, so they evaluate holistically. Cross-model divergence is the point; each has caught real bugs the other missed. When two competent readers model the topology differently, that is the signal to go look, and you diagnose from the code, not a reviewer's summary. Reviewer claims about engine scheduling get verified against the local Bevy checkout before any code moves.
- Two voices at design time. Big cuts get stress-tested by independent models before code; the findings fold into the tracker, not a rewrite.
- TDD, red-first, and crash over corruption. The recurring bug class is the silent fallback; the recurring fix is fail-loud verification plus a test that fails against the old code. Falsification is the lead's job: a lane that writes a test against the code it is building will honestly report green; only a targeted fault shows whether the test guards anything.
- Demolition as practice. Rhai, rig-core, the config flush backend, the in-process MIDI path, the KV store (its one caller moved first, then 1,600 lines deleted whole), the CRDT replica in the MCP, the text engine itself — parity first, then delete whole, never strand a transitional path.
- Docs are living, not stratified. Chronology belongs here and in git;
design docs state the present;
docs/issues.mddeletes entries when they ship. Shared docs get edited, never re-emitted from model memory. - Run the exact thing against the real system. Twice in one afternoon a
thing that appeared dead was merely never asked a question it could answer.
Verify against the binary you think you shipped.
git log -Sa comment before building on it. - Fan-out and merge. Sonnet lanes in disjoint file territories, no git from lanes, the lead commits path-scoped and re-runs every red-test mutation. It held on a single shared file with explicit region ownership.
A player's-eye sweep of the kj surface, picked by one question: what does a
model hit mid-turn that a human wouldn't tolerate? Five parallel worktree
lanes; the first real test of fan-out-and-merge. kj fork worked from kaish
again once the kaish→kj bridge learned Value::Json. Contexts learned what
day it is through datetime rc seeds, and the load-bearing choice was the
block kind: a Notification hydrates as an appended message, while a System
block would invalidate the cached prefix daily. Config stopped lying about
unknown provider types. kj block cat --latest <mime> answers "give me this
turn's artifact" in one call. The unknown-command 300 s hang closed as a
proof that the dispatch fall-through is bounded at every await. Two kaish
bumps were zero-source because we ride the embedder API through low-level
primitives; the second fixed the /v/cas shadow where kaish's overlay had
reserved the whole /v tree.
The time well had proven kernel state could be a place; the scenes charter
(docs/scenes/) asked what building the rest would mean. Two days of design
(28 mockups culled to one canonical image per surface), then three days from
spec to a finished station.
Navigation grew one level up without a new grammar: Up/Down between detail levels, Left/Right within one, Esc always walks up, and the well's mouth exits through a double-tap speedbump. The camera taught the room's first hard lesson: in a radial room every pose is a claim about what stands between you and the center, so a focused station is approached from its own side, looking outward. The patch bay went from black blob to instrument on a round table. A two-hour hunt for a "missing" traffic pulse ended with staged shader probes proving every layer correct — the 0.42 s default was faster than screenshot sampling. Distinguish "the mechanism is broken" from "my observation can't see it" before touching the mechanism.
Amy settled one scene graph, not separate ones, and the lifecycle bill came
due immediately (a context switch mid-dive leaked the room). Furnishing day
taught that inhabitable is mostly camera height and that you cannot light a
1% albedo — the material's diffuse response, not the lamp, is the knob. Then
walls became screens: eight content panels in an octagon, and "we could
almost drop the dive if the walls were 16:9" turned out to be structural.
With fullscreen as a camera pose plus a zoom field inside one Room state,
Screen::PatchBay dissolved and took the dive-exit lifecycle machinery with
it, including the leak fix built that morning. Deleting a state to delete a
bug class is the day's best trade.
The terrace glyphs shipped ornate and Amy named the real problem: "muted like
the rest of the octagon… more synthwave than anything." The mutedness was
structural. Hues were governed, brightness was thirty scattered constants,
and the tonemapper had never been chosen. One kernel-owned theme.toml now
carries both lanes — sRGB UI and linear-HDR scene with a brightness ladder
and a hot-applying post chain — a ScenePalette resource absorbed every
scene constant, and a live A/B over BRP picked ACES; the muted look was
literally TonyMcMapface. docs/color.md is the contract. The mirror test
between compiled and file defaults caught a palette 13× off on one channel
family on its first run.
Three quiet debts in the semantic index went in one afternoon: HNSW cannot delete, so eviction left dead vectors forever; nothing noticed a model swap; synthesized gists evaporated at restart. The design that made rebuild tractable is that slots are never renumbered, and a red test proved they must never be reused either. First boot on the live kernel reclaimed eight dead slots the real index had been carrying. Sonnet lanes wrote the code; the lead's review, the outside models and the running kernel each found bugs the other two missed. That triangle is the lesson.
The September synthesis pass exposed a second distinction: the search projection and the synthesis input are different documents for hashing purposes. A prefix hash cannot validate a gist drawn from the whole context. Amy's direction to use lfm2d moved embedding inference out of the kernel, and made purpose, normalization, and checkpoint identity explicit parts of the interface. Caching unchanged work and bounding service calls came first; changed contexts still need per-block reuse before automatic synthesis can return. Keeping that switch off made it possible to replace the inference mechanism without repeating the September startup load spike.
The fsn landscape went from vocabulary to a rendering world in one evening of
three lanes: relaxed-Voronoi layout in kaijutsu-viz, a Vfs.snapshot RPC
with generation stamps, and a dive-through scene. The live pass taught that
the unit trees were too polite: the real host tree killed the walker three
ways in an hour (a root-only directory, /v existing only in the mount
table, /proc PIDs vanishing between readdir and getattr), and each fix was
a design decision — denial renders as a seam, the mount table answers for its
own namespace, churn under the walk is operational.
Then Amy reframed it: the fsn world is not a file browser. Agents work
at the file level and the shell covers the rest; the filesystem is a free
source of ambient data that looks good in 3D. That sentence deprioritized
bloom, dive-to-vi and search, and promoted heat from the kernel's own hands
(the MountTable chokepoint already sees every mediated op), recency from the
wire, and the vessel inhabiting the world through two wall panels rendered by
an off-screen camera. docs/scenes/vfs.md is canonical.
Reverse the SFTP we already have, so a client can share ~/src into the
kernel the way code . shares a directory: the client opens a channel and
speaks the server role, and the share describes itself with an in-band
index manifest. Gemini's pre-build review reshaped slice 0: VfsOps::read
is stateless and SFTP is stateful, so a naive pump pays OPEN/READ/CLOSE per
chunk; open_read_stream became the first thing built. A post-build
deepseek pass found six bugs the tests missed, the worst a readlink stub
that lied. docs/slash-r.md.
The TRACKER station replaced a promissory nameplate with the instrument — a pattern grid where each column scrolls at its own tempo past one fixed playhead, because tracks are independent clock domains — and the first jam on it found the clock bug. Amy heard the metronome "bumping a few times, not evenly spaced." Beat references were stalling behind a musician's streamed output on the single callback stream, arriving in bursts, and the receivers folded every buffered reference against one frame-now. The burst behavior had been encoded in a unit test as correct.
The fix was symmetry with the clock-in path: every timing artifact carries
its emission wallclock, sinks back-date, stale ones are demoted on a ladder,
and a metronome never stacks clicks — missed beats are missed. Then the
kernel grid went scheduled-periodic (re-arm on the deadline, not the wakeup)
and the phasor earned a deadband inside which it simply is the local
clock. From zero-millisecond click blobs and six-second holes in the morning
to four hundred click-to-bass pairs holding +0.2 ms with zero slope by late
afternoon. docs/midi.md "The one timebase" is that day written as
doctrine. The jam also showed that a track played for hours drowns every
musician who sits down at it, which filed the windowed band view.
An intermittent five-second tax on the MCP shell turned out to be one line
reading another: the server dedupes block subscriptions by (principal,
instance), and kaijutsu-mcp passed a literal "mcp-server" as its
instance, so every concurrent process for one principal was the same client
and whoever subscribed last silently evicted the rest. The app had the same
bug spelled "bevy-client". Both now use per-process instances, and the
registry warns when a different connection displaces a live bridge. The same
day kernel.db got its first backup story (kj db backup, VACUUM INTO
against a live writer) and restore stayed a documented procedure, because a
live file swap under a kernel full of in-memory state is a lie waiting to be
discovered.
Reading kaibo's cast concept against our llm/ layer made the gaps obvious,
and Amy pushed past "adopt kaibo's TOML": "I'd like to see the cast data
modeled in SQL directly and that's the source." models.toml was demolished
for normalized tables edited through kj backend|cast|alias, registry rebuilt
live on every write. Presets survived on purpose once the code showed they
are patch recall over verb args: cast = who plays, preset = the patch.
Roles are context_types. Old aliases didn't make the trip — "they were
guesses" — so every row above the floor is something someone chose.
Amy's rule arrived as a one-liner — DB errors are P1, we fix them now — and
the kernel spent two days proving why. A warn scrolling past every restart
since mid-July was two definitions of "already knows this context" a few
lines apart. create_document decided whether a failed insert was benign by
asking e.to_string() for "UNIQUE constraint", and two failures wore that
disguise: a benign PK conflict and a different document claiming a taken
path. The fix refuses to read messages at all: on a violation the DB layer
reads itself back and returns a typed answer, so classification depends on
the database's state rather than its prose.
The drift router's dead letters were being written by a caller that logged and carried on; three lines above sat the invariant being broken (a registered handle implies a row). Then the live run found the joke: restart, orphan a drift, flush — nothing to flush, because the early return for an empty caller queue sat above the global dead-letter half. The existing test had met this and accommodated it in a comment that reads like a confession. All three fixes were unit-green before that flush ran.
A gap analysis against hermes-agent and QwenPaw found one gap kj had to fix:
no task state. Both harnesses write JSON to disk; a task that is a block gets
the block log for free. task_status reused the per-field LWW register
content_type had already established, and got its own enum because Error
means the tool crashed and a cancelled task is a choice. Mid-conversation
edits are cache-safe for the same reason Notification blocks are: the mailbox
translates a block id once. builtin.tasks sits beside block.rs so a
household agent can be grantable "groom tasks" without block surgery. Codex
later joined through two deliberately separate channels — its MCP subprocess
for identity, command hooks for lifecycle — because MCP identifies the player
but cannot observe prompts or session boundaries.
"Using toad with kaibo was a delight." Four lanes before lunch; the
headline was kaijutsu-acp, a thin ACP bridge in kaijutsu-mcp's image, whose
session picker served the app's ring-0 rank with zero schema changes. Then
Amy bounced the kernel and the first flight died on its first prompt: the
task merge had added an at-rest CBOR field without a serde default, and the
breakage sat latent until the bounce — every pre-task document undecodable,
rc unreadable, no create-time bindings, deny-by-default locking the
operator's own kj. A missing attribute became a total lockout through four
links of chain. New at-rest fields decode old bytes or they don't merge.
The afternoon was the best shakedown the project has had. Four incidents traced to one defect — the FlowBus was a shared broadcast ring, so one slow subscriber's overflow evicted events for everyone — and Amy set the doctrine in a sentence: "no lossy solutions. I'd rather be disconnected." Per-subscription bounded queues, lossless-or-terminated for ordered topics; timing topics keep their own law and drop oldest. Sequence numbers mean a gap without a termination is by definition a kernel bug. Per-lane guarantees say nothing about ordering across lanes, and the completion event beat the final text to the bridge until the bridge learned to settle before answering. And the crosstalk stance stopped being theory: the model toured the repo in Amy's toad session while the lead watched the same blocks from kj and Amy from the app — three players reading one score.
The Bevy 0.19 plan was written by reading all 104 migration guides against
the app rather than from memory, because training data predates the rename
that matters. Seven of 104 touched us. What was load-bearing was a dependency
whose version number lied: bevy_brp_extras = "0.19" requires Bevy 0.18.1.
Read a dependency's manifest, never its version number, and prefer the
cargo registry cache as truth over a checkout that can be five months stale.
An unused avian2d was hard-pinning the old engine; dead dependencies vote
on your version constraints. Amy ruled rodio migrates first, on its own, so
the one-copy invariant stays true throughout. A workspace test that failed
after the bump in a crate with no Bevy got checked at the pre-bump commit in
a worktree, where it failed identically, and went into the backlog with proof.
A failed create rc lifecycle left a context holding nothing, and the backlog
called that a lockout. It isn't: switch and every read verb are ungated by
design, so diagnosis works and only action is blocked. Amy's question — when
would create actually need to abort? — answered almost never, and aborting
destroys the Error blocks that explain why. Reading the gate found the sharper
defect: every refusal advised kj binding allow, which the caller reading the
message could not run, and a KernelDb read failure was collapsing into
"denied." The gate now keeps three outcomes three, the unbound case names
exits that exist, and kj context rebind re-runs create ungated on the
same argument that leaves create ungated: the loadout comes from rc, not
the caller. Gating the repair on a capability the broken context cannot hold
would have rebuilt the lockout as a feature.
kaijutsu-mcp was a CRDT replica with a sole-writer task, resync coalescing
and an event bridge. By the end of the day it held none of that. Amy's
framing made the shape right: a tool call takes a quick lock at startup, then
runs independently — so the atomic part is only the reservation, and a
ToolCall at Running is a legitimate pending state. Reserve, then flow.
authorBlock and completeBlock replaced the raw-op path. Then the layers
came off in order, each smaller because the previous had removed its reason,
and the last inversion was the key: polling became the guarantee and events
became a hint. A dead feed is no longer a condition to detect; it just
means nothing arrives early. 624 lines went. The hook boundary consolidated
the same way later — kaijutsu-mcp hook claude|codex absorbed both native
protocols and two Bash-plus-jq wrappers disappeared.
What the day was really about was tests that cannot fail. Five defects passed
careful reading and died on execution: a tokio::time::timeout that drops
its future stranded a ToolCall at Running in a cancellation window the old
single-lock design could not have; a wire field the server ignored,
reproducing one ordinal later the exact defect that justified the verb; a
"survives a dead feed" test running echo, which finishes before the first
poll. An untested mechanism is a claim, however carefully its prose is
worded.
spawn_periodic_refresh was correct, tested, and called from nowhere. Proof
it ticks came from the live kernel: samples of /run/roster/index landed on
the loop's own grid, ten thousand milliseconds apart.
Then a cat of that index exited 3 and the thread ran all day. kaish caps
captured output by replacing it with a preview and remapping the exit code,
which is right, but its audience was wrong: it also reaches $?, so inside
an rc or hook body a command that merely printed a lot reads as failed, and
the gate's classifier would have escalated on a good long answer. The fix
asked who consumes the output: model-facing shells keep the cap; rc, hooks
and the editor's :r !cmd get a runaway backstop. The test pins kaish's
current wrong behavior so the workaround can go when upstream fixes it.
The part worth keeping is what the machine could not do. Clearing 194 stale
contexts needed a safety filter, and roster liveness is not it: recent
means "appended a block in fifteen minutes," not "someone is attached." It
reported four live contexts while a Codex lane sat mid-review, connected and
thinking. Amy held the fact — "there should be moltar app, this claude code,
maybe subagents, and a codex session" — and the instrument had no way to
represent it. Use last-activity age, and treat "attached" as a question the
roster cannot yet answer.
Replacing ROOT made the same point structurally: special by convention,
ordinary to every mechanism. Amy settled the graph — "I had thought to make
it a dag but the data is naturally a forest and drifts create cycles if you
count them" — and checking that invariant found kj context move orphaning
a context it refused to move. Anchors exist for fork cost: forks copy history,
so every block in a root is paid for by every descendant forever. Three weeks
later the archive cascade itself went: archive is a fact about one context,
and a lineage is worth more intact than tidy.
The CRDT position paper had ruled: one authoritative sequencer, rich RPC authoring, projected event streams, no client dependency on the text engine. The migration began, and went one step past the paper — replacing CRDT-shaped storage wherever semantic operations suffice. Amy declined to gate the git work on principal provenance: the gap is a pattern across seams, not a config bug, and fixing it under whichever lane stands there fixes it in one place.
The MCP shell-completion path had a Phase 2 that decoded the whole oplog to
re-read a block, justified by a careful comment about three reorderable
topics. The argument was sound and about a mirror the August 13 demolition
had already replaced; the variable was still named local. A stale comment
is a false premise parked where the next reader will pick it up. Two more
were corrected the same day, one citing three schema ordinals that were all
wrong.
A throwaway question — does a projected block query match a decoded sync
payload? — found two fields the server wrote and no client read, excluded
and created_at. Harmless only because the clients that care still read the
other path, which the migration was about to move them off. A wire field no
client reads is not dormant; it is armed, and the migration pulls the pin.
The zero case for created_at is the house style in miniature: propagate a
1970 faithfully rather than substitute "now." And a "flaky" kernel test was
reproducing a real bug: mount printed past the output cap, and the durable
record of a capped command carried a failure code under a healthy status.
Everything until then assumed the wire was near-frozen. Amy lifted it in a
sentence: every client is in-repo and rebuilt together, so flag-day changes
are fine where they reduce debt. Freezing collapsed into deleting: pushOps
and pushInputOps went, a thousand lines, and with them merge_ops' only
caller. Concurrent merge into kernel documents became impossible, and the
instrument built that morning to measure whether it ever happened was deleted
hours later, because a structural impossibility beats a metric reading zero
forever.
The replacement design was right in shape and wrong in placement. A DeepSeek
review confirmed append-or-replace events and refuted the reasoning (content
comparison, never a list of tool names). A Gemini review, asked to counter our
anchoring, found that classification had been specified at the wire against
per-subscription state, where the bridge would have to link the library being
removed; it belongs inside the mutation lock where both texts are in hand.
The same review found gap recovery unimplementable because the snapshot query
returned no version, and argued for one ordered per-context change feed
carrying events and the version they bring the client to. Amy: "it's been
creeping around my thoughts and is the right move." Timing artifacts keep
their own path; the timebase doctrine forbids the batching trade for them.
docs/change-feed.md.
Building it taught two things. The snapshot query hides that BlockSnapshot
has no ordering key, which is exactly what ACP had been using the CRDT for,
so the insert event carries a position. And a client cannot edit a block's
text at all — pushOps was the only path — which "all authoring goes through
rich RPC" had been written as though the verbs existed. The client-side
follower is written once and refuses rather than guesses; its own tests
caught a stale-index bug in the move path. All three clients moved the same
evening and each deleted a staleness apparatus that had been load-bearing for
a replica that could quietly diverge. The one bug needed no concurrency: the
version rode a delivery while the rule reasoned per event, and a delivery
is a batch. The last holdout on the old surface was the time well's activity
glow, which counted events and never looked inside them; Amy disabled it
rather than migrate it, and four thousand lines went.
The renumber exposed an assumption the least visible way available: the
client crate's build script had never declared the schema as a dependency,
and every previous change had been additive enough to hide it. Amy reading
the test output rather than the code found the shipped default mcp.toml
pointing at her own kaibo, so every test kernel opened her live state
database. A shipped default is a decision made on every machine that has not
overridden it.
Then the text engine left. A read-only copy of the 861 MB production database
showed diamond-types-extended's snapshot encoding costing about four times
the text it stored, flat across version counts — a structural cost, not
debt. A forced compaction pass materialized every document's text first,
because any document edited since its last compaction held newer text only
in the oplog. Block content became a plain String: streaming is all append
and push_str is amortized O(1). The editor and file surfaces keep ropey,
where splice-heavy text earns it; a draft stays a String with a revision
counter. Three surfaces, three representations, each chosen against what it
does to its own text. docs/crdt-position-2026-08.md.
The approval ledger had carried a second, quieter system beside it for
months: kj cc send and shell_write went through run_gate with durable
rows and rules; a hook's Ask went to a Uuid::new_v4, a thirty-second
budget and a blocking round trip with no record. Three pieces of evidence sat
in the ledger's own source — Origin::Hook never constructed, hook_id
never populated, NewOption mapping one-to-one onto ACP's permission
options — so the ledger had been built to absorb the hook path and never
told. Amy retired the doorbell.
The instructive half was what fell out. GateOutcome could say allowed: false and nothing more, so run_gate had been reporting its own faults as
denials, and a ruling from the day before required a model to distinguish a
refusal from an absent control. Both became one Option<AskRef>: a row
exists with a status, or there is no row. Then a gated call held eighty-one
seconds and returned after a human answered from another surface, the loop
working end to end for the first time — and the same day, driving kaijutsu
from an editor, no approval ever reached it because the editor launched a
binary compiled two minutes before the wire it spoke was retired. The answer
was a handshake that refuses a mismatched peer and names which side is stale.
Four primitives that day had been built, tested and never called; nothing in
a green suite says this code has no caller.
No gain constant could make a wheel detent cheap while its cost scaled with
block size: the conversation was a Bevy flex column of per-block textures,
taffy in the scroll path and a silent clamp truncating blocks over ~273
lines. Amy's idea was to cache blocks as textures; the survey moved the cache
one level up, because the expensive step was shaping, not pixels. Blocks
became cached shaped glyph runs, assembled into instanced buffers over a
±1-screen window and drawn with scroll as a 64-byte uniform. Five slices in
one sitting, each review-gated and live-verified over BRP, and slice 5
deleted the legacy path the same day — Amy: "no reason to keep legacy in
this project." docs/conversation-surface.md.
A cold miss served a kernel document written in June and wrote it back over
a file that had moved on: the kernel was the one player that could revert a
human's work and call it a save. docs/file-buffers.md's thesis is that disk
is the source of truth and vim solved the rest decades ago: a buffer is a
view, an unsaved buffer is a swap, a swap that survives a crash is announced.
A dirty buffer leaves a durable row whose presence is the flag; :w
refuses when the disk generation moved unless the player types !. Kernel
came to own its block store and file cache by construction, deleting a
OnceLock pair that had let two caches exist over the same documents. The
test database stopped being :memory: across 228 call sites, because the
in-memory path was a second code path production could never reach. The
20th's audit found an evicted entry had made :w a silent no-op, and every
new test was broken once, on purpose, by the lead before it was kept.
A guard against sh -c and a risk scorer were written, tested, reviewed and
deployed, and neither had ever run. The kernel seeds rc only when the tree is
entirely empty — deliberately, so a deleted script stays deleted — and the
other half of that rule is that a script added to the embedded set after
first seeding never lands, and no surface said so. The fix was not to seed
harder but to make the discrepancy visible: a fourth status, "a seed exists
here and nothing is installed," which Amy chose over a repair verb because a
status announces itself to anyone who looks. It proved itself within the
hour. The scorer had armed itself only in one seat and fallen silent
everywhere else; unarmed and broken looked identical, which is the actual
defect. A system that cannot report an absence will keep the absence.
The coder became something you could hand a job to: kj fork, kj drive,
kj wait. The first thing it produced was a bug reading had never found:
kj wait reported completed mid-flight, because between a tool result
reaching Done and the next model block there is a provider round trip
during which nothing anywhere is Running. An instantaneous read of a
multi-writer log cannot tell "finished" from "between two blocks"; the
information is not in the log. Amy's ruling was to stop inferring: the
kernel owns a registry of turns in flight, set where a turn is committed and
cleared at every one of the stream processor's exits.
The day's rule, sharpened with kaish's lead who found the same shape twice in their own tree: the dangerous comment is the one that is the reason something else is switched off — an append path excluded from a gate because "append never destroys content." A test asserts behavior at one point; a load-bearing comment asserts reachability, and nothing checks those. Green tests are the condition under which they rot unnoticed. The day closed with a delegated coder concluding git was not installed when it was on a shell that refuses external execution, and the shell said both with one sentence; each project now names the condition it owns.
Then the gate went next. A gated call had been holding an RPC open while a
human decided, and the ask was asked to last an hour, an errand, a night. It
cannot: the client's deadline is a compile-time constant in a process that
cannot read kernel config. Amy: "perhaps we should consider a state machine
and not actually having anything block on the wire." The kernel announces,
the client may block locally, and when the answer lands the kernel performs
the action itself. The next day she killed restart survival too — "the
kernel is really reliable and the only reason it restarts a lot right now is
because we're actively advancing it" — because the one outcome that design
could produce, an approved destructive action running twice, was reachable
only on that path. docs/gate-resume.md.
Where should a terminal client live? The client crate has no Bevy in it, the
ACP bridge proves it renders anywhere, and of the app's ninety thousand lines
the part a terminal could reuse fits in three thousand. So the tui is a
standalone binary in the ACP bridge's shape with ratatui as the edge; a
kernel-served tui was feasible and declined because a client inside the
kernel process reaches around the wire rules the first time they are
inconvenient. Amy: inline viewport, not fullscreen; one bindings.toml for
both clients keyed by vim notation. The ssh-shell design retired into it.
The first cut shipped September 2 from five lanes in one day, live against
zorak: transcript printed once into the terminal's own scrollback, a
static-height band pinned at the bottom, vi compose over the kernel draft,
the Ctrl+Z shell with real suspend, the picker, the ask card, editor and
diff on alternate screens (docs/tui.md). Amy played it that evening and
her notes drove the next week: the thinking pane, the in-flight strip as one
fixed row, the draft that grows the band, an ask answered from another seat
the client holds. On September 8 a switch marker and a per-context copy mode
were tried and shelved — "marker and copy mode aren't gonna work" — and the
buffer question is open: a single-context app that composes with tmux, or
the tui is the mux on the alternate screen "all modern like in rust." A
session of its own.
Probing a gated surface minted five asks from one probe, because its refusal
arrived as a transport error, and a transport error means "I could not tell
you what happened," so the caller retried with different text and each
retry minted a row. A verdict is a result: the machinery worked and reached
an answer, even no, and the caller must not retry. A fault is an error. The
kernel knew the difference; the return path had nowhere to put it, because
every capnp::ErrorKind describes a fault, and a careful comment above the
collapse said exactly what was being lost.
An inventory of 152 RPC methods found one in eight with any second channel;
throwing was the default and the gate was merely where it hurt most. Amy's
ruling: no general verdict facility; the unit of work is the family. The
VFS family shrank from seventeen wire methods to four with callers (SFTP had
superseded the rest months earlier and nobody said so in the schema), and its
errno crosses the wire as POSIX semantics under a stable encoding because
ENOTEMPTY differs between Linux and macOS. The gate family got a Refusal
union. Then the fix that stopped at the wire bought nothing: the client's own
actor flattened the typed error back into a string one hop later, and the
receipt was in the tree, a roster function lowercasing kernel prose to search
for "not found." Every boundary that stringifies is a place the type
dies, and counting the methods in a family is not counting the work.
The state-machine ruling had said that when an answer lands the kernel
performs the action; nothing had. Amy ruled it the rest of the way: approval
triggers execution. kj ledger allow <id> runs it and fills the command
and output blocks already sitting Waiting. The driver reserves the rc
thread stack after the first heavy approval aborted the live kernel
mid-rotation of ROOT. The digest match was found to be the authorization key
for the retry path, not duplication, and stayed. For a free ${VAR} Amy
asked "how hard would it be to snapshot kaish state along with the
request?" — easy, since both gated paths run on a single-use shell seeded
from durable state — so the names are captured with their values, restored
before the source runs, and shown to the human and the classifier alike. The
executor claims the redemption row first because the primary key is the only
exactly-once there is; a crash between claim and run loses the action rather
than doubling it. Its archived-context test went red on the first try and
found a bug older than the lane: archiving stamped a timestamp and left the
state column at live.
The first live probe said the ending was not there yet: only the shell gate
recorded executable source, and with hooks installed every production ask
comes from the hook gate, whose doc said a hook ask has no free variables —
false in a way sharper than the executor, since an allow rule on dd of=${DEV} would have redeemed every future value. The hook gate plans a
shell call the way the shell gate does now. And the hook feeding the scorer
from Amy's terminal was shelling to whichever kaish was on the path, which
the 0.17 bump had swapped underneath it. "Let's use kaish as a library which
mcp already does so there's no way to have version skew." docs/gate-shape-b.md.
Amy opened a Saturday with her own routine on the table: she restarts every Claude Code session each morning, and again whenever a context's prompt cache has gone cold, and the thing that makes a restart cheap is a handoff she and the lead maintain by hand. She had been wondering whether it was time for "a lil message board." Within a few exchanges she had reframed it: "really message board would be a rework of the drift queue."
The musician already had the answer in miniature. A player's score lives in a track's context, written ahead of the playhead; a page-turn forks a thin child that re-attaches; the child rehydrates from a window plus the last eight phrases the kernel hands it. Map that onto the lead and the seat is a track, the handoff is the score, the restart is a rotate, and the morning read is the window. The signoff file's flaws, whole-file rewrite, no stamp, no author, no window, all fall out of using blocks instead.
What the conversation could not get past for a while was the connecting noun. A context is ephemeral. The context graph is a forest, and Amy's instinct that it could not be the container was right: the identity would be a whole tree, and no tree is a thing you can hand a note to. The name is the home, she said, but "the name needs a home in code. A bare string or identifier probably isn't it." Game design supplied the shape: a character sheet, a collection of things with an opaque id in the records and a given name at the table. Character also turned out to be the missing word in a set kaijutsu already used, cast and role and context, the thing an actor is cast as. Amy is a character. So, eventually, is everyone in the house.
Then she pruned. Party went, because the accountability chain covers it. Chair and seat went, because no concept needed them. The purse and an availability field were deferred. Presence stayed, with a caution against ever tying one client to one character, since she is connected two to five ways on a normal day. And the night shift became a janitor first, with a proctor sweep as its second job: notice an idle session, ask Amy, then drive the session to write its own handoff note while its cache is still warm, because "the session's own model is both the best summarizer and the cheapest one."
The code review at the end changed the plan more than the design did. A
principal already has an opaque id and many credentials mapping into it; the
roster already knows principals, contexts and liveness. So a character is a
principal with a sheet, and the design adds no new identity type. The lead
first read the turn path as stamping a model's blocks with the human who
drove the turn; a kaibo review corrected it: the blocks carry the system
principal, because there has never been a principal for the model to be. A
frontier deliberation that evening moved the handoff onto an ordinary context
with a hydration window and split every turn's identity into the requester
who caused it and the character who performs it. docs/character.md carries
the slices.
The prework went out as six small lanes, and two turned into corrections: a
comment said the roster's refresh loop was never called, and it had been
called since August. The rule that came out is small: run git log -S on a
comment before building on it. The third lane found the restart mystery from
Thursday, which was never the kernel: the hook listener archived its own
context on any session's end, because the id it thought was its own came
from a transcript scrape that named the session before.
The sheet and the keyring shipped the next day, and the keyring changed
shape while it was being built. The reading that moved it was that the
kernel never reads auth.db: the join key was always a bare principal id,
and every name in the system was a display label cached wherever the
identity had been seen. Amy took it further in two steps, first "maybe
authdb should bind a key to a principal id only, and nicks melt into the
character," then "why have a name in principal at all at that point?"
The struct lost its name fields and then its reason to exist. add-key --as <character> binds and never mints; retire takes a character's contexts with
it; a fresh kernel seeds hajime, a character built to be retired, with a
minted id because "deterministic feels like a choice we'd regret." The
migration ran on Monday morning after a full rehearsal on a snapshot, and
every rehearsed number held. The one thing found on the way was that an
unmigrated restart would have come up half-working and silent, since the old
table satisfied CREATE TABLE IF NOT EXISTS; the server now refuses to start
on a pre-melt keyring, and the first draft of that test proved the guard by
hanging.
The handoff followed the same afternoon. A note is ordinary authoring, so it
is not gated; a tail never mints, because a read-only verb's whole flag
surface must be incapable of a write; --for lets one character write into
another's log under its own name, which makes "a message board is this log
read by someone else" true before addressing exists. S16-handoff.kai
injects the character's recent notes at create. Its last mile was a power
cut: zorak lost power mid-verification, the unit restarted itself on the
right binary, and the only write in flight had never run, because the
advisory gate had escalated kj handoff note from an MCP seat. That
escalation became the next design: Amy asked for a health survey of the
safety hooks and then for layered allow and deny lists, and a Crush session
driven by qwen wrote docs/gate-policy-tuning.md — one evaluator over
builtin, global, per-type and learned tiers, with the user's explicit list
outranking everything shipped. signoff.md gave up its durable third to
docs/operating.md the same day, on the way to being retired by the log it
described.
Banto made the next missing part concrete. Its director instructions knew its
name through KJ_CHARACTER, but the context had no performer in played_by.
Amy asked to align AGENTS.md with the character system, then added, "I'm open
to completing some of that character work too." Explicit create-time --as
now records the performer without changing the requester. Director and handoff
scripts read that metadata, removing the environment bridge. Retirement can
therefore find these contexts through the existing relationship. Provider block
attribution remains a separate change because it changes block identity.
A runnable handoff example exposed why dispatcher tests alone were insufficient:
the kj adapter rebuilt split arguments with flags after positional text, turning
--for into note content and writing into the caller's log. Kaish 0.17.2 already
had the ordered-argument interface the backlog said was missing. Using it removed
the reconstruction code. The regression executes the published advice and checks
which log received the note, including names that require literal preservation.
Amy wanted MIDI presence and music to survive closing the 3D app, then
widened the task to PCM: "a realtime audio daemon we can put on various
machines that have audio hardware." Realtime meant an ordinary service with
Linux RT priority available, not a new scheduling architecture. The DJ, the
PCM scheduler and the MIDI workers moved into a reusable library; each node
connects over SSH and performs kernel cues; the kernel remains the sole
sequencer while nodes keep local beat phasors for display. Device-open
failure is explicit, RT-priority failure is a warning, and the hardware
lifetime no longer depends on a window. docs/audio-daemon.md.
Retrospective recording followed from "grab a happy accident real quick." MIDI input feeds bounded per-source RAM history independently of recording a context; a keep copies a complete recent window, protects it from eviction, and exports it through the existing SFTP path into CAS before the RAM is released. Source generations and explicit loss reject incomplete windows instead of claiming a complete recording.
The daemon's first fitness review settled two things. The app's opt-in in-process host was deleted and the last ALSA read left the app with it: the patch bay now reads the kernel's projected audio inventory, so a remote node's rack is visible from any app. And the live probe found moltar's clock 100.9 s behind zorak's with NTP on neither host, which the one-timebase doctrine had quietly assumed away. Amy: "I want to consider if we can be resilient to some clock skew, even lean into it a lil." The answer made the kernel's clock the timebase by definition: every node models its offset from the ping round trip and mints and ages stamps in the kernel's domain, and NTP became optional. The same morning explained a ghost peer registration — the bridge task's self-detach lived on a LocalSet that was dropped before it could run — and moved that cleanup onto the connection's own Drop, the only teardown that runs.
Amy read contrib/kj-expectations.toml and said something felt off without
being able to say what. The file was the authored half of a probe corpus:
clap reflection listed every kj leaf, and the TOML said for each whether
it mutated, whether its handler asked for confirmation, and which severity
the lfm2d scorer was expected to assign. Two of those three were facts about
the handler, written down beside it in a sidecar that a coverage test could
check for presence and never for truth. "Does this verb write?" had three
answers that never checked each other: the TOML, a two-token pass list in
kj/readonly.rs that could not see past the second word and so gave up
backend default show and block cat wholesale, and the handler body. The
gate-policy plan was about to add a fourth column to the same file and say
in the same breath that it must not agree with the third. And the one
column that was genuinely an expectation was the scorer's vocabulary, not
ours.
Amy: "the declaration whether something mutates should be required on every
kj verb, and can be accessed there directly as source of truth. I think at
one point I asked for a file export and it got over-generalized." And: "I
don't expect the classifier to ever learn kj vocabulary and we control the
code here." So the declaration moved onto the verb. Every subcommand enum
carries an exhaustive match to Effect::Read | Write | Destroy; a new
variant with no arm does not compile, which is the entire coverage
mechanism. Because the match is over the parsed value and not the leaf name,
an argument that changes the effect is visible to it: block cat is a read
and block cat --out is a write, in one arm, where the old table had to
refuse both. A root clap enum wraps the forty-two domains so the reflected
surface and the classified surface are one parse, and classify(argv) runs
no handler.
Three mechanisms fell out. The read-only module kept its five structural
conditions and lost its tables. The eight handler-local confirmation checks
became one gate in dispatch, and the decision to always latch a Destroy cost
two handlers their state-dependent messages, accepted. kj transport list
became a read on its own merits, undoing an earlier instruction to keep the
whole verb out of the pass list, and a ${VAR} in a typed slot fails
closed rather than earning a second, lenient parser. And the lfm2d fixture
left the kernel: severity derives from effect, the probe keeps only the
calibration deltas beside itself, and the word "expectation" is not
kaijutsu vocabulary any more.
Two lessons. Reflecting the keys of a hand audit mechanizes half of it and leaves the values to rot at the old rate; the fix is to put the judgment where the compiler can demand it. And the corpus, built to remove clauses that do not parse, had been scoring eighteen of them: synthesis had filled positionals and never required options. Classifying every leaf through its own synthesized clause found that on the first run.
We started with a source dossier: omp's assembled prompts and setup, then DeepSeek Harness, Codex, Crush, Goose, Hermes, Aider, Kaibo, and Polytoken's published documentation. The aim was to learn from their accumulated failures without importing their whole execution model. Complete Kaijutsu prompt bodies went into an offline comparison so Amy could read the old and proposed wording together. That comparison now reads current seeds against a pinned baseline; its source hashes keep regeneration stable after a commit, and its dark mode makes it usable alongside the rest of the working environment.
The reviews made the input mechanism matter more than the word choice. Distillation ignored exclusions, lost every block's tail after 2000 bytes, and asked models to preserve identifiers the formatter had never supplied. Compact forks also lost the coder's stance: their fork hooks did not run its create script. The fixes share hydration admission, carry source and tool evidence, bound complete turn groups, and retain chosen instruction blocks and a recent native turn. Briefing and continuation now have separate host-file instructions and source-context length controls. A stale source version fails compaction explicitly; a filtered copy cannot point at a child block it omitted.
Claude's review exposed a conflicting contract in the proposed universal base: working guidance about investigation and handoffs could interfere with a musician's ABC-only performance. Amy removed the universal premise: “I don't think kaijutsu should force any one prompt into every context type.” Shared prose is an ordinary rc file, selected by symlinks in coder and default. Other types keep their own instructions. There is no new prompt registry and no hidden global fallback. The shared file still ends with the encouragement Amy wanted to keep: 頑張(がんば)って!
docs/prompts.md owns the implemented contract and migration. The source
comparisons remain in docs/oss-comparisons.md; checked review mistakes became
evidence-status examples in the Kaibo proposal. Temporary drafts and review
plans were melted into those documents, with their earlier versions recoverable
from git and the private review archive.
The Kaibo implementation review also corrected our own explanation of timing: stored instruction blocks are read again before each turn, even though ordinary history edits wait for hydration. The distinction supports quick prompt iteration. A failed instruction read now stops preparation instead of silently running a context without its chosen prose. The review confirmed the main contracts and left metadata snapshot and fork-initialization fault handling as named follow-up work, with their limits recorded rather than called solved.
The same separation now governs the repository instructions. AGENTS.md keeps
work rules, essential invariants, and pointers; docs/writing.md owns the full
writing guide and terms. The character document separates current behavior from
its original inventory and rollout. This removes planned sheet fields from the
working contract and corrects the claim that all block edits wait for hydration.
The aim is fewer competing explanations, with correctness checked against code;
source length alone is not evidence of better model behavior.
Banto's seat was rotated onto the character work the morning after it
landed: build, force reseed, bounce, one kj context create --as banto,
then archive and rename. The first job through the new seat was the context
cleanup Amy had promised it: 421 no-model contexts, most of them mirrors of
dead MCP sessions from mid-August. Banto reconnoitered on its own before
acting, submitted the archive as one gated loop, and the shell gate held it.
The lead denied that first ask: the filter excluded rows aged now, and a
live seat reads 1m after a minute idle. The deny resumed banto with the
corrected filter already in its history, it re-verified the two live seats
were absent, and the second ask ran 412 archives. Two facts about driving a
seat came out of it. A user block written into a running turn's log does not
reach that turn; it lands on the next drive, or on the resume a deny triggers.
And kj context list reports "(no model)" for cast-resolved seats and handoff
logs too, so a cleanup rule needs cast and handoff exclusions, not just the
model column.
The P1 that made the tui feel slow turned out to be an fsync per word. An
Opus lane measured the live kernel while a probe context streamed: about
200 KB of block-layer writes per delta, 428 MB for a 10 KB turn. Each delta
was two autocommits on one connection, the oplog insert and the context's
activity touch, and init_connection had never set synchronous, so SQLite
ran at FULL and fsynced both. On btrfs with DUP metadata each fsync writes
its metadata twice. A control on the same volume with the same shape fell
from 175 KB to 11 KB per delta at NORMAL with identical syscall bytes, which
put the cost in the filesystem's fsync path rather than the payload. Amy
took the durability trade with no UPS: "go with NORMAL, seems fine." Under
WAL, NORMAL stays consistent through a process crash and only a power loss
can drop the last commits. The two writes now share one transaction.
Measured live after the bounce, a prose-only probe wrote 13 to 15 KB per
delta with syscall bytes equal to block-layer bytes: the amplification is
gone, and what is left is the payload of two WAL pages and a log line per
token.
A smaller storm rode along in the logs. The llm.turn span recorded its
usage fields from every Done event, and tracing-subscriber's fmt layer
appends each record to a span's formatted fields instead of replacing it, so
by iteration ten every log line under the span carried ten usage blocks. A
drop guard now records the final call's numbers once at turn exit. Its test
taught a lesson about tracing in a large test binary: a callsite caches its
interest against the default subscriber of whichever thread hits it first,
and under a full suite that is a sibling test with no subscriber, so a
thread-local test subscriber saw no spans at all. The test installs a global
default and tells spans apart by the thread that created them.
With the unit at RUST_LOG=info, INFO still read like a debug log: a line
per recovered context at boot, four per SSH connection, a reconnecting
client's hang-up as ERROR Session error: IO(…), the shell command text
twice per command, the audio inventory every ten seconds, and eleven lines
per turn iteration with tool params in full. The diet keeps what an
operator reads a turn by: the stream start with context and model, each
iteration's completion with stop reason and usage, interrupts,
cancellations, and tool refusals; one line per connection and one summary
per boot. The kernel's own disk writes did not move with the log level,
which put the log cost on journald where it belonged.
The ask link went live in the afternoon, written by a DeepSeek coder driven
from the tui, and a probe coder confirmed it: an allow filled the Waiting
pair with the real output, authored no second pair, and the next turn
hydrated clean. Asked for that output, the model then insisted nothing had
run. Two things in the durable log said so. The gate.pending error child
authored beside a Waiting result outlived the ask, because the fill touches
only the linked pair, and error children sorted after every later block of
the turn, because the loop re-anchored on the result block rather than the
child it had just hung off it. A pending ask is not an error, so the
mapping now withholds the payload for a Waiting dispatch and no child is
authored; and both dispatch paths anchor the next block past an error
child when one is. Four tests pin it, two of them reproducing the exact
#18 #19 #17 order from the probe before the fix.
Saturday opened on a five-item list, smallest first, and the first three
landed by mid-morning. The approval chain from Friday had one piece left:
an allow filled a model's own Waiting pair, but the driver's rule dated
from when only a human's shell pair was linked, so a linked pair "told
nobody" and the coder read its own stale "waiting on a human" text as the
last word. The design doc already had the answer: a pair whose turn ended
at the gate gets a seed block and a turn request. The ledger now records
who owns a linked pair, turn from the model tool path and session from
the interactive shell, and a turn-owned fill wakes the model exactly as a
driver-authored one does. Amy confirmed the consequence: "kj ledger allow
should auto-resume", with a staleness check to follow, since an approval
answered hours later revives a turn whose prompt cache is long gone and
that costs real money off a subscription. The seed is written even while
a turn is running, because the fill is an in-place edit a cached mailbox
never re-reads; only the turn request waits.
Two smaller ones rode along. The activity stamp on every streamed delta
rewrote the contexts page in the WAL to move a millisecond timestamp the
readers show in minutes; it is throttled to once a second per context, to
be measured on the next bounce. And kj rc list, the one surface that
compares live rc against the shipped seed, walked lib/hooks and then
dropped every entry because a hook body is not a lifecycle script path.
That was how Thursday's stale lfm2d hook stayed invisible for a day. Hook
bodies now list and show beside the scripts.
The morning also had two of Amy's sessions in one checkout. A second
session began an approval-identity change in the same ledger files the
pair-owner lane had just finished in, building on the uncommitted work and
re-indexing its row reads. Separating the hunks by hand would have broken
the other session mid-edit, so the lead rebuilt its own versions of the
five shared files from HEAD plus the lane's known edits, staged them with
hash-object and update-index without touching the working tree, and
proved the staged tree on a detached worktree with its own target
directory before committing. The other session's diff against the new
HEAD is then only its own change. Four coder-stance e2e tests turned out
to have been red since the September 10 prompt rework, pinning phrases
that no longer ship; they pin the current tier lines now.
The next approval problem was identity. Amy asked why the TUI could not let her answer a coder in the context she was already reading: "When I connect via app or tui, I should appear as myself." The ledger compared contexts, the TUI chose a second one to answer, and model tools still carried the requester's principal. Replacing only the comparison would have made Amy and her coder indistinguishable. Her second scenario supplied the reviewer: "the coders would come back to the lead model which can evaluate & approave"
Invocations now carry requester, performing character, and assigned reviewer.
A model turn resolves its performer from played_by and its reviewer from
the context, then keeps them through tools, shell commands, hooks, and
replay. The requester stays the capability and redemption identity. Asks
snapshot the three identities; reviewer decisions work in the raising
context, and switching contexts never makes the performer eligible. The
reviewer can pass an ask onward, and its requester or performer can cancel
it. A performer change revokes learned session rules and prevents replay of
a linked approval raised by the old performer. Decisions recheck reviewer
eligibility under the ledger transaction, so escalation cannot race that
check.
The TUI's second-context workaround is gone. It displays the connected
character and the ask's identities, implements full detail, and keeps
unshown asks available for later presentation. ACP uses the same reviewer
rule and sends decision failures into the client conversation. New ACP
sessions select their performer explicitly with --character; creation
uses kj context create --as so identity is present when rc runs.
The regression tests exposed the useful mistakes: the MCP RPC entry point lost the reviewer, shell completion hooks rebuilt a requester-only caller, and recursive context queries still read depth from the column now holding the reviewer. The wire test exercises two real credentials: Amy answers the coder from the same context, then the coder is refused from another one. Terra agents implemented the ledger, invocation propagation, and clients; Astra integrated and reviewed the resulting paths. Amy authorized sending source through Kaibo for its independent review. It found that ACP treated a prompt timeout as the reviewer denying the ask. Timeouts and cancelled prompts now leave the durable ask pending; only a selected decision records a verdict. Review also separated the TUI's pending snapshot from its shown card set, preserving the next ask while another card is open.
Fable's review had also found that settling a denied model tool pair did not tell its turn. Denials and cancellations now settle the pair, evict the cached conversation, and resume it with an explicit no-run receipt. A failed shell materialization tells the waiting model too; connected session pairs continue to receive their result through the block feed. Wire regressions cover both denial and cancellation without executing the command.
The local deployment verified the same contract with a DeepSeek Flash coder.
Its ask carried coder as performer and kaijutsu-lead as requester and
reviewer. The coder stopped; the lead approved the exact echo; its output
filled once and the coder resumed without a second tool call. The probe was
archived and its scoped hook removed. Banto's ROOT now names Amy as reviewer.
Amy then asked to look at the Bevy app and to "double check we are attaching
the character info to otel spans where appropriate". The app already knew
its SSH identity but used a random principal to find and hide its own draft.
That principal now comes from whoami, is cleared while disconnected, and
is refreshed on reconnect. Explicit key selectors make the credential choice
available at launch. Submission errors reach the UI, and failed text stays
with its original context and character. The app still needs a dedicated
ledger surface; its shell is the current approval interface.
Execution traces now distinguish requester, performer, reviewer, and deciding actor. Model turns also carry the character names resolved at turn start. Tool spans take IDs from the invocation, while approval replay takes them from the durable ask. The trace audit found two contributing factors beyond missing attributes: RPC injection read OTel's thread-local context instead of the active tracing span, and the MCP server-call span held an entered guard across suspension. Injection now uses the tracing span's OTel context, and the future carries its span while polled. Character IDs remain off metric attributes.
Kaibo's GLM-5.3 review confirmed propagation and found a missed-status path
in the app: a replacement actor could connect before the UI subscribed, so
the old identity survived until the new whoami arrived. Connection status
now identifies its actor and transport; lag recovery asks for the current
state, and actor exit clears authenticated identity. The full headless app
suite and client suite passed, with focused capture tests for exported trace
parents, concurrent tool actors, and approval replay. GUI and macOS execution
remain unverified; this followup did not restart Amy's running processes.
Amy clarified the default: "Amy approves by default; delegate approval explicitly", then "Allow an explicit director-wide delegation". A creator's role as director is now separate from reviewer authority. Resolution prefers an explicit context override, then a grant for the director, then the configured default reviewer. A grant may name the director or a separate adjudicator; it neither creates that character nor schedules a review turn. Amy controls grants and routing changes, and can explicitly reclaim a pending ask from an unavailable reviewer. The audit keeps the caller separate from the old and new reviewer.
Revocation exposed two timing boundaries. A running turn may retain an old reviewer, so the gate resolves the current assignment while inserting its ask. An assignment command may await configuration after checking authority, so its transaction checks authority again before committing. Pending asks must settle or cancel before their context's routing changes. The migration clears old automatic reviewer assignments with the new director column in one transaction; an injected reset failure verifies rollback and reopening.
The SSH/RPC regression uses Amy, lead, coder, and judge credentials. It checks Amy-default review, refused self-grants, judge approval after explicit delegation, Amy reclaim, pending-ask refusal on revoke, and Amy routing after revoke. Kaibo's DeepSeek review found that broken reviewer metadata also hid the context information needed for repair, and that eager default resolution overrode valid explicit assignments. Inspection now exposes the error; invalid default configuration clears its cached authority without replacing a valid explicit reviewer.