Releases: nathansutton/chad
Release list
1.0.9
Decode is 5β7% faster on the 35B. The MoE block β the largest remaining
inefficiency in the per-token decode step β now runs as two custom Metal kernels
(JIT-compiled at load, no wheel rebuild): one dispatch computes gate|up for all 8
routed experts plus the shared expert with the SiLU epilogue in-register, the other
folds every down-projection, the routing weights, the shared-expert gate, and the
residual add into a single pass. Routing itself is bit-identical to stock; measured
on the shipped checkpoint: 70.7 β 74.3 tok/s at 8k context, decode-path perplexity
a hair better (the fused combine accumulates in fp32), prefill unchanged
(756 tok/s @7k vs 723 baseline), peak memory unchanged. Engages only on the exact
35B geometry, falls back to the stock graph anywhere else; opt out with
CHAD_NO_MOE_FUSED=1.
1.0.8
/speechnow names an install command that can actually work. The voice-mode
install hint was hardcoded touv sync --extra speechβ true only in a git clone, so
every published install (uvx chad-code,uv tool install chad-code) was sent to a
command with no project to sync. An extra rides on the install spec, not on a
separate step, andspeech.install_hint()now derives the right form from how chad was
actually installed:uv tool install --force 'chad-code[speech]',uvx --from 'chad-code[speech]' chad,uv sync --extra speechin a clone, oruv pip install 'chad-code[speech]'in a plain venv.- Extras documented in one place. The README install section now states the
install-spec rule with all three commands side by side, so the PyPI project page
carries it too. Also drops a stale line claiming LSP-precise find-refs/rename need an
lspextra β that extra is gone and precision ships in the base install as of 1.0.7.
1.0.7
- Precision code intelligence now ships in the base install.
find_refs/
rename_symbolprecision used to live behind an optionallspextra
(serena-agent, 33 transitive dependencies) thatuvx chad-codenever installed β
so every published install ran name-match only. chad now drives language servers
through its own ~350-line LSP client (lspclient.py) plus a declarative registry
(lspservers.py, the nvim-lspconfig shape): pyright via uvx (guaranteed present on
the uvx install path), the TypeScript 7 native LSP via npx, gopls / rust-analyzer /
clangd / solargraph / intelephense from PATH. Zero new Python dependencies; the
lspextra and serena-agent are gone. Servers that would answer confidently-wrong
empties without project context (clangd withoutcompile_commands.json, TS without
atsconfig) are refused, keeping the honestly-labeled name-match fallback. - Qualified symbol paths in every language.
view_symbol("Engine/process"),
replace_symbol("Engine.process", β¦)etc. now resolve the method β not a same-named
free function β in Python, TS/JS, Go, Rust, Java, Ruby, C#, PHP, Kotlin and C++
alike, via scope chains derived from tree-sitter spans (plus receiver/impl context
for Go/Rust/C++, whose methods don't nest lexically). This was Python-only via
jedi before; jedi is deleted and every language takes one code path (verified
against jedi on this repo: 230/230 span-identical). A qualified path whose
container doesn't match is now a miss instead of silently editing the bare name. - Broken grammars fixed for 4 of 12 fixture languages β and shell added outright.
First polyglot measurement found the language pack ships NO tags query for
TypeScript/TSX, a PHP query that doesn't compile against the current grammar, C
with no reference captures, and C++ missing out-of-class method definitions. chad
now carries its own tags queries for those (repomap._TAGS_OVERRIDE), plus a new
bash query the pack never had: shell functions and top-level variables now appear
inrepo_map/overview,find_refsfinds invocations across scripts, and
replace_symboledits a shell function by name β shell is the one surface nearly
every real repo (and 89 of 91 terminal-bench-2.1 tasks) contains. All pinned by a
13-language coverage test. - Post-edit typecheck in the edit result. When a language server is warm, every
edit/symbol-edit result appends the server's type errors (errors only, capped,
~50 tokens) β a type error surfaces immediately instead of one failed test run
later. Edits never pay a cold server start (lever:post_edit_diagnostics). - New
hovertool. The resolved type signature + docs of one symbol via the
language server β one line instead of opening the defining file. - mcp 2.x, single code path. The
mcp<2cap (below) was serena-agent collateral;
with serena gone chad runs natively on mcp 2.0 (protocol 2026-07-28). The two
renames that fail silently through getattr defaults (isErrorβis_error,
readOnlyHintβread_only_hint) are each pinned by a test that fails on the 1.x
spelling. - Fixes a broken install.
uvx chad-code(and any other fresh resolve) crashed on
startup withImportError: cannot import name 'streamablehttp_client'. Themcp
dependency was unbounded, so a clean resolve picked up the SDK's 2.0 major, which
removed the HTTP transport chad uses, moved tohttpx2, and split the wire types into
a separate distribution. (The cap has since become the deliberate 2.x port above.) - A broken MCP SDK can no longer take the agent down. Every other MCP failure already
degraded β a server that's missing, misconfigured, or crashing costs you its tools and
nothing else β but the SDK import itself ran unguarded atchad.mcpload, which happens
on every Agent. It's guarded now: an incompatible SDK means no MCP tools plus one
warning line in/mcp, and the rest of chad runs. Relatedly,/mcpno longer reports
"no MCP servers configured" when servers are configured but all failed β it shows the
warnings that explain why. - CI now catches this class of break. The fresh-resolve job and the weekly install
canary only ranchad --help, which exits inside argparse and never imports the modules
that load on the first turn. Both now import everychadsubmodule against a clean
resolve and fail if the MCP SDK guard fires.
1.0.6
- Voice mode, all local.
/speechin the TUI turns on push-to-talk: ctrl-t opens the
mic (a redβ recpin in the status line β an open mic you can't see is a bug), ctrl-t
again transcribes into the input box. The transcript is reviewed text, not a sent
message β you read what the model heard and Enter submits, same supervision contract as
every edit. Replies are read aloud (code blocks dropped, cut at a sentence boundary;
ctrl-c hushes). STT is Parakeet TDT v3 on MLX β the engine
Hex ships as its default, faster than Whisper per
clip, more accurate on dictation, and without Whisper's repetition-loop hallucination
mode β via a copy vendored intochad.parakeet(Apache-2.0, from
parakeet-mlx); TTS is macOSsay. Nothing
leaves the machine and the audio never touches disk. Vendoring is deliberate: the PyPI
packages drag torch (mlx-whisper, ~2 GB) or librosa's numba/scipy tree (parakeet-mlx)
for code paths MLX-format weights never run, souv sync --extra speechadds exactly
one package β the mic library (sounddevice). The tokenizer vocabulary rides inside the
model repo, so no data blobs ship in chad. Dictation hygiene is built in:
trailing/leading silence is trimmed before the model sees it, and a repetition-collapse
pass keeps any decode loop out of the input box. The weights are quantized to 8-bit
at load (no separate quant repo): A/B'd against bf16 on a 24-clip multi-voice
battery β transcripts identical up to case/spacing, twice more correct β for 786 MB
resident instead of 1296 and ~40% faster decode. Tune withCHAD_STT_MODEL(default
parakeet-tdt-0.6b-v3, multilingual; ~2.5 GB fetched on first use),CHAD_STT_QUANT
(8default Β·4halves memory again, clean on the battery but unproven on noisy
mics Β·nonefor bf16),CHAD_VOICE,CHAD_SPEECH_RATE. - Dictation length is linear, not quadratic. The encoder ships as
rel_poswith
att_context_size [-1,-1]β full global attention, so a take's cost grows with the
square of its own length, on the same GPU the coding model is resident on. chad now
switches it to the banded local kernel at load: the score matrix goes fromTΓTto
(T, 2w+1), and w=256 encoder frames is Β±20.5s of audio at 12.5 frames/s, so a
dictation take still sees its whole self. The transcript is byte-identical to full
attention on a real clip (and 8-bit local is byte-identical to bf16 local). A 359s take
now decodes in 3.9s with peak memory flattening (58sβ2.0 GB, 175sβ3.1, 359sβ3.3) where
full attention keeps climbing. The switch happens before quantization, because
set_attention_modelrebuilds the attention modules andload_weightsrejects a
quantized layer's scales/biases. A take is also capped at 600s as a backstop: recording
doesn't stop and the status line says so, the take just stops growing β a mic left open
is truncated, never lost. /speechoff gives the memory back, and enable tells you what's broken. Turning
voice mode off now unloads the STT weights (747 MB β 0 measured), not just the mic β
it won't fire mid-transcription, where the worker still holds the model. Enable-time
validation covers the two failures that were previously invisible until after you'd
spoken: a misspelledCHAD_VOICE(sayexits nonzero, so nothing ever raised β you
just got silence forever; now it names the typo and suggests the nearest real voice)
and a badCHAD_STT_QUANT(which only raised from inside the decode). Both warn rather
than block β a machine withoutsaycan still dictate. And a transcript that finishes
after you've turned/speechoff is discarded with a note instead of typing itself
into an input box in a mode you already left.- Voice mode capture + correction, borrowed from the best. Three ideas adapted from
Hex (MIT), the open-source macOS dictation app:
while/speechis on the mic stays warm feeding a ~1s ring buffer, and each take is
seeded with 0.45s of pre-roll from before the keypress β opening a CoreAudio device
takes 100β300ms, so a cold-opened mic clips your first syllable ("ix the failing
test"). The status line saysmic openthe whole time, because an open mic you can't
see is a bug;/speechoff releases it. esc during a take discards it instead of
transcribing β a mistaken recording shouldn't cost a decode plus an input-box cleanup.
And a personal word table (~/.chad/speech_words.json, orCHAD_SPEECH_WORDS) of
{"heard": "meant"}fixes what no ASR model can β your identifiers:{"pie test": "pytest", "you vee": "uv"}. Case-insensitive, whole-word, longest match first,
loaded fresh each take;/speechreports the count (or the parse error) at enable. - numpy 2 allowed. The
<2cap dated to 1.0.0 and encoded no known break β it also
blocked modern ASR dependencies that floor at numpy 2. Now>=1.26,<3, verified on
numpy 2.4.6: full unit gate, live on-device transcription, andchad prove4/4
(Ornith 9B smoke model) at normal throughput. - You can see what you're approving. The confirm prompt used to render into the
one-row status line with newlines flattened toβand a 160-character clip, so a
multi-line bash command was unreadable β approving it was approving blind. Approvals
now open a panel above the input that shows the command on its own lines (bounded, so a
giant heredoc can't swallow the screen), names in plain words what is about to happen
("chad wants to run a terminal command"), and puts the destructive-command warning next
to the command instead of in scrollback. - New demo GIF. The old one predated the banner entirely (no logo, a dead model id,
and a fixture that no longer matched the checked-in tape). The new one is a 35B session
end to end in 51 seconds: banner, type-ahead while the weights load, the model finding
the cent that floor division loses, fixing it, verifying itself, and your ownpytest
confirming it stuck. auto-accept editsnow means what it says. It auto-approved everything, bash
included. It now auto-approves file edits only β terminal commands and mutating MCP
tools still ask, which is the mode most people actually wanted. The old
approve-everything behavior is unchanged but now has its own name,yolo, still where
--yolostarts you and now reachable with shift-tab:normal β auto-accept edits β yolo β plan. A sub-agent'stools: "all"request is honored only underyolo, since
allincludes the shell
1.0.5
The release that makes a 24 GB Mac a first-class target: it runs the 35B now, a Linux container can drive the real local MLX model through chad serve instead of a different quantization on a remote box, and the CLI surface is smaller and honest β serve, prove and levers are real subcommands, and five harness-only governor flags became CHAD_* env vars. Plus completion and reasoning-budget accuracy, long-running commands that survive their timeout, a destructive-command guard that stops crying wolf, and long-context decode throughput that ships to a uvx install β measured end-to-end on stock PyPI mlx, no custom wheel anywhere. Each behavior change is individually reversible via CHAD_DISABLE.
24 GB Macs now get the 35B. The 35B's floor was 32 GB because its working set SIGKILLed a 24 GB machine mid-turn. The fused attention kernel and the 8-bit-from-the-start KV cache it enables cut the per-token cache cost enough to give the headroom back, and the compaction trigger sizes itself from the live Metal budget rather than a fixed constant, so a tight box narrows its window instead of dying. 16/18 GB still get the 9B, and --model 9b puts a 24 GB box back on the small model.
--model replaces reaching for an env var to change model. Takes 35b, 9b, auto, or any Hugging Face repo id / local model dir; works on chad and chad serve. CHAD_MODEL still works and the flag outranks it, so a globally exported var can't pin every run, and --model auto forces the RAM-aware pick even when the env var is set. Forcing 35b on a box whose RAM is too small (or unreadable) is honored but warns first β chad advises, you decide.
serve, prove and levers are real subcommands with their own --help. They were dispatched on the literal task string, so chad --help never mentioned them while still listing --host/--port as "chad serve only" β flags that parsed fine and were then silently ignored on every other invocation. --host/--port now belong to chad serve and are rejected elsewhere. chad --levers still works as an alias.
chad --help lists 12 flags instead of 19. The unattended-run governor knobs (--think-ceiling, --turn-budget-tokens, --turn-budget-s, --auto-continue, --review-pass) are set by benchmark harnesses, never by hand, and every one already had a CHAD_* twin, so the flags are removed rather than hidden β passing one is now an error instead of a silent no-op. CHAD_AUTO_CONTINUE fills in the one twin that was missing, and the bundled TB2 adapter (benchmarks/tb2/harbor_chad_tb2.py) sets all three it used through the CHAD_* env dict it was already building. --think-budget stays on the CLI: unlike the rest it is a capability trade a person reaches for interactively.
A dead remote backend explains itself. An unreachable --base-url exited through a 22-line traceback ending in chad.base_engine.BackendError, which reads as a chad crash rather than "the server you pointed me at isn't up". Now problem/cause/fix, like the model-load and download failures.
Removed the draft-model code path. chad has been single-model since before 1.0; Engine(draft_id=...) was never set by any caller, but its branches still shaped cache construction, KV-quant resolution, warm-prefix eligibility and prefill. Prompt-lookup decoding is unaffected β it shares the word "draft" and none of the machinery.
chad serve β drive the local MLX model from a Linux container. chad's remote arm (--backend llama) exists because MLX can't run in a benchmark container, which in practice meant measuring a different quantization of the model on a remote GPU box. chad serve exposes the in-process MLX engine β real prefix cache and all β over the same llama.cpp /completion protocol that arm already speaks, so the container drives the model people actually run, unchanged and with exact server-side timings. --host/--port (CHAD_SERVE_HOST/CHAD_SERVE_PORT), loopback and unauthenticated by default; CHAD_SERVE_API_KEY guards a wider bind. One KV cache means one agent at a time; concurrent clients queue.
Cache quarantine and warm-prefix come back over the wire. Against a stock llama.cpp server these are unavoidable no-ops (its cache is opaque and its disk isn't ours). A chad serve server advertises both in /props and the client feature-detects, so a sub-agent no longer evicts the main transcript's prefix and the stable system+tools prefix warm-starts from disk. Nothing changes against a real llama-server, and a failed call degrades to the old no-op β these are latency, never correctness.
A client that stops reading can no longer take the server down. Streaming wrote each token to the socket from the one thread allowed to touch MLX, with no timeout on it β so a client that stopped draining without disconnecting (a suspended container, a slept laptop) blocked that thread indefinitely and no other request could be served, while /health still answered ok. A blocked write never raises, so the hung-up-client cancel couldn't fire either. Generation now hands SSE lines to the request's own thread through a bounded queue: the engine runs ahead of a slow reader instead of being paced by it, and a peer that stops reading is written off the same way a disconnected one is.
A request no longer silently zeroes the server's sampler config. min_p, top_p and temperature absent from a /completion body were defaulted to 0.0 and applied anyway, wiping a server started with CHAD_MIN_P/CHAD_TOP_P on essentially every request (the client only sends those knobs when they're armed) and forcing greedy decoding. Absent now means "leave it alone"; an explicit 0.0 still disarms a knob for that request only.
Generated token ids on the wire are the engine's own. They were recovered by slicing the engine's cache past the prompt, which only holds on the main decode path β the prompt-lookup path came up one token short every turn, and after a Metal OOM the client was told a prefix was resident when the cache had been dropped. The engine now reports what it generated, and says so when it reset the cache, so the client's mirror and its prefill estimates stay honest on every path.
Long turns stopped paying quadratic CPU to watch for stop markers. The remote backend re-scanned the entire generated text after every token; at 16k tokens that was 846ms of pure scanning on the thread draining the stream. Now incremental: 4.5ms.
A long cold prefill no longer kills the turn it is working on. The stream stayed byte-silent from the response headers until the first token, and HTTP clients measure a stall in bytes, not in your progress β chad's own passes its timeout to urlopen, which applies it per read. A big prefill therefore timed out just before the first token, and because a socket timeout looks transient the agent re-issued the step and paid for the same prefill again. The stream now sends an SSE comment during silence; stock clients discard it, so nothing on the wire changes for llama.cpp.
A prompt that cannot fit is a 400, not a Metal OOM. Nothing compared the prompt against the context window before prefilling it, and n_predict was never clamped to the room left beside the prompt β so a client that mis-budgeted (a stale pinned context, a plain curl) walked the engine into an allocation it might not survive, taking every other client with it. /completion and /warm now refuse an oversized prompt up front and clamp the generation budget to what the window can hold. The admission wall is live, not the number pinned at load: the Metal budget is blind to other processes, so a docker stack or a second model started afterwards takes physical pages the KV cache needs 1:1, and a prompt that fit an hour ago no longer does. /props keeps advertising the stable window clients budget against; /health reports safe_ctx and ctx_pressure so a tightened wall is visible before a request bounces off it.
chad serve honors CHAD_TEMP / CHAD_MIN_P / CHAD_TOP_P. It built its own engine and read none of them, so a server started with a sampler config sampled as if it had none β the one drift the server exists to eliminate. The three now travel as a single shared call the CLI and the server both make, so they cannot drift apart again.
Long-context decode is faster on both models. The fused attention kernel's split factor now widens past 16k context instead of staying flat: measured end-to-end (one process, interleaved arms, adopted only where the spread cleared 1.000), the 35B decodes +1.5% at 32k and +0.6% at 16k, the 9B +0.5% at 32k, both unchanged at 8k where flat was already optimal. It is the same math on a different grid β the split only regroups the partials pass 2 combines β and the widened tier gets its own kernel-vs-reference test plus a retile-agreement case at that size.
CHAD_FUSED_LAYER is removed. The opt-in fused GDN+MoE compiled region was measured on real weights against the two-call path it replaced and moved decode 1.001x-1.002x β indistinguishable from noise β so the code is gone rather than shipped as a dead knob. The measurement lives in the source so the idea isn't re-derived. Setting the var now does nothing.
CHAD_NO_MLX_TUNING is removed, along with the tuning it disabled. The MLX_* runtime knobs (MLX_METAL_FAST_SYNCH, MLX_MAX_OPS_PER_BUFFER, MLX_MAX_MB_PER_BUFFER) were swept on the 35B and every arm lost 1-4% at both 8k and 32k, so chad now sets none of them and the module that applied them is gone rather than shipped as an empty table with a flag to switch it off. mlx reads these vars directly if you want to experiment.
A done with nothing landed gets the task's own requirements back, once. Previously, ending a turn with no landed-and-verified change stopped the turn immediately and banked a progress note β so the done-audit, which quotes the task statement's requirement lines and reports what actually exists on disk, never ran on exactly the turns that needed it most. That ending now hands off to a single audit bounce first, in context with the work the turn already did; a second empty ending stops as before....
1.0.4
Tool-result economics: three additions, each individually reversible via CHAD_DISABLE.
Edits survive typographic-punctuation drift. A fourth edit-match rung: when
exact, escape-normalized, and whitespace-flexible matching all miss, the edit
retries with curly quotes, en/em dashes, ellipsis, and non-breaking spaces folded
to ASCII on both sides β the drift when the model re-types prose or docstrings it
saw rendered (in either direction). A unique match is still required, and the
result discloses the recovery.
Duplicate read-only output is elided. When a read/grep/glob/symbol-tool
result comes back byte-identical to a result still in the transcript, chad appends
a short pointer to the earlier copy instead of re-sending the full body β on a
non-trimmable prefix cache every duplicate body is prefill paid again on every
later step. Byte-equality against the live transcript is the safety proof: a
changed file, different arguments, or a compaction rewrite all break equality, so
content is only elided while a verbatim copy is provably still in context.
Sub-agent reports with no evidence are flagged. A task sub-agent that
returns a confident, non-empty report having dispatched zero tools answered from
model memory, not this repository; its folded result now carries an explicit
"verify with grep/read before relying on it" warning. Warn-not-reject on purpose:
a re-spawn doubles the local GPU cost, and a wrongly-rejected report breaks the
turn while a wrongly-accepted one merely restores the old behavior.
Plus a privacy default flip:
Local diagnostic traces are now opt-in (privacy-first default: off). The
readable ~/.chad/session.log trace β user query, tool-call args (bash commands,
write/edit content), and result previews β and the persistent input history at
~/.chad/history are no longer written by default. chad is a local, single-user
agent so nothing ever left the machine, but the log still landed plaintext previews
under ~/.chad, so it's now enabled only when you opt in with CHAD_SESSION_LOG=1.
CHAD_NO_SESSION_LOG=1 still works as a hard kill switch and wins over the opt-in.
And a session-start orientation change:
A lightweight workspace map replaces the flat file listing. The system-prompt
tail now opens with a small, ranked repo_map digest β signatures only, ranked by
reference centrality β instead of a flat file listing, so the model orients
structurally at session start rather than burning a reflexive step-1 repo_map
call (the tool stays for a wider or focused map). It's built once per session from
the on-disk mtime cache, so it doesn't churn the KV prefix or add per-turn cost, and
degrades cleanly to the old flat listing wherever repomap is unavailable (e.g.
wheel-less platforms). On by default; reversible via CHAD_DISABLE.
1.0.3
[1.0.3] β 2026-07-21
Guardrail-interaction fixes: three cases where the model was doing fine but overlapping guardrails ended the turn early or accepted a non-answer.
A malformed tool call is never accepted as a final answer. When the model slips into a wrong tool-call dialect, garbles now get their own re-nudge budget (separate from the token-cap-truncation counter), a canonical call exemplar once it's stuck, a scrub of the previous garbled message so the model stops few-shotting its own broken dialect, and β if the budget runs out β a clean hard stop with a banked progress note instead of shipping the garble as the answer. On by default; reversible via CHAD_DISABLE.
Done-audit re-bounce for still-missing deliverables. If the audit flags a task-named output path as absent and it's still absent when the model tries to finish (with real time left on the budget), chad bounces once more naming only the missing path, then accepts unconditionally. Stops a required file from going unwritten while the turn ends "successfully."
Investigation gate exempts ops commands. A bash step that isn't provably read-only (git merge, apt-get install, redirects, make, β¦) now counts as action, not investigation, so the "you've only been looking, make an edit" gate no longer fires in the middle of a legitimate ops workflow.
1.0.2
[1.0.2] β 2026-07-20
Reliability tuning for long, budgeted tasks, plus one interactive papercut fix.
- Syntax gate no longer flags prose & data files. Plain-text deliverables β a
.txtanswer file,requirements.txt, markdown, CSV/TSV β were being run through a
tree-sitter grammar (the language pack maps.txtβ VIMDOC) and warned on, sometimes
reverted, at exactly the write that produced the deliverable. The gate now polices
code languages only; prose/data formats are skipped uniformly across warn, edit-revert,
and write-reject. run-style tasks no longer bail with prose. System-state asks ("start the
service", "boot the image", "install X") are completable with zero file edits, and
used to fall through to the weakest completion path β a give-up in prose could ship
nothing (qemu-startup did, with most of the wall unspent). They now get their own
intent class that arms the anti-bail nudges, while still completing cleanly with zero
edits (the no-empty-diff "done" gate only ever applied to edit tasks).- Reasoning budget throttles instead of muting. Once a turn's cumulative
reasoning-token budget is spent, chad now forces one no-think action step per ~3k
further reasoning tokens (a duty cycle) rather than muting thinking for the rest of the
turn β thinking is restored as soon as the model stops over-spending, avoiding the
garbled tool-call tails a blanket mute produced. - Wall-aware auto-continue. When most of a task's wall budget is still unspent, an
exhausted turn is granted a fresh relaunch (bounded), instead of giving up after a
fixed two attempts with the clock barely touched. - Rejected "done" no longer poisons the relaunch. When a completion claim is
rejected for landing no verified change, the carried-forward progress note now leads
with a warning that the claim was rejected and drops any hypothesis that itself asserts
completion β so a relaunch stops inheriting "already complete and verified" as fact and
re-confirming it.