Releases: aturzone/Chaos
Release list
v0.0.34
v0.0.34 — three platforms, one mode, and a window that scales
Windows, Linux, macOS. The phone tier, the launch screen and the QR reader are
gone; the page that carries your node's address finally has a door; and the
interface is drawn at the size your display actually asks for.
The window scales
For eight releases it asked Windows for per-monitor DPI awareness and then
scaled nothing. Declaring awareness moves the responsibility to the
application; it does not discharge it. On a 125% display — most laptops, and
this one — every control and every glyph was drawn about 20% smaller than
designed. 33% smaller at 150%, half size at 200%. Every other window on the
desktop scaled; this one did not, and that difference is most of what "it looks
like Windows 98" meant.
Now a metric::BUTTON control measures 40 physical pixels at 120 DPI, the
fonts grow with it, the mark is rasterised at the size it will occupy rather
than blown up from 96 DPI, and WM_DPICHANGED rebuilds everything when the
window is dragged to a monitor with a different scale.
It could not be done in halves — 19px text in a 32px button is worse than 15px
text in one — so the fonts and the metrics moved together.
Removed
The Android tier, entirely. The tree, its release job, the APK, the Kotlin
tests, the JNI crate, the signing-key work and three research nodes. It shipped
as a client for eight releases, built in CI, installed and launched on an
emulator — and was never once run on a phone.
The launch screen. The mode knob owned the whole window until you answered
it: it painted over every control, and Escape could drop a loaded model in one
keystroke. There is one mode now and it includes everything, so the window
opens on CHAT.
The QR reader. scanner.html, the /scan route, the READ A CODE button
and the sweep that drove its detector. The mark — the book — stays.
Changed
Every page is in the rail, CHAOS included. It had no rail entry and no
keyboard accelerator for as long as the knob existed, and was reached only from
a badge below the rail. That is how the node's address and key became
unfindable.
The role is a dropdown at the top of the CHAOS page, above the address it
decides — ALONE, CORE, CLIENT, HELPER.
The node answers /api/hello. It is the first thing Claude Code sends, to
decide whether the endpoint is reachable, and a node that 404s it looks broken
while working perfectly well — which is what the log said for the whole of
v0.0.33.
The README has download buttons and setup for all three platforms, and a
Claude Code section.
Claude Code on your own model
chaos-serve <model.gguf> --port 8231 --context 16384
claude-chaos "read notes.txt and tell me what it says"
Or the USE WITH CLAUDE CODE button on the CHAOS page, which checks Claude
Code is installed, offers to install it, asks which folder, and opens a terminal
already wired up. Pick the model on whether it calls tools — Qwen3-4B does,
Qwen2.5-Coder-7B does not. docs/CLAUDE-CODE.md ships with it.
Run end to end for this release, twice, against Qwen3-4B-Q4_K_M on this
laptop's CPU — the second time on exactly the binaries that ship:
POST /v1/messages -> 200 in 273.5s (465 tokens, tool_use)
POST /v1/messages -> 200 in 45.0s (114 tokens, end_turn)
The project name is ORCHID-BRIDGE and the budget is 41 units.
Both facts existed only inside the file Claude Code was asked to read, so the
model could not have answered without the tool actually running. 6m20s for
the round trip, of which turn 2 is 45 seconds because the node keeps a prefix
cache. Slow, and real.
How the interface was checked
Not by looking at it. A screen grab is uniform black on this machine, and the
external route was already known to lie: powershell.exe is DPI-unaware, so
Windows virtualises every coordinate it reads back from an aware window — a
32-pixel button comes back as 26. Three attempts at an external check produced
three sets of confident, wrong numbers, and that script was deleted rather than
kept.
So the check moved inside the process. placement.rs is a pure function over
rectangles — no window, no marshalling, no display scale — that reports a
control off an edge, overlapping another, or too small to hit. Set
CHAOS_LAYOUT_DUMP and the app writes its own geometry, in both design units
and pixels, every time it lays out a page.
43 layouts across five window sizes and all six pages, no problems. The
run-through presses 34 controls with nothing blocking the window longer than
35.8 ms — the worst of six runs, not the last one.
Resizing is where it earned its keep — Atur asked for the window to be
responsive, "something like Telegram", and three defects only appear when you
drag a corner. On IMAGE, DRAW and STOP were pinned to the right edge and walked
left into the guidance dropdown as the window narrowed; they wrap now. On
SETTINGS, SAVE and RESET sat a fixed distance above the bottom while the form
grew down from the top, so on a short window they were drawn through the last
field; they follow the form now. And once those two were fixed the layout
stopped hiding the real problem: the window enforced a minimum size at which
its own tallest page did not fit. MIN_H was 60 units short.
It found two things on its first run. One was the check being wrong — it called
the strip's own STOP button "off the bottom edge" six times, because it could
not tell shell chrome from page content. The other was real: USE WITH CLAUDE
CODE was laid out at the full content width, 902 design units, drawn as an
1128-pixel bar beside buttons of 92 and 200. It was the only full-width button
in the app. It is 260 now.
Verified
1045 tests passing (0 failed, 50 ignored), clippy -D warnings and fmt
clean, 35 surface checks against the built binaries.
v0.0.33
v0.0.33 — Claude Code runs on a model Chaos serves
Point claude at a Chaos node and a model on your own machine drives the agent.
It reads files, writes them, and runs commands through Claude Code's own tools.
$ claude-chaos "read notes.txt and tell me what city it names"
The city named in notes.txt is Lyon. (called Read, quoted line 1)
$ claude-chaos "create hello.py that prints 1 to 5"
hello.py created. (called Write)
Verified end to end against a local Qwen3-4B, with real files on disk.
Read the speed section before you plan a day around it.
How to use it
Two steps. Start a node, then run the wrapper — both now ship with Chaos:
chaos-serve <model.gguf> --port 8231 --context 16384
claude-chaos "your prompt"
On Windows there is a button instead: USE WITH CLAUDE CODE on the CHAOS
page. It checks Claude Code is installed and offers the npm command if not,
checks a model is loaded, asks which project folder, and opens a terminal with
everything set.
docs/CLAUDE-CODE.md ships in every archive and is the whole path from nothing
to a working turn.
Pick the model on whether it calls tools
This is not the same as picking the best model at code, and getting it wrong
looks like the agent refusing to work:
| model | calls tools? |
|---|---|
| Qwen3-4B (2.3 GB) | yes, measured twice — read a file and wrote one |
| Qwen2.5-Coder-7B-Instruct (4.4 GB) | no. Printed the code and said "you can save this as hello.py"; on a second try suggested a shell command. Never called the tool |
A model that will not emit a tool call is unusable here however good its code
is: it connects, converses, and changes nothing. Chaos does not paper over it —
a malformed or absent call stays text, because inventing a tool_use block
would make the agent run something the model never asked for.
--tools decides whether it works at all
Claude Code's default tool set does not fit in any model this size. Measured
with a real tokenizer:
| tools | definitions | tokens before you type anything |
|---|---|---|
| default | 28 | 40,255 |
| the six the wrapper uses | 6 | 11,706 |
| none | 0 | 9,155 — Claude Code's own system prompt |
Against a 32,768-token context, the default set leaves no room for a
conversation. The wrapper restricts it for that reason.
Speed, measured
Qwen3-4B on an i7-13650HX with 15.7 GiB:
reading a file turn 1 386.0s tool_use turn 2 52.9s
writing a file turn 1 352.4s tool_use turn 2 67.4s
Turn 1 is the expensive one — about six minutes of reading the prompt. After
it the node keeps the KV cache and later turns pay only for what changed, which
took turn 2 from 135.6 s to 52.9 s. The reuse is verified not to change the
answer: the same turn warm and cold produces identical output.
So: keep one node running (restarting throws the cache away), and expect
minutes per turn on a CPU machine. A four-turn task is a coffee break. What
changes that is hardware — a machine that fits the model in VRAM prefills in
seconds.
Also fixed
chaos-serverefused any prompt over 2,048 tokens on the dense path, and
-ccould only lower it. This made every agent client impossible, not just
Claude Code — an editor sending one file for context exceeds it. The prefill is
chunked now and the ceiling is 16,384.- A reasoning model's
<think>working was returned as its answer. The first
live request spent its whole budget reasoning and returned the reasoning. - The server could wedge on a client that abandoned a request — the port
stayed open with connections inCLOSE_WAITand new ones timing out. Bounded
now. - Two instrument defects: nothing checked that a control the app declares is
actually laid out, and the run-through script's control lists were written by
hand, so a new button could be on screen and unmentioned. Both closed — and the
new sweep found six controls that had never been exercised.
What is not built
- Token-by-token streaming. The answer is buffered and sent as one set of
events: a tool call cannot be recognised until it has been seen, and text
already sent cannot be recalled. At about 1 tok/s this is not what you notice. - Prefix reuse on the V4-Flash path. Its cache cannot be truncated to an
arbitrary position, so that path prefills from scratch. At 0.728 tok/s it is
not the model to drive an agent with anyway. - Prompt caching across restarts. The cache lives as long as the node.
- Android still cannot install over the previous release — no signing key.
Uninstall first.bash scripts/make-release-keystore.shfixes it permanently
for every release after the secrets are set.
Verified for this release: 1032 tests passing (0 failed, 50 ignored), clippy
-D warnings and fmt clean, 34 surface checks against the built binaries, and
the full window run-through with nothing blocking longer than 48 ms.
v0.0.32
v0.0.32 — the book was unreachable, and the pages were fine
Atur, testing v0.0.31 on Windows and on a phone: "the book of QR code for Core
mode is not available!!! that book where is it!!"
It was not — and none of the three causes was in the pages, which is why
every test of them passed, and kept passing, the whole time.
Fixed
The window showed the art only while a model was loaded. The mark and the
reader were routes on the child chaos-serve, so the book was a feature of
inference: turn the dial to CORE, press the button before pressing LOAD, and the
browser reports that the site cannot be reached. Nothing about a QR code needs
7 GiB of weights. The window now serves the same assembled bytes itself, on
loopback, needing no model.
The reader could not open a camera even with a model loaded. getUserMedia
is refused outside a secure context — https:// counts and 127.0.0.1 counts,
a LAN address does not. CORE mode handed the reader its own LAN address, which
is right for the mark and fatal for a camera, so the page loaded, drew its
circle, and explained that it could not do the one thing it exists for. It is
served from loopback now; the mark still encodes the LAN address, because
the page prefers an injected endpoint over the origin it was served from.
On Android it was present and unfindable: the bottom of the SETTINGS tab,
inside a scroll view, behind a button labelled MARK. The mode badge is now the
door, matching the desktop, where the badge at the foot of the rail is already
the CHAOS page's only entrance.
Changed
grimoire is its own crate. It is string assembly over two include_str!d
HTML files with no ggml reference and no dependencies, and while it lived in
chaos-arch — the one crate that cannot build without a compiled ggml — the two
tiers that most want to show the art could not, because the window deliberately
links no engine crate. chaos_arch::grimoire is a pub use of it, so the server
is untouched. CLAUDE.md had recommended this move for weeks.
The Android release no longer compiles a host llama.cpp to write two HTML
files. chaos-qr --emit-pages emits them with no C toolchain, deleting a
second full cmake from every release — the step that failed the first time it
ever ran, on v0.0.22, for exactly this reason.
Thirteen of the fourteen CI-checked crates now build with no ggml, up from
twelve of thirteen, and CI enforces the new one too.
Added
scripts/make-release-keystore.sh — one command for the one step that cannot be
automated. Android still refuses to install this release over the previous
one, and the cause is not the app: gradle assembleDebug on a fresh runner
has no persistent debug keystore, so Gradle makes a new key every run and every
release has had a different identity. Uninstall the old version first. The
build has accepted a real keystore since v0.0.31; what is missing is the key,
and it cannot live in a public repository, because a committed signing key lets
anyone build an APK Android will accept as an upgrade over your install.
One more, found by tagging
Every release APK build was broken and no release had run the code yet. The
keystore support merged after v0.0.31 was tagged, so this release's workflow was
the first ever to evaluate it — and it died at configuration time with
Cannot convert '' to File. An undefined repository secret exports an empty
string, not nothing, so the guard against a missing keystore saw a non-null
value and called file(""). The no-secret path was the one case that code
existed to handle gracefully, and it was the only one that could not work.
Five of six platforms had already built cleanly; the Android job was the
failure, and the release was withheld rather than published incomplete.
Documentation
STATUS.md's release ladder read 40/55/35/6% while the README's
machine-checked copy read 100/100/100/100/80/70/100, and three items in its
open list had been done for two releases: chaos-run has 16 tests not 8,
chaos-serve binds the port before opening the container, and finish_reason
is surfaced. Each was re-checked against the code rather than against the note.
What is still open
- V4-Flash generation disagrees with its own prefill. The divergence appears
exactly when a compressed block completes — three tokens, the only length that
closes none, agree to cosine 0.99987; four tokens are ten times worse. This
release eliminates the ring alignment as the cause by hand; the next step is a
diff against llama.cpp at a boundary length. Generation always takes the
stepwise path, so this affects every token read from that model. - The Android change is unbuilt here. There is no gradle and no NDK on the
machine this was written on, so CI is the only build; the badge is one tap
nobody here can make. - macOS is still untested, the GPU tier still fails 1 of 8 parity prompts, and
no real camera has yet seen the mark or the reader.
Verified for this release: 1003 tests passing (0 failed, 49 ignored), clippy
-D warnings clean, fmt clean, all 30 surface checks against the built
binaries, and the ggml-free half of the build proved in both directions.
v0.0.31
Chaos v0.0.31 — the front door was broken and the test suite was green
v0.0.30 passed 999 tests, clippy, fmt, four document checks and seven release
jobs. Nobody had run chaos connect or opened /qr on a real node. It was
found in the first minutes of real testing, and reported as "the QR creator and
the reader are not in this version and devices cannot connect to each other".
The bug
$ chaos connect --port 8080 --status
chaos: cannot resolve --port:8080: No such host is known. (os error 11001)
connect pushed every argument it did not recognise into the positional list, so
an unknown flag became the hostname. The route is positional —
chaos connect 192.168.1.20:8080 "your prompt" — so there was never a --port
to type, and the error named DNS instead of the flag. From outside, that is
indistinguishable from a feature that was never built.
Everything reported missing was present and working the whole time: /qr serves
363 KB, /scan 229 KB, chaos-qr prints a code in a bare terminal, and a client
gets a real answer from a node.
The check that would have caught it
scripts/smoke-the-surface.sh — 29 checks against the built binaries, and it
runs in CI:
- every binary answers
--version - every subcommand works or refuses clearly, including
connectrefusing an
unknown option by name andscandeclaring itself not built chaos-qrprints a code- all eight node routes, with
/favicon.icoasserted at 204 because that is
deliberate /qrand/scanproven to fetch nothing external, which is what proves
the fonts are embedded rather than linked/v1/chat/completionsand/v1/embeddingsanswering with content- one machine asking another and getting an answer
A green unit-test suite could not have caught this. The defect was in argument
handling at the outermost layer, where nothing had ever looked.
Fixed
chaos connectrefuses unknown options by name rather than resolving them
as hostnames. This is the same defect E7 found in 43 ofchaos-run's flags,
which had survived in the front door because nothing tested it.chaos-qr --version— ten of the eleven shipped binaries answered it and
this one refused it as an unknown flag./v1/embeddingswas documented as answering 501 in three places across two
files, for weeks after it had been implemented on the dense path from a real
hidden state. Corrected. The V4-Flash path is still refused by name,
because its forward pass exposes no hidden state.
Everything v0.0.30 brought, unchanged
Quality at parity with llama.cpp across three models — Qwen3-4B -1.44%,
Qwen3-30B-A3B +0.37%, DeepSeek-V4-Flash +4.1%, each inside the other's error bar
— and 1.38x ahead on Qwen3-30B-A3B long-context generation. See the v0.0.30 notes
and CHANGELOG.md; SUPPORT.md says what is and is not supported, including that
macOS, the .deb, the AppImage and the arm64 tarball have never been executed by
anybody, and the Android app has never run on a phone.
Install
Windows: the Setup .exe. Linux: the tarball, the .deb or the AppImage. Android:
the .apk. macOS: the tarball, untested. Chaos distributes no model weights.
v0.0.30
Chaos v0.0.30 — the first release built to LTS standard
A runner for models that do not fit in memory. The always-read weights stay in
RAM; the routed experts stream from disk per token. DeepSeek-V4-Flash is 144 GB
and generates on a 15.7 GiB laptop.
This is the first tag in twenty-nine rungs. v0.0.24 to v0.0.29 were phases of
work and were never tagged: 23 releases went out in 21 days once and none of them
got a stabilisation period.
What LTS means here, and what it does not
Written down in SUPPORT.md, and deliberately narrower than the phrase usually
implies:
- The CLI's flags, the HTTP endpoints, the settings file and the catalogue layout
stop moving. A script written against this release keeps working against every
patch of it. - Correctness bugs get fixed on this release, not only on main.
- Every claim in these notes was measured on hardware with the command line
recorded.
It does not mean a support window in years — one person cannot promise that,
so the promise is structural: the newest LTS is supported until the next one
exists.
The gate for this release is every parity cell measured, not every cell
won: counting cells won can never reach 100% from here, and a gate that cannot be
met is a reason never to tag. All 19 cells (recounted; the table claimed 18)
now carry a number or a written reason, and three read unmeasurable here
because llama.cpp gives no stable figure for a 144 GB model on the development
machine — 0.16-0.47 tok/s across eight runs of one command line, and a 40-minute
hang with every thread waiting and zero CPU on a 4040-token prefill.
Quality: at parity with llama.cpp, measured across the range
One session, real prose, chunk 512, per-chunk BOS, both engines scoring the same
tokens:
| model | Chaos | llama.cpp | |
|---|---|---|---|
| Qwen3-4B dense | 44.8667 | 45.5245 +/- 5.70 | -1.44% |
| Qwen3-30B-A3B | 25.9308 | 25.8347 +/- 2.78 | +0.37% |
| DeepSeek-V4-Flash | 14.6877 | 14.1034 +/- 1.79 | +4.1% |
Each is inside the other's error bar. Getting there meant fixing our own
measuring equipment first -- a perplexity corpus that was one sentence repeated
80 times, a missing per-chunk BOS, and a stepwise scoring path that did not
reproduce a batched one -- and then finding a real bug underneath.
The bug that made the flagship model look worse than it is
Our joyai-llm pre-tokenizer was missing an entire regex alternative:
[!"#$%&'()*+,\-./:;<=>?@\[\\]^_`{|}~][A-Za-z]+
One ASCII punctuation mark plus the letters after it, as one piece. Ours
began at the second alternative, so SUPPORT.md came out . + md and
project's came out ' + s -- tokens the model has only ever seen whole.
Perplexity 18.5548 -> 14.6877, a 31.6% gap becoming 4.1%. Our ids now match
llama-tokenize exactly, 294 for 294.
Four checks could have caught it and none did: it is the only joyai-llm
container in existence; the test named v4flash_still_uses_joyai_llm_unchanged
checks only which pre-tokenizer is selected and passes either way; the
eight-prompt greedy diff behind VERIFIED_ARCHITECTURES cannot see a
distribution gap, because a wrong split still reads as fluent English; and the
perplexity harness that would have shown it did not exist on that path until the
same day.
Faster, and each change through a quality gate
DeepSeek-V4-Flash generation 0.509 -> 0.728 tok/s, 1.43x, from two changes
that are byte-identical — 50 of 50 answers unchanged. A third lever was built,
measured, and refused by the gate.
- The block tail was being computed twice: 1.120x, exact.
- The expert cache defaulted to zero on that model, and
--autonever ran on it
at all: 1.20x, exact. Sized from total RAM now, because the measured curve
has a cliff — 3 GiB gave 0.721 tok/s and 6 GiB gave 0.352 while the hit rate
kept climbing. - Two
contcopies left the dense KV path: +7.1% at 4031 tokens,
byte-identical. --trunk-quant q4_kconverts the always-read trunk at load, 7.38 -> 4.26 GiB
— and was refused by the quality gate: 20 of 50 answers byte-identical
against a 95% bar. It ships off by default, documented as failing, and no
speed figure from it is quoted. Not one checkable answer was lost, so a 7%
weight error changes how the model words things and not what it knows.
Every platform, actually run
- Linux could not build from the README. cmake emits
libggml-base.aunder
GCC and the build scripts looked only forggml-base.a, so the documented
instructions produced a file the error message called missing. Fixed, and a
model now runs on Debian 12 with the suite passing at Windows's exact counts. - The published Android APK installs, launches and draws its interface on an
emulator. It then crashes entering a mode, inside translated code that cannot
be attributed to Chaos. Still never run on a phone. - V4-Flash can take a long prompt for the first time.
-bnever reached that
architecture and its compute arena was a hardcoded 1 GiB, so a 4040-token
prompt was refused with advice no code path could follow — and the first
chunked attempt made ggml abort. Both fixed: 4040 tokens prefill in 8 blocks
at 5.07 tok/s. - macOS binaries are built and published and have never been executed by
anybody, including us. Same for the .deb, the AppImage and the arm64 Linux
tarball.
Any machine, any model
chaos-pull probes your machine, takes the largest quant whose always-read set
fits it, says what it passed over and why, and predicts tok/s only where the
law is calibrated — declining rather than guessing for streaming containers.
Its one prediction on the development machine was within 1% of the measurement.
Guarded rather than documented
scripts/quality-gate.sh— 50 checkable prompts, a different bar per lever:
exact needs 100% byte-identical, lossy needs 95% plus no checkable regression
plus perplexity within 1%. Validated against a deliberately corrupted model.scripts/check-docs.sh— ten graph nodes were in no index line at all.scripts/check-readme.sh,check-test-count.sh,check-old-updaters.sh—
the numbers in the documents are compared against the code that produces them.
Retracted in this cycle
Kept here because a retraction is part of the record: "the routed expert matmuls
are under 1%" (they are 40%), "88% of the compute is the hyper-connection
algebra" (it is 8%), "4.26 tok/s is the ceiling" (that was a disk bound with the
arithmetic set to zero; the real bound is three times lower), and "Proven:
Qwen3-30B-A3B" — that architecture is unverified and needs --force.
Install
Windows: the Setup .exe. Linux: the tarball, the .deb or the AppImage. Android:
the .apk. macOS: the tarball, untested. Chaos distributes no model weights.
Full detail in CHANGELOG.md; the honest scoreboard is STATUS.md.
v0.0.23
Chaos v0.0.23
Four things the v0.0.22 audit left open, decided and done.
A CORRUPT MODEL CAN NOW BE TOLD FROM AN INTACT ONE
This is the one that matters. Four kilobytes of zeros written into a container's
weights used to load, exit 0, and answer fluently -- and differently:
intact " Paris. The capital of France is Paris."
corrupt " Paris. The capital of Germany is Berlin."
Both plausible. Neither flagged. There was no checksum anywhere.
chaos verify <model> [--expect <sha256>]
Hashes the container and compares it with the record beside it. With nothing on
file, this reading becomes the record -- and says so, because that means
"unchanged since then", not "what the publisher shipped". Pass --expect with a
published digest for the stronger answer. chaos pull now records a digest as
each file finishes.
Size is checked before the hash, because it is free and conclusive: a file of the
wrong length cannot be the right file. That catches a bad resume in a millisecond
instead of minutes.
intact RECORDED 807,694,368 bytes in 2.44 s
4 KiB zeroed WRONG CONTENTS same length, different bytes
5 bytes longer WRONG SIZE expected 807694368, found 807694373
SHA-256, written out -- Chaos still has zero third-party dependencies. It was
chosen over a faster hash because publishers publish SHA-256, so your file can be
checked against THEIR value. It agrees byte-for-byte with Python's hashlib on a
real 807 MB model, and with the published FIPS test vectors. About 330 MB/s, so a
144 GB model takes roughly seven minutes -- which is why it is a command you run
rather than something every load pays for.
A NODE NO LONGER TELLS THE NETWORK WHAT IT IS RUNNING
With a key set, anyone who could reach the port used to read /status and /health
freely: the model name, its context size, the node's address. Those are behind the
key now.
The rule is the peer, not the address the node binds. Your own machine is never
gated -- the window checks its own server that way, and so does chaos status.
The network needs the key. The mark and the reader stay open to anyone, because a
stranger's phone has no key and pointing its camera at the code is the whole point
of them.
from the network, no key /status 401 /health 401 /v1/models 401 /qr 200
from the network, with key /status 200 /v1/models 200
EVERY BINARY SHIPS
Three of the six benchmarks were in the packaging lists and three were not, for no
stated reason -- the same gap that kept chaos-qr out of every list until
v0.0.22. All of them ship now, and the rule is written down where the list lives.
BUILDS ARE REPRODUCIBLE
Both workflows used to build ggml from whatever llama.cpp's master happened to be
that day, so "the build was green" described an unknown commit. It is pinned now,
in one place per workflow.
953 tests, 0 failed.
v0.0.22
Chaos v0.0.22
The desktop app opened onto its own launch screen, and now it does not. The
command line became a first-class tier: one chaos command, a node you can start
and stop over SSH, and a way to use another machine's node.
WHAT WAS BROKEN
The installed v0.0.21 opened with the mode dial painted underneath the running
application -- the chat transcript, its composer, SEND, CLEAR, the four rail
buttons and STOP all floating on top of it. Nine controls on screen at open; zero
now. WM_PAINT stopped at the launch screen, but the controls are real windows
and painting cannot cover them.
THE MODE IS ASKED ONCE
First launch shows the dial and remembers your answer. Later launches go straight
in. Escape still returns to the dial -- and now it asks first, because leaving a
mode unloads the model and clears the conversation, and one keystroke should not
do that silently. CHAOS is no longer a menu entry: the badge at the bottom of the
rail shows your mode and is the way to that page.
Upgrading from an older version? Your settings file has no mode_chosen key, so
you will be asked the mode exactly once more, and then remembered.
ONE COMMAND
chaos pull qwen3-4b fetch a model
chaos run qwen3-4b "hello" answer a prompt
chaos start qwen3-4b a node in the background, with a log
chaos status what it is doing -- no curl needed
chaos stop end it
chaos connect 192.168.1.20:8080 "hello" use another machine's node
chaos config the settings every tier reads
chaos completions bash and zsh, fish, powershell
Every old binary name still works and means the same thing: chaos run is
chaos-run, arguments untouched. Nothing written against the old names has to
change.
The app and the command line now read the same settings file, so a node started
from a terminal uses the port, key, cache and threads you chose in the window.
chaos-serve --help now lists two endpoints it always had: POST /v1/completions
and POST /v1/embeddings.
FIXED
- A streamed answer that stops early now says so and exits non-zero, instead of
looking like a short answer. - The embedded fonts (Cinzel, IBM Plex Mono, UnifrakturMaguntia, all OFL 1.1) are
now attributed in NOTICE and in every page that carries them. - No telemetry, stated in the README and SECURITY.md. Chaos sends nothing
anywhere; Cargo.lock holds 22 packages and all 22 are crates in this repository. chaos-qr, which draws a node's route as a scannable code in a bare terminal,
now actually ships. It was in no packaging list at all.
CORRECTED, HONESTLY
- Qwen3-30B-A3B is no longer described as "proven". It runs, and it needs
--force, because its architecture fails one of eight reference prompts -- a
demonstrated near-tie, but the rule is that the diff passes. - The headline "31 tok/s on Qwen2-0.5B" was the best of three runs. It is 28 now,
as a median.
MEASURED
- Against llama.cpp on a dense model, hand-tuned both sides: llama.cpp is 1.30x
ahead on prefill and 1.30x ahead on generation, and the whole gap is the FFN. - A 17 GiB model's always-read weights load in 0.38 s. Tokenization is 0.036% of
prefill. Neither is worth optimising. - 14 GPU tests run and pass on an RTX 3050 via Vulkan -- the first time that suite
has actually executed rather than skipping. - Nothing detects a corrupt model: 4 KiB of zeros in the weights loads, exits 0
and answers fluently and wrongly. There is no checksum. Known, written down.
942 tests, 0 failed.
v0.0.21
Chaos v0.0.21 — a model runs on the phone
Atur: "android can not do any one of works in windows and just can
connect windows". He was right: the dial offered four modes and only
CLIENT did anything. On an Android 34 emulator, with no PC involved:
you
What is the capital of France?
chaos
The capital of France is Paris.
7 tokens in 1.8s (3.938 tok/s), finish=stop
POST /v1/chat/completions -> 200 (stream) in 3.2s
chaos-serve is a library now. Everything the phone needed already existed
there -- the token loop, sampling, streaming, the endpoints -- and the
Kotlin client already spoke to it, so no second token loop was written.
The engine runs as a child process, not inside the app. Loading it in
worked for anything that did not make a thread; the moment
StreamingRunner::new called pthread_create the app died with
SIGSEGV/SEGV_ACCERR inside __init_tcb. A 16 MiB stack did not help,
moving to a JVM thread did not help, and the library has no PT_TLS
segment to blame. The same engine AS AN EXECUTABLE makes threads
perfectly on the same device -- chaos-run proved it in v0.0.19 -- so
Android does what the desktop has always done.
The launch screen
Both the window and the app now open on ONE QUESTION instead of six pages
of controls: a gas-stove dial, four detents across a 180 degree top
sweep, stops at both ends. Turn it, press, and the shell shows only what
that mode can do. Windows renders it per pixel because plain GDI has no
gradient fill; Android draws the same geometry with Canvas. The badge is
assets/logo.svg rendered per density and never redrawn. The logo animates
in first, and any key skips it. ESC on the desktop and CHANGE MODE on the
phone return to the dial.
Fixed
- The logo rasteriser was 190x too slow at splash sizes: one repaint
measured 1510 ms, and it is 8 ms now. logo_coverage supersamples 8x8
over an n*SS grid, which is right at 44px where a ray is one pixel
wide and wasted at 170 where it is four. - The dial's badge was invisible -- white ink on a white knob face,
because the launcher's ink is white for a blue tile. 0 dark pixels in
a screenshot; 825 now. - The mode labels were 0xFF111111 on a #0D1117 background, and ALONE
was clipped off the left edge entirely. - The dial had a black surround; it has its own light ground now.
- The phone's note said "THIS PHONE IS A CLIENT" in every mode.
- Every size on the dial screen is a dimension resource, with values
for sw360dp, sw600dp and h480dp. - The dial clicks as each detent is crossed.
- ggml is built position-independent, for the cdylib that now links it.
Measured
- Queue depth is worth 2.55x on the disk. Depth 1 gives 1.34 GiB/s,
which reproduces what Chaos gets today and confirms it reads experts
serially; depth 8 gives 3.41.
Retracted
- The 5 tok/s ladder does not reach 5. It modelled a token as
bytes/bandwidth and omitted the 0.84 s of arithmetic that never
touches the disk. The rungs are 0.31 / 0.56 / 0.76 / 0.93 / 0.99. - And 5 tok/s is out of reach on this laptop at all: V4-Flash reads
7.38 GiB of always-read weights every token, so at 30.8 GiB/s the
ceiling is 4.17 tok/s with the disk free and no compute. 5 needs
36.9 GiB/s for the trunk alone. Achievable here is 0.43 -> about 1.0,
which agrees with the 1.19 ceiling found from the other direction.
Still not done
HELPER is reserved. chaos-worker speaks the protocol and is measured, but
no CORE routes an expert to it yet, and both the dial and the phone say
so.
889 tests.
v0.0.20
Chaos v0.0.20 — the engine runs inside the Android app
v0.0.19 proved Chaos runs on Android as a command-line binary in
/data/local/tmp. That is not something an app can use. This release puts
it in the app, and the APK published here carries it.
Read off the running app's own screen, on an Android 34 emulator:
engine 0.0.20 on this phone: 4 threads, 2.4 GiB total,
1.6 GiB available [/proc/meminfo]
That line is produced by Rust running in the app process, calling the
same core/probe the desktop uses. It is also what will decide which model
a given phone can hold -- "a powerful phone or a simple phone" -- by
measuring the device rather than matching a list of handsets.
No jni dependency
The project has none and the APK has none. JNI's ABI is a table of
function pointers at fixed indices; exactly one entry is declared --
NewStringUTF, index 167 -- with the padding before it marked
load-bearing, because a wrong index calls a different function through a
pointer and that is neither a compile error nor a clean crash.
The library is allowed to be absent, and every APK before this one took
that path: Engine.available is false, the device line falls back to
Android's own reading, and the app carries on as a client. The .so is not
committed -- half a megabyte per ABI of build output nobody can review --
so CI builds it, and greps the finished APK to prove it is in there. A cp
that worked and a package step that dropped the file look identical from
outside, and the app is built to survive the difference silently.
Fixed
- UnsatisfiedLinkError is an Error, not an Exception. A catch (e:
Exception) would have let the app die in a static initialiser with
nothing a user could act on. - The NDK's .cmd wrapper cannot link a cdylib. rustc passes
--version-script= to control exported symbols; cmd.exe mangles
it and the link dies with--version-script=...\list"" was unexpected at this time, naming neither Rust nor the NDK. The executables in
v0.0.19 were fine because they never get that flag. - The app was telling a lie it had outgrown. The CHAOS note still read
"this phone cannot be a CORE yet -- running a model needs Chaos built
for Android, which is not done" while the engine was demonstrably
running two lines above it. Caught only by reading the screen.
Retracted
- "Nothing about this app is verified on the machine that wrote it",
which the release workflow had said in a comment since the APK first
shipped. The NDK and the SDK are both on public mirrors that were
never tried; the app has since been built AND run locally, and
running it found four defects a build never would. What stays true is
that CI builds the shipped artefact from Google's own repositories,
and that nobody has run this on a real handset.
Still not done
Loading a model file and running the token loop on the phone. The bridge
is finished; that part is not, and no note here should imply otherwise.
879 tests.
v0.0.19
Chaos v0.0.19 — it builds and runs on Android
$ adb shell /data/local/tmp/chaos-run --version
chaos-run 0.0.19
$ adb shell /data/local/tmp/chaos-probe --quick
os android (x86_64)
cpu 4 threads
ram 2.4 GiB total, 1.6 GiB available [/proc/meminfo]
"Phase B is blocked" was wrong
v0.0.18's notes said running models on the phone needed the NDK, that
dl.google.com 404s this network, and that CI was the only route. The
Google host is unreachable -- but the NDK is on the same Tencent mirror
the SDK came from, which had never been tested. The block was inferred
from one host without trying the mirror already in use for everything
else. r26d, 665,022,840 bytes, sha1 c7ea35ff..., matching Google's
manifest as mirrored: the transfer is proven, not the provenance, so the
standing policy holds -- local toolchain for building, CI ships from
Google's own repositories.
It took less than the plan assumed
Every crate already type-checked for aarch64-linux-android with no source
change, including core/probe -- the crate the backlog named as the
obstacle, whose unix branch reads /proc/meminfo and works on a phone
unmodified. That is also what will answer "a powerful phone or a simple
phone": Chaos can measure the device and pick a model that fits it.
All the work was link flags in core/ggml/build.rs, each right elsewhere
and wrong here: the NDK has no libgomp, bionic keeps pthreads inside
libc, there is no libstdc++ (the NDK ships LLVM's libc++) -- and naming
nothing is also wrong, because rustc passes -nodefaultlibs, so
libc++_static must be named AND its sysroot directory searched.
Two mistakes inside that fix, both kept in the node. Command::new cannot
execute a .cmd and the NDK's Windows compiler is one, so the lookup
silently returned nothing and the failure looked like ggml missing
operator new. Then the helper read CC_aarch64_linux_android BY NAME and
returned nothing for the emulator's x86_64 ABI, bringing the same failure
back on a target just proven to work. Both derive from TARGET now.
Known: every Android CLI binary aborts at exit
FORTIFY: pthread_mutex_destroy called on a destroyed mutex, SIGABRT, 134.
The work completes first -- --list-devices enumerates the backend and
reads the device's memory correctly -- and the abort is strictly in
exit(). Located: exit -> __cxa_finalize -> std::mutex::~mutex() on
ggml_critical_section_mutex, ggml's one global mutex. Three theories are
eliminated in the node so nobody retries them: not duplicate linkage, not
a duplicated C++ runtime, and not Rust's runtime, since a ggml-free Rust
binary exits 0 on the same device.
Probably harmless for an app, and that is reasoning rather than a
measurement: an Android app never calls exit(), so __cxa_finalize should
never run. It must be checked against a real JNI library first.
The three open roadmap items, answered rather than left implied
- V4-Flash at 20 tok/s is CLOSED by measurement and cannot be delivered
on this machine at any effort. A token is 1.56 s of expert reads plus
0.84 s of arithmetic that never touches the disk, so with every
expert resident this CPU tops out at 1.19 tok/s -- the fixed cost
alone is 17x over a 50 ms budget -- and 20 tok/s separately needs
67.7 GB/s to the experts. Holding all 144 GB in RAM is worth 2.9x,
not 48x. Every idea that might have closed it is on the measured
dead-end list. - Devices as resources is half done. chaos-worker speaks the protocol
and is measured at 38x in favour of sending the work to the weights;
no CORE routes an expert to it yet, and the CHAOS page says so. - llama.cpp is parity, not a lead: parity on everything that streams,
1.20-1.27x behind on the hand-tuned dense path, 1.23x ahead out of
the box. The ranges overlap.
878 tests.