Repository navigation
Releases: jaivial/mini-tui
Release list
v0.43.0 - Bots that run your board
mini-tui is now a team of bots that organize work as a kanban, with workers that carry the cards out,
WhatsApp groups synced with the web app, a real browser through cu, and a closure check that makes
"done" mean "the evidence is on the card". This release covers everything since 0.42.0, with a new
README and a new landing page.
Measured
- Closure check on, against pi: 112.9 s vs 145.6 s, 30/30 PASS on both sides. Ten tasks, three
interleaved repetitions (pi and mini alternate per task, so API drift hits both alike),
MiniMax-M3.1-Flash-Previewon both sides, mini on the deployed binary and config with Laya as the
judge. That is 0.78x of pi's time as the sum of per-task medians, and mini is at least as fast on 9
of 10 tasks. Ten tasks, three repetitions, one model, one machine; earlier rounds were a tie and
the report keeps them. - Where it came from. Baseline with the model judge: 7 of 27 runs hit the 900 s timeout, because
the judge rejected finished work up to 158 times (#300 caps rejections at 3). Laya had always
answered "evidence truncated" and sent every closure to the slow model judge (#302 gives it a
compact window). Its latency grows steeply with input, about 5 s per closure at 17k characters
(#303 shrinks the window, and a closure now costs about 0.1 to 0.5 s).
Added
- Bots and a board. Windows hold bots and each bot holds the delegates it started: a nested
sidebar, a team map drawn as a live graph, and a read-only view of other bots' cards through a
shared graph layer. The durable task graph keeps leases, so a card in flight is not lost when a lane
stops. - WhatsApp groups, synced both ways (WA-2 to WA-7, v4 to v6). One group per bot, group photos from
the bot avatars, a no-echo mirror between the group and the web app, bounded media with local voice
transcription, a 👀 acknowledgement, typing state, semantic reactions,/newsessionand/rename,
and pairing with a phone code or a QR. - cu in the agent. The
computer_usetool drives a reusable pool of cu daemons and posts
screenshots to the chat. - Closure oracle.
MINI_AGENT_GOAL_ORACLE=layawithMINI_AGENT_GOAL_ORACLE_URLpoints the
closure check at a self-hosted classifier. The model judge stays the default and the fallback. - Web app. A silver-metal palette, an Apple-style 18 px shape language, a floating prompt pill,
compact tool cards, generative UI in the chat, voice-note recorder and player, inline images and
attachments, AI session titles, a model router with three tiers, usage metering per model, and a
PWA with cookie login.
Fixed
- Around fifty fixes from QA rounds E1 to E51, among them: a stale save no longer overwrites a newer
one, a stopped consumer tears its turn down, a bodiless upload is no longer read as an empty one,
a remote folder listing is bounded in work as well as in size, and the web server compares origins
against headers an attacker cannot choose.
Security
- Per-IP rate limits on the expensive routes, loopback-only CORS with no wildcard on private reads,
attachment sniffing before storage, and a host guard for the web server.
v0.42.0 - Agent submit-checklist
Agent submit-checklist
The agent's mini.yaml now ships a pre-submit constraint checklist plus workspace hygiene rules. Benchmark notes, with the run each number comes from:
- 75% (12/16) on the targeted dev run after the checklist landed (failed tasks re-run, polyglot-c-py solved; composite vs the previous full sweep)
- Full Terminal-Bench 2.0 dev sweep (16 tasks, GLM-5.3-flash, Rust agent, measured Oct 11): 62.5% (10/16), wall-clock parity with the pi harness baseline (~384s vs ~383s avg)
- 0.69x wall-clock came from the earlier 10-task round-5 suite; it did not hold in the round-6 full-suite measurement
Graphics
- docs/img/benchmark.png - progression vs pi with public leaderboard Pareto reference
- docs/img/update.png - release card
v0.41.0
mini wins the pi benchmark for the first time. Round 5 of docs/pi-vs-mini-benchmark.md:
the same ten tasks, the same model on both sides (minimax/MiniMax-M3), sequential, no
orchestration — 204.5 s against pi's 295.0 s (0.69x), 8 of 10 tasks won and 10/10 quality
on both sides. The speed was not bought with correctness.
| R1 | R2 | R3 | R4 | R5 | |
|---|---|---|---|---|---|
| aggregate ratio | 3.65x | 1.28x | 0.95x | 1.07x | 0.69x |
| mini total | - | - | 410.6 s | 293.7 s | 204.5 s |
| pi total | - | - | 430.1 s | 276.0 s | 295.0 s |
| tasks won | - | - | 7/10 | 4/10 | 8/10 |
| quality | - | - | 10/10 | 10/10 | 10/10 |
Why it moved. The front was never the number of turns but the length of the worst one: over
the round-4 corpus the longest single turn of a run was a median 30 % of that run's wall time,
and those turns are almost entirely reasoning — the worst wrote 19 427 chars of <think> to emit
a 193-char tool call, while the longest non-reasoning payload across 73 turns was 1 241 chars.
The per-turn output budget is now capped at 4096 (it was an unstated 8192), measured back to
back per task over the whole corpus: 0.73x the wall time on 7 of 10 tasks, zero truncated
turns, median longest turn 8.0 s → 3.5 s and median generated text 2 513 → 1 266 chars. 4096 is
where the measurement is, not a round number: 2048 was not faster enough to pay (0.69x) and
truncated a turn, and 3072 lost t8_js_bug outright. Override with MSWEA_MAX_TOKENS or
model.max_tokens.
The two tasks that led round 4 moved: t10_crash_report 62.1 s → 12.0 s (5.2x) and
t8_js_bug 72.2 s → 28.1 s (2.6x). Model time fell 279.1 s → 179.1 s.
Fixed
- A truncated turn is never read as the final answer. It has no
tool_callsand minimax puts its
reasoning incontent, so thetool_calls.is_empty()arm was reading that reasoning as the
run's result and submitting a run that had done nothing — measured, that is how a run lost a task
it would otherwise have passed. It now falls through toparse_toolcall_actions, whose "no tool
call" branch is already a format error the loop retries.
Added
- Jev (TypeSafe System One) as two independent, optional toggles (#115, #116): the
jev
decision service and its API key (vault →$TYPESAFE_API_KEY→ config, resolved in one function
every surface shares), andjev-verifier, an optional post-code phase where the run's own model
proposes candidate defects on the diff and Jev answers three typed questions per candidate.
Both ship off and neither reads the other's state. The verifier now runs in the agent
(agent-rs/src/jev.rs), not only in the terminal UI, so a run started by the web app, by
mini-tui -por by a subagent gets a verdict;info.jev_verifierintraj.jsoncarries the
structured report.
Full report: docs/pi-vs-mini-benchmark.md (PR #116). Changelog: CHANGELOG.md.
v0.40.0
0.39.0 — Claude Haiku 5.5, and dispatch --join with enforced lanes
Claude Haiku 5.5 is in the curated model picker, and agent dispatch can end the parent's turn
on the wave itself (--join), with each child confined to its lane.
Added
cliproxy/claude-haiku-5-5. The cli-proxy gateway advertises Anthropic's new
Claude Haiku 5.5, and mini-tui lists it in the/modelcatalog next to
cliproxy/claude-opus-5-5andcliproxy/claude-sonnet-5-5. It runs over the same Claude
subscription credential and gets the 1M-token context window (automatic compaction),
in both the Python agent andagent-rs.agent dispatch --join(#89). The parent's turn ends on the dispatch; when the last
child finishes the hub runs--verifyonce and answers with every child's result, so the
parent spends one step after the wave instead of 41–103 s re-checking it.- Enforced lanes (#89). A plan task's
files(and optionallane) are the only paths its
child can write: the bash tool runs under bubblewrap with the work tree read-only outside
the lane (unchanged behaviour wherebwrapis missing).
Changed
- Dispatch shard sizing and wave admission (#89). Shards are sized in lines, the wave warns
when the heaviest is over 1.5x the lightest and starts it first; a wave is admitted as a
whole on memory, and its children are no longer re-gated one by one on CPU load. - Child task header (#89) states
## Where(the work tree) and## Scope(the gate is
the proof; no test-harness side quests).
Fixed
- Headless agents wrote task cards into other sessions (#89). The session-tasks rule told
every agent to runmini-tui tasks set; a headless run could not find its own session and
overwrote a live one. The rule now renders only whenMSWEA_CONTROL_FILEis set, and never
for lane children.
0.38.2 — the two contract-check fixes that missed the 0.38.1 tag
Patch release. 0.38.1 contained defects 1–5 of agent contract-check reporting pass while having examined nothing. Two more defects in the same arm were found by an orchestrator probing the fixed checker; their fix (PR #84) landed on main after the v0.38.1 tag. 0.38.2 adds defects 6–7.
| # | Symptom | Cause | Fixed in |
|---|---|---|---|
| 1–5 | vacuous pass, comma-invented filename, prose read as identifiers, --repo ignored, worktree mistaken for repo |
see 0.38.1 | 0.38.1 |
| 6 | a control came back 4/4 pass |
--prompt-file was parsed into req["text_file"] while the arm read only req["handshake"], so a stored child's handshake was checked instead |
0.38.2 |
| 7 | "no handshake to check" for one that was given | the positional handshake was read from index 2 even when --repo had consumed the repo slot |
0.38.2 |
The answer text is now taken from handshake, else text_file, else a stored child — with the positional slots indexed correctly in both spellings. All three invocation forms examine the text they are handed.
The honest limit reported in 0.38.1 still stands: the checker has teeth on file existence; it does not compare a Go json tag to a TypeScript property. Not fixed here.
Full detail: PR #84 (defects 6–7), #85 (docs), #86 (release).
0.38.1 — a contract-check that reported pass over nothing
Patch release. The shared context (ContextStore) shipped in 0.38.0; this fixes
five of the seven ways its central contract-check could report pass while having examined nothing.
Every one of them is the same bug in different clothes: a mechanical check parsing free
text with a rule that free text does not obey. None was visible by reading the code.
Each was found by running a real orchestration in the web UI — and the last three were
found by an orchestrator probing the checker itself, not by a human.
| # | Symptom | Cause |
|---|---|---|
| 1 | 3/3 pass over an empty corpus |
a child wrote good prose with no surface: tag, so nothing was claimed |
| 2 | [FAIL] files: which registerRoutes (backend/routes.go |
a comma inside a sentence invented a file name |
| 3 | nothing in the touched files carries There; neither |
There/Neither are Capitalised and 5+ chars, so they passed as identifiers |
| 4 | 4/4 pass with the wire field deleted |
--repo was parsed into a key the arm never read |
| 5 | green for a child in a worktree | the hub took the last cwd segment as the repo |
| 6 (fixed in 0.38.2, not in this tag) | a control came back 4/4 pass |
--prompt-file was parsed into req["text_file"] while the arm read req["handshake"] |
| 7 (fixed in 0.38.2, not in this tag) | "no handshake to check" for one that was given | the positional handshake was read from index 2 even when --repo had consumed the repo slot |
The fixes: a fourth coverage check that fails — and says "This is NOT a pass" — when no
file was claimed and no clause names the repo; path_tokens() so a claim must be a real path;
is_identifier() so There is prose and groupMenuEnabled is a field name; --repo honoured
in both spellings; and Child.repos carried from repos_of() at spawn. Defects 6–7 (--prompt-file ignored, positional
handshake index) were fixed on main after this tag (PR #84) and ship in
0.38.2.
Measured
Same 6-file two-repo task, orchestrator exploring once, in the web UI:
| before | after | |
|---|---|---|
| discovery tokens per subagent | ~1,962 | ~0 – 884 |
| subagent steps | 40 | 28 |
Fewer steps for more work: the children stopped re-discovering code their parent had already read.
Honest caveat: the orchestrator pays for the exploration up front, so on a repo this small its own
cost still roughly matches what a child's would have been. It amortises with more children or a
bigger repo — the shape the baseline measurement (66% of tokens spent rediscovering) came from.
The best proof came from an agent
An orchestrator received 4/4 pass, did not trust it, copied the tree to /tmp, deleted the wire
field, re-ran the check — and got 4/4 pass again with the field gone. It then reported its own
integration check as toothless. Nothing prompted it to. That negative test is how defects #4, #6
and #7 were found; a test suite would have asserted the checker works and never asked what happens
when you break the thing it is checking.
And an honest limit, reported by the agent, not by us
After sabotaging its own tree five ways, the orchestrator concluded the checker has teeth on
exactly one dimension — file existence. It greps the claimed files for the contract's tokens;
it does not compare a Go json tag to a TypeScript property, does not check a value is non-empty,
and does not catch a missing mapping. Two of its five sabotages were genuine integration failures
that passed. It then stopped relying on the mechanical check and verified with tsc --strict plus
an end-to-end render walk instead. That limitation is real and is not fixed here.
Full detail: PRs #82 (defects 1–5, this release), #83 (docs). Defects 6–7: #84/#85, released in 0.38.2.
mini-tui 0.38.0 — the shared context: explored once, handed over
A subagent that re-reads what the orchestrator already read was the single biggest cost in a fan-out. On a measured orchestration (6 subagents, 2 repos) 122,039 of 184,342 tokens — 66% — went on discovery, 28 files were re-read that the parent had already read, and 3 integration failures were left for the parent to find later by reading diffs.
So the exploration is now paid for once, in agent-rs, not in a skill and not by good manners.
What is new
The shared context (ContextStore) — markdown with a schema in the run tree's context/:
mini-agent-rs agent state put findings.md --prompt-file f.md # symbols, files, lines
mini-agent-rs agent state put contracts.md --prompt-file c.md # the contract, producers/consumers
mini-agent-rs agent state put decisions.md --prompt-file d.md # what was decided, and why
mini-agent-rs agent state put surface.backend.md --prompt-file s.md # callers, types, payloadsHanded over by default, scoped to the repos. A child receives the documents that name the repos it works in, cut down to its own repo: a backend child reads surface.backend.md and the ## backend sections, and is never billed for the frontend's. No cooperation from the parent required — --repo R names the repos, otherwise the child's own --cwd does. MINI_AGENT_CONTEXT=0 turns it off.
agent surface <symbol> — who calls it, which type carries it, which contract it obeys, in one call from what is already indexed, instead of grepping the tree again. agent surface ls lists the index.
The handshake goes back. When a child's turn ends the hub parses its final answer into ## surface, ## contract and ## surprise and stores it as <child>.md for the next child. surprise is the point: it is the one place a subagent can say the brief is wrong without stopping.
agent contract-check, automatic at the end of a child's turn, with no model in the loop: every file a child claims to touch exists and is under the repo, every field the contract assigns to that repo appears where it should, and a no <invariant> clause is checked literally. A claim that does not hold fails instead of reaching the parent as a summary.
The RLM harness seeds the same store, so a scripted orchestration sees what a chat one sees, and the orchestrator is told to use it in the system prompt (<orchestration_rule> in mini.yaml) — not in an optional skill.
Measured, not estimated
Three real runs of the same task in the web UI with subagents (2 repos, orchestrator + 2 children), the first without the store as a control:
| control | with | |
|---|---|---|
| child discovery tokens | ~3,197 | ~1,456 (-54%) |
| child discovery steps | 4 | 3 |
| orchestrator tokens | ~26,699 | ~12,093 (-55%) |
| orchestrator discovery tokens | — | -83% vs the first store run |
| subagents spawned after | — | call 14 (vs call 31 before the fixes) |
And the one that is not a ratio: in the final run a subagent found that the backend emitted group_menu_enabled while the frontend struct expected groupMenuEnabled, with no mapping layer — the field would have been false forever — and wrote it in its surprise line, escalating the contract decision instead of deviating silently. That is the class of failure that cost 3 integrations in the villa run, caught by the child that found it instead of by the parent reading diffs.
What did not work, honestly
- Duplicate reads do not go down (8 vs 9 vs 10 across the three runs), and should not: a child still reads the file it has to edit. What the store removes is the orientation work around it — the tokens, not the I/O.
- The first measured run tested 2-day-old code. systemd pins
MINI_AGENT_BINto a binary the web app never rebuilds, so the run reported, verbatim, thatagent state,agent surfaceandagent contract-check"don't exist in this build". No amount ofcargo testcan catch that; only driving the UI did. - The model still spends a few calls checking that the commands exist (
agent --help). Cheaper now, not zero. - n=1 per arm on a 4-file toy repo. The villa baseline is the only realistic reference. The direction is consistent across runs; the percentages are not a law.
Fixed along the way
is_discovery counted cd/export as exploration (re-validated against the villa journals: 120,718 vs 122,039, 1.1% off in the honest direction). The hub put mini-agent-rs on a child's PATH but not mini-tui, which the prompt names — the measured model burned ~20 calls hunting for it. agent contract-check run by hand was vacuous. A copied template became a false claim and failed the check on a file named <file (the check was right; the input was garbage). A large store refused every spawn while blaming a flag nobody passed. And the integration tests were inheriting the ambient MINI_AGENT_SOCKET, so they talked to a live session's hub — 5 of 5 agent plan tests failed on main for that reason alone.
120 tests green (cargo test in agent-rs: 82 unit + 38 integration), with every fix pinned by a test that fails when the fix is reverted.
PRs: #79 (feature), #80 (what the browser E2E found), #81 (release + docs).
mini-tui 0.30.0
Added
- Subagents use your connected models, with no extra key. A run the agent starts itself (a
subagent on another model, a model switch mid-run) now takes the key of that model from your
/connectproviders (~/.config/mini-tui/providers.json), as mini-tui already did for the runs
you start. Before,agent spawn -m minimax/MiniMax-M3.1-Flash-Previewfrom a session failed with
HTTP 401unlessMINIMAX_API_KEYhad been exported beforehand, even though MiniMax was
connected. Both agents resolve it the same way when the model is built (models/connections.rs,
models/connections.py), and a variable you already set still wins. $subagentsand$orchestration: the parent reviews the work and sends improvements back to
the same subagent. A subagent's summary is a claim, not proof. The parent now checks the diff,
the build and the tests itself. When something is missing or wrong it does not fix it itself and
does not start a new run: it sends a review to that subagent's own session (agent send, or
orch followupfor detached runs). The review lists each finding with its file and line and the
check to rerun. The parent reviews again until nothing is left, and moves a subagent stuck 3
rounds on one finding to a stronger model. The report gains a review-rounds column.- Bundled
$e2e-armyskill: agentic e2e tests with TesterArmy'se2e. It teaches an agent to
set up, write, run and debugnpx e2etests (e2e.config.ts,
tests/*.e2e.ts,agent.act/assertplus exactexpect, the replay cache,e2e explore).
Agent steps run on MiniMaxMiniMax-M3.1-Flash-Previewthrough@ai-sdk/minimax. The config
loadsMINIMAX_TOKEN_PLAN_API_KEYfrom~/.envbecause e2e reads no.envfile itself, and the
key is never printed. It is installed like$e2eand$subagents. - The model picker no longer offers a
cliproxy/id the gateway cannot serve. cli-proxy
advertises its catalog on/v1/modelsand drops a model from that list when the subscription
behind it signs out, socliproxy/claude-opus-5-5stayed in the picker while every run of it died
withauth_unavailable. The terminal picker and the web app now read that catalog and annotate
the entry with a warning, highlighted in the web list. Annotated, not hidden: the id stays
selectable, because the gateway may be serving it again by the time the run starts. A gateway that
cannot be reached is left unannotated, since a probe that found nothing is no reason to block a run. mini-tui doctor [-m <model>]- why iscliproxy/...failing? cli-proxy is a local gateway,
so acliproxy/model has four separate things that can break a run: the gateway process, the API
key both sides must agree on, the upstream subscription's OAuth login, and the model id the gateway
advertises. All four read the same in a transcript (a refusal, a 401, a 503 in the gateway's own
jargon), sodoctorwalks them in the order they break a run and names the first one that is
wrong, with the command that fixes it. Its last stage is mini's owntest-model, bounded to 20 s
so a gateway mid-backoff cannot turn a diagnosis into a hang; a green report therefore means a run
will work, not merely that a port answers.--jsonfor scripts.
Fixed
-
cliproxy/claude-opus-5-5works again. Two things were wrong behind the gateway. The Claude
OAuth subscription had to be re-logged-in (cli-proxy-api --config ~/cliproxyapi/config.yaml -claude-login -no-browser, plus an SSH tunnel for thelocalhost:54545callback on a headless
host) — without it the id vanished from/v1/modelsand every run died with
auth_unavailable. With it back, Anthropic still rejected the model: the gateway advertises a
built-inclaude-cli/2.1.63user agent and the API now requires 2.1.280 or newer for
opus-5-5.claude-header-defaults.user-agentin the gateway config now reports the version of the
Claude Code actually installed (2.1.281), which is what a real client would send. The gateway
hot-reloads the config, so no restart was needed. -
A failed
cliproxy/run now says which of the four cli-proxy stages broke, above its log tail.
The error banner reads the diagnosis frommini.logand prefixes it (cli-proxy (auth): ...),
keeping the raw log underneath for the details. -
A 401 that is the provider's own login is no longer reported as a bad API key. Behind a gateway
(cli-proxy, Rosetta) the key mini sends is usually fine while the upstream OAuth token is what
expired or was revoked, soOAuth access token has been revokednow reads "re-authenticate the
subscription behind this endpoint" instead of sending the user off to edit a working key. Both
agents (Rust and Python) agree. -
A refused connect to the local gateway fails in seconds, not after four minutes of backoff.
Whencli-proxywas down,cliproxy/claude-opus-5-5reported
ProviderError: ... :8317/v1/chat/completions: Connect error: Connection refused (os error 111)
only after the full exponential schedule (10 attempts, 4 to 60 s, 248 s), because the retry
policy treats every non-abort error the same. A refused TCP connect is the one such error that
retrying cannot fix by itself - the gateway is not listening, not slow - and the one whose cause
clears fastest, so it now re-probes at once and caps the rest at 2 s: the same ten attempts
report in 16 s. A gateway that comes back mid-retry is caught just as fast as before
(measured equal at a 3 s restart), and other transport errors keep the shared curve. The error
now also says what is wrong and what to check instead of only naming the socket.
mini-tui 0.29.0
Added
- Subagents owned by the session, in Rust (
mini-agent-rs agent). Orchestration used to start
every child as a detached headless run: a Python supervisor, thenmini-tui -p, then the agent,
with no control channel. The orchestrator could not talk to a running child, every follow-up was
a new run replaying the whole history (21 of the 64 runs on this machine), and it learned
progress by polling with its own steps. Now every Rust session runs a small hub, and the model
drives it from its bash tool:agent spawnstarts a child as one process for its whole life, with its own control file kept
open. A follow-up (agent send) is one message to the same process, with all its context, and
a message sent mid-turn lands before the child's next model call. A child that exited
continues from its saved conversation under the same name.- When a child finishes, fails, stalls or asks something (
agent ask, from inside it), the
parent gets a[subagent <name>] …message before its next step. A parent that already ended
its turn is woken by it. No polling. - Children stop with their session and count toward its cost limit; a child's budget is capped
by what is left. AfterLimitsExceeded, the nextsendgrants another turn's budget (new
STEPS/COSTcontrol lines). Children run on the parent's current model by default. agent ls | wait | result | tail | model | stopcover the rest. Limits: 8 live children,
nesting 2 deep. A run that starts no child writes exactly what the Python agent writes (the
parity suite stays identical).
- Subagents in the web app and the TUI. Each child is saved as a session under its parent
(parent_id), and its live agent is announced like any other. The web app shows a strip of
subagents above the transcript (state, steps, cost): click one to follow it live in the pane,
and its own strip links back to the parent./subagents [name]lists them or opens one. In the
TUI,/subagentslists them and/resumeopens one. - The
$subagentsskill (bundled) teaches the loop: split, spawn, keep working, steer, collect,
verify.$orchestration'sorchforwardsstart,followup,ls,status,tail,wait,
resultandstopto the session's hub when run inside a Rust session. Its detached headless
runs remain for your own terminal, the Python agent, and runs that must outlive the session
(ORCH_DETACHED=1).