Skip to content

Releases: aturzone/Chaos

v0.0.34

Choose a tag to compare

@github-actions github-actions released this 07 Sep 23:47

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

Choose a tag to compare

@github-actions github-actions released this 07 Sep 14:05

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-serve refused any prompt over 2,048 tokens on the dense path, and
    -c could 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 in CLOSE_WAIT and 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.sh fixes 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

Choose a tag to compare

@github-actions github-actions released this 03 Sep 12:19

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

Choose a tag to compare

@github-actions github-actions released this 02 Sep 23:14
5fdd79d

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.sh29 checks against the built binaries, and it
runs in CI:

  • every binary answers --version
  • every subcommand works or refuses clearly, including connect refusing an
    unknown option by name and scan declaring itself not built
  • chaos-qr prints a code
  • all eight node routes, with /favicon.ico asserted at 204 because that is
    deliberate
  • /qr and /scan proven to fetch nothing external, which is what proves
    the fonts are embedded rather than linked
  • /v1/chat/completions and /v1/embeddings answering 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 connect refuses unknown options by name rather than resolving them
    as hostnames. This is the same defect E7 found in 43 of chaos-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/embeddings was 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

Choose a tag to compare

@github-actions github-actions released this 02 Sep 19:54
e000857

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 --auto never 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 cont copies left the dense KV path: +7.1% at 4031 tokens,
    byte-identical.
  • --trunk-quant q4_k converts 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.a under
    GCC and the build scripts looked only for ggml-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. -b never 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

Choose a tag to compare

@github-actions github-actions released this 28 Aug 17:36
dee30a9

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

Choose a tag to compare

@github-actions github-actions released this 28 Aug 16:48
3840a48

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

Choose a tag to compare

@github-actions github-actions released this 26 Aug 14:06
e7f839b

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

Choose a tag to compare

@github-actions github-actions released this 25 Aug 00:01
00e3c2d

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

Choose a tag to compare

@github-actions github-actions released this 24 Aug 23:07
ee81d1c

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.