Repository navigation
v0.39.0
Added
-
How it compares (
docs/comparison.md, a section inREADME.mdand its five translations). What
each engine that runs MoE models larger than a GPU runs on - llama.cpp, Strata, ds4, colibri,
KTransformers - which models, how it handles experts beyond RAM, and whether it configures itself,
beside the decode and prefill speeds each reports, with the source and date of every figure. It says
where the others lead (Strata and its Blackwell fork, two to four times faster where a model fits a
gaming GPU and RAM) and where they do not; the CPU-beside-the-GPU work that gap calls for is in
docs/roadmap.md. -
A run with flash, RAM and a GPU cache all in play: a 466 GiB file on a rented 96 GB
workstation card with a 283 GB memory limit, so the experts fit neither the card nor the RAM.
--autoput an 84.5 GB expert cache on the card and 207-215 GB in RAM, and decoded 6.2-6.6 tok/s
over 1024 tokens from a cold page cache against 2.8 for llama.cpp's ownllama-completion
(docs/bench-data/2026-10-28-rented-96gb/,docs/benchmarks.md). The file keeps a table in a type
upstream does not have yet, so the run used an experimental build, not the pin. A session of long
prompts on that box was killed by its control group: the plan's margin does not yet cover a long
prefill there. -
The first run on ROCm: a rented 192 GB AMD datacenter card, the model resident
(docs/bench-data/2026-10-28-rented-rocm/,docs/benchmarks.md,docs/platforms.md, the README
table). Qwen3.8-Flash-Next Q2_0 fits the card whole:--autodecodes 53.4 tok/s over 1024 tokens
against llama.cpp's own 55.6-55.8, and long prompts prefill at 2.1-2.3k tok/s. With ROCm's clang
the build hangs on ggml-cpu's AMX kernels; the bench notes give the line that builds the host code
with gcc. -
The agent, in the server (
docs/agent.md,docs/server-api.md#agent). A conversation held
by the server can work in a folder a person adds (a workspace): list, read, search, write and edit
its files, run commands, use the tools of MCP servers, and follow skills. Three modes say what
it may do without asking (ask,read_only,full); an approval card carries the diff an edit will make, and nothing has run when it
appears. Paths are confined to the workspace after resolving..and links; a command runs in
the workspace with a time limit as one process group, killed on a stop; a short list of programs
that only read runs without asking. A run goes on in the server with no page open and reports on
/api/events(conversation,item,delta); rounds wait in the engine's turn line like any
chat, and keep the prompt the KV holds. A context that runs short shortens the oldest tool
results, and says so. Conversations, workspaces, tasks, skills' switches and providers are kept
in<data>/agent/; a UI can import the conversations it kept before. -
Skills:
SKILL.mdfolders shipped beside the server (review-change,write-tests,
summarize-document,explain-codebase), written in the app, or kept in a
workspace's.bmoe/skills/; switched on and off per name. -
Tasks: a prompt that runs on a schedule (once, every so many minutes, daily, weekly, in local
time) as a conversation, read-only unless chosen otherwise, with approvals refused at once and
reported. -
Providers: a conversation can run on any OpenAI-compatible chat service, streamed, with the
tools as OpenAI tools. The key is stored in<data>/credentials.json, readable by its owner only,
and never returned, logged or put on a command line. -
Connecting other apps (
GET /api/connect,POST /api/connect/<id>,catalog/clients.json):
the settings for Claude Code, Codex CLI, OpenCode, an OpenAI SDK program and Open WebUI, filled in
for this server; the server writes Codex CLI's and OpenCode's settings files with a backup. -
Other coding agents in a conversation (
docs/agent.md, "Other agents"): on a computer that
runs programs, a conversation with a folder may be worked by OpenCode, Claude Code, Codex or Pi
instead of the built-in agent (still the default), over the Agent Client Protocol. How each starts
is theacpentry of its row incatalog/clients.json(OpenCode itself, the others through their
published adapters, versions pinned). Its text, reasoning and tool calls become the
conversation's items; the permissions it asks for are the mode's to answer, or an approval card
with its diff; its file reads and writes are served inside the folder only.GET /api/agentsays
which agents are installed; the web UI's composer offers them before the first message. -
--chat-only: conversations are plain chat (no tools, no agent prompt, no workspaces or tasks),
for the phone apps;GET /api/agentsays so. -
--skills-dir PATHnames the folder of bundled skills, for a package that keeps them elsewhere. -
The agent in the web UI (
docs/screens.md). A sidebar with the server's conversations by
folder and by date, the loaded model and every screen, folding to a rail and turning into a drawer
on a phone. Conversations are drawn from the server's items and follow itsitem,deltaand
conversationevents: tool calls with plain titles and states, consecutive calls summarised in a
line ("Read 3 files, edited 2 files · +14 −3") and expandable, edits as diffs, approval cards
that say nothing has happened yet, and notices; a past question can be edited and
sent again. The composer picks the folder (the app's folder dialog, or a typed path), the mode
(Ask first, Read only, Full access with a warning), the model (the local one or a saved provider)
and reasoning. A resizable preview pane shows a file the agent wrote: a page or a drawing runs in
a sandboxed frame, Markdown is rendered, code highlighted. -
Skills, Tasks and Connect apps screens, and a first-run setup: what this computer has in
plain words, the graphics pack that applies, the catalog's model that suits its memory, its
download, then a conversation. Settings gains Appearance (theme, text size), the desktop
app's own preferences, Providers (the key is write-only), graphics backend packs with Install,
and Check for updates. -
Every word in one dictionary (
ui/src/lib/text): components name a key, never the text,
and a test fails when the code names a key the dictionary lacks. -
Desktop notifications through the app's bridge when an approval waits or a run ends while
the window is in the background. -
A mock server for the web UI (
ui/mock/server.ts,npm run dev:mock): every route the UI
calls, with made-up data and a short agent run on each message, to work on the UI with no engine. -
The phone apps are chat apps (Android and iOS,
--chat-only): conversations are held by the
server, through the same API as the desktop's, but are plain chat with no tools, no agent prompt,
no workspaces and no tasks; the agent is the desktop's. The new look stays. Answer again is
under the last answer: it sends the last question again through the edit route, as the web UI
does. The empty chat says the model runs on the phone and leads to Models while none is loaded. -
Settings show what a person sets (web, Android, iOS alike). The parameter table marks the rows
a form offers (forminGET /api/params): every basic row and the few deeper ones a person
picks rather than the plan (which devices, speculative drafts, the context's precision, the
active experts), 14 of 79. The rest stay flags of the command line; one the person set anyway is
shown so it can be reset. The level filter, the per-level folds and the search are gone, and
every card starts closed. -
The desktop app serves the network too, the phones' card first in Settings > General: the
server listens on every address behind a key kept in<data>/network-key(this user only, New
key replaces it), the window's page sends it as well, and what reaches files, commands and keys
stays loopback-only.platform::lan_addresses()lists the addresses to show. -
Settings gather what you set in one card, "Set by you", first and folded like the others, on
the three UIs: every value the person set, in or out of the form, each with its reset. The rows
there leave their groups, so each row appears once. -
The network reads no agent data: listing workspaces (which name folders on the disk), skills,
tasks, providers and the clients to connect is loopback-only now, like changing them. -
The release feed has its key:
server/feed_key.hcarries the public half of the Ed25519 key
the release workflow signsfeed.jsonwith, so backend packs and updates read the feed. The
installers stay without an Apple or Windows publisher signature;docs/desktop-app.mdsays how
to open them the first time. -
The Plan screen names its model: "For " under the title on the three UIs, with
Change to Models, so Measure says what it measures for. -
The phones pick their theme like the web UI: System, Light or Dark in an Appearance card
(with the Metrics switch), applied at once, the system bars following it. -
Serve on the network is a card of its own, first in a phone's Settings: its state and address
in the closed header; the addresses to copy, the key hidden until shown with New key (on iOS too
now), the base URL a client needs and a warning that the traffic is unencrypted. -
The engine's errors name no model family: the MTP refusal said which models carry a head.
-
Metrics is a section of Settings, shown once the Metrics switch is on (off by default; on a
phone it also records the per-token CSV), instead of an entry of its own in the navigation. -
The plan's memory bar tells its parts apart: a six-colour series palette (
series-1…
series-6, light and dark, each 3:1 on a card) in place of theme colours that looked alike. -
bmoe-desktop, the app's own window (desktop/,docs/desktop-app.md). The web UI in the
system's web view (thewebviewlibrary, a pinned submodule: WKWebView, WebView2, WebKitGTK 4.1),
withbmoe-serveras its child process: started on the port of the app's preferences (the next
free one when it is taken, said in a notification), restarted when it ends on its own, stopped on
Quit, and stopping by itself when the app is gone (--parent-pid).window.bmoeDesktopgives
the UIinfo,pickFolder,notify,openExternal,setPrefs,restartServerand
applyUpdate, answered for the server's own pages only. A tray or menu-bar item (Open, Restart
Server, Quit) with close to it on macOS and Windows, and on Linux where libayatana-appindicator3
is found at build time; notifications, the system's folder dialog, one instance per user. -
Backend packs (
GET /api/backends,POST /api/backends/<id>/install). CUDA and HIP leave the
installers for packs of their own, which the app installs where one of a pack's driver libraries
loads: downloaded, size and SHA-256 checked, the archive refused if an entry would leave its
folder, unpacked into<data>/backends/<id>/. The engine loads modules from each folder there
after the executable's (register_backendstakes the folders;docs/backends.md), so a
read-only install takes packs too. -
Updates on request (
GET /api/update,POST /api/update/download): the installer of a newer
release, checked, for the app to run. Nothing reaches the network unless a person asked. -
A signed release feed. Every release carries
feed.json(scripts/make-feed.py, packs from
packaging/packs.json) and its Ed25519 signature, made with the repository'sFEED_SIGNING_KEY;
the server checks it against the key compiled in (server/feed_key.h, Monocypher vendored for
the check) before it believes a byte of it. The release does not go out with an unsigned feed. -
One installer per OS runs the app:
BigMoeOnEdge.app(its main executablebmoe-desktop, the
Swift launcher gone), the Windows setup and the AppImage (--serverfor the server alone). -
One-line install of the command-line tools:
scripts/install.shandscripts/install.ps1,
attached to each release, installbmoe-cliandbmoe-serverwith its UI into the user's folder
and ontoPATH, the archive checked against the feed. Thebmoe-cliarchives now carry the
server and the web UI; the separate desktop archive remains for the NPU build only.
Changed
-
A shorter, plainer README (
README.mdand its five translations): what it does, install, use and
how it works in short sentences. The planner section shows the Plan screen on a 16 GB Apple-silicon
laptop (docs/assets/screens/plan-mac.png) in place of the drawn plan, the speed figure goes since
the table says the same, How it works names the three tiers (flash, RAM, a card's own memory), and
How it compares is one row per platform and feature. The translations catch up with the desktop
app and the agent.scripts/readme-diagrams.pydraws the decode step alone, without the font the UI
dropped. -
llama.cpp moves to upstream
8ca4060of 2026-10-28, 132 commits pastd88040c
(docs/seam.md). The pin is still upstream plus the one expert-ready-hook commit; the 37 gates pass
on it. -
The Android chat renders Markdown with multiplatform-markdown-renderer (0.41.0, Apache-2.0),
GitHub-flavoured: tables (bordered in the line token, cells wrapping, scrolling sideways when
wide), task lists, strikethrough and autolinks now render, in the app's tokens and fonts. No image
loader is included, so an answer still fetches nothing, and HTML is not run; a link opens on tap.
The hand-written parser is gone. The app now builds with Kotlin 2.3 and its Compose compiler
plugin, AGP 8.13, Gradle 8.13, compileSdk 36 and Compose BOM 2026.06.01 (minSdk and targetSdk
unchanged), which the library requires. -
The iOS chat renders Markdown with MarkdownUI, GitHub-flavoured: tables (bordered in the line
token, scrolling sideways when wide), task lists, strikethrough and autolinks now render, in the
app's tokens. MarkdownUI 2.4.1 and swift-cmark 0.5.0 are vendored underthird_party/and built
as CMake targets; their network image loading is left out, so an answer still fetches nothing and
runs no HTML. The hand-written parser is gone. -
Engine messages name settings the way the reader sets them. A refusal from
validate()or a
model that will not load names a setting as a{key}placeholder from the parameter table
instead of a flag or a field (n_ubatch=… exceeds n_ctx=…,--device-cache-mb); one function,
name_params(), renders it, as flags inbmoe-cliand as the settings form's labels in
bmoe-server(configerror, the load error, chat and decide failures, the plan's reasons). The messages say what
to change in plain words, and a test fails on anyvalidate()message that names a flag or a field. -
A new visual language (
docs/design.md): slate surfaces with one cyan accent, light and dark
following the system or the person's choice, Inter for words and JetBrains Mono for code (both SIL
OFL, bundled), 8 px corners, 1 px lines, soft shadows, tabular digits for live figures, a visible
focus ring and reduced motion honoured. Every token is listed with its hex for both themes, and a
test checks every text pair against WCAG AA. The pixel logo stays as it is. Models, Plan,
Settings and Metrics are restyled with loading placeholders and empty states; Metrics is a calm
monitor of speed, disk reads, memory and the cache with their recent history. -
Conversations live on the server. The web UI no longer keeps them in the browser: those it
kept are moved to the server once, then removed from the browser. -
The preview of a workspace file has a policy of its own (
docs/server-api.md, "Security
model"):GET /api/conversations/<id>/fileanswers with a sandboxing Content-Security-Policy that
lets its own script run in an opaque origin and reach nothing, framed only by the UI; the UI's
policy gainsframe-src 'self' blob:for that frame. -
The phone apps take the agent and the new look. Android and iOS drop the pixel style for slate
and cyan, light and dark following the system: Inter for text and JetBrains Mono for code (SIL
OFL, bundled with the licence; the pixel fonts leave the apps), an 8-point corner, 1-point lines
and a soft shadow, Material 3 on Android and the system's own idioms on iOS. The pixel logo stays.
Each app keeps its colour values in one tokens file (ui/Theme.kt,Design/Theme.swift). -
The chat on a phone is the server's agent (
docs/agent.md): conversations held by the engine
process, rendered from its items (Markdown answers, folded thinking, tool cards with their state,
runs of tool calls folded under one counting line, diffs, approval cards, notices), with
edit-and-resend and Stop, and a composer that picks the mode (Ask first, Read only, Full access),
the model (the phone's or a provider's) and the folder. The conversations the apps kept on the
phone are handed to the server once and deleted only when every message arrived. -
Local-only routes are one list (
local_only_routes(),server/access.h), enforced before
routing with403andcode: "local_only"; deleting a model and adding one from a link moved
onto it, with the pack and update routes. -
The built-in agent follows llama.cpp's agent design (
docs/agent.md), so a model used to one
works with the other. Its tools are llama.cpp's server tools by name, parameters and results:
read_file(start_line,end_line,append_loc),file_glob_search(include,exclude,
type,max_depth,limit) in place oflist_files,grep_search(a regular expression or a
literal,include,exclude, line numbers, case, context lines) in place ofsearch_files,
write_file,edit_filewithedits: [{old_text, new_text}](several exact, unique,
non-overlapping replacements at once),exec_shell_command(timeout, default 10 s and at most
120,max_output_size, its output ending[exit code: N]) in place ofrun_command, and
get_info. Workspace confinement, the modes and their approval cards with diffs, the read-only
command allowlist, the process-group kill on a stop and the repeated-failure stop are unchanged. -
MCP servers (
docs/agent.md, "MCP servers"):<data>/mcp.json, or the file
--mcp-servers-config PATHnames, in the common{"mcpServers": {"<name>": {command, args, env}}}format, starts stdio servers the first time their tools are wanted and keeps them for the
server's life. Their tools reach the agent as<server>_<tool>(a name already taken is left
out), only in a conversation with a workspace and never for a request from the network; a call
is approved like a change (a card inask, refused inread_only, run infull).
GET /api/agentlists the servers, their state and their tools to a local client. The JSON-RPC
line wire is one class for ACP and MCP (server/json_rpc.*). -
The system prompt is short (under 4,000 characters with the tools apart, checked by a test)
and carries the workspace'sAGENTS.md, its first 8 KiB, when the workspace has one. -
The README's speed table is grouped by hardware, in whole tokens per second.
README.mdand its
five translations list each machine once (phone, iPhone, Apple-silicon laptop, Windows laptop, two
NVIDIA desktop cards, an AMD datacenter card), with its memory and where the experts are read from
(flash, RAM, GPU). The phone rows of gpt-oss-120b, Qwen3-30B-A3B and Gemma-4-26B-A4B and the
laptop row of Qwen3-Coder-30B-A3B repeated what the others show: they are indocs/benchmarks.md
now, with every exact figure.scripts/readme-diagrams.pylabels its
figures the same way (the bars keep the measured values). The description catches up with what
shipped since 0.36: CUDA and HIP among the released backends, the device memory as a tier of the
expert cache, plans from the model's own routing record, and the coding agents the server serves.
The translations also gain the CUDA and HIP accelerators and the rented-card runs they lacked.
Removed
- The agent's
fetch_urlandweb_searchtools, theweb-researchskill and the per-conversation
web approval: the web is an MCP server's job now. delegateand helper conversations (thehelperitem, a conversation'sparent, the UI's helper
row): one run per conversation.- The Obtainium link for Android.
README.md(and its translations),examples/android/README.md
anddocs/platforms.mdno longer point to an Obtainium entry: its redirect page depends on the
browser handing the link to the app, which often does nothing, and Android's developer verification
(from 2026-2027) makes a third-party installer a second thing that has to keep working. The APK is
installed from the release page, as before.
Security
-
The agent's routes that reach this computer's files, processes or keys answer only to a
connection from loopback, whatever--hostand key (403,code: "local_only"); a message from
the network runs with no file, command or MCP tools. -
A conversation with a workspace or a provider route is this computer's alone: from the network
it is not listed, read, changed, deleted or stopped, and its events and the tasks' reach local
event streams only. A client on the network may not give a conversation a workspace or a
provider route, nor turn on full access. -
A run stops when the model repeats the same failing tool call three times, with a notice saying so.
-
Commands that run without asking are only those that cannot run another program:
gitasks (a
repository's configuration can makegit statusrun one), and so dosort --compress-program
and the abbreviated long options of the refused ones. -
The first run on ROCm: a rented 192 GB AMD datacenter card, the model resident
(docs/bench-data/2026-10-28-rented-rocm/,docs/benchmarks.md,docs/platforms.md, the README
table). Qwen3.8-Flash-Next Q2_0 fits the card whole:--autodecodes 53.4 tok/s over 1024 tokens
against llama.cpp's own 55.6-55.8, and long prompts prefill at 2.1-2.3k tok/s. With ROCm's clang
the build hangs on ggml-cpu's AMX kernels; the bench notes give the line that builds the host code
with gcc.
Fixed
- An API client that never asks about thinking gets it again (#448). Since 0.35.0 thinking was off
unless a request turned it on, so an agent that sends no reasoning field (Pi with a hand-made model
entry, for one) lost the reasoning it had on 0.34.1, and a quantized model then often answered with
a tool call and no text. On/v1/chat/completions,/v1/messagesand/v1/responses, a request
that says nothing now thinks, unless the person setthinkat launch or in Settings; the apps'
conversations, which do not use these routes, are unchanged. - A session's teardown no longer frees the gate weights under the router's prediction worker.
The worker could still be scoring a layer when the source's shutdown freed the dense copy it read
from; TSan caught it onmainin the Nemotron-H gate. The teardown now joins the worker first. - A row-gathered table larger than RAM is no longer copied beside the expert cache. With a routing
record, the plan priced the expert cache past the device's share on the record's curve, which is
flat past the experts a few hundred tokens touched, so a 194 GB row-gathered table outranked
it for 0.1 ms a token, was called resident, and was copied by the dense policy outside the ledger
(417 GB committed against 215, the run killed when the cache filled). A row-gathered table now
competes at the cache's linear credit; one the ranking does call resident is held back unless its
bytes are charged or fit; and a table left mapped keepsrelease-mmapoff (docs/hardware-planning.md). - The device expert cache fits itself beside the context. The capacity fitter measures a graph
without the cache, and with it a wide graph needs a layer's experts in the device's compute buffers:
on a 24 GB card a 15.3 GB cache left a model's 2.4 GB of prefill buffers no room and the run
could not start. The session now retries the context with the cache a quarter smaller at a time,
down to one token cycle, and reports the size it kept. bmoe-server's launch flags outrank saved settings, as--helpsays (#435). The settings
layers applied a value saved by an earlier run after the flags of this one, so
bmoe-server -m A.gguf -c 32768loaded the model and contextsettings.jsonheld instead. The
order is now defaults < plan < saved values < launch flags; a value set from a client while the
server runs still replaces the flag's, as before.- An answer cut by a full context is said in the server's log (#436). A request that fits is
served with only the room its prompt left; an agent that summarizes its conversation near the limit
then got a cut or empty summary and reported only that compaction failed, while the server logged
nothing. It now logs awarnline with the tokens asked and given and the parameter that raises
the context. The README anddocs/server-api.mdsize an agent's context from its first request:
measured on a 16 GB Mac, an agent whose first request is ~17k tokens failed to compact at 32768 and
answered ten consecutive questions at 65536.