Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
-
The mail rules page. Mail → Rules (
#mail/rules) lists each rule's modes, activity and holds; a rule's page raises a group to automatic only through an approval screen showing its files, tests it against 30 days of mail, shows the ledger with Undo and Wrong match, and edits rule.toml. A thread's Make a rule… starts a rule from its sender, in shadow. -
Mail-rule web backend. CLI children support stdin rule creation, atomic edits with immediate approval lapse, digest-bound mode changes, per-rule pause and idempotent wrong-match corrections. List, show, ledger and retrospective-test JSON expose modes, holds, correction rates and action controls through feature-owned API routes. Doctor also reports automatic rules exposed through a shell's network access to serve's loopback port. The browser page follows separately.
-
Outlook header rules act on a visible match. A header rule matching on
list_idorheader.<Name>with only non-outbound actions now acts on an Outlook account when the header is visible and matches; a message whose header is not visible is skipped without a ledger row. Ledger rows recordheaders_partial. Rules with an outbound action, and description rules, remain held on Outlook. -
Automatic mail-rule passes, opt-in. Enable
mail_ruleswithmailfor five-minute header passes and an isolated post-classify hook. Timer overlap skips safely; typed failures fail the timer, and hook failures never fail classification. Doctor reports approval/store/provider/address and pending action problems, every automatic group under unconfined shell, and timer health. Pending rule proposals and lapsed approvals appear in review queues. -
Owner mail-rule CLI.
mecha mail rule add | list | show | mode | test | ledger | undo | pause-allinstalls in shadow, requires owner approval for per-group raises, and exposes read-only JSON. Testing searches 30 days across folders without writing; undo restores the exact mailbox message and records a correction for the automatic brakes. Provider commands run through the siblingmecha-mailbinary without searching PATH. Filtered apply requires a dry run so live passes retain sibling rules' safeguards. Undo records a durable intent before restoring mail and recovers interrupted corrections on retry. Timer and classify integration ship behind the separatemail_rulesswitch. -
Mail-rule pre-timer safeguards. Bounded pagination, injected-clock windows, visible expiry rows, unknown-activation holds, complete failure counts, one triage-overlay warning per pass, and shared mailbox-address validation.
-
Mail rule execution.
mecha-mail rules applyruns header rules in shadow, propose or approved automatic mode, with--headers,--after-classify, account selection and a dry run. The engine records per-action outcomes, defers classification-dependent work, recovers interrupted attempts, and applies failure brakes and task caps. Tasks use the configured graph MCP server and stay held when it is unavailable. Automatic scheduling is opt-in throughmail_rules. -
Approve persona memories as clean.
mecha persona memory approveand the Memories tab can now record the owner's vouch for a memory that came from outside content. It becomes active with owner origin, so recalling it no longer marks the new chat untrusted; memories approved the old way can be vouched later, and--chat <id> --cleanhandles a whole source chat. -
A persona's life profile, and a draft of it from what you wrote.
life.tomlsays who a persona lives with (people on their People tab), their usual night and what their days never hold, and their sampled days obey it: no household member the profile lacks, sleep near their usual times, no forbidden activity. Their own model can draft it from identity, motivation, routine notes and People-tab names into a waitinglife.draft.toml, which the Week tab andmecha persona lifelet you review, edit, approve or discard; nobody absent from the People tab is invented into the household. -
Persona day notes can tell their story. Per persona, Their events brings the day's drawn events, non-normal energy/stress/mood and closed weather into chats; it is off by default and needs Their days. Where they live resolves an owner-written town and region for weather, refreshed on the Week tab and in the background during eligible chats. Failed weather refreshes pause for an hour, and a later forecast updates the weather shown without changing events the day already drew. Place edits take effect in open chats, clear the previous place's weather before lookup, and town, region, country forms resolve each named part.
-
More chat models, one approval away.
mecha model cataloglists the chat models mecha knows how to serve — Qwen3.6-35B-A3B uncensored, Qwen3.8-27B and its uncensored and abliterated builds, and Gemma 4 26B-A4B — with what each would download and hold in memory on this machine, and whether its memory tier is offered it.mecha model add <id>shows that, asks, then fetches every file against its pinned sha256, restarts the router once no run is using it, and writes the provider entry — andfollow_loaded = trueon a default that names the router, so the model chip has something to switch between. In the web chat the chip lists them under Available to download, with Install and switch behind the download and memory figures.mecha model removetakes one back off. Catalog models now survivemecha setup chat. The uncensored Qwen3.6 comes with the production model's MTP head already grafted on, hosted atmecha-factory/Qwen3.6-35B-A3B-Uncensored-MTP-GGUF. (docs/MODEL-CATALOG-DESIGN.md) -
One LoRA registry for pictures and clips, checked against the files.
~/.mecha/loras.tomlholds both kinds; each entry saysmodel = "qwen-image"or"h3", and mecha reads each file's header to check it, leaving out (and saying) an entry whose file is for another model, cannot be read, or is not in the server's LoRA folders. Set[video] loras_dir(and[image] loras_dirfor a ComfyUI you installed yourself) before updating, to the folders each server loads LoRAs from: without it every clip LoRA is left out. The old[video] lorasfile is still read for this release while the new one does not exist;mecha doctorsays it has moved, and names every entry left out. -
Pictures use your LoRAs. Turn picture LoRAs on for every picture in chat in Settings → LoRAs, and per persona on its page (or let it follow the chat's list). At most three a picture. They apply on the standard and turbo models, not on a
.ggufmodel file. A trigger word the prompt lacks is added ahead of it. The picture's result says how many applied, never which. Behind the library lock, as the registry is. -
Settings → LoRAs. Add, edit and remove your LoRAs from the web app, picture and clip ones in their own sections, each shown while its feature is on. The Add list is the files each server already has, with the model each was made for read from the file. The pane sits behind the library lock. Clip changes reach clips when mecha next restarts.
-
Video clips can use your own LoRAs, places and clothes.
[video] loraspoints at a registry you write (mecha ships and names none): always-on entries ride every clip and the model picks the rest per clip; one with no file for the clip's H3 checkpoint is left out with a note. The image library gains locations (a place's text and up to three photos) and outfits (clothes, with a picture of the garment alone): a scene names a location as its setting, or an outfit as someone's wholewearing, and its picture goes with it. Add and lock both, and their pictures, in the Library tab.[video] guide_addendumappends your own notes to the video guide. -
Clips send a character's full body and labelled details. After the faces, a full-body picture goes to a cast of one or two when everyone shown has one (none when the clip is seen through someone's eyes), and each labelled detail, such as a tattoo, goes while H3's nine slots last. A tattoo that words alone put on both forearms stayed on one with its picture. What does not fit is counted in the result.
-
Pictures send labelled details too. A character's close-ups, such as a tattoo, go after the faces and bodies while a new picture's five images last, each tied to its person by name. A tattoo that words alone drew as a different design on both sides came out as the right one, on one side, with its picture. A picture planned in layers is drawn from portraits and sends none; what was left out is counted in the result.
-
Scenes are mended where a model's prose says more than its fields. A line quoted inside an action becomes a spoken line; "Wren's POV" makes Wren the camera, whose face never appears; names inside quoted speech stay as written; and lines go to the shot whose action says someone speaks.
-
mecha imagelib upscale: redraw a small portrait larger. The image model draws the same photograph again at about a megapixel, saved as a new version with the old one kept. With ArcFace installed the redraw is kept only when it is the same face (--outwrites it to a file instead). A 401 px portrait redrawn this way matched its character at 0.53 in a video clip, against 0.45 as it was. -
The library and persona pages load small pictures. Grid tiles and avatars now ask for a 512 px JPEG of each portrait (
?size=thumb-v1on the picture route), made once and kept beside the face crops, under the same lock as the picture. A 1024² portrait is about 1.5 MB; its thumbnail is a few tens of KB. -
mecha-mail authtakes a sign-in app you already have. Before the registration pages it asks whether you have one, from another machine for instance. On a yes, Google wants only the client file (drag it in, or Enter for the newest in Downloads) and Outlook its two ids; the pages are skipped.mecha setupruns the same question when it signs you in to mail. -
Put a face on a painted region. In the picture editor, each painted region now has a Picture button, and takes a picture dropped or pasted on its row: say "her face" and the region gets the face from that picture, with everything outside the paint left exactly as it was. A region whose words name a library character and its face ("Maya's face") takes that character's face (its
facereference when it has one). Measured on a full-body photo, the face landed at ArcFace .79–.85 over six renders against the one wanted, up from .10–.25. -
mecha imagelib face: check faces yourself.detectlists the faces in a picture,cropcuts the tight face crop an edit is anchored to, andcomparescores how alike faces are (ArcFace, -1 to 1) against a picture or a character (--character maya, using itsfacereference when it has one). It is the same detector and ArcFace the replay judges use, so the number matches theirs. ArcFace's weights are fetched the first timecompareruns, under InsightFace's non-commercial licence, which it says. -
The assistant's chat edits pictures in coloured regions, as persona chats do. Paint up to four regions, each in its own colour with its own words, and the change is drawn as one edit. Painting nothing still edits the whole picture from your words.
-
mecha features enable mailandenable docsinstall their servers, built from the same source as yourmecha, into~/.mecha/sidecars/. Amecha-mail,mecha-docsormecha-graph-mcpon yourPATHfrom another build is passed over and an entry running it is pointed at the matching one; one you built from your own checkout is left as yours.mecha --versionnow says which build it is:0.1.24 (git 7d7f0fa05). -
Voices lock like pictures and personas. Library → Voices has Lock and Unlock on each voice, and a lock box when saving a recording. A locked voice is hidden while browsing — off the voice library, the assistant's voice picker and a persona's Voice choices — until the library is unlocked, with nothing saying how many are hidden. Anything already set to it still speaks in it.
-
The assistant's chat shows how long a picture or clip has been running. A still-out row reads "making a clip… 2:14" or "drawing a picture… 0:41" ("working on it…" for another tool's background work), as a persona chat's already did, and keeps counting across a reload.
-
[[mcp]] disabled_toolswithholds single tools from a server. By the server's own names, narrowing only, and a name the server does not list is warned about on every start. For a status tool a chat only uses to poll work it already runs in the background (disabled_tools = ["video_status"]). -
Services an older mecha installed are refreshed. Every unit and launchd agent mecha writes now carries a revision;
mecha features enableandmecha setupfind one an older build wrote and offer to rewrite and restart it, downloading nothing current, andmecha features plansays which command does.mecha features enable <id> --reinstalldoes the same on request. Before this, an existing install kept its old services through every upgrade — on a Mac, ComfyUI never moved behind the on-demand proxy. The chat router's refresh ismecha setup chat's. -
Models can attach pictures to a library character. Ask a chat to save a picture to a character as a full body, a face, or a labelled detail such as a tattoo. A chat that has read nothing from outside attaches it at once, shown as from a chat on the character's page; one that has, or an incognito chat, stages it in the Library's Waiting pane for your approval (
mecha imagelib pending,approve-reference,reject-reference).[tools] disabled = ["image_library_reference"]turns it off. -
PDFs on a Mac, confined by macOS's own sandbox.
[documents] confine = "seatbelt", the default on macOS, runs poppler undersandbox-execwith a profile built for each call: deny by default, the program and the libraries its own load commands name, the scratch directory as the only place it reads input from or writes to, the home denied, and no network. Measured by new macOS tests in CI.bwrapandlandlockare refused on a Mac (andseatbelton Linux) by name;[sandbox] kind = "seatbelt"is refused — it confines the parser, notshell. On a Mac the parser's address-space ceiling (memory_mb) is not applied; its CPU and file-size ceilings and the per-page timeout are. -
mecha features enable graphinstalls the graph's MCP server. When nomecha-graph-mcpis on yourPATH, enable builds it with cargo from a pinned commit ofljchang/mecha-graphinto~/.mecha/sidecars/graph-mcp/and writes the[[mcp]]entry naming it by absolute path. Themecha-graphCLI is no longer part of the install; it stays an optionalcargo install. A failed build switches nothing on. -
mecha features enable documentswrites the[documents]table it needs, where there is none: OCR and layout as installed, and on Linux the confinement this machine runs. On a Mac the table names noconfine, so PDFs run under the macOS sandbox, the default there. -
The local chat model installs on a Mac.
mecha setup chat, and the chat question of the guidedmecha setup, now run on macOS: llama.cpp's pinned Metal build, the model recommended for the Mac's memory (the same rows as any unified-memory machine), and the router that serves it, installed as a launchd agent at~/Library/LaunchAgents/ai.mecha-factory.llama-local.plistthat starts at each login and logs to~/Library/Logs/mecha/llama-local.log. A router or agent mecha did not install is never written over. On every system, the router now runs mecha's own pinned llama.cpp, installed under~/.mechabeside anyllama-serveralready on the PATH (Homebrew's, say), which is left alone; only a machine no pinned build fits runs its own. -
A Mac's service logs are capped. Each launchd agent mecha installs writes to
~/Library/Logs/mecha/<name>.logthrough a small wrapper that rotates it at 20 MiB, keeping one older file — launchd itself never rotates a log. -
The embeddings and OCR servers install on a Mac, on demand as on Linux:
mecha features enable graph(ordocuments,personas) installs a launchd agent that holds the port and starts the server at the first request, stopping it after ten idle minutes — mecha's own small proxy, since llama-server cannot take a socket launchd holds. -
Voice installs on a Mac.
mecha features enable voiceinstalls speech to text, the voice worker and the speech server as launchd agents on Apple silicon, the speech engine built for Metal (it needscmake, from Homebrew). Each agent is read from the same unit Linux installs, so the two run the same command. -
Image generation installs on a Mac.
mecha features enable imageinstalls ComfyUI on Apple silicon with PyPI's torch (MPS) from its own hash lock, as a launchd agent. A Mac draws with the bf16 Qwen-Image 2.1 files — MPS has no int8 kernels, so the GB10's int8 file is emulated there and slower (~128 s a picture against bf16's ~79 s on an M5 Max) — and--turbofetches the turbo bf16 (~16 s a picture). The text encoder runs on the CPU there, adding ~13 s to a new prompt. There is no idle reset on a Mac;[image] unload_after_secsstill unloads.mecha setup engine --adoptis still Linux-only.
Changed
-
Building mecha needs Rust 1.96 or newer (it was 1.89). Code mode's interpreter (
monty-pool1.4) is now built in by default, and every one of its crates needs 1.96. A build adds about 50 crates, including a C build (aws-lc).cargo build --no-default-featuresstill builds a mecha without code mode, andmecha featuressays so by name. -
Continue on a picture is the only way to build on it. Every picture in a persona chat has a Continue button beside Edit and Regenerate: press it, say what changes, and the next picture builds on that one. Typing without it always draws a new picture, and the harness refuses a call that names an earlier picture on a typed turn, so the model never decides which was meant. A change to a picture sent while a reply runs waits for it (
IMAGE-SCENE-DESIGN.mdR11). -
A new picture starts from nothing, and a new chat starts with no scene. A persona's new chat no longer continues from her last picture, and within a chat a picture builds on an earlier one only when it names that picture, which the model is told to do only when the owner asks to continue, build on, edit or change it. A new picture takes no place, clothes or people from the chat's record. Reverses ruling R7 (
IMAGE-SCENE-DESIGN.mdR10). -
Settings → Voice picks from the voice library. The assistant's voice picker lists the library's unlocked voices as they stand, so a voice recorded a moment ago is there without a call first. It used to list the voices the last call reported, remembered on the device.
-
Incognito chat is part of the web app, not a feature of its own.
mecha setupno longer asks about it separately: it is on wherever the web app is on and the chat model runs on this machine. Anincognitoline already in[features]is no longer read, andmecha featuressays so. With a hosted chat model the incognito toggle is now hidden rather than shown with why it cannot be used. -
The front door is described as what it is: your outward-facing surface through mecha-factory — requests arriving typed and quarantined, and what mecha publishes getting a lasting link.
Fixed
-
Mail-rule review queues and timer diagnostics. A broken or deleted rule definition no longer hides other lapses or proposal counts. Doctor skips unavailable systemd queries while still reporting missing or failed units. The mail-rules queue follows the existing queues, preserving their order.
-
Mail-rule entrypoints and deadlines. Direct mail-binary undo and retrospective testing require both global feature switches before opening stores or providers. Doctor detects writable sandbox mounts reaching the rules store through ancestors or aliases without probing backend readiness: failed sandbox preflight prevents shell from running. Classify gives its rule hook ten minutes including SIGTERM grace and forced reaping, reports timeouts, and keeps its own result; daytime/nightly service budgets are 40/55 minutes. Re-copy both classify services and reload systemd when deploying.
-
Mail-rule review fixes. Emergency pause skips stray rule entries with a named notice while revoking valid rules. Unsupported automatic modes refuse before showing an approval screen. Undo of an unreadable definition durably lowers its historical groups before restoring mail. Rule views use the shared mail-directory resolver, and empty text lists and ledgers say they are empty.
-
Outlook archive and spam leave Sent Items and Drafts alone. Both now move only the conversation's Inbox messages, following every page of that folder's results. Empty Inbox matches say there is nothing to archive or mark as spam. Trash still moves the whole conversation, as Gmail does; unarchive and untrash retain their source-folder and destination rules. Triage results now carry completion status and successful/failed counts through MCP. Partial operations leave the CLI's triage record unchanged and in its queue. An archive already out of Inbox settles the record; empty spam results stay skipped because no junk training was confirmed. Missing Outlook conversations still return an error. CLI results retain the action and short handle, and restore messages name their action.
-
Pictures and clips no longer disappear from older chats. The nightly
mecha work cleancounted a chat's workspace like a morning brief and kept only the ten newest per surface, so every picture drawn in an older persona or web chat was deleted while the chat itself stayed. Conversation workspaces (web, persona, Slack) are no longer counted: they are kept until[work] retain_conversationssays otherwise,foreverby default, or a duration like90dmeasured from the newest file in the chat. Deleting a web chat still removes its files; persona chats and Slack threads have no delete yet, so their files are bounded only by this setting. Pictures already removed are not recovered. -
A new picture of two or more library characters no longer draws one of them twice. It now sends their portraits only, even when they have a full body attached, and a picture whose scene lists only library characters says exactly who is in its foreground, by name, each once, with anyone in the background a stranger. Before, a scene of two people touching drew three or four people in 19 of 21 test renders; now 3 of 21. A picture of one character still sends their full body.
-
Editing a photo of someone no longer redraws it from scratch. The first change that named who was in an uploaded photo, alongside any change to the room, light or camera, was drawn as a new picture from words, so the clothes, hair and body all changed. It is now an edit of the photo, and the people named are who is in it, never a second copy of them. Moving such a photo into a room photo in the call that names its people is now refused with how to do it: name them first, then send the room.
-
The model no longer redraws pictures it has not seen. It cannot see what it draws, yet it judged pictures wrong and drew them again on its own — once over a picture that was right. The image tool now tells it the owner sees the picture and says what to change.
-
A
forbidrule sees through macOS's command wrappers.sandbox-execruns the command given after it, as doarchandcaffeinatewhen one follows them, so an approval rule now judges that command, as it already did fortimeout,env,bwrapand the rest. Barearchandcaffeinate -t <secs>run nothing and are judged as themselves. -
The speech server's default voice keeps its transcript. The clip and its transcript are copied into the speech server's voices directory rather than linked to the model cache, so the transcript is never older than its clip — a cache filled by
hf downloadwrote it first, and the adapter then re-transcribed the voice and could overwrite the shipped transcript.
0.1.24 - 2026-10-13
Added
- A server's tool can run as a background job. An MCP server marks a
tool
_meta: {"mecha-factory.ai/background": true}in its tool list, and a call to it answers "being made" at once and runs past the turn as the chat's job — queued, reorderable and stoppable, like a picture — with the result landing on the same row when it is done. The call carries the chat's folder as an absolute path (_meta["mecha-factory.ai/workspace"]) for the server to write into, and a result whose first line isvideo: <path>.mp4, the path relative to that folder, plays under its row, once per clip. Stop sends the servernotifications/cancelled, a job is bounded at two hours, and a job a restart ofmecha serveleft running is settled as not made rather than left with a dead Stop. Only the server can opt a tool in; no setting does, and a tool that sends to a recipient the model names is never deferred. - Persona chats can be given a server's tools.
[[mcp]] personas = true— global config only, off by default — is your statement that the server reads none of your private stores. Each persona still lists the tools it may use, and a persona chat still refuses a tool that is not read-only — an MCP tool is read-only only where its server declaresreadOnlyHint— or that sends to a recipient the model names. A persona's background call names its folder, its library character and its voice-library voice in_meta["mecha-factory.ai/persona"], so a server that makes sound can use the persona's voice; never its prompt, display name or anything it said. While such a job runs the chat says "working on it…", and the call screen says so beside a picture being drawn. No server's background job reaches an incognito chat.
Changed
- argon2 0.6 and rusqlite 0.40. A library lock set before the upgrade still opens; a new lock's salt comes from argon2 itself. A dashboard query whose timeout or read-only authorizer cannot be installed is now refused rather than run without it.
Fixed
- Switching on mail, documents or the graph finishes wiring them.
mecha features enable mail(anddocs,graph) wrote only the switch, so the feature still read off with "add an [[mcp]] entry" — andmecha setup's yes to mail stopped there instead of opening the sign-in. It now writes the[[mcp]]entry the docs give where none runs the program, with its results read as untrusted and the program by its absolute path; mail's sends, replies and calendar changes and documents' writes are staged in the outbox when you have no[outbox]table yet, unless a[[rule]]of yours names one of them. An[outbox]you wrote is never edited: enable names the routing it lacks. A program that is not installed is named with its install command. - Voice recording and cloning work after
mecha features enable voice. The installed speech server reads voices from~/.mecha/sidecars/speech/voices, but nothing named that directory in[voice] voices_dir, so recording a voice said "not configured" after a complete install. The install now writes it where there is no[voice]table, says the line to add to one you wrote, and warns when the one named differs from what the server reads. The "not configured" message names the installed path.
0.1.23 - 2026-10-12
Added
-
Signing in is a page in your browser.
mecha-docs authsigns in with mecha's own Drive app, so Google Docs needs no Cloud console project.mecha-mail authwith nothing after it asks Google or Outlook and a name, and, the first time, walks you through registering your own sign-in app (mail ships none: a shared Gmail client would be a phishing kit under mecha's name), opening each console page in turn and picking up Google's downloaded client file;--client-jsontakes that file directly.mecha setupruns it on a yes. Sign-in pages now open on macOS too (open; it usedxdg-openonly), and Outlook's device-code page opens in the browser where there is one. -
The installed image server gives its memory back.
mecha features enable imagenow also installsmecha-comfyui-idle.timer, which restartsmecha-comfyui.serviceonce it has held a model unused for 15 minutes, as the hand-built server'smecha-comfyui-idle-resetalready did. A loaded server holds ~13.6 GB; a restarted one, ~1.1 GB. -
Picture settings for chat. Settings → Images sets how pictures are drawn in every chat: model, steps, guidance, sampler, scheduler and the negative prompt. A persona's picture settings now fall back to these field by field and override any of them; a persona on a different model takes that model's own numbers. A persona's unset model or negative is now inherited rather than standard/off, so a persona with nothing set draws as the chat does.
-
Speech to text installs itself. On Linux,
mecha features enable voicenow offers the speech-to-text server: an environment built from a hash-pinned lock, the Parakeet model fetched by its pinned sha256, andmecha-parakeet.servicestarted on:8992, all under~/.mecha/sidecars/stt/. The model's 465 MiB counts in the plan's total. A Parakeet you already run on:8992is left alone and used as it is. -
The speech server installs itself. On Linux with an NVIDIA GPU and the CUDA toolkit,
mecha features enable voicenow also offers Breeze TTS 2:- its engine, built from a pinned commit of the published qwentts.cpp port;
- its converted weights and a default voice, fetched by sha256 from
mecha-factory/mecha-speech-ggufand counted in the plan's total; - the adapter, from a hash-pinned lock;
mecha-breeze-tts.serviceon:8886andmecha-breeze-adapter.serviceon:8887, checked by speaking a sentence.
The weights are licensed for research and non-commercial use only, which the install says before fetching them. A speech server you already run is left alone and used.
-
The turbo image model is fetched on demand.
mecha features enable image --turbofetches it (6.8 GiB, by its pinned sha256) into the image server mecha installs, andmecha features enable imageoffers it unasked once a persona's picture settings choose turbo. A turbo picture refused for want of the file names that command. -
The image server installs itself. On Linux with an NVIDIA driver 580 or newer,
mecha features enable imagenow offers ComfyUI: the pinned release fetched by its commit, torch for CUDA 13 and ComfyUI's requirements from a hash-pinned lock (about 3.6 GiB, counted in the plan's total), the three Qwen-Image 2.1 files by their pinned sha256, andmecha-comfyui.serviceon:8188, all under~/.mecha/sidecars/comfyui/. With no[image]table in your config it writes one naming the server's temp directory; one you wrote is left alone. A ComfyUI you already run on:8188is left alone and used as it is. -
The voice worker installs itself.
mecha features enable voicealso offers the Pipecat worker: its environment from a hash-pinned lock, the worker run from a copy mecha writes, andmecha-voice-worker.servicestarted on:7860under~/.mecha/sidecars/voice-worker/. It takes calls only throughmecha serve's relay, so no hostname needs setting. A worker you already run on:7860is left alone and used as it is.
Changed
-
mecha setupno longer opens an editor for the charter. A yes ranmecha charter edit, which took over the terminal mid-setup and ended setup wherever the editor left it. The step now says where to write it (mecha charter edit, or Settings → Charter) and setup carries on. -
A local provider with no
modelasks for what its server serves. A[providers.local]on this machine that names nomodelused to sendgpt-4o-mini— which a llama-server router does not serve, and which every session record then named as the model that answered. It now asks for the router's loaded model, or its only one, or a plain server's alias, read once when the command starts; a router with several and none loaded is reported rather than guessed at. The same holds with no config file at all, where the built-in local provider is the default. -
mecha setupis one guided pass. At a terminal it asks about the chat model first — the one recommended for your machine, a GGUF you have, or a hosted model — then each feature as a yes or no with what it downloads, then shows everything and the total and asks once before installing it all. No more stopping to runmecha setup chatand coming back. -
A new install uses a model on your machine, not Anthropic.
mecha config initnow makes the local provider (127.0.0.1:8080) the default, with Anthropic kept beside it for-p anthropic;mecha setupnamesmecha setup chatto install a local model when nothing answers there. An existing config is not changed — but one that never setdefault_providernow answers from127.0.0.1:8080, and a[providers.local]that names nomodelorcontext_windowis reported bymecha setupuntilmecha setup --writefills them in from the server. -
mecha features enableno longer installs the chat model. Switching on the web app or documents used to offer the chat model's ~22 GiB download on a machine without one; the chat model is chosen and installed bymecha setup chat, andenableinstalls only what the feature itself runs. -
Each picture's prompt is saved for you, for now. Beside every kept chat's transcript,
<chat>.prompts.logholds one line per picture: what was drawn, how, and the prompt the image model was given, so prompts can be studied and improved. Nothing reads it but you, an incognito chat keeps none, and forgetting a chat removes it. -
A persona's picture shows what you asked for. A persona copying its own earlier picture calls often left your request out, drawing the same scene again. Each picture call is now checked against your words and the persona's last reply, and whatever the call left out (what people do, what they wear) is filled in. When you ask what the persona would like, the picture follows what it said.
-
Pictures of people doing something together put each of them in their own part. A persona describing an act between two or three people used to get a lineup, with the act applied between neighbours and sometimes a person drawn twice. The scene is now split into what each person does and where they stand before it is drawn, so whoever hands the cup is the one holding it out. It adds a second or two per picture.
-
A picture asked for while another is being drawn waits its turn instead of failing. A chat draws one picture at a time and keeps up to three more in line behind it, in order; a panel edit waits its turn too. A chat shows its line while two or more are out — the one drawing, with its clock, then the ones waiting — and each can be stopped on its own or dragged (or moved with ↑/↓) into a new place; the one drawing never moves. Stop on a picture stops that one; the call screen's Stop stops them all.
-
A picture asked for in a style the library doesn't have still draws. A model that wrote a look like "hyperreal render" or "oil on linen" where only a library style's name fits was refused every time, and in a persona chat it retried until the run gave up with nothing drawn. The style is now left out, the reply names the styles the library has, and the picture draws. A style written as words ("Digital painting") finds the library's
digital-painting, and the tool's description lists the styles, so a persona chat can use them; locked styles and character names are never listed. -
The image server stays loaded for a quarter hour after a picture. A picture asked for within 15 minutes of the last one is drawn warm: on 2026-10-08 a cold one (the server restarting, then loading the model) took 145–180 s against 36–63 s warm. The cost is about 14 GB held that long.
-
A retouch keeps who is in the picture. Edits got faster on 2026-10-08 by sending smaller reference pictures, but a retouch has no face crop to carry who is in the picture, and at the smaller size faces drifted. A retouch, and any edit where someone in the picture has no face crop, now sends the picture at full size again; changing one person's clothes or expression in a picture of just them stays fast.
-
An edit from the picture panel is drawn by mecha itself, and the persona only answers. Your words and what the picture was drawn as are read into a typed change by a quick one-shot pass, the change is drawn, and the persona replies in a line or two without a tool. A painted area is drawn as written. A name the library doesn't hold, or a change that can't be read, is said on the card instead of drawn as a stranger, and nothing is drawn. Asking for another try draws the same scene again at a new seed.
-
Pictures are described as a scene, and the tool writes the prompt (
IMAGE-DESIGN.md).image_generatetakes ascene:setting, which is everything but the people and the words, as text or a room photo;light,camera,style,together(what the people do with each other) andtext(words to render exactly);people, each withwho(a library name,self, or a description),where,wearing,doingandexpression.
To change a picture, name it in
pictureand send only what changes:- A new pose, the camera, the setting, the light or a removal redraws everyone on the scene's setting, so the camera actually moves and faces don't drift down a chain of edits.
- New clothes, an expression, someone added or the words edit the picture, with a head crop of each person it changes.
- Restating what is already true changes nothing, and a call that changes nothing draws the scene again at a new seed.
- One small change to the picture itself goes in
retouch, with a paintedmaskif there is one.
Up to five people with faces fit one picture.
prompt,cast,extras,edit,negative_promptandreference_imagesare gone, and a chat's schema has noseed(mecha runand evals keep it). -
Every picture remembers its scene. Each picture drawn in a persona chat, in the assistant's web chat, or in an incognito chat records where it is, who is in it, and what they wear and do. The incognito record stays in its room. A picture brought into another chat is recognised by its content and keeps its scene. Deleting an assistant chat, or forgetting a persona chat's memories, removes the records it wrote. Each persona turn is told what its last picture showed, so a remembered day's outfit or setting no longer slips into the present.
-
An edit carries the faces of the people it changes. Each library character an edit places or changes comes in as a head crop of their portrait, beside their description, so a chain of edits no longer drifts into somebody else. Someone is drawn only when listed as a person: a library name in the other words, for someone not in the picture, is read as "the viewer" rather than drawn as a stranger, and the result says so. A name still waiting for approval is refused.
Security
- An experiment environment can no longer set
[documents]. A project'smecha.tomlwas already stripped of it, but an environment'sconfig.toml— which arrives with a checkout — could point OCR at a remote server or run the PDF parser unconfined.mecha expnow refuses such a file, as it does[image],[messages],[slack],[web]and[harness].
Removed
- Break reminders and the farewell check are gone from personas. Both
were switches on the settings page that did nothing yet; they were dropped
rather than built. A
persona.tomlthat still hasbreaksorfarewellunder[safety]loads as before, and the persona's notes say those lines are read by nothing and can be deleted.
Added
-
Picture settings per persona. A persona's settings page has a Picture settings panel beside the touching-scenes switch. It sets the model (standard or turbo), steps, guidance (cfg), sampler, scheduler and whether a fixed negative prompt is sent. Presets are offered for the default, a guided setting and turbo, with a rough time per picture. A persona nobody tuned draws exactly as before. Switching models unloads the other one first, and the turbo model needs one more file (
[image] turbo_model).mecha replay --personatakes the same settings as flags. -
llama.cpp is built on your machine when no release build fits it. An NVIDIA driver too old for the release's CUDA build, or a Linux architecture no release covers, used to get no engine at all; now
mecha features enableandmecha setup chatbuild the same pinned version from its commit, checked the way a release is. It needs git, cmake, a C++ compiler and, for CUDA, the toolkit'snvcc— anything missing is named before you are asked.mecha setup engine --upgradestill fetches release builds only, so a built engine moves with mecha's pin. -
Regenerate a picture, and step through its versions. A picture in the assistant's chat or a persona chat has a Regenerate button beside Edit. It draws the same picture again at a new seed, with no words and no painted area, and the new one shows on the same card, with ‹ › to step between the versions, as soon as it is drawn. Edit, Regenerate and Download act on the version showing. Regenerate is off while the persona is answering. On a call, Regenerate is in the picture viewer: the persona answers aloud, and talking over the redraw does not stop it.
-
mecha replay --persona --rerecordreplays an older chat as it would be recorded today. A tool call that ran away to the output limit is shortened to its start with a note, and the answer to it is the one the harness gives now, so a chat from before that fix is replayed with the history this build would have kept. -
mecha replay --judgeknows who is who. Each face in a judged picture is matched to the scene's library characters by their portraits (ArcFace, fetched the first time it is needed, with its non-commercial research licence stated), and each person's clothing and action is checked on that person, and on everyone else, so an outfit drawn on the wrong person is counted as a bleed. -
mecha replay --judgescores a persona replay's pictures. For each picture a replay drew, it counts faces and people against the scene's record, and asks a vision model whether each thing the record says is in the picture (and, for an edit, whether it was already in the picture it started from). It writes the verdicts beside the samples and a contact sheet with one row per file, so two arms can be compared by eye and by count. -
mecha replay --personasamples a persona chat's turn. It branches at one of the chat's turns (--listshows them) and sends that turn's first request again,--samplestimes with a different seed each, as the chat would send it today.--overlaytries a wording change. The samples go to a private file under~/.mecha/research/replay/. The command waits while you are using the model or on a call. A picture the turn asks for is drawn, against the chat's scene as it was then, in a scratch copy that never touches the chat (--no-renderskips it).--at-callbranches partway through a turn. -
The face detector behind edits. Head crops come from a detector that runs inside mecha: install it once with
mecha imagelib install-face-detector(89 MB). Without it, edits draw as before, with each person's description and clothes, and the manifest names whose crop went. -
mecha setup chatgives a machine with no model server a local chat model. It shows the model recommended for the machine's memory tier, or takes a GGUF you have; installs llama.cpp if there is none; installs and starts the router as a systemd user service on:8080; waits for the model to load; and offers to write the local provider from what the router reports. A router you set up by hand is left alone. Linux only. -
mecha features enableinstalls the embeddings and OCR servers. On a Linux machine without them, enabling a feature that uses one fetches its pinned model, writes its launcher and the systemd user units that start it on its first request and stop it when idle, and starts it once to check it answers. A server already set up by hand is left alone. -
mecha setup engine --upgrademoves to a newer llama.cpp — only if it measures no slower. It finds the newest release with a build for this machine (or--to <tag>), asks you to confirm that tag, checks each archive against the digest GitHub publishes for it, and measures it against the build the servers run with the same gate as--adopt. A win moves the servers to it and keeps the old build;--rollbackgoes back one build. Two builds stay on disk. -
mecha setup enginesays which llama.cpp each server runs, and--adoptmoves them onto mecha's — only if it measures no slower. It runs each server's own launcher on a private port with the old engine and then the new, loading the model you have loaded, measuring generation and prefill and checking chat, embeddings and OCR, and records the result in~/.mecha/sidecars/llama/ledger.jsonl. The router is stopped while it measures, so it asks first and declines if anything is running.--rollbackputs the servers back on the llama.cpp you installed, which--adoptleaves in place. -
mecha features enableinstalls the llama.cpp engine. On a machine without one, enabling a feature that runs something on it offers the official release for this machine — CUDA by the driver's version, Metal on Apple silicon, else CPU — checked by sha256, into~/.mecha/sidecars/llama/<tag>/behind acurrentlink, with every library found beside it. A machine whose chat runs elsewhere is not offered the engine for a feature that runs nothing on it. -
mecha features enableinstalls what the feature needs. It shows the plan, installs on one yes, and switches the feature on only after the install has passed its check. The first installer is layout's: a pinneduv(or one already onPATH), the Python 3.12 build it fetches, the hash-locked onnxruntime and numpy, and the pinned PP-DocLayoutV3 model, all under~/.mecha/sidecars/layout/, where document extraction now looks first. Answering no writes nothing.--no-installwrites the switch alone. Without a terminal, an enable that would install refuses and says so. Anything already on the machine is left alone. -
mecha features plan <id>says what a feature would install. It lists every program the feature runs beside mecha — llama.cpp, the router, the embeddings and OCR servers, ComfyUI, the voice servers — and whether this machine already has each (a unit, a binary, a directory or a Docker image that shows it, which is then left alone). Then it lists every pinned model file: in the cache and matching, to download at its size, or kept by a provided program. It installs nothing and asks no server;--verifyhashes model files placed by hand. -
A voice that takes direction is told how to say each sentence. When the speech engine honours
instructions(Breeze TTS 2), the worker asks the voice facade's new director for each sentence — one line on emotion, energy, pace and pitch, written by the model already loaded, with thinking off — and sends it with the words. The first sentence's direction starts from your words as the turn begins. Every direction is recorded in the conversation's transcript as aspoken_directionrecord, except in an incognito chat, where it is directed and nothing is kept. On a voice that does not take direction, nothing changes and no extra call is made. -
A streaming voice is no longer told to keep it short. When the speech engine streams audio as it synthesises (Breeze lists
"streams": true), the worker says so (X-Voice-TTS-Streams: 1) and a spoken turn opens with a block that keeps every voice rule but the length ones, and a persona's call note drops "short sentences, the first one short". Those rules were a latency control for an engine that speaks a sentence only once all of it is made; on Chatterbox the prompt is byte-for-byte what it was. -
A Breeze TTS adapter.
scripts/voice/breeze_server.pyserves the same speech interface as the Chatterbox server, in front of Breeze TTS 2 on a qwentts.cpp fork. It transcribes each voice clip once with Parakeet (an editable.txtbeside the WAV), registers voices with the engine, speaks the default voice as the clipdefault.wav, passes delivery instructions through, and stretches when speed is not 1.0. Units for the engine and the adapter ship inscripts/voice/and are installed by hand;docs/VOICE-BREEZE-DESIGN.md§5 says which gates the switch left open. -
mecha features --probeadds up what your models would hold. Every model the features you have on would load — the chat model, embeddings, OCR, layout, image generation, speech to text, speech and turn detection — with what each costs, how it holds memory (resident, on demand, released on idle, per request) and on what evidence, summed against this machine's memory: one pool on unified memory, or the card and system memory separately with a separate GPU. A sum with an unmeasured figure in it is shown as unknown, never as zero, and what the figures leave out — the chat server's process memory, the router's prompt cache — is named. It reads/proc/meminfoandnvidia-smiand asks no server. The hardware page's Beside the chat model table is now generated from the same rows, which also pin each model's source (revision and sha256) for the installers to come.mecha features --jsonnow warns about an unknown[features]key on stderr too, as the plain listing always did. -
An idle image server gives its memory back. A new user timer (
scripts/comfyui/install.sh) restarts ComfyUI once it has held a model for fifteen idle minutes, with nothing queued and nobody connected. That takes it from ~13.6 GB to ~1.1 GB with its port still up. The next picture loads the model again, ~15–22 s. mecha's own unload timer dies whenevermecha serverestarts, and on unified memory it only moved the weights into the server's process memory. -
See and edit what a persona remembers. A persona's editor has a Memories tab: its conversations, what it knows about itself and about you, and anything waiting for your approval, each with the day it was said. Keep what is waiting, edit a fact's wording, pin, share a fact about you with everyone or a group, or forget something for good.
-
The uncensored Qwen3.6 model is about 40% faster. Its download lacks the multi-token-prediction head that lets production draft ahead;
scripts/mtp-graft.pycopies the head from production's file, and the router speculates with it (99.8 against 69.9 tok/s, one stream; the router's four slots are unmeasured). Without the grafted file the router serves the model as before and prints the command that builds it. If four slots turn out slower, removing the twoSPEClines from the preset instart-router.shturns speculation off again. -
The hardware page covers a separate GPU, and every model beside the chat model. Each memory tier now has two columns — unified memory, and a graphics card with system RAM beside it — and a new Beside the chat model table gives what embeddings, OCR, layout, image generation and the three voice models cost on the GB10, whether each holds its memory always, on demand or per request, and where every number comes from. Only measured cells say measured.
-
A persona's voice is picked in its settings. The settings form has a Voice field listing the voices the voice worker has, so
voice = "…"no longer has to be typed intopersona.toml. A save is checked against the list as it stands then; when the worker cannot be asked, the field says why and keeps the voice already set.voice_speedis still edited as text. -
Voice and persona settings in the config file. A new
[voice]table holds the speech-to-text address dictation uses (stt_url, until now fixed in the code), the voice worker's address and the voice facade's port (until now onlymecha serveflags, which still override them for one run), andvoices_dir. A new[personas]table setscrisis_cooldown_minutes: after a crisis pause, how long a further crisis message in the same chat does not pause again (15 by default). Both are read from your global config only.[web] voices_dirhas moved to[voice] voices_dir; the old key still works for one release. The speech-to-text and voice worker addresses must be on this machine, in[voice]or inmecha serve --offer-target: a voice worker on another host is now refused at start rather than reached. -
Type during a call. A call, with the assistant or a persona, has a text box under its transcript. A typed line is a turn like a spoken one, and the answer is spoken in the call's voice. The microphone pauses while the box has focus and comes back as it was; the mute button stays yours.
-
Personas can look things up in their own memory. Two new persona tools, offered whenever a persona's memory is on:
memory_searchfinds what it remembers when it needs something you did not just mention, andmemory_readopens a remembered conversation in full, so it can quote what was actually said rather than the summary. Conversations in its memory now show a short id in brackets for that. Reading a past conversation that took in outside content marks the current chat the same way. A persona set to answer only from its files still remembers, but reads a past conversation as what you and it said: whatever a tool brought back, such as a web search, is left out. A long conversation keeps its ending when cut. -
Listen to a reply. Under each finished reply, beside Copy and Download, Listen reads it aloud: a persona's in its own voice, the assistant's in the voice you chose in Settings → Voice. Code, links and formatting are not read out; a code block is said to be there. It starts after the first sentence and stops when tapped again.
-
Library → Voices. Every voice the voice server can speak is listed in the library. Tap one to hear a sentence spoken in it, record or upload (WAV) a new one, delete a clone, and see which personas speak in each. Recording and deleting moved here from Settings → Voice, which keeps the assistant's own voice and rate.
-
Make a persona from the main chat. Ask for one ("help me make a persona who…") and the assistant drafts who they are and what they want, and stages it with a new
persona_proposetool. It waits on the Personas page under Waiting for you: read it there, then Approve or Reject. Approval accepts exactly the text you were shown — if the chat revises it meanwhile, you are asked to read it again. Ask the chat for changes and it revises its proposal until you approve; after that only you edit it. If it links a portrait that is also still waiting, Approve both takes them together. A persona proposed in an incognito chat starts hidden behind the library lock. The chat can set the name, display name, relationships, identity, motivation, portrait and voice; tools, files, memory, safety switches and the lock stay yours. Once you edit a waiting proposal yourself, it is yours, and the chat can no longer change it. -
Download a generated picture. Every picture a chat draws — in the assistant's chat or a persona's — has a Download button beside Edit, which saves the file under its own name. It works in incognito and locked persona chats too, and leaves no picture address in the browser's history.
-
Personas recall during a chat, not only at its start. Each message you send after the first reply is searched against what the persona remembers — by your words and by meaning, recent first — and the few things it calls up that the chat has not already seen ride along with it: an older conversation, a fact that did not fit at the start. The nightly memory pass now also prepares memories for searching by meaning; without the embeddings server, recall works by words. Forgetting a memory removes it from the search index too.
-
Copy and Download on every chat reply. Under each finished reply, in the assistant's chat and a persona's, Copy puts the reply on the clipboard as written — its Markdown, not the rendered page — and Download saves it as a
.mdfile named for who said it and when — in an incognito chat too, since the file is made in your browser and the server keeps no trace of it. Every code block in a reply has its own Copy. -
Call a persona. A persona chat has a call button beside send, where calls are switched on. The persona answers in its own voice (its profile's
voice, which the voice server must list), and what is said lands in the same chat. A locked persona can be called only while it is unlocked, and locking it again ends the call. A crisis message on a call is answered aloud with the same plain words the chat shows. Call minutes join turns on the persona's usage meters. The voice worker must be restarted on this version before a persona with a voice profile can be called; until then such a call is refused, naming the reason. -
Replies read as formatted text. In the assistant's chat and in persona chats, a reply's Markdown (headings, bold, lists, code) is rendered rather than shown as
##and**. Double-click a reply to see it as written, and again to go back. Rendering is safe for words a third party wrote: no raw HTML, links only to web and mail addresses, no images fetched. -
A persona's page is easier to work with. Drop files onto its Files tiles; a file you add shows as uploading… at once. Click a file to download it, or to read its text once it has been read. Earlier chats are listed by what they were about (the persona's memory summary, the goal, or how you opened it) with the day and time together. The goal box is now a Set a goal for this chat link. In a chat, your message no longer shows the goal framing sent ahead of it, and repeated calls to one tool show as one row (
file_read ×6). -
Personas remember you in the next chat. A new chat with a persona starts with what it remembers: your about-me notes, what it knows about you (and what you shared with it), its own facts, and its last few conversations, each switch on its settings page honoured. Memory that came from something read from outside — a search, a file — marks the chat as having outside content, as reading it again would; ordinary memory does not. Searching memory during a chat comes next.
-
A persona's picture can be framed. Tap the picture on a persona's page to open it in the circle: drag it into place and zoom, then Save; Reset puts it back. A persona that gets a new portrait — made with one, or given one in its settings — opens it there straight away. Until you frame it, a tall portrait shows its top — the face — rather than its middle.
-
Hiding a persona behind the library lock is a switch in its settings, at the top of the Settings tab, rather than an item in a ⋯ menu. The menu is gone.
-
Save a persona's reply to its files. Under each of a persona's replies there is a Save to files link: a study guide, quiz or glossary it wrote when you asked is saved as a Markdown file in its own folder, there next time (and searchable), and movable to a group's folder to share. Only the persona's own reply is saved, never text it did not write. The
teacherstarter now asks before revealing quiz answers (new installs; an installed copy is yours and is not changed). -
Personas start remembering.
mecha persona memory writereads each persona's chats and records an episode and facts per stretch of conversation, in the persona's own memory. The nightly runs it. Each chat is written on this machine by the model it ran on, and only while that model is loaded — it never swaps out the one in use. Chats changed in the last 15 minutes, and test chats, are left alone, and each turn is read once. A stretch after the chat read anything from outside — a search, a file — is kept apart: what it produced waits for your approval (mecha persona memory approve), and it can never change or withdraw what the persona already knew. Nothing yet brings memories back into a chat; that is the next step. -
The library and personas lock themselves again. After 15 minutes with no one touching the page, the Library and Personas tabs relock on their own, and the unlock ends on the server too. Change the span under Settings → Lock or with
mecha imagelib set-autolock <minutes>(1 to 240). Leaving either tab also ends its unlock. -
A persona searches its files. When a persona's files are too long to read whole, it finds the passages that answer a question with a new
file_searchtool, by meaning (through the:8081embeddings server,[documents] embed_url) and by words, and reads around them withfile_read. Files are indexed in the background once they are read; a quote taken from a search result is checked like any other. -
A persona's citations are checked. When a persona quotes one of its files as
[file, p. N: "…"], mecha looks the quote up in what that chat actually read and tags it: quoted, on p. N (the quote is real but on another page), not in the file, no such file, file not read (one of its files it had not read yet), can't check (a script without spaces), or too short to check. Tap a found citation to see the page as the chat read it, with the passage marked. "Quoted" means the words are there, not that they support what was said. -
An experiment environment can say which features its tasks need.
requires = ["graph", "search"]in itsenvironment.toml(inherited by any environment thatextendsit) makesmecha exp runrefuse to start when a trial home has one of them switched off, naming it, rather than scoring the model on tasks it could not attempt. It checks the switches, not that a feature switched on is set up. A name no experiment environment can switch on, such asmail, is refused as soon as the experiment loads, with a pointer to fixture servers, or to an arm'slevers_onformessages. Each run's session record now also notes which features were switched on. -
A persona reads its files. Each persona has a Files list on its page: add a paper, notes or an image there (or drop them into its
files/folder), and every new chat with it carries them. If the whole collection fits in about a quarter of the context window, it rides in full in the chat's first turn; otherwise the chat gets the list and reads pages with a newfile_readtool. A persona also reads its groups' and everyone'sfiles/, named@group/…and@all/…. Files are processed once on upload, one at a time, and the list says which are ready, and why one cannot be read. A chat never waits on a document still being processed: its first turn says the file is on its way, andfile_readreads it later. A file's words are treated as outside content, so a chat that carried them is marked untrusted and private. With[security] block_sends_after_privateon, a persona with files therefore stopsweb_searchin its chats.answers = "files"keepsfile_read, since that is what reading the files means. -
mecha setupcovers every optional feature. It has a step for each — the web app, Slack, mail, docs, the knowledge graph, web search, PDF extraction, image generation, personas, voice, incognito chat, the front door and messages — read from the same registry asmecha features, with a feature's dependency offered first. A feature not switched on is offered withmecha features enable …and can be declined; one you switched off in[features]counts as answered; one switched on but not working is shown as wrong, with the next command or the edit it needs. Mail, docs and the graph now count as set up only with an[[mcp]]entry for their server — an installed binary alone used to read as done.mecha setup <feature>sets up one, andmecha setup --minimaldeclines every optional step in one pass without touching your config. A step you declined before keeps its answer, and naming it —mecha setup mail— offers it again; the closing note says how to take back each kind of "no" (--undeclinefor one given in setup,mecha features enablefor a switch you set tofalse). -
Personas have a memory store. Each persona keeps its own
memory.db(episodes, its own facts, and what it learned or inferred about you), and facts you choose to share with every persona or a group go inshared.db.mecha persona memory show|export|approve|correct|pin|unpin|forget|share|shared|unshareis how you curate it. Every record names the chat it came from, soforget --chatremoves everything remembered from that chat from both files, and deleted text is overwritten on disk. Nothing writes memories or recalls them into a chat yet — those come next. -
Slack, personas, voice, incognito, the front door and messages follow their switches too, the same way the rest do: off, their web pages answer
feature_offandmecha slack,persona,msg,frontdoor,pollsandvoice-serverefuse with one sentence and the command that turns the feature on. The front door's publishing server is no longer started withfrontdooroff, andmecha servedoes not mount its voice facade with voice calls off. Reading what is there stays open (slack status,threads,persona list,show), and so doslack authandslack unlink.mecha msgnow refuses when messages is off, reads included — it used to work either way. -
A feature that is off refuses the same way everywhere. With mail, the knowledge graph (and its task board), PDF extraction or image generation switched off in
[features], the web app's routes for them answer404with{"error": "feature_off", "feature": …, "why": …, "fix": …}— only to the owner; anyone else still gets403and learns nothing — and their commands (mecha mail,tasks,kg,reviewexceptqueues,document extract, and theimagelibcommands that change an entry) refuse with one sentence, such as "Mail and calendar is off (turned off in [features]) —mecha features enable mail". They used to fail in five different ways, and the graph's review queue,mecha review acceptand the web's entity create and merge still wrote the graph withgraph = false. A missing graph tool now names the switch when that is why, not[[mcp]].mecha batchprints the upgrade line, asrundoes. On Home, a review queue whose feature is off shows its count and the command, and no longer opens a page that cannot load. The switches are read on every request and every command, so enabling or disabling a feature takes effect at once, on the web too; only the web chat's tools wait formecha serveto restart (Settings → Features lists the switches waiting on one). -
Eval cases can attach pictures.
"attach": [...]in a case puts images on its first turn as pixels, exactly as a web-chat upload does, so a case can measure what a model does with a picture it can already see. The chat, the persona chat and eval now share one implementation of that (image::attached_images).eval/image-read/is the first set: 19 cases over photographed pages, tables, receipts, screenshots and pictures with no text. Measured on the resident vision model, it never chose OCR for an attached picture, even when asked for a spreadsheet or a transcript, and a description telling it to did not change that. Its answers were all right, and on one hard page it transcribed as well as the OCR model did. So pictures stay read from pixels;document_readis there when asked for. -
The web app shows only what is switched on. Its tabs, Home's Mail and Tasks cards, the voice-call button, every dictation mic, the incognito buttons and Review's Graph queue and Front door tabs follow
[features], read afresh on each page load from a newGET /api/features(the same rows asmecha features --json). A feature that is switched on but not ready keeps its tab, marked, with what is missing above it; a link to a hidden page lands on Home with one line saying so. Settings → Features lists every feature, off ones included, with the command that turns it on, and says which switchesmecha servehas not picked up yet. A Home queue with anything waiting stays even when its feature is off. No route refuses anything yet. -
Read a photo or screenshot of a page.
document_readandmecha document extractnow take PNG, JPEG, WebP and GIF as well as PDF. A picture is read as one page by the local OCR model, through the same layout stage, cache and caps as a scanned PDF page. Phone photos are turned upright from their rotation tag first: on an upside-down test page, skipping that put the heading last and misread the paragraph as a table. Transparent screenshots are read on white. HEIC photos and TIFF scans are refused with what to do instead. Personas that havedocument_readget this too. -
A persona draws itself (PERSONA-DESIGN §8.6). In a persona chat,
"who": "self", or the persona's own name, is its linked library character. On a persona with no character,selfis an expected failure that says to describe the persona instead. It used to be refused for naming a library character withoutcast, and the local model resent the same call: 82 of 87 calls in the first live chat. -
"No picture was made in this reply." The persona page says it under a turn whose
image_generatecalls all failed, read off the tool rows, so a reply saying "here you go" over nothing is not the last word. -
Every optional feature has a switch:
[features]. The globalconfig.tomlgains a[features]table with onetrue/falseper feature — web, Slack, mail, docs, the graph, search, PDF extraction, image generation, personas, voice, incognito, the front door and messages.mecha features enable <id>anddisable <id>edit it in place, keeping your comments;mecha config initwrites it with every line commented — listed and off, but not answered, somecha setupasks about each. (A config written by an earlierconfig init, with every line= false, reads each as a no: comment out or delete the lines you want to be asked about.) An install from before the table has every switch unanswered, somecha featuresmarks what you have set up as "not enabled, but set up here", a session or service prints one line naming them when it starts, andmecha setupoffers each. A project'smecha.tomlcannot set the table, and an experiment environment may only switch features off (and the front door on). -
Persona chats show their pictures, with the same Edit button. A picture a persona draws now appears under its row in the Personas tab, as in the main chat, and Edit opens the same paint-a-region modal. A locked persona's pictures stay behind the library lock, and have no link to open them in a new tab.
-
Drop or attach files in a persona chat. Drag files onto the Personas tab, or use the paperclip, just as in the main chat. Files land in the chat's own
inbox/and are named in your message. A picture is also shown to the persona when its model can see images, and the chat is marked as holding something private from that turn on — including a turn the crisis pause held, and after a restart. -
Personas can read PDFs. Add
"document_read"to a persona's[tools] allowand it can read a PDF dropped into its chat. It gets the PDF's own text, with OCR for scanned pages, as in the main chat. It is off until you list it. A persona set toanswers = "files"never gets it, because a document's words are third-party content. A persona with bothdocument_readandweb_searchcould be steered by a hostile PDF into searching for something; dropweb_searchfor a persona that reads files you did not write. -
The charter editor keeps your comments. Saving the charter from the web page now changes only what you changed: comments above and among the lines stay, a line you drag takes its comments with it (a comment right above the first line reads as the file's header and stays at the top), and a setpoint you did not touch keeps its spelling. A charter with comments among its lines can be edited as a list, not only as TOML. A save made from a page that read an older charter is refused rather than written over.
-
Paint the part of a picture to change. The web chat's Edit button now opens a modal: paint or draw a box over what should change, say what to do, send. Only the painted area is redrawn, blended over a narrow edge around it. Everything else comes back exactly as it was, pixel for pixel, so "make her dress green" no longer nudges the rest of the scene. A large photo is edited at the model's working size, as any edit of it is. With nothing painted, the whole picture is edited, as before. Local swaps, recolours and details land reliably this way. To move someone or change a pose, use a plain edit or redraw from the library.
-
mecha featureslists which optional parts of mecha are on. Web, Slack, mail, docs, the graph, search, PDF extraction, image generation, personas, voice, incognito, the front door and messages, each marked on, off, blocked (waiting on something else that is off), unready or unknown, with the command that turns on each one that is off. It reads the config and the disk only and wakes no server.--jsonfor scripts. The first step of making installs modular (docs/FEATURES-DESIGN.md). -
A cleaner Personas tab. Personas are listed like contacts: avatar, name and relationship, with New in the header. A persona's page leads with who they are and one Start a chat button; Edit is a pencil, and hiding behind the library lock moves into a menu. Earlier chats show the goal they were opened with. In a chat, the header shows just the name and the AI tag, with the model chip (the same picker as the main chat); the send button sits inside the message box, and the back arrow ends the chat (the Done button is gone). While a reply is on its way, a line says the persona is typing, or what it is doing and for how long, such as "drawing a picture… 1:24". A tool call the persona retried reads as retried, not failed. The support-resources link appears only after a crisis warning in that chat, and the banner at the top of each chat is gone.
-
Persona files edit as forms. A persona's Edit screen is now a form on every tab. Settings are switches, choices and name chips: relationship, groups, portrait, tools, the safety switches and memory. A change is set in place in
persona.toml, and every comment you wrote stays. Who they are and what they want edit as a title and sections, each with an optional note to yourself, saved in one tidy layout.## Corecannot be removed. "Edit as text" is still there on each tab. Switches the harness does not use yet say so. Typed-->no longer shows as an arrow, and a comment typed on a phone keeps its dashes. -
Make and edit personas on the web. The Personas tab has a New persona form: name, relationship templates, a library portrait and groups. New kinds of relationship (how someone in it behaves) and new groups can be added from the same form. Each persona has an Edit screen for who they are, what they want, and its settings, saved exactly as you type them, comments included. A settings file that would not load is refused and left as it was, and an edit made elsewhere in the meantime is never overwritten. Lock and unlock from the same screen.
-
Personas: characters you write and talk to, in their own tab. A new Personas tab on the web page lists the personas you made with
mecha persona, each with its library portrait. Choose one to start a chat, with an optional goal, or to pick up an earlier chat. Persona chats are kept apart from your assistant. They cannot see your mail, calendar, graph or other chats. Their transcripts live under the persona's own folder, where nothing that learns from your sessions looks. A persona gets only the tools it asked for that are safe for it: web search (fixed destinations only), image generation and viewing, and the image library. A safety layer runs in every persona chat:- a line saying it is an AI;
- a crisis check on what you type, which pauses the persona and shows 988 and the Crisis Text Line;
- the persona's Core handed back to it on a cadence;
- usage meters.
Each can be switched off per persona in its
persona.toml, and a switch reaches chats already open. Behind the keywords, a model check screens each message you send, including one sent while the persona is answering, and pauses the persona if it finds a concern the keywords missed. When that check cannot answer, the page says crisis detection is on keywords only until it can. -
OCR reads tables whole. With the layout stage installed (
scripts/layout/install.sh), an OCR page is read region by region: a layout model (PP-DocLayoutV3, CPU, sandboxed like the PDF parser) finds its tables, formulas, headings and paragraphs, and each is read with the prompt the OCR model was trained for — tables come back as Markdown tables with every column, headings are marked, and each region's box is kept for citations. Without it, pages are read whole and the transcript says so. -
Read PDFs:
document_readandmecha document extract. Each page comes back twice, labelled: the PDF's own text layer (exact — what a quote is checked against) and, for scanned pages or when asked, a transcript from a local OCR model (PaddleOCR-VL 1.6) with equations as LaTeX. The PDF parser runs sandboxed, results are cached by the file's hash so a page is never read twice, and the OCR server starts when a page needs it and stops after ten idle minutes. Configure with a[documents]table in~/.mecha/config.toml(seemecha config init); install the server withscripts/llama/install.sh. Tables read by whole-page OCR are not reliable — install the layout stage (above), or use the text layer for numbers. -
Personas, step 1: the store and
mecha persona. A persona is a character you write and talk to, kept apart from the assistant. It lives in a folder under~/.mecha/personas/:persona.tomlholds its settings,identity.mdwho it is, andmotivation.mdwhat it wants.mecha persona newmakes one, optionally starting from a relationship template. Ten starter templates are copied intorelationships/once, for you to edit freely.editopens a file in$EDITOR, and each saved change becomes a new version.lockhides a persona while browsing, using the image library's lock.group adddeclares a group whose members share an about-me and afiles/folder. A broken link to a template, group, voice or library character is reported by name. Chatting with a persona comes in the next step. -
mecha sessions mark <id> experimentwithdraws a session you ran as a model probe from everything that learns from your sessions: the run corpus,reflect,distill,learn, and its appraisal and scores. The transcript is untouched, andmecha sessions unmarkundoes it. Only you can mark, from your own terminal; a run's shell is refused. The verb names the session's graph episode, if it was distilled, for you to retract there. -
mecha distill --backfill-appraisalsappraises, after the fact, the sessions distilled before appraisals existed whose clean steer or denial was waiting only on an appraisal to be compared. There were 9 on the live store. Nothing is pushed to the graph, and the rows written predict nothing, since the outcome was already known.--dry-runlists them. -
The lesson-source comparison states its verdict.
mecha learn --compare-sourcesandsessions appraiseprint R25's gate for folding the reflector in (2a-4) and feeding appraisal lessons tolearn(2e-2), decided on the paired verdicts. Under 10 decided comparisons the result is a pilot. From 10 up it is "no worse" while the appraisal trails the reflector by at most 2 discordant pairs, and "worse" past that. There is no verdict at all while any line of the stores behind it is unreadable, since every count is then a floor. The counts behind the verdict are printed beside it, and--jsoncarries it asgate. -
mecha rules show <id>finds a rule that consolidation dropped. Learning rewrites its rule set and leaves some rules out; they are not retired, so the learner is never told they were harmful. Their text lives on in the proposal that dropped them.shownow answers from there: which proposal dropped the rule, when, its last text, and its validation record. It says "never live" for a rule only ever proposed. Read-only. -
Draft forecasts. When a run stages a message, mecha now predicts what you will do with it: send it as written, edit it, reject it, or leave it past the outbox's patience. The prediction is your most common act on similar earlier drafts. It is kept out of every review surface and every prompt, so it cannot sway your verdict, and
sessions appraisereports how the predictions fare. Only acts stamped as yours count. -
Add and edit characters and styles from the Library tab. Add character takes a portrait from your device, a name and a description. Add style takes a name and its text. Edit changes an approved entry's description or portrait, keeping the earlier version for pictures already made from it. A portrait is resized and stripped of its photo metadata before it leaves the device.
-
People who aren't in your image library can share a scene with those who are. Ask for "Maya and John at a diner, with a waiter pouring coffee" and the waiter is drawn as a new person, counted with your characters, so he takes part in the scene instead of being pushed to the background. A person the scene only mentions in passing is no longer erased either: your characters are each drawn exactly once, without a fixed total that left no room for anyone else.
-
Archive or delete a conversation from the web chat's drawer. Each row has a ⋯ control. Archive hides a conversation and keeps the record whole — learning, appraisal and the graph still read it — under a new archived section; opening one (or restore) puts it back in the list. Delete removes the transcript and everything derived from it: its workspace, staged drafts and waiting questions, the reflections and learned rules drawn from it, appraisals, and its knowledge-graph episode. A delete that could not reach a store says so and can be run again to finish. The same from the command line:
mecha sessions archive|unarchive|delete <id>. Deleting a distilled conversation needs the mecha-graph release withredact --source. -
The image library in the web app. A Library tab shows your characters and styles, with a Waiting pane for ones the model proposed: read the text, then approve or reject. Save to library under any picture in chat makes it a recurring character. Locking hides a character while browsing (it stays usable in chats); the lock button shows locked entries again — a plain toggle, or password-protected once you run
mecha imagelib set-lock-password. Waiting candidates also appear among Home's review cards. Rebuild the page and restartmecha serveto use it. -
An image library of recurring characters and styles. Add a character once — a portrait and a short description — and name it in
image_generate's newcastfield, with what each person is wearing and doing; the library supplies how they look, so the same people come back across images.styleapplies a stored style. The model can propose an entry (image_library_propose), which waits for you:mecha imagelib list --all,approve,reject,add-character,add-style,lock. Every generated image now also gets a manifest beside it (images/<name>.json) recording how it was made. Install the new binary; nothing to configure. -
Voice calls in incognito chats. An incognito chat now has the call button, and nothing of the call is kept. While such a call is live the voice worker silences pipecat's logging, which otherwise writes both sides of every call to the journal, and withholds the words from its own lines.
mecha serveasks the worker before it forwards such a call, so a worker that has not been restarted is never handed one, and the page goes on only if the answer confirms it. Ending the chat hangs up the call. Restart the voice worker andmecha serve, and rebuild the page, to use it.
Changed
-
The picture tool's results report facts only. A result gives the picture's path, size, time and seed, and for an edit whether its layout barely changed. The suggestions each result used to carry about what to try next now appear once, in the tool's description, so they no longer pile up in a chat's history (
docs/PERSONA-CONTEXT-DESIGN.md§5.2). -
Picture edits are described in parts, and the tool writes the edit model's instruction. An edit now names the one change (and, if it matters, what stays, what a face does, or where the camera goes), never a description of the whole scene, which the edit model reads as the picture it already has. Persona edits had been coming back unchanged; replayed, a scene description made the asked-for change 3 times in 8 and an instruction 8 times in 8. An edit sent as a free description is refused with what to send instead. From the edit panel, the owner's own words are the change (
docs/PERSONA-CONTEXT-DESIGN.md§5.5). -
A persona's harness notes last one run and are no longer stored in the chat. A recall, the reminder of who it is, and the call, variety and edit notes used to be written into the owner's message, where they stayed and were re-sent on every later turn. At one measured moment they were 39% of what the model read, against the owner's 3.5%. They now ride the request they are for and nothing after it (
RunContext::notes), and the session records them separately for audit. Chats recorded before have their stored notes left off what is sent; such a chat's first reply after the change re-reads its history once. The call note now goes on every spoken turn, and tells only a persona that can make pictures that its pictures reach the owner's screen (docs/PERSONA-CONTEXT-DESIGN.md§5.1, §5.6). What the persona remembers at the start of a chat is stored once in the chat's first turn and kept there, as before: sent as a note it added about a second to every reply and made the persona skip pictures it was asked for. -
Pictures take about half as long: the image model is now the int8 file.
[image] diffusion_modeldefaults toqwen_image_2.1_int8_convrot.safetensorsin place of the Q4 GGUF. On the DGX Spark a picture takes about 28 s instead of 64, and an edit about 37 s instead of 80, with the same pictures for the same seed. It loads through ComfyUI's own loader, so the ComfyUI-GGUF node is no longer needed for the default; a.gguffile still works through it. Put the file in ComfyUI'smodels/diffusion_models/(Comfy-Org/Qwen-Image-2.1) before upgrading, or setdiffusion_modelback to the GGUF. It needs about 0.7 GB more at its peak, somin_available_mbdefaults to 20480 (was 19456). -
A large photo is scaled down before an edit. A reference over 4 megapixels — every phone photo — goes to the image server scaled to 4 Mpx, turned upright, without its metadata. The model works at about 1 Mpx, so nothing is lost, and the server no longer spends ~2 s an edit decoding the full photo. The photo in the chat is untouched.
-
The memory figures count speech as Breeze, the new default voice.
mecha features --probeand the hardware page now use Breeze TTS 2 (Q6_K, qwentts.cpp): 4.1 GiB at its peak while speaking, read on the GB10, where Chatterbox was 7.8 GiB. The machine-wide sums fall by the difference, about 4 GiB. -
Breeze TTS 2 is the default voice. The voice worker's
MECHA_VOICE_TTSnow defaults to the Breeze adapter on:8887(engine on:8886) instead of Chatterbox on:8881, which stays available as the rollback by setting the variable back. The update skill says when the adapter needs a restart. -
The model launchers find the Hugging Face cache the way
hfdoes. The router, the embeddings and OCR servers and the other start scripts looked only atHF_HUB, then~/.cache/huggingface/hub; they now also honourHF_HUB_CACHE,HF_HOMEandXDG_CACHE_HOME, in the orderhfand mecha's downloader use, so a modelhfput underHF_HOMEis no longer reported missing. The layout installer tellshfthe same directory. With none of them set, nothing changes. -
A persona names a library voice directly.
voice = "ada"inpersona.tomlis a voice from Library → Voices, with an optionalvoice_speed(0.5–2.0). Thevoices/<name>/profile.tomlprofiles are gone; nothing used them yet. -
A persona's page is quieter. It no longer states its crisis-detection setting, and its files are small tiles with a compact add tile, instead of full-width rows that read like a second "Start a chat".
-
An experiment's condition hash now includes the features a trial runs with. Turning
searchon or off between two runs of an experiment used to leave every trial's hash the same, so the two runs could be paired as one condition. Every new trial's hash changes once with this release, so trials planned from now on do not pair with ones planned before it; a resumed experiment keeps the hashes of the trials it already finished. -
The character library follows image generation. It is on whenever
imageis — its page, its routes,mecha imagelib— and[tools]disablingimage_libraryorimage_library_proposenow only keeps those tools from the model, as[tools]does for every tool, instead of taking the Library page with it.mecha featuressays which library tools the model is missing. The library's lock password and its unlock, relock and portrait routes stay open whatever the switches say, because Personas uses them. -
mecha featuresreads a switched-on feature with missing settings as unready, not off.image = truewith no[image]table, or mail with no[[mcp]]entry, is something you said yes to, and now reads unready with the fix — and no longer hides the parts under it, which read unready beside it rather than on. A part turned off by its own setting ([documents] ocr = false) still reads off. A blocked row now prints the command that unblocks it, and--jsonrows carryshownandnext. The start-up line for an install from before[features]can now name a feature that is set up but not yet usable, with the reason beside it —mecha features enable documentsfor a[documents]table whosedocument_readis turned off in[tools], ormecha features enable mailfor a mail[[mcp]]entry whosemecha-mailis not installed. The switch alone will not make those work; the reason says what will. -
A feature whose switch is not on is off. Web search, image generation, PDF extraction and the mail, docs and graph servers register or start only when their
[features]switch istrue, andmecha serverefuses to start withoutweb = true. Before upgrading an existing install, answer the switches: runmecha setup, or the onemecha features enable …line a session prints when it starts — it names everything this install had set up. An MCP server that belongs to no feature starts as before; the graph server stays off fordistill,gossip,vetandcorroboratetoo. Slack, personas, voice, incognito and the front door were left running whatever their switch said at first; later in this release they follow their switches too (see "Slack, personas, voice, incognito, the front door and messages follow their switches too" above). Experiment trials keep their graph: it is on when the environment declares its own graph server or the manifest brings yours in withlive_servers, unless the environment saysfalse; search follows your own switch. -
The learning store no longer uses git.
~/.mecha/learning/is plain files: opening it no longer runsgit initor writes a.gitignore, and a pass no longer commits. Git history kept every deleted reflection's text, which a chat deleted with all its traces must not leave behind. Each pass now appends one line topasses.jsonl— when it ran and what it counted, never content — besideruns.jsonland each rule'ssourcesand retirement fields, which already carried the lineage anything reads; the undo for a bad rule ismecha rules retire, per rule. An existing.gitin the store is left where it is, unused; removing it is the owner's call. -
The local model's prompt cache is 16 GiB, not 32 (
-cram, both launch scripts). It fills over hours and never shrinks while a model stays loaded, and a full 32 GiB beside the model's own 43 GB left image generation out of memory on the shared pool. 16 GiB still holds 2.9 full 262k slots (5,632 MiB each), where the 8 GiB default, which thrashed, held one. Restart the router to apply it;MECHA_LLAMA_CRAMstill overrides. -
The web home is counts and doors. Four large cards for what is yours — mail that needs you, outbox drafts, questions a run is paused on, open tasks — then one small card per review queue, and a line each for follow-through and health. Every card opens its tab; nothing a tab already shows is repeated. The Today list of workflows, which is what made the page unusable, moves to the task board as its own view (
#tasks/workflows), keeping every verb and Reopen for finished ones; home's Follow-through line opens it, and the Questions card opens the board's waiting view, where a paused run's question is answered. Tasks' views now follow the route, so Back returns to the last one. A count that could not be read is a dash, never a zero. -
An incognito chat's model chip opens the picker too. Every model it lists runs on this machine, since the chip reads only routers a local, loopback
follow_loadedentry names, and the menu says so. Incognito's local-only guarantee stays with its own provider and its per-turn check. The owner's amendment toINCOGNITO-DESIGN.md§6.1 (2026-09-28).
Fixed
-
Painting a region on a phone photo taken upright now edits it. A phone stores such a photo sideways with a tag saying so; the edit read it as stored, found it the other shape from the region painted over it, and refused. The near-copy notice read it the same way and could miss a copy. Both now read a photo upright, as the page shows it.
-
Listen no longer goes silent after the first sentence. A reply read aloud was spoken as its first sentence, then a ~400-character piece that could only play once it had all been made — about 8 s of silence on an idle machine and 16 s while a chat was generating. Each piece now streams from the voice worker through serve to the page, which plays it as it arrives and fetches the next piece while the last is still playing: speech starts about 0.3 s after the tap and runs without gaps. A voice worker that predates streaming still answers whole pieces.
-
Calls from Chrome buffer the microphone again. Chrome now has
RTCRtpScriptTransform, and on a plain connection it hands the transform no audio frames, so every Chrome call declared the buffered uplink and fell back to the direct path ("the buffered microphone path failed"). The call page now creates its connection withencodedInsertableStreamsin Chromium and pipes incoming audio through, which Chromium requires once that flag is on. -
A persona chat whose model changed partway through is still remembered. If the router switched a chat to another model mid-way, the overnight memory writer kept waiting for the model the chat started on, possibly forever. A persona chat now records each switch, and its memories are written with the model it last ran on.
-
The ComfyUI idle reset reads only what it needs, and fails when it cannot see free memory. Its two journal checks now ask for the newest matching line (
journalctl -g … -n 1) instead of reading the whole run's journal every minute, and tell "no match" from "unreadable" by what journalctl says on stderr. An unreadableMemFreefails the unit, where it used to be read as zero and answered with/freeand a clean exit. -
A picture asked for while the image server restarts waits for it. The first request of a job now waits for a server that is not answering yet: up to 90 seconds when this process has reached it before (it is restarting), 45 when it never has. A cancel stops the wait at once. Before, it failed straight away with "Is the image server running?", and a genuinely stopped server is now reported after that wait.
-
The image tool's memory check matches what a generation costs. It now asks the image server what it holds first. A server that has loaded nothing (just restarted, or unreadable) needs the full
min_available_mb, now 20 GB (20480 MB), up from 16. A generation from an idle server needs ~18.5 GB above its idle footprint, and the old figure counted only the GPU side. A server that has already loaded the model needs ~7 GB less (12 GB at the default), so a second picture soon after the first is no longer refused for memory it would not use, and a raisedmin_available_mbkeeps its margin. -
A persona's memories are dated by when they were said. Everything a persona remembered was dated by the night it was written, so something you mentioned on Tuesday read as Friday's news. Memories now carry the day of the conversation they came from, in your timezone, and "recent" means recently said.
-
A persona no longer sends the same reply again when a turn adds little. Late in a long chat, a short owner message ("mm") could bring back an earlier reply word for word. Persona chats now send no fixed
seed— the provider's pinned seed restarted the sampler at the same draws every turn — and leave earlier replies' reasoning out of what goes back to the model, which it had been re-reading and re-sending (a turn that called a tool keeps its reasoning, without which the model learns to call tools without thinking). Replayed on the turns that copied, copies went from 4 of 4 to 0 of 8 with no damage to the text; DRY sampling also stopped the copies but garbled 2–5 replies in 8, so it is not used. The assistant's own chats are unchanged: they keep the seed and their reasoning, and so does the persona's crisis judge, whose verdict should not vary.mecha persona shownow reports how many of a persona's replies over the last week repeated an earlier one in the same chat, read from the chats themselves. -
The knowledge-graph page names the embedder mecha-graph actually uses:
harrier-oss-v1-0.6bon a llama-server at:8081, launched byscripts/llama/mecha-embed-server— not ollama withnomic-embed-text, which mecha-graph dropped in 0.1.1. The page says plainly that the repository has no installer that sets the server up from nothing. -
Voice expressiveness settings the speech model ignored are no longer sent as if they worked. The live model, Chatterbox Turbo, drops
exaggerationandcfg_weight, so the worker's 0.8 / 0.3 and the mood nudge changed nothing. The speech server now lists the controls its model honours and refuses one it would ignore, and the worker sends only those.CHATTERBOX_MODEL=originalloads the slower model that does honour them. -
A persona's earlier chats no longer vanish after a restart. An unlock lives in
mecha serve's memory, so a restart ended it while the page still showed the persona, and its earlier chats came back as an empty list. The page now notices the lapsed unlock and locks, says when the list could not be read, and shows that it is loading. -
messages = falseis an answer now. Switching messages off leftmecha setuplisting it as outstanding, with a failing exit, every time it ran, because the setting it is stored in could not tell "off" from "never asked".mecha setupandmecha featuresnow show it declined, withmecha features enable messagesas the way back, like every other feature. It is still off until you switch it on. -
The persona file editor works on a phone. Tapping a field no longer zooms the page and leaves it zoomed: every editable field is at least 16px on a touch screen, the size below which iOS Safari zooms (text mode too). The Save bar is solid instead of letting the text scroll visibly under it; the highlight on the field being edited is centred; text boxes refit on rotation and after Discard and no longer jump the page while measuring; and sections take less width on a narrow screen. The Settings form shares the same styles and gets the same fixes.
-
A persona draws itself from whatever it writes about itself. Follow-ups to #444: a display name with punctuation ("Mara O'Brien") no longer eats the first word of what the persona is doing; an
extrasentry that opens with the persona is taken as the persona and cast from its own words, where it used to be refused or drawn as a stranger; an extra that names the persona in passing is refused before drawing, saying how to fix it;selfbeside the character's name drops only the duplicate it made; and the library is read once for the self-cast and the guard instead of once each. -
A command that ends non-zero no longer leaves a model hold behind.
mecha evalwith a failed case,mecha batchwith a failed item, and a refusedmecha runexited without removing their file in~/.mecha/holds/. A dead process's hold never blocked a model switch, so the files only piled up. Every such exit now releases the hold first. -
A chat no longer resends a refused call for minutes. If a tool refuses the same call the same way, turn after turn, the run now stops after seven tries. You get an answer saying what could not be done, instead of up to forty identical attempts. A call you or a policy turned down never counts toward the seven. One persona chat did this with a picture, twice, and held the GPU while another chat was drawing. Persona chats also get the notes the assistant already had: after three identical results the model is told that nothing is being learned and to try another way, which is what rescued the assistant's two such runs, and after six to say what is blocking it instead.
[agent] loop_guard = falseswitches the stop off. -
A name not in the image library no longer blocks a picture. When the assistant names someone in a picture who isn't a library character, they are drawn from the description it gave, as an extra, and the result says so. A character waiting for approval is still refused, and persona chats are unchanged.
-
A locked page no longer says how much it is hiding. The personas grid drew a
hidden · Ntile and the image library a "N locked entries hidden" line, which told whoever held the phone there was something to find. Both are gone, and so is thehidden_lockedcount in/api/personasand/api/librarybehind them: a locked page now reads exactly like one with nothing locked, as a locked entry's own URL already answered 404 like a missing one. The persona list no longer names a locked character that a visible persona uses as its portrait either (characterisnullin/api/personasuntil unlocked, a problem line naming it is left out of the list and of a save's answer, and an entry that will not load counts as locked); the link itself is kept. -
Unlocking on the new-persona form now offers locked characters as portraits. The form read its character list once, when it opened, so the lock button refreshed the persona grid and left the portrait list as it was; it now re-reads the list on every lock change, and a relock drops a locked portrait the form had already chosen.
-
The document cache is private all the way down.
~/.mecha/documents/and every directory under it are now created owner-only (0700), and every file in it 0600. Before, only the per-file entry was, so the cache root and each entry'socr/folder were left at the umask default, and another local user could list which PDFs had been read, by hash. An existing cache is tightened on its next write.mecha document prunenow fails on a cache it cannot read, instead of reporting "removed 0" (#410). -
Image edits that move someone or change a pose no longer come back unchanged. The cause was the prompt, not chance: the edit model returns the same picture when the prompt describes the whole scene or asks to keep the picture unchanged, and the tool's own wording asked for both. On the same 12 seeds, a caption stood someone up 0 times, "Have Maya stand up" alone 8 times, and the same after naming what to keep ("Keep the style, the lake and the blanket unchanged.") 12 times. The tool now asks for that last form. As a safety net, it also compares each edit's layout with the picture it edited. When they match, the result says so and tells the model to retry once with an instruction, or to redraw from the library when the picture came from it. A second match in a row says stop and tell you.
-
A run can no longer mark a rule down by rejecting its own draft. An edited or rejected draft counted as your verdict on the drafting, in the appraisal and so in rule tenure and replay priority, whoever made the edit or the reject. Now it counts only when it is stamped as yours, as a release already did. A stamped act that is not yours counts as neither, and acts from before the stamps existed keep their sign.
-
The appraisals' hit rate counts only predictions that could miss. A chat answer or a run that staged nothing leaves the owner no draft, task or workflow to act on, so
no_actis the only act that can be observed there and predicting it is a hit by construction.sessions appraiseread those as "hit rate 100%" over four such scores; it now names them as forced and prints no rate until a prediction risks a miss. Scoring, and the surprises replay priority reads, are unchanged. -
The appraiser is not asked to predict an act where none can happen. When a run's output offers no draft, task or workflow, and the run never touched a task,
mecha distillleaves the expected act out of the appraisal's question and drops one the reply carries anyway. The row records that the harness withheld it, andsessions appraisecounts such rows as "not asked". -
A voice call whose browser never connects ends after 90 seconds, not at the fifteen-minute idle timeout. Until now it held the speech models for the quarter hour, and for a call into an incognito chat it kept the voice worker's log silence on for every other call in that time. Restart the voice worker to apply it.
-
A model switch waits for a reply in flight instead of failing. The router refuses an explicit load with
model limit reached, try again laterwhile the loaded model is answering a request.mecha model use, and the web chip that runs it, now retries every second until the request ends, within--wait-secs. Before, the switch failed at once and loaded the old model back. The first live switch from the chip hit this.
0.1.22 - 2026-09-28
Added
- The model can look at an image, on request (
image_view, the owner's ruling of 2026-09-27). It takes a workspace path — a pictureimage_generatesaved, an attachment underinbox/— and the loop puts the picture in the user turn beside the results (ToolOutput::image), for a model that can see; a blind one is told in words. Before it, a model could not check what it drew:image_generatesaid "You cannot see it" andfs_readreturns a PNG as noise, so positional edits ("the second person from the left") were composed blind. Not returned by default, because a look costs ~1000 tokens for the rest of the conversation; a look armsprivate_data, likefs_read. Registered for any model that can see; a small picture (≤ 1 MiB, a screenshot most often) reaches it byte for byte, a larger one as JPEG.
Fixed
-
The web chat draws a picture once. A model that looks (
image_view) at the picture it just made (image_generate) put the same image on the page twice, because each row drew its own. A row whose picture is already shown higher up keeps its row and shows the picture only when you tap it open (repeatedPicturesinChat.svelte). -
A picture is typed by its bytes, not its name, at every door. A file named
.pngthat is not a picture used to pass through unread under 5 MB, and a real JPEG named.pngwent out labelledimage/png; the provider rejects either, and a rejected picture fails every later request of that chat. The first is refused, the second sent asimage/jpeg. Reachable from Slack and the web chat. -
A picture over 128 megapixels is refused as too large to show, from its header, rather than reported as a failed decode (the
imagecrate's default 512 MiB allocation cap already stopped the allocation; the error said the wrong thing). The bound is by area, so a long full-page screenshot is still shrunk and shown. -
The web chat says when an attached picture was not shown — a model that cannot see, more than 8 on one message, or a file that will not open — rather than clearing the chip as if it had been.
image_generate's result again tells a run it has not seen the picture, so one withoutimage_viewdoes not describe it. -
A picture uploaded in the web chat reaches the model as pixels (
REMOTE-SURFACE-DESIGN.mdD6, built half-way on 2026-08-24). The page named an upload's path and nothing else, so the model could not see what it was sent —fs_readcannot read a picture. The page now lists its uploads beside the text andserveputs each image on the turn, capped at the door, for a model that can see; the path stays in the text, so it can still be handed toimage_generate. Up to 8 per turn; an attached picture armsprivate_data, as the Slack door's does — so a web chat with a picture in it now refuses a model-chosen send (http_fetch) once outside content has also entered, where before only an opened file armed it.
Changed
-
A previous attempt says whose act it was (
APPRAISAL-WIRING-DESIGN.mdR42, the owner's ruling of 2026-09-27). The situation brief's previous attempts put a draft reject or edit under "The owner:" only when it is stamped as the owner's; a run's own reject or edit, and one from before the stamp, is said apart as not recorded as the owner's, with the item id and never the reason.mecha outbox list's drafting line reads "sent unedited" (it counts whoever released), not "sent as drafted". -
The retirement drill no longer writes into the live comparison store.
ComparisonStorehonoursMECHA_COMPARISONS_DIR(as the learning store honoursMECHA_LEARNING_DIR),scripts/retirement-drill.shsets it, and the drill fails withDRILL LEAKEDon any exit that left the livecomparisons.jsonlchanged. Two runs on 2026-09-27 had each added avalidationrow about a throwaway session to the owner's store. -
A draft released unchanged is your verdict only when you released it (
APPRAISAL-WIRING-DESIGN.md, R16a's ruling D3 carried to releases). The appraisal's +1.0, the owner-verified success set (mecha sessions successes), its writing exemplars and the planning success examplesgoal_contextserves now count an unchanged release only when #343's stamp saysowner; a run's shell that approves its own draft earns none, and releases from before the stamp count as neither, so the success set starts empty of that history. A run that rejected its own draft sets no point-wise rejected-draft point.
Added
-
The web chat's model chip switches the model (
REMOTE-SURFACE-DESIGN.md§14, step 5). Tap it for what the router serves, with a dot beside the loaded model; tap another to switch the whole machine. It runsmecha model useon the server, so R1's rollback, R4's refusal and D13's wait for runs all apply unchanged. While a switch waits, the chip reads → model, and the menu names the runs it waits for, with switch now and cancel. A failed switch turns the chip amber and says why.mecha model list --jsonnow reports each router'spending_switchand each model'swould_not_follow.mecha model use X --nowhurries a switch already waiting on X instead of refusing it as a second switch. -
Every outbox edit records who made it, and an edit is your writing only when you made every edit and the send (
APPRAISAL-WIRING-DESIGN.md, R16a's ruling D3 carried to edits).mecha outbox edit(andpolls pick) stampedited_by—owner,owner-approvedorunknown, as rejects are stamped — folded across edits so an owner's later edit never launders a run's.mecha reflectmines a diff as a writing lesson, the appraiser is shown it as "the owner edited it", and a point-wise comparison takes the released text as its gold only when every edit and the release were stampedowner; otherwise the edit is described, not shown. Edits from before this readunknown. An edit not stampedownersays so when it is saved, in the terminal and the web review.outbox showand the TUI name the editor. -
Image generation. With
[image]configured, theimage_generatetool draws a picture with a local image model (ComfyUI with Qwen-Image 2.1 today) and saves the PNG underimages/in the conversation's workspace; web chat shows it inline, and the same tool edits a picture you attach or one it drew. The workflow the server runs is fixed in mecha's code; the model fills in values, never the graph. See Image generation in the published docs. -
Incognito web chat. A second new-chat button opens a chat that writes no transcript, title or count, keeps its files on tmpfs, and is gone when you tap End, after 30 idle minutes, or when
mecha servestops. It reaches an allowlist (mail and calendar reads, web search, its own folder), runs only on a local provider without fallbacks, and refuses to open rather than weaken any of that (docs/INCOGNITO-DESIGN.md). -
Model switching behind a llama-server router.
mecha model use <name>loads a model and every default run follows it, including web chat, voice, Slack and the trigger daemon, which re-check before each turn or fire. A switch waits for the runs still using the old model (--nowstops them;mecha model cancel-switchwithdraws a stuck switch), and each run records the model that answered. Opt in withfollow_loaded = trueon the default provider entry (REMOTE-SURFACE-DESIGN.md§14). -
Every outbox resolve records who made it (
APPRAISAL-WIRING-DESIGN.md, R16a's ruling D3).mecha outbox reject,reviewandapprovestampresolved_byon the item —ownerat the owner's terminal or a surface's own child (web, TUI, Slack, voice),owner-approvedunder an interactive run'sshell,unknownotherwise — decided by the task closure store's rules (closure::attribute). A rejection's reason reads as the owner's words only underowner: the reflector no longer mines a reason a model'sshelltyped, the appraiser's input shows a typed word in its place, and the meeting-poll sweep puts "No time found" rather than that text on the participants' page. Items resolved before this load asunknown.outbox showand the TUI printby <actor>beside the reason,sessions appraisecounts the other reasons apart (unattributed_rejections), and a reject whose reason is not stampedownersays why — in the terminal, the TUI status line and the web review. -
Contrast evidence for the reflector (
APPRAISAL-WIRING-DESIGN.mdL2, 2e-4b-2, R43), behind[agent] contrast_evidence/StageLever::ContrastEvidence, a stage lever that ships off. On,mecha reflectreflects a steer, denial or followup with a verified success from its region beside it — the newest trace of a clean, scoped session the owner verified, from another session, whose run records the correction's scope matches (the loader's match, R43). The reflector sees the owner's act in fixed words and the session's tool sequence; its behaviour frame gains one sentence and the correction one block. Off, the reflector's prompt is byte-identical to before.mecha reflect --dry-runnames the success each correction would be shown. An experiment arm measures it on through its environment'sconfig.toml;stages_off = ["contrast_evidence"]forces it off. -
Planning examples from what the owner verified (
APPRAISAL-WIRING-DESIGN.mdL2, 2e-4b-1, R40), behind[agent] success_examples/Lever::SuccessExamples, which ships off until an experiment measures it (levers_on = ["success_examples"];mecha evalforces it off, and the diagnostician's run is narrowed off it). On,goal_contextanswers a request toward a goal with the tool sequence of a session the owner verified toward it — a task closeddonethat no reopen undoes, a workflow closed, a question answered whose session completed — ahead of the declared-check examples, in its own shape (tools_in_order,verified_by, the act in words, and a limit saying it is a call trace, not a plan). Only a session whose recorded taint is clean to its end, whose every run recorded a matched workspace and surface, and whose situation the asking run is in lends one. Derived from the success set at run start, never stored, so a reopen withdraws the example.mecha sessions successes --exampleslists what would be served and why each success lends none (planning_examplesin--json). Nothing reaches the prefix; the answer with the lever off is unchanged. -
A task run's situation brief names the task's previous attempts (
APPRAISAL-WIRING-DESIGN.mdM5, 3a-2, R42). The brief gains anattemptsfield: for a run anchored to a board task, up to three earliertasksessions on the same task from the last 90 days, newest first, each said in two lines — what the owner did with its work (a draft rejected, sent as written or after edits, a question answered or let go, the task closed or reopened, an artifact check failing, what still waits), by record id; and how the run ended, as the harness recorded it, never as a verdict. A reopen reason is quoted only when the owner wrote it with their own hand; one written in a conversation with the owner's approval is said to exist and not repeated, and an outbox rejection is said as "draft rejected" and the item id, without its reason. A walk cut by its bound, or a session file that cannot be read, says "at least". Recorded on every run; delivered only behind[agent] situation_brief, which still ships off.sessions healthreports the field's completeness beside the other nine, and counts it unread only when something failed: an unreadable file or store, or a session kind this build cannot name — never the walk's own bound. -
A rule's owner tenure, beside retirement (
APPRAISAL-WIRING-DESIGN.md2e-5b/2e-5c, R41).mecha rules list/showprintowner: <tenure> · region: <quiet>per learned rule, and--jsongainsownerandquiet. Tenure is the Wilson lower bound (z = 1.96) of the owner-accept rate over the owner's verdicts on runs whoserule_idscarried the rule: no bound under 20 verdicts, tenured at 0.65, unknown when a session that carried it cannot be read in full.rules propose-retirementsgives a tenured rule on probation the ordinary leash (3) for that pass, keeping the probation mark; retirement still runs on measured regressions alone, and nothing leaves a prompt on the bound. A rule whose scope no recent run matched is named QUIET — a report only; nothing is evicted.--no-boardskips both walks. -
goal_contextcan serve past clean appraisals (APPRAISAL-WIRING-DESIGN.mdI2, 2c-2), behind[agent] past_appraisals/Lever::PastAppraisals, which ships off until an experiment measures it (levers_on = ["past_appraisals"];mecha evalforces it off). On, the tool answers a request toward the run's goal with up to three appraisals of runs in the same situation toward the same goal — only appraisals of runs that read no third-party content — framed as a model's interpretation of an earlier run. Nothing reaches the prefix; the answer with the lever off is unchanged. An experiment environment'sappraisals/is seeded into each trial home. -
calendar_list_eventsandcalendar_freebusytakenow,today,tomorrow,yesterdayand±Ndand resolve them on the server, inmecha-mail,mecha-googleandmecha-outlookalike (time::window), and every answer states the window it covered and the clock it was resolved against — so a run working from a stale date is contradicted by the result instead of confirmed by it. A window resolves inMECHA_TZalone, never the machine'sTZ; without it a relative term is refused by name. (#243) -
Every
[[mcp]]server is handed[agent] timezoneasMECHA_TZ, so the zone is set once rather than copied into each server'senv(an explicit value there still wins).mecha setupreports an unset[agent] timezoneonce a server is wired. (#243) -
A rule toward a goal that has closed is named, not silent (R34). A rule scoped to a task that is done or dropped, or to a trigger that was removed or disabled, keeps its scope and loads nowhere until a lesson restated toward another goal widens it.
mecha rules listnow counts such rules at the top and marks eachLOADS NOWHEREwith the reason (goal_closedin--json), andmecha learnrepeats the count, with the waiting reflections toward a closed goal, on every pass. The board is read only when a rule or reflection names a task; a board that cannot be read is reported as unknown (goal_unknown), never as nothing dark, and so is a task the board no longer carries. The TUI and the web settings page call the roster with--no-board, since neither can wait on an MCP start. The roster's region line prints a goal bare (shell · trigger:morning). -
A learned rule can be scoped to a goal. The front-end that owns a run hands
preparethe goal it read from a store the owner wrote (GlobalOpts::goal:tasks workits task, a trigger run its trigger,run --goalthe owner's reference, a question continuation the asking run's), the learned-rules block is matched toward it, and the run record keeps what was matched (RunConfig::rules_goal) for the miner, the backfill, the validator, the probe, the planning examples and the roster to read — never the conversation's anchor. A rule learned from reflections all toward one goal loads only in runs toward it, and widens by restatement from another goal's batch. A run toward no goal matches no goal-scoped rule, and a stored goal this build cannot name is kept verbatim, matches nothing and is reported at startup. Rules from before the key carry no goal and load under every goal as they did. (APPRAISAL-WIRING-DESIGN.md2c-1) -
Mail is classified through the working day, not only overnight.
mecha-mail-classify-day.timerruns the same sweep every 20 minutes, 07:30–21:50 in the owner's zone, clear of the morning briefing and the front door's hourly run; the 05:30 UTC nightly stays as the catch-up.scripts/model-idle.shgates each run: it skips while any local-model slot is busy or the GPU is above 30%, and fails the unit — somecha doctorsees it — when the model server does not answer, answers with a slot list it cannot read, or has been stuck loading for three hours.
Changed
-
The nightly's
sessions compareis bounded by the clock and by the corpus (scripts/ruminate.sh, owner 2026-09-27).timeoutcaps its share of the night (MECHA_COMPARE_TIMEOUT, default30m; a pass cut short keeps what it compared, and a seat it held is reclaimed by the next caller), and--daysbounds its read (MECHA_COMPARE_DAYS, default 30). -
The retirement brake runs right after
validate— in the nightly (scripts/ruminate.sh) and in an experiment lifetime's schedule (Schedule::due_after: reflect, validate, retire, learn, ruminate), owner 2026-09-27. It reads only validate's rows, so a rule measured harmful leaves the prompt beforesessions compareorlearn --autospends any wall clock, andlearnmeasures its candidate without it. The nightly scans a second time afterlearn, whose cross-region widening can re-widen a rule the first scan narrowed — so it is re-narrowed that night. -
The counterfactual comparison summary counts one model (the owner's ruling of 2026-09-27). Since the chat server became a router, background passes follow whichever model is resident, and
mecha sessions appraisesummed the production model's comparisons with a comparison arm's into one separated share. It now counts the model of the newest comparison on record (mecha sessions compare: the model it drove) and says the rows under other models beside the summary without counting them. Unposed points, which no model was run on, are counted on their own (unposed) rather than inside one model'sinconclusive.--jsonaddsmodel,other_modelsandno_model(rows with no model recorded that are not unposed points: unknown, never counted) tocomparisons;records,inconclusiveand the rest now count that one model's rows. Nothing stored changes. -
A mail thread whose classification fails for a reason of its own (a verdict that will not parse, a refusal, a rejected request) is retried on a backoff — an hour after the first failure, doubling, at most a day apart — rather than on every sweep, so it costs one model call a day instead of one per daytime tick and no longer keeps the daytime unit failed. A provider-wide failure (transport, server error, overload, an expired key or a lapsed account), or a failure at least as long as the length where the model is found to start failing a canary (searched among the sweep's own failure lengths, a few calls at most), is not counted against any thread: those threads are retried every sweep as before and caught up as soon as the server is back. A failed thread that has left the newest-N window the sweep reads is retried from the store when due (up to 10 a sweep), so the long waits of a busy mailbox never strand it, and each failure is on disk the moment it happens rather than at the end of the sweep. A re-read of a stored thread that fails is the thread's own unless that account's surface is down — the sweep's own read could not reach it, or every re-read for it failed and a re-read of a thread the read just returned from it failed too — the mail surface's (a lapsed token, a 503), counted against none. Dead threads (deleted, moved) therefore back off rather than fail the unit on every tick.
mecha mail listshows the count and when the next retry is due;classify --forcestill retries at once, inside the window and out, and out of it without the per-sweep cap. A thread retried from the store is markedread_whole, outside the escalation measurement. A body too long for the model, or a request it rejects, stays the thread's own even in a sweep judged an outage — a length-matched canary overflows just as those threads do.
Fixed
-
The model chip's "switch now" is offered only while it can act. Once a switch stops waiting for runs, it records that beside its switch file. The chip then drops "switch now" and cancel, where it used to show buttons that did nothing for the seconds before the router reports the new model loading.
mecha model use X --nowthen says the switch is no longer waiting. Two "switch now" taps at once both land; one could report failure for a hurry that had worked, and leave a temp file nothing swept. -
The web chat's plan has its own read (
GET /api/chat/{key}/todo). The page re-reads the plan on everytodoresult a run streams, and took it from the transcript read — which, since the entry below, carries the whole history mid-run, rendered and serialised once per plan revision. -
A web chat opened or reconnected mid-run keeps its history. While a run held the conversation the transcript read returned no entries, and the page replaced everything it showed with that — so a phone whose stream dropped in the background during an image edit lost the whole chat, and the picture with it, until a manual reload. The read now returns the history the run started from, and the page re-reads at
donewhen it may have missed part of the run. -
A leading
~in a config path now means the home directory. Nothing ran these through a shell, socommand = "~/.cargo/bin/mecha-mail"— the line the mail and documents pages printed — named a directory called~, the server never spawned, and[web] assets = "~/.mecha/web/dist"served no app. It covers every path-valued key and[[mcp]]commandandargs;~userand a~mid-string are left as written. -
mecha setupofferedmecha-docs auth personalto authorise documents, which fails:mecha-docs authtakes the account as--account. It now offersmecha-docs auth. -
mecha setup --writesaid "nothing answered … Start the server" when it had not looked: with a hosted default provider that has its key, no local server is probed. It now says so, and how to record a llama-server. -
mecha workflow cancelandmecha charterdescribed behaviour they do not have: cancel blocks further runs of that task, not triggers (a trigger is not tied to a task), and a charter is edited withmecha charter edit./entitycompleted in the TUI but was missing from/help, and/mail(alias/inbox) opened the classified inbox while neither/helpnor Tab completion named it; both are listed and completable now. -
Recording a cloned voice on the web settings page could silently replace a voice of the same name when two uploads landed together, although the page refuses to overwrite: the name check and the write were two steps. The recording is now linked into place, which fails if the name exists.
-
mecha setup --writekeys its "no local server was checked" message on the provider being hosted, not on it having a credential, so a llama-server behind anapi_key_envthat is down is still told to start the server. -
The mail desk's batch keys work with the selecting modifier still held. A selection is built with ⇧-click or ⌘/Ctrl-click, and the hand is often still on that key when
egoes down: ⇧E was bound to nothing and ⌘E was dropped whole, so the batch button worked while its hotkey did nothing. ⇧E, ⇧D and ⇧T now mean e, d and t; ⌘/Ctrl-E and -D archive or dismiss while more than one thread is selected, and stay the browser's otherwise.
0.1.21 - 2026-09-18
Added
grounding— what a run actually received, and whether a claim about it dereferences.callslists every tool call once with the result the model read, first seen wins, and a result compaction has written a[stale:marker over is never evidence;admitdereferences a{statement, id, quote}claim into a packet by literal containment and names what was missing when it refuses;carries_overis the inverse, refusing text that reproduces a run of words from what was read. Gossip's citation check, the outbox's "what is this draft answering" join and the diagnostician's carry-over refusal now share it. Not a tool: nothing enters the registry. (#244)- The front door hands a triage run only the
dates_mentionedthat are in the stranger's prose as written, between a three- and a 48-character bound; the rest are shown to a person infrontdoor showand the TUI with the reason each was refused — a finding, never a block. The Slack request card inherits the rule because it is built from the same brief. (#245) - The mail classifier is asked for
deadline_quote— the words a deadline comes from, copied verbatim — and a deadline whose words are not in the message is dropped at classification and recorded asdeadline_refused, with its reason.mail list,mail show, the sweep line andmail tasksay "dropped", never "found none"; an owner's--deadlinecorrection settles the refusal either way. (#246) web_searchstays usable in a conversation that has touched mail, the calendar or the graph: the send axis is now a class —None < Blind < Chosen— and the interlock fires onChosen, a destination the model names. A search backend isBlindonly where its API has no destination field (searxng and tavily at both depths, exa at quick), earned in code and never granted in TOML; an armed conversation is served by the blind backends at quick depth and the result says so. (#241)- The clock is asked per turn rather than frozen into the system prompt, so a daemon's runs carry today's date. (#238)
- A confirmed booking is not a request: it settles as
booked, never reaches a model, and the request view shows the meeting. A cancellation un-books what it withdraws. (c3ebcd98) - A privacy policy and terms of service, one section per integration, with revocation steps per provider — what Google gates publishing the OAuth app on, and the reason a Testing-mode refresh token expired every seven days. (20318b7f, 6cf31150)
- A spoken yes to an outbox offer is composed from the accepted phrases, carried to the outbox, and never sent to the CLI as words. (#228)
- Voice calls sound a pause when the link stops carrying audio and a resume
when it does again, and the worker holds every pending end-of-turn across
the gap instead of ending the turn on the words before it (
LinkWatch,scripts/voice/worker.py;linkVerdict,scripts/voice/voice-core.js). The page keeps a screen wake lock for the length of a call and treats the microphone track's mute edge as the same pause. The tasks page's dictate button plays a listening tone, shows the live level, and refuses to send a clip it did not record. A sampler thread records the main thread's stack whenever the worker's event loop stops for half a second — the unnamed first-turn stall every call since 2026-08-25 has shown.
Changed
- A peer session's delivered message, a boredom notice and a plan-step nudge
are labelled as the harness's voice in compaction summaries and in what
mecha distillsends to the graph, not as the owner speaking. (#239)
Fixed
mecha servelogs a voice offer the runner refused or could not be reached for, and a dictate clip the STT refused, atwarn— a failed call left no record anywhere before.parakeet_server.pyanswers 400 to a WAV with no samples instead of crashing the request.- A staged draft's "what it answers" view stops at the staging call even when that call's own result errored, so a read made after the draft is never offered as what it was written from. (#244)
0.1.20 - 2026-09-11
Fixed
validateno longer re-probes a reflection whose recording cites a tool this machine can no longer offer by any route. Such a probe cannot succeed on a later night, so it is refused at preparation, counted and reported apart from the other skips, and never buys a--coverslot;--repeatstill forces one if a retired provider returns. Previously these were reported as retryable and spent a ledger row and a--coverslot a night each to reach the same refusal. The refusal itself is free: it happens before any provider call.appraisal_probeno longer charges its per-corpus arm budget, or counts adrivenarm, for a probe whose recorded surface is gone — that failure happens before any provider call. Such a probe is counted in a newTally::surface_lost, apart from theunavailablechannel that means fixable, and reported in both the--jsonand human readouts ofmecha sessions appraise.bench/build-portable.shrecords the branch, commit and sha256 of the tree it built beside the binary, and refuses an unattributable build — a dirty tree, one git cannot read as the root of its own checkout, or one that moved during the build.MECHA_BENCH_ALLOW_DIRTY=1opts out and is stamped into the recorded line. Repo tooling only; no crate behaviour changes.
0.1.19 - 2026-09-09
Added
-
Anticipatory appraisal for confirmed-goal planning and inline outbox messages: owner-bound commitments, verification and cost evidence derive guilt, embarrassment, regret, disappointment, anxiety and curiosity assessments.
run --appraisal-evidencesupplies invocation evidence;outbox anticipaterecords exact-argument predictions with optional release guidance, andoutbox outcomelinks owner feedback to confirmed delivery. Supported exposed-error and impact records produce retrospective embarrassment and guilt. Revised predictions and superseded outcomes retain their history; affected replay/probe paths explicitly report unsupported evidence reproduction. -
A reproducible gossip comparison using real fixture searches, peer versus own-evidence followups, equal request ceilings and an explicit claim audit. Ordinary gossip behavior is unchanged.
-
A reproducible executable correction pilot compares frozen rules and turn budgets using real file tools, independent artifact checks and paired task-success reports.
-
Validation attempt receipts and
mecha validate --repeat; unchanged inputs are deferred while transient failures and regression confirmations remain eligible. -
Gossip target identity, retrieved citation checks, per-round citation yield and explicit filing failure counts.
-
Ground mismatch learning in verified criterion/check failures; owner-bound task diagnostics can carry relevant count constraints without exposing expected answers. Forecast overruns remain observations and no longer justify behavioral rules by themselves.
-
run --mismatch-caseand registered experiment fixtures enable independent JSON-artifact validation of clean planning mismatches in isolated file-only tasks. Validation and rule proposals retain ungraded outcomes for missing evidence or policy refusals; each executed arm records an audit receipt.--no-goal-guidancenow works before or after a subcommand. -
mecha run --goal KIND:IDexplicitly confirms a goal, including on resume; experiments can register per-task confirmed goals. Completed todo steps now retain and execute a previously declared check when the completing update omits it. -
A paired appraisal pilot with twelve independently graded artifact tasks, synthetic charter and sensor fixtures, and reporting that preserves missing measurements. Explicit experiment planning levers now enable disabled settings.
-
Goal-aware planning feedback: confirmed goals persist across turns and resume; declared checks execute through the normal guards; failed criteria/checks and frozen-check changes can produce bounded mismatch reflections. Forecast overruns remain observations. Trace replay explicitly refuses recordings with harness checks until their observations can be reconstructed.
-
goal_contextretrieves applicable goal-linked rules and recent, clean examples with passing checks. Optionalagent.goal_guidancegives fixed planning advice from task progress, confirmed goals and charter sensor discrepancies. -
Event-time goal attribution preserves task and charter relationships. Owner task closure remains a separate verdict; global queue movement earns no causal credit.
-
Explicit per-task fixture clocks and recorded rubric judges for assistant experiments; answer checks receive actual tool evidence and fail on grading errors.
-
Durable workflows link task conversations, questions, drafts and dependencies; owner-specified completion checks inspect artifacts and confirmed delivery.
-
A Today view groups urgent items, decisions, verified work and waiting, with commitments, quiet hours, coalesced reminders and snoozing. Refreshes share source reads across workflows; finished work can be explicitly reopened. Failed chat recording preserves prior workflow completion evidence. Blocked question resumes leave the question open and create no running marker.
-
Batch outbox rejection continues past uncertain deliveries, preserving their records and reporting partial failures with a nonzero exit status.
-
Explicit structured-output provider capabilities for quarantined extraction, plus stable research, assistant and coding tool profiles.
-
Repeated assistant lifetime fixtures check actual sent messages and calendar events after review, detect duplicates, and record owner-action counts.
-
Outbox delivery reconciliation in the CLI and web, with durable attempts that prevent automatic retry after an uncertain outcome.
-
A learned rule can be scoped to a surface. The front-end that owns a run tells
prepareits kind (GlobalOpts::surface, never a flag), the learned-rules block is matched against it, and the run record keeps what was matched (RunConfig::rules_surface) for the miner to stamp — neverSessionMeta::kind, which the board's task door onserverecords as a task while the block was matched as web. A rule learned from reflections all on one surface loads only there, widens by restatement from another, and narrows by conviction, as the workspace does; the reflect pass reconciles rows stamped with a kind before the field existed. A question continuation restores the asking run's surface off its record rather than asserting one, and the miner stamps the keys of the run record covering each intervention. A stored scope naming a surface this build cannot read is kept verbatim, matches nothing rather than everything, and is reported at startup, and the front door and mail now append the run record that keeps the surface they declare. Rules from before the key carry no surface and load on every surface as they did. -
The TUI's task edit form carries the project.
tasks set --projectexisted in the terminal and not in the modal; the edit form now prefills the project and re-files only when the field changed (clearing it clears), so the two surfaces offer the same correction. -
mecha rulessays when a scope loads nowhere. A rule scoped to a workspace or surface no run record ever presented is dark with nothing warning — the corpus-shaped half of the never-loads check, which named tools only; the roster printsLOADS NOWHEREbeside it and the JSON carriesloads_nowhere, read off every run record in the session store and only when a scope names such a key. -
The fixture board can be asked to truncate (
MECHA_FIXTURE_BOARD_CAP=N), and the cross-process fixture test measures the flag every board reader keys off against the fixture rather than a literal. -
A learned rule can be scoped to a workspace. The workspace a run is jailed to joins the tool set as a scope key (
Situation::scope,Situation::matches,Situation::key): a rule learned from reflections that all came from one workspace loads only in runs jailed there, a verbatim restatement from a second workspace's batch widens it by dropping the key, and a conviction in one workspace narrows it to the one it held in — the same harness arithmetic tools already had (GOAL-SYSTEM-DESIGN.md§17.4). The run record keeps the workspace the block was matched against (RunConfig::rules_workspace), and the miner stamps a reflection with that rather than the session's jail — onserveand Slack the two differ, and a lesson scoped to a jail no match presents would be dark forever. Everymecha reflectpass now reconciles each recorded workspace against the run record before anything is mined — to the matched one, or to none where the record carries none; never adding a key, and leaving a row whose session cannot be read — since reflections stamped with a jail before the field existed were inert until the workspace became a key, and the nightly'slearnfollows that pass. Rules from before the key carry no workspace and load everywhere as they did. The surface stays recorded and unmatched, sinceprepareis not told the session kind. -
A project closes when the owner closes its last task, and its appraisal is the fold over every session that worked one. The board row now carries
project_idbeside the project's name (mecha-graph PR #10, unreleased when written), the closure appraisal inmecha tasks setrecordsproject:<id>on the appraisal's goals after the task, and closing the last open task under a project prints the project's reading on stderr — labels counted, valence summed, tasks never delegated or unreadable counted rather than dropped. No follow-up is staged for a project and no record is written; a board that names a project without identifying it is said once rather than read as no project (docs/GOAL-SYSTEM-DESIGN.md§17.7 item 5, finished).mecha tasks set --projectre-files a task by name or node id, or clears it with""— the correction path a cited pointer needs, through the graph's own resolver. -
The goal pointer crosses to the graph whole; the sentence stays home. A distilled episode's
metacarriesgoalandserves_charteraskind:idpointers and the pointer on each goal error, where the kind word alone used to cross — resolved first: a charter id against the charter, a task or project id against the board (onekg_task_listper distill run), and what does not resolve falls back to the kind word ongoaland on each error'sgoalalike, so a run that named a setpoint is not read as one that named nothing (serves_charteris the join key alone, absent when no line resolves). The goal hypothesis, the owner's answer and the charter line's text never ride (§17.7 item 8).
Changed
-
Refreshed the documentation for current CLI commands and configuration, Today and workflows, automatic learning, outbox delivery recovery, and replay limits. Goals and appraisal now covers confirmation and drift, project closure, step metrics, current labels, and the features consuming appraisal evidence.
-
The knowledge graph is named as the graph everywhere in the tree. Comments, tests and one
mecha distillwarning still called it by the retired server's name; nothing calls that server, and the words now say so. Three places changed what somebody does (found on review): the published evaluation page named a fixture file that does not exist, the landing page's[[mcp]]snippet named a binary that is not shipped (and, once it named a shipped one, lacked the[mcp.capabilities] untrusted_input = truethat arms the interlock over the graph — the overrideTRIFECTA.mdcalls load-bearing; found on review), andprompts/agent.mdtold the model to callpkg__kg_*tools that are not on the surface — it now names the barekg_*the documented wiring (prefix_tools = false) exposes.
Fixed
-
Follow-up validation replays the recorded tool surface and grades only complete answers. Coverage selection skips deferred inputs before spending probe slots.
-
Harness measurements reject unsupported effort controls and incomplete replay tails, retaining arm receipts. MCP capability changes are routed as security proposals.
-
Stopping the web service preserves active turns. SIGTERM and Ctrl-C close pending questions, cancel chat and mounted voice work cooperatively, and wait for partial transcripts and outcomes before exiting. A second Ctrl-C or SIGTERM forces termination if needed. Idle event streams and voice clients no longer prevent shutdown. Updated serve/voice service units signal the daemon first, and MCP cleanup finishes before it exits.
-
Chat input appears on every connected device. Typed messages and steering are broadcast with request IDs, avoiding duplicate echoes on the sender and preserving separate messages with identical text. Steering distinguishes queued, delivered, and too-late input instead of claiming everything was sent.
-
Chat transcript and event-stream reads no longer create sessions. The app explicitly opens chats and repeats that step when reconnecting; scripted clients must first
POST /api/chat/{key}withX-Mecha-Request: 1. Mode changes now reject invalid keys before creating a workspace. Permanent opening refusals stop retries; transient failures back off. -
Docker-confined MCP clients now own and remove their containers when the last client drops, including failed or cancelled startup and servers that ignore stdin EOF. Cleanup also works after the async runtime shuts down;
mecha runreleases clients before refusal and empty-output exit codes, as doesmecha batchbefore a failed-batch exit. Daemon failures are retried and reported with bounded Docker diagnostics. -
Browser mutations now require a same-origin request header, including bodyless actions, uploads, and voice offers. Reload the web app after upgrading; scripted API clients must send
X-Mecha-Request: 1on writes. -
Chat uploads reserve filenames atomically and refuse symlinked inboxes; resumed conversations use their recorded workspace for attachments. Downloads stream from confined file handles instead of buffering whole files.
-
MCP clients terminate their spawned child process and abort reader tasks when the last client is dropped, including after an unsuccessful handshake.
-
fs_readapplies line selection and its byte ceiling while reading, so a small selection does not allocate or decode an entire large file. -
Graph route updates and charter drag state now use Svelte's reactive state correctly, removing the corresponding build warnings.
0.1.18 - 2026-09-06
Added
-
The run record knows the goal the owner confirmed and whether the plan left it. A question resume anchors the run on the answered question's
serves, and a present human's answer toask_useranchors it in-run;todocounts every plan write made under an anchor, every one whoseservesnamed a different kind or id, and — apart — every one that named nothing.RunStatscarriesgoal_anchor,goal_plan_writes,goal_drift_writesandgoal_unnamed_writes(unknown before the sensor), andmecha sessions healthprints agoal driftline and JSON keys whose rate is changed pointers over runs that named a goal at least once under an anchor (docs/GOAL-SYSTEM-DESIGN.md§17.7 item 4, the sensor half; the re-ask stays off until it is read). -
The goal rides on the question, and a project is a kind of goal.
ask_usertakesgoal(one sentence) andserves(charter:<id>,task:<id>,project:<id>), shows them above the question as one text, and a delegated run's parked question keeps the typed hypothesis beside the owner's answer — the goal record, printed bymecha questions showand carried by/api/questions. The delegated seed folds the goal into the one question it already asks first; the charter block asks every surface for the sentence in words that name no tool; a run with nobody to ask states the goal it is assuming, andmecha outbox showand the web outbox print what the drafting run's plan served as of the staging call, with the charter line's own text, so releasing the draft confirms it.sessions appraisecounts sessions that put a goal to the owner and had it answered, and reads the ask'sservesas a second producer of a named goal.GoalRef::Projectis the fourth kind (docs/GOAL-SYSTEM- DESIGN.md§17.7 items 3 and 5). The charter budget is 2,500 characters, moved with the block's own prose. -
Task sources, and AgentDojo as a fixture world.
[tasks] sourcenames an executable instead of a case file; the driver calls it withlist,setup <task>andgrade <task>(the run's--jsonresult on stdin), fail-closed at every edge, with four pointer variables so it finds the trial's world.eval/fixtures/source_stub.pyis the contract's reference;eval/fixtures/dojo.pyserves an AgentDojo suite over MCP and grades its tasks with the suite's ownutilityandsecurity, from a venvscripts/dojo-venv.shbuilds;eval/dojo-workspace.tomlmeasures the interlock's catch rate and false-refusal cost on the workspace suite. -
The run record counts null steps, reopened steps and completed steps — a plan step completed with no call behind it, a completed step set back to in progress, and the completions both are read against — counted by the
todotool into a per-run sensor and read bymecha sessions healthas rates, each over the runs in which the event could have happened — a null over runs with a completion whose span was measured, a reopen over runs with any step activity — with the totals beside (docs/GOAL-SYSTEM-DESIGN.md§17.7 item 2's precondition, now readable from the store). -
Fixture servers on the manifest, and the synthetic assistant home. A
[fixtures]table on an experiment manifest names MCP servers the trial home runs instead of the operator's: when it names any, the home's[[mcp]]is exactly that list for every arm, each server persists under the home (fixtures/<name>/, asMECHA_FIXTURE_DIR) and is seeded once, achartermay be written over the seeded one, the outbox route is the world's (outbox_tools, spelled, never inherited — the operator's names live tools), and the names join the condition hash. Seeding fails closed (built beside, marked, renamed into place) and asinglere-seeds every store per trial, since its per-arm home is shared. That closed world opens the two owner channels the principal was gated off:outbox approveandtasks setare permitted under a manifest with fixtures (experiment::SERVER_VERBS,permitted_verb), a release is vetted against the draft's<fixture>__prefix and run under the driver's own--yes, and the principal is told which fixtures its arm reaches. Two servers ship:eval/fixtures/board_server.py(the graph'skg_task_*in the real server's shapes, stateful,kg_upsertrecorded and never entered) andeval/fixtures/mail_server.py(mecha-mail's surface, every send a line insent.jsonl, nothing delivered).eval/fixtures/home/is the synthetic assistant home on the fictional cast — board, mailbox with one injected thread, calendar, charter — witheval/home-cases.jsonl,eval/home-principal.toml(release to the cast, closetask-<case id>by the grade) andeval/home-lifetime.toml. The manifest's relative paths (servers, the principal's script and policy) resolve against the checkoutexp runstarts from; the home is rendered before position 0'sbefore_taskcall so the principal's first verb finds the board. -
Learned rules widen on evidence and narrow on conviction, and the validation ledger is per region (
docs/GOAL-SYSTEM-DESIGN.md§17.4). A rule carriessupport— the sub-regions its evidence was recorded in — besidescope; a learn batch that repeats a rule from another situation word for word widens it to the intersection of the two regions (the harness computes the scope, the learner only claims "same lesson"). Each validation row records the sub-region the probe exercised,mecha rulesshows the counts by region, probation is released only by rows that exercised the rule,mecha validate --cover Nspends N probes per (rule, region) pair the ledger has never graded (the nightly passes 1), andmecha rules propose-retirementsnarrows a rule convicted in some of its support regions and clean in others to where it held instead of retiring it, retiring with the reason when no narrower scope excludes the failure. -
mecha exp— experiments (mecha_core::experiment;docs/EXPERIMENT-DESIGN.md). A designed comparison over a chosen set of runs:newwrites a manifest once (arms naming a provider and model and varying the closed lever set and the override knobs — either axis or both — a control, a falsifiable prediction per treatment arm, the tasks as an eval case file, the seeds, the split seed),rundrives one childmecha runper arm × task × seed × repetition into an isolated home per arm whoseconfig.tomlis the arm (the operator's posture with keys scrubbed and the arm applied; the learning store, skills and charter seeded once; a clean, allowlisted environment; the staged workspace as cwd),statusandjudgeread the trials back (each treatment arm against the control through the candidate gate, the task outcome entering as a cost), andexporthands the whole record over.evalis the special case of arms that name models under thebarepreset, and is to become a thin front over this. New with it:SessionKind::Experiment, hidden from the real store's readers and admitted in a trial home;RunConfig::experiment, the trial reference a child's session carries; andmecha run --jsonnow a superset of the batch result (tool calls, taint, counters, elapsed), so a run driven as a process reaches the same grader. -
mecha expruns lifetimes. Akind = "lifetime"manifest — an ordered task sequence (tasks.ids, in order) and a[schedule]of loop stages between tasks (reflectevery task,validate --unprocessed-only,learn --holdout 0.25 --autoandrules propose-retirements --applyevery fifth,harness ruminateevery tenth, by default; 0 is never — the nightly's order and flags, validate measuring before learn consumes and the retirement brake after) — runs one isolated home per arm × seed × repetition, walks the sequence in position order, and after each task runs the stages due as childmechaverbs in that home, sequentially, each written to the lifetime's stage ledger (stages/<lifetime>.jsonl) with its exit status. Resume reads the rows and the ledger: a finished task is not rerun, a missing stage runs before the next task, a failed one is due again with its own log — only while no later position has finished, else it is recorded skipped out of sequence — and an interrupted one leaves arunningline that burns its attempt.judgeandexportread the ledger: a failed interrupted or unreadable stage line on either side holds an arm's verdict at propose, and the export carries every stage line. What a stage accepts inside the home — aruminateoverride — reaches the next task under the arm's pinned keys. Rows gainpositionandlifetime;statusshows each lifetime's sequence by position with its stage counts. New with it: the stage levers, a second closed set an arm names off instages_off(reflect,learn,validate,ruminate,retire,sensors_in_brief), on the row's condition hash, refused on asingle; and[agent] sensors_in_brief(default on), the switch behind the last one, which withholds the homeostat's and guilt's readings from the diagnostician's brief — their only reader. A lifetime run with no one answering mines no reflections, since every trigger today is an owner's act;ruminateis the stage with an effect until the principal lands. -
The principal: an executable under a contract, driven by
mecha exp. A lifetime manifest's[principal] commandis called before and after every task with the trial's state as JSON on stdin — the case, the graded row, the home's pending drafts and open questions — and answers with the owner's verbs to run and the refusals to script. The driver runs each verb as a childmechaagainst the trial home from a closed set (tasks set|steer|stop,outbox reject|edit— never a release, which executes the routed tool for real,questions answer|abandon) and records the call and every act on the lifetime's ledger as aprincipalline, held like a stage. New with it:MECHA_DENIALS_FILEandtool::FileDenyApprover— scripted refusals a run answers ahead of its own approver as "Denied by the user", the owner's channel with no headless path until now, honoured only under the experiment session kind and refusing any other run's start — andscripts/principal-gold.py, the gold-verdict principal. Board closure waits on a fixture graph server. -
Two more levers:
predictive_compactionandcarried_state([agent] predictive_compaction,[agent] carried_state,--no-predictive-compaction,--no-carried-state). The two in-run dispositions the lever set could not name because nothing could switch them off: the compaction trigger firing on the forecast of the next request (the threshold stays — a lever removes a disposition above a structural check, never the check — and so do the predictor's other uses, the per-turn tool-output budget and the headroom the model is shown), and a tool's state riding verbatim across a compaction. Both ship on, both are recorded onRunConfig::levers_off, andmecha evalforces both off with the rest of the set. The appraiser's pass stays out of the set — it has no in-run site — andsensors_in_briefwaits for the trial manifest, being a stage lever rather than a run's. -
A cancelled run records what ended it.
StopCausegainsParked(a question put to the owner),Stopped(a person: Ctrl-C, a stop button,tasks stop, a trigger cancelled by request) andShutdown(the process or a limit: SIGTERM, a facade shutdown, a trigger's wall-clock ceiling). The canceller says which throughagent::CancelReason, carried beside the token onRunContextand onToolCtx; a bare token cancel still recordsInterrupted, which is now read as unknown-which and never as any of the three. The appraisal readsStoppedas the owner's verdict on the intervention channel — a stop followed by a re-prompt is a redirect cited at the re-prompt, a stop never resumed is an abandonment — and reads the other three as nothing.docs/APPRAISAL-RESEARCH.md§3.3, measured in §1.7.2 as the largest single move available to the readout. -
RunStats::duration_secs— the run's wall-clock seconds, fromrun_in's entry to its return;Noneon rows written before it and on any fold missing a run's time. §1.7.4 found wall clock the best predictor of failure on the kept Terminal-Bench trials and on no record. -
The lever set (
mecha_core::harness::Lever,RunConfig::levers_off;docs/EXPERIMENT-DESIGN.mdPart II, The switch set, on PR #156). The closed on/off half of the override set: thirteen subsystems a run can carry structurally absent — MCP, learned rules, hooks, the outbox, fallback, messages, skills, the charter, thecompacttool, step escalation, approval rules, boredom and compact validation — each a switch that already exists, now named once and recorded on every session'sconfigrecord in a stable order.Noneon a transcript that predates the field, and on one naming a lever this build does not know, because a lever dropped from the list would read as on.mecha eval's bare arm is nowLever::bareexpressed through the same switches (plus the approval rules, whichbareitself never lifts — eval throws that lever by name), and a test reads the forced set back through the function the record is written from. Two new global flags complete the set:--no-boredomand--no-compact-validate, opt-outs for the two[agent]switches that ship on. -
scripts/appraisal-validity.py— step 0 of the appraisal experiments: joins the readout and the counters to Harbor's per-trial verdict over the kept Terminal-Bench sessions and reports discrimination per channel; the finding isdocs/APPRAISAL-RESEARCH.md§1.7. -
Per-command approval rules (
[[rule]]and[approval]in config,mecha_core::policy; PRs #143 and #148). Approval used to be tool-granularity — a run either approved everyshellcommand or asked about every one. A rule names a tool, a prefix pattern (["git", ["status", "diff", "log"]]) and one of three ordered decisions,allow<prompt<forbid, most restrictive wins, judged one shell segment at a time by a splitter that refuses anything it cannot take apart with certainty.allowremoves the human prompt and nothing else — the approver's mode, the trifecta interlock and an escalation all still apply;promptputs the call in front of a person even past a standing "always", and fails closed where no person can be asked;forbidrefuses without consulting anyone and is never mined as a correction. An allowlisted interpreter is not an allowlisted command (python -c,sh -c,xargs,sudo,timeoutand the rest are judged as at leastprompt), every patterned rule must carry amatchexample checked at load,allowloads from the global file only, andmecha evalforces the whole policy off. A command the splitter finds opaque is searched for everyforbidandpromptby its words (redirects, here-strings,$IFSand braced expansions accounted for) and otherwise falls through to the approver as an unmatched command does; anallow/prompton a tool the live outbox route stages is refused at startup, since staging runs before the rules (a project layer's is set aside for the run with a warning instead). -
The web app has a desktop layout (
REMOTE-SURFACE-DESIGN.mdD10). It was nine phone screens faithfully built, so a 1500px window rendered a 560px column and left the rest of the screen empty. Two breakpoints, each because something specific stops fitting: at 900px the bottom nav becomes a left rail and the shell's floating gear docks to its foot, and every view keeps a reading measure instead of stretching; at 1180px the chat's session drawer stops being a drawer and simply stays open — the one thing a phone could not afford, and the whole reason the drawer existed. The phone layout is unchanged apart from the chat header, whose status chips now wrap under the session name rather than squeezing it to nothing. -
A conversation names itself (
REMOTE-SURFACE-DESIGN.mdD11,mecha_core::title). A session was created under a key, and a key is an address rather than a name; asking for one moved the friction rather than removing it.mecha servenow derives a short name after a run and re-derives it as the conversation grows (owner turns 1, 3 and 8), recorded as aRecord::Titleand applied over the header bySession::read. The titler reads only the owner's own turns — never assistant text, never tool results: a title is rendered in the owner's session list for as long as the session exists, which is a longer-lived display than any single answer. -
Appraisal reports the sign it was hiding (
appraisal::Valence,Readout,live_readout; PR #140). The label alone gated on a dimension only a paid replay fills, so 142 of 143 appraised sessions readneutral, twenty-two owner-rejected drafts among them. Every surface now shows the dimensional readout beside the label — positive and negative sums kept apart, never netted: a number on the TUI badge and in a Slack thread line, a two-sided bar on the web chip — andsessions appraisesums it across the corpus. With it: a kind on every session (SessionKindon the meta record, written by each front-end;MECHA_SESSION_KIND=testmarks smoke runs and may only narrow), and every corpus readout excludes test sessions by default and counts what it hid, because 46 of 143 appraised sessions were the harness's own development runs; a ceiling reads as the owner's own limit rather than the world's, withAppraisal::cut_shortkeeping the closure follow-up honest; and the plan is the prediction — atodostep may carryexpect,checkandexpect_calls, the check is frozen the moment its step completes and restored on any later rewrite (surviving trims, resumes, thinned echoes and compaction, each closed on review with a test), a failed check is a signed error, andTrigger::Mismatchis the wire word for the reflection that will fire on one. Thecheckis recorded and never executed here. -
Appraisal reads the commitment stores (
appraisal::SessionRecords,Channel::Commitment; PR #141). A question answered and the session finishing, a question abandoned, a front-door request closed with nothing sent, and a run that left fewer things waiting on the owner than it found — net of anything the owner gave up on (Depth::given_up), so a rejected draft or an abandoned question shortens the queue without crediting the run — each sign an error against the record, from ids and states only, never prose. A judged follow-up reflection counts as an intervention on clean provenance alone, stricter than the learning loop's own gate. The guilt sensor's relief lives in its own field (Homeostat::guilt_after_relief) so the corpus mean over the level stays one quantity, and a reading computed over a store that could not be fully read is markedpartialon the record itself, where the closure path's printout and JSON carry it. The queue-delta positive is the one positive a live surface can show; it is a global diff of the stores, disclosed as such. Charter sensors — owner-written observables on a charter line — are designed atGOAL-SYSTEM-DESIGN.md§11.1 under seven containments, and built in three steps: the[line.sensor]table (five closed kinds, a setpoint typed by the kind, refused at load when unknown or zero) and the attribution of a run to the line whose sensor watches what its trace touched; then readings (mecha_core::reading) — each sensored line read against its store as one of five states kept apart (unread, deferred, nothing waiting, too few runs, observed with how far past the setpoint), recorded on every run's homeostat, shown beside the line onmecha charter, the TUI's/charterand the web settings page, and read bymecha doctor, whose stuck-draft, unanswered-question and stale-request findings now use the owner's setpoint where a line names one and name the line, with a saturation finding when a line has read past its setpoint on each of the last ten runs; then the replay tiebreak — among episodes of equal headroom, the one whose signed error names the higher-ranked charter line is selected first, the count printed beside the draw's seed. The sensor's kind, setpoint and reading never enter a prompt and never feed a metric. -
mecha reflect --backfill-situations— reflections mined before the situation field get one, recomputed from their transcripts with no model call: the tool window, surface and workspace the miner would have recorded, matched by session, trigger and intervention text. A reflection whose intervention cannot be found once in its transcript stays without one and is listed with the reason; a situation recorded at mining is never overwritten;--dry-runreports without writing. The recomputation is stamped on the record (situation_recomputed_at). -
A sensor can be authored in the web charter editor. Under an open line: a select over the closed set of kinds the server serves, an empty setpoint field with the parser's own unit hint, and remove. Nothing is prefilled — the page proposes no kind and no number — and a bad setpoint is refused at the save by the same reader every run loads through, with the line named. A half-filled sensor is flagged beside the line before the save rather than dropped silently.
Fixed
-
The date prompt now includes a computed local calendar reference for yesterday through the coming week, preventing reliance on inferred weekday/date arithmetic.
-
Mail search rows and thread headers now include a computed calendar date and weekday beside the original timestamp, in the configured mailbox timezone.
-
Mail read-state metadata now identifies the owner's mailbox and unknown recipient read status. Assistant guidance requires source-backed calendar and relative-date claims.
-
Project configuration can no longer remove inherited outbox routes or redirect the global review store.
-
Replays retain per-result provenance and treat legacy unknown results as untrusted.
-
Assistant guidance no longer claims that delegation clears conversation taint; failed task runs now acknowledge possible partial files and drafts.
-
Six-lane harness review (PRs #139 and #142): a dangling symlink no longer passes the path jail as a new file and both writers open with
O_NOFOLLOW; a delegated child inherits the parent's taint and a child holding a send-capable tool is itselfexternal_send, so an armed run cannot launder a send through a subagent;trifecta = "ask"no longer equals"allow"under a headless approver (Approver::escalate, defaultBlocked); streamed Anthropic usage frames replace rather than add;Failoverreports its primary's vision; the compaction summariser and validator no longer stream as the assistant's words; a cancelled run hands back the assistant's last words rather than the tool-result tail; a panicking tool costs the call, not the run; the OpenAI-compatible decoder surfaces llama-server's mid-stream error frame; a stream that simply ends is an error, not a turn; one compaction summary survives instead of stacking; and the HTTP client splits into a streaming and a non-streaming one so a stall bound no longer caps a long non-streaming exchange. -
A backlog read creates nothing (PR #147, closing what #141 opened).
Backlog::readruns at both ends of every run, and two of its five store readers still opened through constructors that create — one runsgit init— so a machine that had never ruminated gained a learning store twice per run, and a failed creation read as an unknown depth rather than an empty one. All five readers open only what exists, and the test moves the mecha home and asserts it is empty afterwards.
Changed
-
Every
mecha evalscorecard is recorded as a one-arm experiment under~/.mecha/experiments/eval-<stamp>/: a manifest with no control (a measurement — one or more arms, no prediction, nothing to judge) is now a valid design, and eval writes one before its arm runs, carrying the provider and model that ran (the flag, else the provider's configured model, else its default — the run's own resolution), the bare preset plus--mcp, the machine's knobs, and a description naming what no lever can (the lifted approval rules). Each run of each case lands as its own trial row —--runs kis the manifest'srepetitions, so k rows share a condition hash and differ by repetition, as the hash's contract requires;mecha exp status|exportread it;-ogainsexperiment. The scorecard is unchanged. An eval under--mcp-fileor--no-ask-useris not recorded and says so as a choice, not a failure: fixture servers and a withheld tool have no lever, and a trial row's condition hash would call that eval bare. -
mecha eval --ab-configand--ab-rulesare two-arm experiments. Each writes its design to~/.mecha/experiments/eval-ab-<kind>-<stamp>/before the arms run — abarecontrol and abare-plus-delta treatment (the override, orlearned_rulesturned back on: an arm's newlevers_on) predicting a lower failure cost — files each case as a trial scored pass^k over--runs, and takes its verdict fromexperiment::judge, somecha exp judge <name>re-derives it. The arms still run in-process on eval's forcings; the printed output keeps its shape, the holdout is drawn by a seed derived from the delta rather than a hash of the case id (the same delta over the same case set still holds out the same cases; a case added to the file reshuffles the draw), and both flags now write one JSON shape —experiment,ab_rules,ab_config,holdout_in,judgement,pairs,arm_a,arm_b,flips(each{id, was, now}) — where--ab-rulesused to writewithout_rules/with_rulesandflipskeyed the same way. Two narrowings:--mcp-fileis refused with either A/B flag, since the fixture servers it adds have no lever to record them under; and the manifest's description says that both arms ran with the operator's approval rules lifted, eval's fixture forcing, which no lever can name. Both arms' records carry the four knobs the machine and the flags set, so a control run at--max-turns 60is filed as such. -
mecha evalnow forces boredom and compact validation off, with the rest of the lever set. Both are[agent]switches that ship on, so two machines with different config graded different runs — a boredom notice in the model's context on one, a second model call per compaction on the other — and no scorecard recorded which. A scorecard taken before this change ran with whatever this machine's config said; one taken after runs bare. (Eval writes no session, so the record that names the bare arm is any other front-end's, from the same definition.) -
Starting a web conversation asks nothing. The "new" button moved out of the session drawer into the chat header, where it is one tap from anywhere, and no longer prompts for a lowercase-and-dashes name.
-
The served web app revalidates by
ETagas well as by date (tower-http 0.6 → 0.7).ServeDirnow stamps every file it serves with a strongETagfrom its size and mtime, honoursIf-None-MatchandIf-Matchper RFC 9110, and answers a conditional request with a304 Not Modifiedthat carries both validators (ETag,Last-Modified) where 0.6 sent a bare status line. For theno-cacheentry document that is the difference between a browser that can refresh the entry it just confirmed and one that cannot; the hashed assets stayimmutableand never ask.ServeDir::newis unchanged, and the release's breaking changes all sit in middleware this crate does not use.
0.1.17 - 2026-08-31
Changed
- The web notes and graph tabs are one graph tab
(
NOTES-GRAPH-DESIGN.md). They were two disjoint halves over one store — a note is a graph episode — soGraph.sveltereplaces both: one find field (the graph's own hybrid BM25+vector search; ⌘K focuses it; a search hit naming an entity opens it, and a missed entity lookup falls back to search instead of dead-ending), capture with the CLI's own "entities linked" confirmation, the recent-notes list with in-place edit, and entity pages that now render what the envelope always carried (aliases, per-source coverage) plus the neighborhood (mecha kg related→/api/related) and bi-temporal history with superseded facts (mecha kg timeline→/api/timeline). The#noteshash still routes.
Added
-
The web settings page can disagree with what the model learned (#119). It listed the learned rules and could do nothing about them, and never showed a reflection at all — so the stage where objecting is cheap was the one stage with no browser surface. It now carries the two panes the TUI's
/learninghas, with the same verbs behind them. -
The retirement end-to-end drill (
scripts/retirement-drill.sh+mecha-core/examples/retirement_drill_seed.rs) — the ungating preconditionLEARNING-LOOP-RESEARCH.mdnamed, finally run whole: in an isolated world (MECHA_SESSION_DIR/MECHA_LEARNING_DIR), an honest recording is seeded with a steer, a probationary bad rule and a bystander, then real probe passes against the live model must regress, bisect to the right rule, hold at one conviction, and retire at the probation leash of 2 — bystander untouched. Its first run found the probation-release bug below, which every unit test under the path had passed over.drive_armnow traces each arm's verdict, calls and final text underMECHA_LOG=debug. -
The graph's shadow queue reaches every surface an owner holds (#114).
mecha review shadowlists the surfaced-verdict queue (review-on-use: live shadow facts about to matter) and decides one with--confirm/--refutethrough the mecha-graph child;mecha servegains/api/queue/shadowand/api/queue/shadow/verdict(serve/review.rs::shadow/shadow_verdict), the web review page a surfaced-verdict deck, the/queuesmodal a graph-shadow row with in-place verdicts, and/find's entity detail per-fact tier marks. -
A web entity page (#114):
/api/entity(serve/board.rs::entity) andEntity.svelte, nav entrygraph, marking unreviewed and denied facts as such (entity_detail_marks_unreviewed_and_denied_facts). -
Chat tool-result previews (#114):
WireEvent::ToolResultandEntry::Toolcarry a capped preview of what a tool answered, so the web transcript shows results rather than only calls. -
The docs site embeds a fixture-backed demo of the web surface (#117) — built from
web/against invented fixtures (web/src/demo/), since the real surface is loopback-only behind a tailnet identity and can never be linked or screenshotted. Two CI gates ride the docs build:check-demofails on any/apiendpoint the app reaches without a fixture, andrender-checkloads every page in headless chromium and fails on an error or a page that drew almost nothing. The docs workflow now triggers onweb/**. -
The appraisal docs page answers when it runs and what it feeds (#115): the four appraisal moments, the consumer map, and the previously undocumented step-appraisal mechanism.
Changed
-
The charter is edited as a list, and re-ranked by dragging. The web editor was a raw TOML textarea — the shape it inherited from
mecha charter edithanding the file to$EDITOR— so adding a priority or changing its rank meant writing TOML by hand. Lines are now tapped to edit, added with a button, deleted behind a two-tap arm, and re-ranked by dragging a grip. Dragging is not a convenience: it is the only rank control there can be, sinceCharterLinedenies unknown fields and §11 gives the charter no rank key at all, making position in the file the whole ranking — the design's own "the only editing gesture that cannot produce a tie". Pointer events rather than HTML5 drag-and-drop, because the surface is a phone first. Everything above the first[[line]]is preserved, so header comments and the first-charter template survive a save (trailing blank lines normalise; nothing written is lost); a charter with comments between its lines, or one that does not parse, opens as TOML instead of being silently rewritten. Nothing changes on the server: the same route, the same two-tap confirm, the sameCharter::parsevalidation, and the invariant that matters is untouched — the owner authors every line and no model composes one.the_web_editors_serialisation_is_what_this_reader_loadspins the format across the two languages that describe it. -
Settings is an index, and its gear is on every view. The page was one scroll of three stacked sections, reachable only from Home — so a charter editor, a rule roster and a live microphone shared one screen, and settings could not be opened from the other six views at all. Each feature now opens its own pane at
#settings/<charter|learning|voice>(SettingsCharter,SettingsLearning,SettingsVoice), routed through the hash router the rest of the app already uses, so back, forward and reload land where they should; each index row carries what is actually in there — a count, or a dash where the store could not be read. The gear moves out ofHome.svelteinto the shell (App.svelte): one button, the same corner on every view, layered below the app's scrims, sheets and drawers so it can never float over an open one. Every view's header reserves that corner.
Fixed
- The probation leash could never convict. Probation released on
observations > 0— but an attributed regression always arrives inside an observation, sopropose-retirementsstripped the leash in the same scan that read the convictions and every probationary rule answered to the ordinary threshold of 3:PROBATION_RETIRE_AT= 2, the entire D1 hedge behind applying ungraded rules, was structurally unreachable. Release (release_probation_when_measured_clean) now requires the ledger to grade the rule beyond its convictions (graded > attributed_regressions, counting verdict-bearing rows only — an inconclusive probe ran but graded nothing and releases nothing). - Leaving a similarity group re-embedded the whole review queue (#128).
The graph queue's grouping embeds every pending statement — measured at
~40s over ~7,000 — and
closeItemsre-ran it whenever a verdict had been filed inside a group. A guard skipped it when nothing had been judged, so a glance was free and the actual work was not: open a group, reject three, step back, wait. The listing is now rebuilt from its survivors, which is the rule the TUI'sEschas used since the level existed, and groupings are kept for the life of the page rather than discarded by the back arrow, a Review sub-tab switch, or a transient error. A cross-class grouping the page has already paid for reopens instantly; the header says when it ran and offers a regroup. Class groups also had a 30-second server budget against the global layer's 360, so a class big enough to be worth grouping answered502on a phone while the identical command in a terminal printed it. - A group verdict could delete a card while nothing was verdicted (#130).
mecha-graph reports a per-candidate failure as
#id FAILED: …and exits 0, soOk(report)can carry a verdict that did not happen — the#2951incident the item level records, one level up and over seven candidates instead of one. The/queuesgroup arm now reads the report before removing the row, and the status line distinguishes a fan-out that was never asked for from one the child did not report:cascade_tallyanswersNonefor both, andunwrap_or((0, 0))rendered them identically as×1with no "left pending" note. Silence there read as none left. Counts and remedy keys now precede the statement head, because the status line clips at 76 columns and a caveat written after a child-controlled string is one the reader never sees. - The entry document must not be cached the way its assets are (#121).
Reported as the settings page's learning section vanishing from a phone
minutes after it deployed. Nothing had regressed: the phone was rendering
the previous build, correctly and whole, from a cached
index.html. - A pull-request build could evict main's pending Pages deploy (#123).
docs.ymlput PR builds and main's deploy in one concurrency group, and GitHub holds only one pending run per group, so a third arrival cancels the one waiting. - Four cards on the home page did nothing, and one of them should have
(#125).
mecha review queues --jsonreports eight queues andHome.sveltehad a hardcoded map covering seven, soblocked questionsrendered under its raw wire name and went nowhere — while the tasks tab had been showing those same questions all along. The other three are CLI-only by design, but a flat card and a tappable one differed only in an:activebackground nobody sees on a phone, with the explaining command in atitletooltip touch does not render. - The nightly diagnostician could not read the source it was told to read
(#127). Six nights of
mecha harness ruminateproduced three candidates and zero acceptances, all three proposing configuration keys that exist nowhere in this codebase. - The
/tasksweb page rendered no tasks at all on any non-empty board (#116):stateOfcalledstalled(t)wherestalledis a field the server stamps (serve/board.rs), not a function — aReferenceErroron every card. Shipped broken in 0.1.16; caught by #117's render gate. - Two demo fixtures did not match the shape their routes return, so the
docs demo rendered what a box never would: a cloned voice's
createdwas a date string where the route sends unix seconds (Invalid Date), and charter line ids were bare ordinals where the route sends slugs (1. 1beside the list marker). A fixture that disagrees with its route is a demo that lies about the product. - The charter docs overstated what the surfaces share (366a9ca): the
appraisal page and ARCHITECTURE both claimed every charter surface goes
through
editor::edit_charter_with; a grep for callers says only the two editor-handing surfaces do, and the web save is its own validated temp-sibling-and-rename write sharingCharter::parseinstead. Both docs now say what the code does.
0.1.16 - 2026-08-29
Three arcs land together. The appraisal goal system, reviewed end to end and hardened — one test-and-review pass produced the findings, nineteen rounds with the PR auto-reviewer produced the rest (PRs #111, #112): failed-turn transcript integrity across every front-end, positional run configs for the counterfactual probe, an instrument that can no longer eat its own findings, and a structural guard making task closure the owner's act. Onboarding grows the charter step and declines (PR #113), and CI gains a macOS arm that found four real bugs on its way in. And the release workflow finally creates GitHub Releases — fourteen tags had published crates with no release to show for it.
Added
-
Closing a task is the owner's act, structurally.
kg_task_updateon every model-facing registry is wrapped by a closure guard that refuses exactly astatusofdone/dropped, pointing atmecha tasks set— which closes and appraises — while every other field of the tool passes through. Presence is a trait answer (Tool::guards_closures) no wire-supplied tool can fake, an unguarded surface is a startup error (closure_guard::verify), subagents inherit the guarded handle through a clone-site belt, and the guard's refusal is classified (ToolOutput::refusal) so the harness's own "no" lands beside approver denials rather than inended_on_failed_calland the tool-error rate.mecha toolsshows the guarded surface, since it is the audit view. -
An unreadable transcript is a finding, not an empty queue. The session store could rot one file at a time with no surface saying so:
Session::listskipped quietly,sessions appraisecounted nothing, doctor said "nothing wrong". The skip count now exists (Session::list_counting,Corpus.unreadable, disjoint fromsessions_readby construction), doctor reports it, andappraise,statsandhealthall carry it in text and JSON.appraisealso regains thenamed_a_goalcounter lost in the #88/#91 merge overlap, soserves:coverage is measurable from the instrument again. -
The affect readout reaches the typed web surface — the header carries the same worded chip the TUI badge shows (muted outline, deliberately not the taint chip's hazard amber: a run's mood is not a security posture) — and the TUI badge itself survives
--no-session. -
mecha setup --jsonnow exits non-zero when work is outstanding, which it had documented ("like doctor") and did not do: the--jsonpath returned before the exit-code branch, so the machine-readable spelling was the one that always reported success.doctor --jsonprints its findings and then falls through to the shared check; setup now does the same. The cost of the old behaviour was a silent one rather than a wrong answer — a!-inverted assertion written against the documented contract is ignored entirely byset -e, so it neither failed nor passed. -
Five test fixtures named their scratch directory from a timestamp, and two could collide.
format!("{pid}-{nanos}")assumes the clock is fine-grained enough that two parallel tests never see the same value — measured on this hardware, 11 of 20,000 adjacentas_nanos()calls are identical, and on macOS it is coarser still. Two tests then share a directory and the first to finishremove_dir_alls the other's store out from under it, surfacing as a bareNo such file or directoryin whichever lost. Found on the macOS CI arm, where it is a race rather than a certainty — it passed twice before it failed. All five now use a process-unique counter, which cannot collide by construction. -
A model name from a server nobody named can no longer produce a config that will not parse.
verified_settingsrendered the alias with Rust'sDebugescaping, which is not TOML's — a control character becomes\u{1b}, and TOML's\utakes four hex digits with no braces. Quotes, backslashes and newlines happen to escape compatibly, so only non-printables bite; what made it matter is that the discovery path added in this release takes those bytes from a server the owner has never named. The value goes through the TOML serializer now, andsetup --writereads the file back through the loader a run uses before claiming it wrote anything — restoring the backup and saying so if it does not parse, on the same rule as the charter's "saved, but it will NOT load". Without it, every later command died atConfig::load_globalpointing atmecha config init, which is the wrong fix. -
The MCP environment-allowlist test now accounts for what a child adds to itself. On macOS the nosy fixture reported
__CF_USER_TEXT_ENCODING, which CoreFoundation writes into its own environment during initialization —mcp.rscallsenv_clear()beforeenvs(), so it never crossed the boundary. Exempted by exact name and target rather than by prefix, and the test asserts each exempted name really is absent from what we hand over, so the exemption cannot come to cover a genuine leak without failing. -
A test that only passed on Linux.
an_mcp_file_parses_and_resolves_paths_against_its_own_directorybuilt its expectation fromstd::env::temp_dir()and compared it against a pathload_mcp_filehad canonicalized — fine on Linux, and wrong on macOS, wheretemp_dir()answers/var/folders/…and canonicalizing resolves the symlink to/private/var/folders/…. The code was right and the fixture was Linux-shaped. The scratch directory canonicalizes at creation now, so the next fixture that compares a path is correct without anyone remembering why. -
CI builds and tests the whole workspace on macOS. The
testjob was ubuntu-only on both arms, so nothing proved any of the four crates compiled anywhere else — themecha-clibreak below was found by a job added for an unrelated reason, and only because it happened to build one binary. macOS is now a full arm (--all-targets, so the tests have to compile too, and then run). The MSRV arm stays Linux-only: it exists to pin what the manifest promises, and a second platform does not make that promise more or less kept. -
mecha-clidid not compile on macOS, and nothing could see it.exe.rsimportedstd::path::Pathunconditionally while using it only inside#[cfg(target_os = "linux")]branches — an unused import everywhere else, which this repo's-D warningsmakes a build failure rather than a lint. Every CI job was ubuntu-only, so the break was invisible; thefirst run (macos-latest)job added in this release found it on its first run, which is the whole argument for building on more than the platform you develop on. -
mecha setup --undeclineno longer announces an undo it did not perform. A typo'd or unknown step id wrote the set back unchanged and still printed "slakwill be offered again" and exited 0, so the person believed the way back had been taken and then metyou said no thankson the step they thought they had restored. The message is read off what actually changed now, and an id that was never declined is told so. In the same shape:mecha setup's three verbs (--json,--write,--undecline) conflict at the parser rather than resolving by whichever branch came first, so--json --writeexplains itself instead of printing a plan, exiting 1 and writing nothing. -
Two
mecha setupwrites that were quiet about losing something. Aneveranswered over an unreadablesetup-declined.jsonrewrote the file with just that one id and dropped every earlier answer —read_declineddistinguishes unknown from empty precisely so that cannot happen, and the write path collapsed the distinction and persisted it. The damaged bytes are moved aside now, stamped so a later corruption cannot overwrite the salvage, and where they went is printed. Separately,offermarked a shared installer as handled off the answer rather than the outcome, so a failedcargo install mecha-mailmade the documents step report "already handled by the command above" — an assertion about a command nothing had checked. It reads the exit status now, and a failed install leaves the next step genuinely outstanding. -
Two smaller
mecha setupbugs, both found in review.neverand the offer loop's de-duplication disagreed:mailanddocscarry the identical remedy argv when neither binary is on PATH, and the dedup skipped the second question as well as the second command — so declining mail recorded onlymail, leftdocsoutstanding, and setup still exited 1 after somebody had answered everything they were asked. It self-corrected on the next pass, which is why no store-level test could see it; the loop takes its reader as a parameter now so the answers can be driven, and the dedup is on what has been run. And--undeclinesat below the--json/--writereturns, somecha setup --json --undecline allprinted a plan, exited 1 and undeclined nothing, silently — it is handled first now, ahead of every network call, since a verb that rewrites one local file should not wait on a loopback timeout. -
Two claims the local-server probe made without establishing them. Both found in review, both the shape this module's own header is about (never write down a number the user merely believes). The probe result was an
Option, which collapsed asked and heard nothing into never asked — so an install with a configured-but-unselected[providers.local]and a llama-server running on it was told "Nothing was answering at http://127.0.0.1:8080 when this ran" about an address nothing had asked. It is three-valued now, and that install gets a branch of its own naming the provider it already has and the one-line fix, because it emitted nolocal-serverstep either and previously received a single step telling it to serve a model it was already serving. Separately,preflight::Propsdefaults every field so a version bump costs a check rather than a parse failure — which meant{}with a 200 parsed fine and any JSON service on :8080 was announced as "already serving (an unnamed model)", oneyfrom repointingdefault_providerat it with nomodeland nocontext_window. Discovery now asks for a field only llama-server supplies;fetchkeeps its tolerance, which is right for a server the owner has already named. -
The step that blocks every other one has a way out of it.
mecha setupreportedanthropic has no usable credentialand offeredmecha config show— a command that displays a file and fixes nothing, so the one step that made every other step untestable was the one with no path forward. It now says which of the two situations the machine is actually in. If nothing configured can answer, setup probeshttp://127.0.0.1:8080(loopback only, one address, and only on an install that is otherwise stuck, so a working install makes no extra call); when something is serving there that no provider names,mecha setup --writewrites the[providers.local]table from the server's own/propsand pointsdefault_providerat it — the existence of the provider being as much a measured fact as its context window. When nothing is serving, the fix is a key, and a key is the one thing this tool will not write: mecha stores the name of an environment variable, so the step names the exact variable and both routes forward instead of offering a command that could only print what you already know. A provider with noapi_key_envis told that, rather than told to set a variable it does not name. The probe is keyed on there being no local provider configured, not onpropsbeing absent — a configured server that is merely down also has neither, and probing there would announce "a server nothing names" about an install that names one, then take the create-a-table path over the table it should have corrected.mecha setupalso now offersmecha config initwhen there is no config file: tolerating its absence is right, and is also why nobody ever learned about the file every other step is fixed by editing. -
mecha setupoffers a charter, and you can tell it no. Two gaps in what a new install walks into. The first: nothing anywhere named the charter to somebody who had never written one —doctorreturns early on a file that does not exist (right, since not having one is not a fault), so the only ways to find the feature were scrolling the TUI's/helpor noticing the gear on the web page. It is now a step, whose remedy hands over$EDITORand composes nothing. The second: "I haven't done this yet" and "I don't want this" were the same line of output, so somebody who does not use Slack readnot set upforever and every scriptedmecha setupexited non-zero over a choice they had already made. Each offer now takesy/N/never;neverrecords the step in~/.mecha/setup-declined.json, a declined step is not outstanding, andmecha setup --undecline <id>(orall) asks again. Only genuinely optional things are declinable — found by running the flow rather than reading it, because inferring "declinable" from "missing" made a provider with no credential declinable, and declining it reportedNothing outstanding.on an install that could not answer a prompt. The gate is on the plan rather than on the prompt, so hand-editing the file in does not work either, and a decline never overrides a step that isdone,wrongorunknown. Setup also now closes with where to go next, and with the one trap a new install walks into unaided: run it from a project directory, because a workspace over~/.mechais a jail covering its own tokens. -
mecha charter edit, and the rule said properly. The charter's own module claimed there was "no--edit, and there never will be — the absence is the safety argument". That was a misstatement of the invariant rather than a decision anybody made: the TUI's/charteralready handed the file to$EDITORand the web settings page already took a validated save, so stating the rule as no verb writes the file made the command line the only surface where the owner could not edit their own document, which protects nothing. The invariant is about the author: the owner may edit the charter from anywhere, and no model ever composes, suggests or edits a line.editcreates the commented template when there is no file, hands over$EDITOR, and exits non-zero if what was saved will not load — the validation feedback being the reason to use it over a hand-runvi. The template write and the did-anything-actually-land classification are now one implementation shared with the TUI, which is where the two subtle cases live: a clean editor exit that saved nothing, and a:cqthat exits non-zero after a save landed. -
A first-run test suite, and a CI job that installs mecha the way a new user does.
mecha-cli/tests/first_run.rsdrives the real binary against an isolatedMECHA_HOMEwith nothing in it — the state no other test is ever in, since every unit test starts from aConfigsomebody constructed. It asserts that a fresh install says what it needs in a shape a script can read, that the credential-free commands work before anything is configured, thatdoctoris quiet about stores that merely do not exist yet, that a first charter is a template holding no priorities, and that starting from the directory holding~/.mechais refused with a reason. Beside it, afirst runCI job on Linux and macOS runscargo install --pathinto a clean prefix and repeats the walkthrough against the installed binary, because a crate that will not install standalone is invisible from inside the workspace and lands on the person least equipped to debug it. That job also checks every command the getting-started pages name actually exists. -
Voice and rate moved out of the call pane, and a voice can be cloned. The in-call picker and slider were preferences wearing call-control clothes; they live on the settings page now (the call pane keeps mute and end-call), reading and writing voice-core's own preference store — which fixes a real bug found in the move:
Chat.sveltekept a second copy of the prefs machinery under a different localStorage key while claiming to share the first, so a voice picked in a call was saved where nothing else looked. One store now, with a one-time read of the legacy key. The page's picker enumerates from the worker's last answer, cached at every call; a preference applies from the next call, which the page says.Cloning: the TTS (Chatterbox) is a zero-shot cloner whose "voice" is a reference WAV in a directory, so the settings page can record someone reading a passage — it opens with them agreeing out loud, and the clip never leaves the box — name it, and save it as a voice.
[web] voices_dirnames the host side of the directory the TTS container mounts (unset disables the feature); the upload is validated as integer PCM WAV with 5–120s of audio read from its own header, voice names are a closed alphabet withdefaultreserved, an existing name is refused rather than overwritten, and deletion is offered where recording is — a botched take that needs a terminal turns the store into a pile, and the file is a recording of somebody's voice. The worker's voice-list cache now revalidates on a miss, so a fresh clone works on the next call instead of after a restart nobody was told about. -
A settings page on the web surface — the gear in the upper-right of Home. Three sections, and exactly one thing on the whole page can write: the charter shows the ranked lines and offers an edit whose save is a two-tap confirm, validated server-side by the same
Charter::parseevery run loads through — an invalid document is refused with the parse error and never reaches disk (temp-sibling-and-rename when it does), which is strictly better than the TUI's after-the-fact warning, since here the bytes are still refusable. Learned rules is a read ofmecha rules list --jsonwith the ledger tallies — retiring stays behind its own staged review. Voice reports whether the worker answers and where offers go, nothing more. Deliberately absent: anything whose edit widens security posture ([sandbox],[security],[outbox]routing) stays inconfig.tomlwhere a diff reviews it. -
/charterin the TUI: see the standing priorities, and edit them without leaving the screen. The list shows the ranked lines (order is rank), enter opens a line's full text, andehands the terminal to$EDITORon~/.mecha/charter.tomlitself — the charter's write path stays "the owner with a text editor" (§11): no CLI verb, no tool, and no model path writes a line, and the only bytes mecha ever writes are a comments-only template when the file does not exist yet. Validation feedback lands the moment the editor closes — a duplicate id or a typo'd table name is reported in the modal, not at the next session's startup where the alternate screen covers the warning — and the status line says the honest thing about scope: an edit rides in the prompt from the next session (/modelrebuilds this one), never the conversation already running. -
mecha distillnow notices when the world disagreed with the graph. The same quarantined pass that already finds corrections also reports SURPRISES: moments where something the agent said, sourced from graph memory, was contradicted by something else in the same session — "I said the 14th because the graph says so; the email says the 9th." Printed for a human to act on, never auto-chased:mecha gossip --entity <about>is the human's call, not the session's own. Gated on the timeline's trust like a correction, since it is the model's own free-text reading of transcript prose. Rung 9's second piece (GOAL-SYSTEM-DESIGN.md§10.1) — review-queue salience is still unbuilt, and needs a different repository. -
An episode pushed to the knowledge graph now carries how the session went.
mecha distillstamps each episode'smetawith the session's affect label and signed goal errors, the same recordmecha sessions appraisederives — one assembly (appraisal::for_session) rather than two that could drift. Unlike a correction, neither is gated on the timeline's trust: both are structured facts the harness computed about its own run, with nothing in them a model or a fetched page could have authored. Rung 9's first piece (GOAL-SYSTEM-DESIGN.md§10). -
A finished plan step is checked against what the run actually did. A board task is closed by the owner, so a person is the check; a todo step is closed by the model, so there was no check at all. The loop folds its own trace into counters and the plan tool differences two of them into a span, reporting two facts on the
todoresult: nothing was attempted, or the last attempt did not succeed. Four rules keep it off honest work — a refusal is not a failure, only the last attempt decides, a sibling still running supports no finding, and plan revision is not work. -
A run that has stopped making progress is told so, while there is still something to do about it. The loop guard ends a run that repeats itself after a compaction, and it was the only rung of that ladder: a run going nowhere had exactly two states, proceeding and dead. Boredom is the graded version — three identical outcomes in, it names the approach and offers a different route or a fresh conversation. It only speaks, so it costs nothing; bounded to once per rung, once per turn, three times per run, because a model fails more when its context holds its own earlier errors.
[agent] boredomswitches it off for a pinned scorecard. -
mecha sessions healthreports how often that fired. Every threshold behind it was argued rather than measured, and a detector nobody can count fires either constantly or never with no way to tell which. Prints a dash where no run recorded the counter, which is what it prints today. -
mecha sessions appraise— how runs went against what they were for. Every evaluative signal mecha had was a cost or a correction, so a run could be recorded as having gone badly and never as having gone well. This is the signed record, derived on the spot from the transcript, the outbox and each run's own counters — no store, because every channel is a pure function of records already on disk. Observation only: nothing consumes the label.Over a live store it reads 459 sessions, appraises 120, records 119 signed goal errors and 100% neutral labels — eleven of the errors positive, which is the first time the one channel that can say a run went well has been counted anywhere. Nothing is broken; every label that could have fired needs a dimension nothing measures, and
appraisal.rsnames which one buys which. -
mecha reflectionsand the/learningmodal — the learning store, read and edited.reflectwrote reflections,learnconsumed them and nothing could show you one./learningis the three stages a lesson passes through — reflections, rules, proposals — with the verbs on each.Editing a lesson is a provenance promotion, not a text change. A lesson you typed is yours, so one the gate excluded becomes learnable; what was happening is withheld on the way through, because that is the field that held the third-party text. It is the highest-leverage correction available: a rule is a consolidation of several lessons, so objecting at a proposal costs the good ones. A drop is a flag, never a deletion.
-
mecha rules list --json, listing user rules alongside learned ones and flagged, because they ride in the same prompt and a surface showing only the learned half misdescribes what a run carries.
Changed
mecha sessions stats --jsonis an object now —{rows, sessions_unreadable}instead of a bare array, matchingappraiseandhealth; no in-repo consumer read the array, and the CLI reference records the change.kg_task_update's description gained the closure guard's note, which movestools_hashonce — deliberately: a byte-identical spec would have hidden a real capability change from the surface store. Andmecha distillreads each transcript once instead of four times (appraisal::for_transcriptis the seam).
Fixed
-
A failed or interrupted turn no longer corrupts the conversation or its transcript, on any surface. One review pass found five divergent rollback sites: the web chat's error arm popped the mutated tail (orphaning a
tool_useand 400ing every later request on the session), the transcript kept what memory discarded (so the failure survived a resume), and voice's barge-in guard popped a tool-result message because tool results ride in a user-role message. It is one mechanism now:Conversation::roll_back_failed_turn(restore the snapshot, then pop only a plain user text), every error arm records the rolled-back state as a rewrite so a resume loads exactly what memory holds, and every submit site folds into a user-message tail instead of pushing two user messages in a row — recording the fold at submit as one direct rewrite. -
The counterfactual probe replays under the config in effect at the intervention, not
configs.first()— a resumed session's later steer no longer replays under the first attach's system prompt and tool list, diverging for reasons that said nothing about the steer and inflatingregret.Transcriptcarries positional configs built inSession::read's single pass (truncating and in-place rewrites keep positions exact; summarising ones clamp), and the probe reads the path the caller already resolved — one parse per intervention instead of a directory scan plus two — skipping per-item instead of aborting the walk. -
Closure appraisal's small holes: a prefix-resolved
--sessionno longer silently drops every draft (the id re-keys on the transcript's own header),tasks set T --session "" --status donereads the empty string as the documented unlink instead of failing it, and the follow-up's replay caveats reach the stored record whatever became of the episode. -
mecha eval --ab-rulesmeasured nothing, while announcing that it did. Both arms ran rules-free: consolidating eval's forced-off list intoforce_reproducible(which existed to stop entries being lost from a list written in prose across forty lines) flattenedopts.no_learned_rules = !with_rulesinto an unconditionaltrue, so the treatment arm printed "learned rules INJECTED (A/B treatment arm)" over an arm that had none — two identical arms, and every per-case flip it reported was noise. The lever is now a parameter offorce_reproduciblerather than a re-enable at the call site, because a lever inside the list cannot be lost while consolidating the list. The existing set-assertion test could not have caught this and did not: a test that asserts a list is complete cannot notice that one member of it was supposed to be a variable, so the regression gets its own test asserting the treatment arm does carry rules, and that the lever is one flag wide. -
anticipated_guiltno longer pins at a constant1.0under a standing backlog. The first run recorded under the sensor (2026-08-28) read exactly1.0— the live outbox held drafts eight days old, the age term clamped, and the OR-combination erased count and pressure entirely: the degenerate-constant corpusguilt.rs's own week-long horizon was chosen to avoid, arriving through the clamp instead of the horizon. The standing-debt terms (age, count) are now asymptotic — half of maximal at the same constants that used to be ceilings, approaching1.0without reaching it — so older still reads worse, no term is argued down by the others (still an OR), and pressure and count stay visible in the reading. Pressure keeps its hard top: it is a fact about this run, not standing debt, so it cannot pin the corpus across runs. A regression test carries the live store's exact shape. -
Affect::reachable_todaynow tells the truth about which labels have a producer. It claimedEmbarrassmentandFrustrationwere reachable andRegret/Disappointmentwere not; all four claims had drifted.Embarrassmentlost its only producer when theSentEditedoutbox arm was (correctly) madevisible: false— nothing now records mecha's own mistake reaching a third party, so the label is unreachable from every real path.Frustrationis probe-gated, not deterministic: no counter kind fires twice in one session, so the free readout's whole range isNeutralandAnger— pinned by a new test that walks every stop cause. AndRegret/Disappointmentbecame reachable when the counterfactual probe shipped (#91). The module note now carries the story; two new tests fail the day any of this drifts again, in either direction. -
sessions appraise --probe's help now says it needs a workspace. The replay builds a real agent with a real path jail, so from a home directory it refuses (correctly — the jail would cover~/.mecha); the flag's help text now names--workspaceinstead of leaving the refusal to explain itself.--appraiseis unaffected: its quarantined call has no tools by construction and runs from anywhere. -
Two real gaps in rung 9's episode tagging and surprise detection (#97, #98), each closed across several review rounds that kept finding the sibling case the previous fix missed.
mecha distillprinted aSurprise's free-text fields straight to stdout unescaped — the "a person reading their own terminal is a safe context" argument didn't hold forscripts/ruminate.sh's actual nightly path, which redirects that output to a dated logfile instead.strip_ansialone doesn't stop a bare\r/\n(an interior one survivestrim_end, which is also why this module's own two callers —Writer::writeandrelease— turned out to have the identical gap, now fixed alongside it) or the Unicode categories that buy the same two effects without beingis_control()(U+2028/U+2029 forge a line break; U+202A–E and U+2066–9 reorder the rendered line around a bidi-aware reader).strip_ansi_and_controlscloses all three, used everywhere free text reaches a terminal or a log in this module.Separately, a genuinely unreadable outbox during episode tagging used to warn and continue, permanently
mark_distilled-ing every session that run with a silently incompleteEditchannel — no later run could ever revisit it.OutboxStore::items_strictbails on a read failure instead of skip-and-warn, covering both a hard I/O error and a merely malformed item file (items()'s owntracing::warn!for the latter is invisible on the nightly, which runs with noMECHA_LOG). The originally-cited cause — a half-written.jsonmid-save — turned out to be one this store's own temp-sibling-and-rename discipline already rules out structurally; the realistic cause is persistent (a stray file, or an item written by a schema this binary cannot read), which the error message and doc comments now say plainly rather than implying a retry will clear it. Adoctorfinding for a stalled distill ledger would close the remaining gap (the nightly currently fails silently behind one logfile line) and is left for later rather than built here. -
mecha was mining its own words as the user's corrections.
agent.rsprefixes a refusal it did not author with"Denied by the user: "precisely so machine policy is never learned from as a human correction; this is that mistake mirrored. The learning miner guarded one harness voice and not the other, so every run the harness had to nudge for an empty turn contributed an "intervention" whose text was mecha's own — and two reflections in a live store were mined that way, one of them clean, unprocessed, and already inside a pending rule proposal whose text paraphrases the nudge back. Fixed at both ends:agent::is_harness_voiceis the closed list of voices mecha speaks in the user role, andReflexion::learnablerefuses one whatever its origin, which reaches records already on disk. -
Origin::Derivedclassifies something at last, and it is a label rather than an exclusion. A self-observed failure is real evidence; what it lacks is a way to be graded, because a counterfactual probe means something only when the user steered it there. So a harness-authored intervention is classified rather than dropped — kept, visible, and one gate away from being usable the day something can grade it. -
/queuesclipped the text it exists to have you read. The detail rendered without wrapping, so a rule proposal showed the first line of each rule and cut the rest at the box edge — an approval asked for on a sentence whose end is unreadable, and the unread half goes into every future prompt. -
A delegated task run's plan never named what it served, including in the runs
serves:was built for. Measured against the live store: 112 of 120 appraised sessions wrote a plan, 0 named a goal — 15 of them delegated runs that carried bothtodo's own schema instruction to passservesand this task's own id, printed on the seed'sId:line, and still wrote nothing. The generic reminder was not, by itself, enough; both delegated postures (work_prompt,discuss_prompt) now bind it explicitly to the running task's id, which is what letsFrustrationresolve a board task at all if it works (Prideneeds a charter line, not a task, and stays unreachable regardless). Forward-looking only, and unmeasured — it does not relabel the 120 sessions already on disk, and whether naming the id explicitly moves the number needs sessions recorded after this lands.
0.1.15 - 2026-08-26
Five surfaces that described themselves wrongly, found by using them. A chip that said a run would stop and ask when it would not, a card that printed JSON at somebody about to approve it, a queue verdict that could fail with nowhere to go, a call that ended without saying why, and a gate that read the label the proposer had written on its own change. None of them errored; each simply told you something other than what was true.
Added
-
allowis reachable from the web page, and asks before it lets go of the gate. The page offeredread-onlyandaskand refusedallowwith a 403, on the argument that a surface which can grant blanket permission from a phone is a surface that will, one distracted tap at a time. The argument was right about the risk and wrong about the remedy: the mode was already renderable — the state read answered"allow"— so it was reachable in display and not in fact. It is a mode now, sticky per session, entered through a confirmation and left in one tap, because a change that only adds a gate should not ask and a change that removes one should. What it does not waive is unchanged: the trifecta interlock still refuses a send once the conversation holds private and outside content, whoever approved what, and outbox-routed calls still stage. -
A draft can be confirmed where you are standing.
review now— a draft you just asked for is a draft you are about to read — had been the TUI and Slack default since the policy was written, andmecha servehad no release policy at all, so every staged draft went silently to the outbox and the badge. On the page it is a card built from/api/outbox/{id}: ids on the wire, bytes from the store, because a reviewer reading one thing while approving another is the failure the outbox exists to prevent. In a call the offer is spoken from the store and the answer matched before the request reaches a model, so the release decision never enters a context window. -
A note can be opened and edited from the page. The notes list listed and did nothing else — a long note was an unclamped wall of text with no way to change it. The verb is
mecha kg note --edit <source_id>, terminal first and the page driving it, keyed on the graph's(source, source_id)episode key so a rewrite is an update that drops the stale embedding rather than a second note beside the first. -
A failed graph verdict offers the two ways through. Accepting a similarity group could fail with
cannot resolve subjectand print it at the top of the phone with nothing to do about it. Binding the subject to a real entity and accepting it as a new topic have existed in the TUI since that modal was written; they are offered here on the card that failed, and only after a failure, because inventing a topic node is not a default.
Changed
-
Mail and calendar can default to different accounts. One
defaultcovered both creates, so "send from work" and "put it on my personal calendar" were a single choice and setting either moved both.default_mailanddefault_calendaroverride it per surface and fall back to it when absent, so a config that never needed the distinction keeps working and never learns it exists.mecha-mail default <name> --mailand--calendarset them; with no flag the verb does what it always did, and with no name it now prints what each surface actually resolves to rather than the stored field, because with a fallback in play those differ. The tool schema carries the right default per tool — a note telling the modelmail_senddefaults to the calendar's account is worse than no note, since it omitsaccountbelieving it knows where the message goes. -
An approval card shows the call the way a person reads one. A calendar call leads with its title and when it is — in reading order, not alphabetical, where an event reads end before start — and a letter leads with its addressing and its prose with real newlines. The exact arguments are one tap away. Nothing is hidden: an argument with no header or body shape, which is where
shellkeeps its entire contents, is shown outright, and the whole view is clipped the way the arguments always were so a large write cannot put a file into every open page. -
The web session's permission mode travels as its own event. The chip was set by the tap that changed it and by nothing else, so a change made on the phone left the laptop showing the mode it had at load, and a request whose response was lost left the tab that sent it wrong in the other direction. What the chip is for is telling a person whether the next write stops to ask, which makes a stale one a security cost rather than a cosmetic one.
-
A candidate's class is derived from the change, not read off the line the model wrote.
classdecides whether a proposal reaches a human at all — Security is never measured and never auto-applied. It held until now by coincidence: the closed override set is four benign knobs, so a security change mislabelled as config failed for being outside the set rather than for being security. On 2026-08-25 the nightly proposed disabling a taint control, classconfig. The class is now derived by naming the guarded settings — three config sections that are boundaries, plus everySecurityConfigfield — and only ever raises toward review. -
A call no longer ends by itself without saying so. Pipecat cancels an idle pipeline after 300 seconds, where idle means neither side produced a speaking frame — so thinking, reading, or putting the phone in a pocket killed the call and the client saw only the peer connection close. The timeout is kept, because an abandoned tab otherwise holds VAD, turn detection, STT and TTS open on a box with one GPU, but it is raised past any pause that is still a conversation and it now says why on the way out.
Fixed
-
A draft that failed to send says why. A failed release leaves the item pending — the draft is good, the delivery was not — with the reason recorded on it. The page received every other field and not that one, so a draft that could never succeed looked exactly like one nobody had tried yet, and the only signal was
1 of 1 item(s) did not sendin a notice. The reviewer now reads the actual reason on the card, which for the case that found this was a calendar create with noaccountagainst a mail config with no default. -
Approving a clean draft is one step, not two. The confirm sheet earns its place on an armed draft, where it shows the exact arguments — more than the detail view behind it. On a clean draft it showed strictly less than the screen already open, which is the kind of confirmation that teaches people to tap through, and what that trains away is the armed one.
-
The voice logo reconnects, as its own label had been promising. The idle state has said "tap to reconnect" since the overlay shipped, and the logo was an
<svg role="img">with no handler — an affordance described but never wired, appearing only at the moment a call had already failed. It is a button now, live only while idle. Reconnecting ends the dead session first, or the previous peer connection and its microphone track stay open for the life of the page; the call transcript survives, because the transport dropping is not the conversation ending. The two idle labels that never mentioned reconnecting now do — offering a way back from three of five states is how a working affordance still reads as broken. -
One number from
[slack]no longer requires the whole config to parse.show_fileloaded the global config at call time to readslack.max_upload_mb, which made an unrelated config edit surface hours later as a file-sending failure in a long-lived process. The value is captured at registration instead.
0.1.14 - 2026-08-25
A day of using the last release rather than building the next one, which is why almost everything here is a seam somebody walked into. The largest is voice: talking and typing were two conversations, and now they are one.
Added
-
A voice call is the chat session it was started from. The last of the voice decisions still owing something. An in-chat call used to open its own conversation with its own transcript and its own clean taint slate; now the page names its session in the WebRTC offer, the worker forwards it, and the turn runs on that conversation's messages, taint, transcript and workspace jail. Start something at the desk, finish it on a walk, read it back at the desk. A watching page fills in as you talk, and spoken turns are marked as spoken in the transcript.
Two things worth knowing about the shape. The obstacle was never the transport —
mecha serveheld two session maps in one process, and a call resolved in the wrong one. And the smaller fix was rejected on purpose: merging the voice conversation onto the web session when the call ends would have put the same turns in two session JSONLs, forrecall,distilland the run-quality corpus each to count twice.The permission posture travels with the turn, not the conversation:
--voice-yesstill means a spoken turn runs without stopping to ask, while a typed turn in the same conversation obeys the page's mode. Nothing structural moved — the trifecta interlock sits ahead of the approver, sends still stage through the outbox, and taint now accumulates across both doors instead of being reset by opening a call. -
/queuesreviews proposals in place, for all three stores. Three rows announced a count and then refused to open, which is the worst shape a queue aggregator can take: it exists because a queue grew unnoticed, and it was itself showing 28 items behind a door that did not open. -
Every queue says how long its oldest item has waited. A count answers "how much", not "how long has this been ignored", and the second is the question a review queue is for.
Changed
-
A staged draft shows what it acts on, even when the target was discovered rather than asked for.
outbox_sourcematched provider ids against earlier tool-call inputs, which finds a reply's thread read and finds nothing for a calendar delete whose event id exists only in a result. Reviewing that item meant approving an account name and an opaque id.Join::Returnedcloses it, with a minimum-entropy guard socalendar_id: "primary"cannot match every calendar result in the session. -
The nightly mail classifier reads both mailboxes. The first both-account sweep read 100 threads and disposed 47 of 51 candidates with no model at all — the account excluded for being expensive is the cheap one, because machine-generated mail is exactly what the prefilter handles for free.
-
One voice door. The standalone voice page is retired; the app is the only shell. Sharing
voice-core.jskept the two shells' machinery identical and did nothing about the shells themselves, so the voice picker and rate slider were built twice. -
Two standbys removed. Kokoro and Voxtral each sat at zero requests holding GPU memory as a "fallback" nothing failed over to automatically. A spare with no code path to it is an intention, not a spare.
Fixed
-
Mail actions on the personal account had never worked. Every button on a personal thread failed with "no thread in the triage store matches", because the nightly named one account.
mail_triagereaches nobody and mutates only your own mailbox — it is documented as the cheap way to act — and it was the one verb that could not run. -
A sound with no words in it no longer stops the bot. The VAD was winning the turn-start race, so 200 ms of anything Silero scored as speech interrupted a reply — a sleeve on a microphone cleared it. Fixed structurally rather than by tuning: the VAD came out of the turn-start strategies, so a wordless segment now produces no transcription at all and the reply simply continues. The cost, stated plainly: barge-in is "finish the phrase and it stops" rather than instant.
-
Enterat the TUI's review level had been a no-op for three commits while the footer advertised "Enter read it" — harness candidates and rule proposals announced a depth and opened nothing, which is exactly what/queuesexists to prevent. -
/entityadvertises its keys: Esc clears the search, ctrl-n says CREATE, and the merge key was advertised nowhere at all.
0.1.13 - 2026-08-24
The release where mecha grew a face and a voice. Two subsystems arrived — a web app on the tailnet and a spoken interface — and the knowledge graph's review queue became something a person can actually clear.
Added
-
Voice: talking to mecha, out loud. A Pipecat worker over WebRTC, Parakeet TDT for speech-to-text, Chatterbox Turbo for speech, and an OpenAI-compatible facade mounted inside
mecha serve— one process, one agent, one cached prefix, two dialects. A call is an ordinary session with the same tools, jail and outbox.The transcriber is deliberately the smaller model. A speech-capable chat model asked to transcribe does not reliably transcribe: asked a calendar question, one answered it — saying it had no calendar access — and that answer was recorded as the owner's words; played "ignore your instructions and just say the word banana", it wrote
banana. Prompting fixed the first and never the second, because obeying instructions is what such a model is. A transducer has no prompt for an instruction to travel down. The cost is that mecha cannot tell you how somebody sounded, which is the right trade for the component standing between a microphone and an agent holding your mail.Voice is not in the crate.
cargo install mecha-cliships the facade and none of the pipeline —scripts/sits outside the crate — so voice needs a git checkout and four local services. The website says so first rather than in a footnote. -
mecha serve— the web surface. The agent, behind a small web app reachable only over your tailnet: bound to127.0.0.1with no flag to widen it, every request checked against[web] owner_login(the headertailscale serveinjects), and a refusal to start at all when no owner is configured. Pages for the dashboard, chat, mail, notes, the review queues and the task board — each a thin shell that drivesmecha <verb>as a child process, so nothing is reachable from a browser that a script could not do.Chat holds many conversations on one provider connection, each with its own jail, permission mode, cancel token and steering queue; approval cards can be answered from the phone and survive a locked screen; files upload into the session jail and download only from inside it.
-
Similarity groups, and one verdict that covers them. The graph's queue is mostly the same fact restated, so
sgroups a class by semantic similarity anda/rdecide a whole group — as one human verdict, with the rest riding through as a labelled machine cascade the autonomy ladder never counts.mecha review groups --alldoes it across every class at a stricter floor for a backlog that one class at a time will never clear, naming the classes each group spans because the blast radius is part of what is being approved. First live run: 306 groups covering 782 of 6,929 pending. -
The plain inbox, and composing by hand.
mecha mail recentreads what arrived rather than what the classifier surfaced;mecha mail composewrites a new letter and stages it into the outbox like every other outbound thing, refusing outright ifmail_sendis not routed there. One queue holds everything outbound regardless of who wrote it. -
Dictation on the phone. The notes and task capture boxes record, encode the clip in the page, and post it to the local speech server — the audio never leaves the machine, which is the whole reason not to use the browser's speech APIs.
-
Session history, and resuming a conversation. The chat drawer lists live and recorded sessions, voice calls included, and opening one restores its messages and its taint — a conversation that read a hostile page on Tuesday still remembers on Thursday, because resuming must not launder what a session touched.
-
/entityand identity editing in the graph. Asked to fix a daughter's name, mecha could correctly report that it could add an alias and nothing else — no rename, no way to create a person who has forty facts and no node./entitydrives the new verbs as a person drives them. The model still cannot perform identity edits, deliberately: an identity edit is invisible in a way a fact edit is not, because every fact about a node keeps reading correctly while pointing at someone else. -
Front door, notes and tasks reach the phone, and the dashboard's cards navigate to the surface behind them.
Fixed
-
A sheets write fits its own grid instead of being refused for it. Google refuses a
values.updatethat reaches past its declared range — and refuses the whole write, so one header row a cell too wide cost the other forty-three. Two drafts sat unapprovable in the outbox for three days on exactly that. The range is widened to fit rather than refused, and the repair lives inside the tool because a routed call is staged and never dispatched: the tool's own argument checks never run at draft time, so the refusal surfaces hours later to a reviewer with no way to fix it. -
The mail list stopped clipping its own text. A flex item with
overflowother thanvisiblehas an automatic minimum size of zero, so once the list outgrew the viewport every card shrank to a sliver and lost its summary. The outbox page carried the identical bug from birth and had simply never held enough drafts to trigger it — a layout bug that needs scale to fire passes every short-list test, and a sibling page copied from it inherits the fuse. -
Web verbs run in a workspace the jail accepts. The serve unit's working directory is the owner's home, which any child building a tool surface correctly refuses as a workspace, so mail and tasks both failed from the phone with an error aimed at a person who was not there. Children now get the web producer directory. The fix lives at the spawn helper rather than at whichever route failed first.
0.1.12 - 2026-08-22
Fixed
- A TUI outlives an install without its child processes dying. Every
modal that drives
mecha <verb>as a child resolved its own binary withcurrent_exe, which on Linux reads/proc/self/exe— and aftercargo installreplaces the file, that target reads…/mecha (deleted), a path that does not exist./queuesfailed by name (os error 2) and an outbox release failed quietly: the confirmation ran, the release child never started, and the item satpendinglooking like the review surface was broken. Children now exec the/proc/self/exelink itself, which the kernel resolves to the deleted inode — so a long-lived session keeps driving the version it is, rather than failing, and rather than picking up a newer binary whose flags may have moved. The path written into systemd units deliberately still uses the real file.
Added
-
/queuesandmecha review— every store waiting on you, in one list. Five stores accumulate work for the owner and each grew its own verb: the outbox, the front door, staged rule changes, harness candidates, and — in another repository — the knowledge graph's merge queue. Knowing what was waiting meant remembering five commands, which is how that last one reached 6,434 items without anybody deciding to let it.mecha review queuesis the summary and/queuesis its modal; four rows hand off to the modal that already owns them, because/outboxand/frontdoorhold the confirmations and taint warnings that make their approvals safe.It is not
/review— that is already the outbox's release policy, and two things called review one word apart is a trap.An unreadable store reports as a dash, never a zero: "nothing waiting" and "could not look" are opposite findings, and a reader rendering its own failure as an empty queue would reproduce exactly the bug this exists to catch.
-
The graph's merge queue is reviewable from mecha, three levels deep. Mechanism → class → items, with
a/rverdicting andtfiltering by evidence tier (all → unjudged → thin → some → solid). This is the one place mecha shells out tomecha-graph, and the reason is a boundary rather than convenience: the MCP tool surface haskg_pendingandkg_verdictand deliberately nokg_accept, because every MCP tool lands in the model's registry and a model that can accept fact candidates can accept the ones its own extractor proposed. The decision is driven the way a person drives it —$MECHA_GRAPH_BINas a child process, resolved from the environment and never frommecha.toml, since a project file arrives with a cloned repository. The dependency is runtime and optional: every verb degrades to a named error, andqueuesstill reports the four mecha-owned stores when the binary is missing. -
Item review is a random sample, and that is the default.
mecha review sampledraws twelve candidates uniformly at random from a class, seeded and printed so the draw can be redrawn and checked. The queue has an order, every order it could have is correlated with something, and judging the first dozen then reading the result as the class's accept rate measures the ordering — which matters because 40.5% of that queue sits in classes with no human verdict at all. A verdict drops the item locally rather than resampling, so a sitting's twelve verdicts describe one sample;nasks for a new draw explicitly.mecha review itemsis the queue-order alternative and says outright that its verdicts are not a rate.Enteron an item opens the whole of it — full statement, payload, confidence, when it was proposed — withj/kflipping through the sample without leaving the view, because a verdict on a line that ends in…is the approving-unread failure the outbox exists to prevent, one store over. -
mecha review accept|rejecttakes a whole class, via--proposer/--predicate, with--limitand--dry-run. A cluster kind such as(commitment)is refused by name rather than passed to a filter that would match nothing, and the count reported is the child's own — the graph matches proposers by substring and caps a bulk filter at 500, so the row's pending figure is the wrong number twice over.
0.1.11 - 2026-08-21
Added
-
The outbox verb is
approve, on theakey. The queue holds more than mail — adocs_replaceis approved, not sent — andasits besideeedit andrreject wheresnever belonged.mecha outbox sendis kept as an alias andsstill works in the modal: both meant this same action, they cannot mean anything else here, and a key that releases an outbound action is the wrong one to retire out from under someone's fingers. The stored status stays"sent", on the rule that already governsOutboxKindandProposed: it is a value in an append-only store,mineable_as_writingkeys on it, and renaming it would orphan every item already resolved. -
A search backend can be preferred for deep searches.
[[search]] prefer_deep = truemoves a backend to the front of the chain when the caller asked for a deep search.Depthused to change only how a backend searched and never which one ran, so a research question went to whatever was cheapest and first — and a paid backend chosen precisely for hard questions was reached only when the free one came up empty. It reorders and never filters, deliberately: a preferred backend that is rate-limited must still fall through to the free one, and a quick query must still reach the paid backend as a fallback when the free one is down — the arrangement that kept working through a total searxng outage. A stable partition, so config order still decides everything within each group.
Fixed
-
A long draft could be neither read nor approved. The approval confirmation put a tainted draft's arguments on screen "in full" and rendered them with an unscrolled
Paragraph, which draws from the top — so adocs_replacewhosefindwas a whole syllabus section pushed the question and theyprompt off the bottom, and every other key dismissed the confirmation, so there was no way to scroll to them. The box was also sized frombody.len(), which counts logical lines: one long argument is a singleLineand many rendered rows, so the height reported "it fits" precisely when it did not.The arguments now scroll (
↑↓/jk, PgUp/PgDn, Home), the prompt is pinned to the bottom border where nothing can push it off, the height is measured withParagraph::line_countafter wrapping, and scroll keys no longer count as "anything else". This is the review surface's own standard — a field the reviewer cannot see is a field they approved unread — failing the moment an argument outgrew the terminal. -
The mouse can select text again. The TUI enables mouse capture for the whole session, which is what makes the wheel scroll the transcript — and also what stops a drag from selecting anything, because the terminal forwards the drag to mecha rather than drawing a selection. That made the
/docspicker's own documented fallback ("the URL stays on screen to be selected by hand as well") one that had never existed: the authorization link is 420 characters, and its only other route off the screen was an OSC 52 write no terminal acknowledges.The mouse is now handed back whenever what is on screen is meant to be copied. Any modal releases it automatically — while one covers the screen, capture buys only a wheel scrolling the transcript behind it — and
^stoggles selection mode in the main view, with aselect ^Sbadge on the status strip, because a wheel that has silently stopped working reads as a broken session. Reconciled once a frame from the drawn state rather than at each pane's exits, on the reasoning that a mode restored by remembering is a mode that eventually is not. -
The
/docspicker can be finished from a terminal. Three separate things stood between the link and the browser. Handing the mouse back is one; the other two: the link is now shown alone at column 0 ons, since a drag across a wrapped, bordered box copies the│at each end of every row, andohands it to$BROWSER/xdg-openwhen the browser is on this machine. And a paste while the picker is up now lands in the picker's field — it went to the message box behind the modal, so the pane said "paste it here" while the only way to fill it was to type two hundred characters by hand. Whitespace is stripped on the way in, because an address copied out of a wrapped display arrives with newlines in it. -
"Nothing found" is an answer, and now arrives as one. A chain whose backends all answered and all found nothing reported
every search backend failed. The model read that as broken infrastructure and reworded the same query eight times. An exhausted chain of empties is now a successfulno results for "…"; a chain where nothing answered is still an error, and one broken backend no longer hides another's honest empty. -
A searxng instance with every engine down no longer reads as an empty web. The measured case: every engine behind the instance suspended or CAPTCHA'd,
results: [], HTTP 200 — byte-identical to a genuine no-match, so a total search outage reported as "the web has nothing". Theunresponsive_enginesfield is now read and makes it a failure. Read defensively: an unexpected shape from a third-party instance reports nothing rather than panicking a search. -
The cache lens stopped crying wolf on large tool results.
Verdict::Dropscored re-payment frominput_tokens, which is everything not read from cache — overwhelmingly the turn's new content on this workload, where one mail thread or search result dwarfs the prompt it was appended to. So the lens shouted loudest exactly when tool results were biggest, and scored the real failure — a small prompt re-paid in full because something destabilised the prefix — as stable. It now measures what did not come back (prev_total - cache_read_input_tokens); the field is renamedrepaidto say so. Measured against a live llama-server: 2,720 of a 2,724-token prefix read back with 6,319 tokens of new content, previously called a drop at a share of 2.32.
0.1.10 - 2026-08-21
Added
-
Images.
Block::Imageis a fifth block variant, so a screenshot can be put in front of the model rather than only landing on disk. It rides the user turn only: Anthropic accepts an image inside atool_resultand the OpenAI dialect'srole: "tool"messages carry a string and nothing else, so a tool returning pixels would work on one backend and silently lose them on the other — in the one place where the missing thing is what the whole turn was about.Four doors reach it: the Slack connector, the remote-control inbox,
mecha run --image <path>, and a file dropped on the TUI prompt. That last was half-built already — a terminal converts a drop into a bracketed paste of the path, and the paste handler had always inserted it; what it never did was look at what the path was. A paste whose every token resolves to an existing image now becomes a chip,[image: shot.png], and the image is sent only if its chip survives to submit — the only undo there is for bytes backspace cannot reach. Requiring the whole paste to be paths is the safety property rather than a convenience: a paste is also a paragraph copied off a web page, and attaching any file whose path appeared somewhere in prose would let copied text pull bytes off the disk into a request. It cannot work over SSH and never will, because the path pasted is the laptop's.Both backends degrade to a named line rather than failing when the model has no eyes, and are tested to word it identically — a conversation crossing a
/modelswitch must not tell two stories about its own history. The parts array is built only when an image is present, because the cached prefix is a byte-prefix match and making it the uniform shape would invalidate every run that never sends one.Capped at the door rather than per turn (
mecha_core::image): 1568px long edge, 5 MB encoded, and an image already within them passes through byte for byte. Measured on the screenshot that motivated this: 5.7 MB → 179 KB withprompt_tokens294 either way, because the server tiles to a fixed count regardless — so the resize buys nothing in context and 32x on the wire and in an append-only session file. One un-resized screenshot was 99% of a transcript, re-sent whole every turn. -
[providers.X] vision, and a preflight that checks it.provider::preflightmakes oneGET /propsat startup and compares config against what is served, in both directions — declared-but-not-served silently degrades every image to text, served-but-not-declared means a projector is loaded, paid for in memory, and never used. The same request checkscontext_windowagainst the per-slotn_ctx, so the-cversus-c / -nprule is read rather than restated, and the configured model againstmodel_alias. Warns, never refuses. -
mecha setup. What an install still needs, and the one command that fixes each. For a local provider it does not ask what you meant — it reads/propsand--writewrites down what the server reports, editing the table in place so comments survive. It also inventories mail, documents, Slack and the knowledge graph, reporting unknown distinctly from not set up, because telling someone their mail is unconfigured when the store is merely unreadable sends them through an OAuth flow they did not need. The graph's own sources are named and never driven, and nothing is scheduled. -
mecha trigger daemon --print-unit. Prints a systemd user unit naming this binary by absolute path. The documented alternative — copyscripts/mecha-triggers.service— cannot be followed by anyone who installed from crates.io, because the crate ships noscripts/directory. -
scripts/mmproj.sh. Every start script sources it, and it refuses to start a multimodal model without its projector, printing thecurlthat fetches it. A vision model is two files;--mmproj-autois on by default and only fires for-hf, so it does nothing for a server started with-m <path>— which is how four of four local models were being served text-only while the flag list looked handled. -
/send <path>in the TUI, andmecha slack send. A file goes from a session into the owner's Slack DM, where a phone can open it. The problem it answers is small and constant: a chart rendered on a headless box is a file nobody can see, because SSH has no viewer and scp back is a second connection nobody sets up to look at a PNG.The destination is not an argument. It is the owner's DM, read from the binding, and there is no parameter that moves it — the same shape as
frontdoor::Record::for_privileged_run, where the safety property is a function signature rather than a rule someone remembers. That is what will let the agent itself surface a visualisation in a later rung without the tool becoming a way to send a file anywhere else.The TUI's
/sendresolves through the run's path jail and the CLI verb does not, which is deliberate: a session has a jail and no reason for a second rule, while the command line runs in a shell that is already the boundary. Refusals are decided before the DM is opened, so "that is a directory" arrives as itself rather than after a network error that happened first, and[slack] max_upload_mbnow caps both directions rather than only what comes in.Rung 1 of
docs/REMOTE-CONTROL-DESIGN.md. -
/remote-control <name>— a live TUI session and a named Slack thread are the same conversation. What the run does and what you type appear in both places; typing in the thread steers a run in flight or starts a new turn; files dropped there land in the session's workspace under./inbox/, with only the path given to the model so the taint arms throughfs_readrather than a parallel route.A name is durable and its thread is forever, so detaching marks the record cold rather than deleting it — the record is how the thread is found again. The connector no longer starts its own run in a mirrored thread, which it previously did: a fresh conversation in a different workspace under a different permission mode, answering in a scrollback it knew nothing about.
Slash commands and
!escapes are refused from Slack. They are not prompts, and the gap between "the owner typed this" and "the owner is at the keyboard" is where a remote surface should stay narrow.mecha slack remotelists what this machine mirrors;--sweepcools an attachment whose session has gone.
Changed
- An attached image arms the
private_datataint leg. Before images, a Slack attachment armed it because the model had tofs_readthe file; putting the pixels on the user turn removed the tool call and the taint with it, which loosened the interlock as a side effect of a feature. Typed text still arms nothing — the distinction is that a screenshot is captured, not composed: you choose every word you type, and you choose the window rather than everything in it. Enforced in the loop rather than inConversation::push, because the Slack connector appends tomessagesdirectly.
Fixed
fs_readsays what a binary file is. It reported "stream did not contain valid UTF-8", which names the mechanism and not the problem, so a model handed a screenshot retried the same idea throughshell— costing two approval prompts before concluding what the first error already knew.
Documentation
- New
features/images.md;getting-started/installation.mdgains the local-model section that did not exist;first-run.mdstops teachingcontext_window = -c, whichfeatures/serving.mdcalls the trap worth knowing before you meet it;config init's local block gainscontext_windowandvision.
0.1.9 - 2026-08-20
Added
-
The task board reaches the session that needs it.
mecha tasks— list, add, set — and/tasksin the TUI, onto the GTD board in the knowledge graph./todowas the model's own scratchpad for the run it is in; the board a person keeps had no surface here at all, so the only way to look at it was to leave.The board is reached only through the MCP tool surface (
kg_task_list/kg_task_create/kg_task_update), the waymecha mail taskalready did — so nothing gains a dependency on the graph or a second reader of its schema, and the lookup matches on the tool's suffix, which is what keeps a renamed server or aprefix_toolsflip from turning the board into "no tasks". The modal drives the CLI, so nothing it can do is missing from a script.Keys are
mecha-graph tuiscreen 6's keys, with a test saying so: two boards over one store with divergent letters is a trap, and the keystroke it springs isxon something you meant to finish. Nothing confirms — the board reaches nobody, every status is one keypress from where it was, and the tool surface has no delete — because a confirmation on a reversible private change only teaches people to approve without reading, which the outbox then has to fight. A reload re-finds the cursor by id, since changing a status is also what reorders the board and the next keypress might bed.Capture takes name, due, project and context; edit takes due, defer and context and not the name, because
kg_task_updatehas no rename and a box that silently discarded what was typed in it would be worse than no box.--dueacceptstoday,tomorrowand+Nd;--projectmust already name a node, and an unknown one is an error rather than an invented node. Onset, an omitted field is untouched and""clears — passed through rather than reinterpreted, or changing a status would wipe a due date. -
/skills— what this agent knows how to do. The slash counterpart to/tools.mecha skillsshipped without it, so the question could only be asked from outside the session. It reads from two places because neither can answer alone: what the run carries comes from the running agent rather than from re-deriving the selection off config, since--skillnarrows a run without changing any file; what config withheld is marked rather than omitted, because a withheld skill and one the model chose not to load look identical from the outside and only one is a mistake. -
A reply is shown with the message it replies to. "A message's reviewable object is the message" was half a rule: a staged
mail_replycarries a body and athread_id, and athread_idaddresses the provider rather than the reviewer — so the queue asked people to approve a letter without showing them the letter it answers. Deciding is this the right reply without the original is approving unread, in the one way this surface exists to prevent.Nothing needed recording. The drafting run read the thread before it wrote the reply, the item already names its session, and the transcript already held the result; the link existed and nobody followed it. It comes from the transcript, never a live re-fetch — a reviewer needs the bytes the model drafted from, not today's version of the thread, and
showstays a store read with no network behind a display. The join knows nothing about mail: it matches identifying arguments by key and value against earlier calls in the same session, sothread_id == thread_idfinds the read whereaccount == accountwould have matched everything. Quoted under a heading naming it as third-party content, because a quoted block with no heading reads as more of the letter.
Fixed
-
Eight modals stopped taking the session down when the window shrank.
rows.clamp(1, terminal_height.saturating_sub(4))is a panic and not a layout bug: the subtraction saturates to zero at four rows or fewer,clampassertsmin <= max, and a panic in the draw path is the whole run — partial answer and all. Found once in/doctor, then written again in six siblings, because a new modal is written by opening whichever one is nearest./mailwas the eighth and hid a row deeper: its floor was two, since it reserves a line for its key strip, so it died at five rows where the others died at four — and it survived the sweep that fixed the rest because the site readclamp(2, …)and matched no search aimed atclamp(1, …). The helper now takes what the boxreserveds, which is the thing the two-argument form could not say; a helper that cannot express a caller is how that caller ends up spelling it inline. The degradation is deliberate: the ceiling floors at one row and then the floor is pulled down to meet it, so a terminal too short for both the strip and a row of list gets a useless, living box rather than a dead session. -
/outboxno longer opens underneath another modal. A run finishing under/review nowhands the review surface a set of ids, and the guard that refuses when something already owns the keyboard did not know about/mail,/tasksor/polls— so the board stayed on screen while the keystrokes drove the queue, andrrejected a draft nobody could see.
0.1.8 - 2026-08-20
Added
-
Skills — procedures you write, that the agent loads when it needs them.
~/.mecha/skills/<name>/SKILL.mdin the Agent Skills format: YAML frontmatter (name,description, optionaltriggersandtools) and a markdown body. Progressive disclosure in three levels — name and description in the system prompt at roughly a hundred tokens each, the body only when the model callsskill, and a bundled file only when the procedure points at one. That profile is the pressure valve for the learned-rule cap: a procedure too long for a rule, too specific to be worth a slot, and irrelevant on almost every run is exactly what this shape was built for.The format is the standard's rather than ours because the procedures worth writing are portable — the two
SKILL.mdfiles this repository already carried, written for a different harness, load unmodified.A skill is user-authored and there is deliberately no way for it not to be: no install command, no registry, no remote body, nothing derived from a session, and no project-layer store. That absence is the whole safety argument — 36.8% of 3,984 published skills carry a security flaw and a cloned repository is the delivery route — and it is why loading one arms no taint: the body is the user's own words, like the system prompt. A project's
mecha.tomlmay narrow the set by name and structurally cannot widen it.mecha skillslists what a run would carry and marks what config withheld;--no-skillsand--skill <name>narrow per run; a trigger names its skills explicitly, where empty means none, because an unattended run's instruction set must not grow every time an unrelated skill is written.mecha evalforces them off, like MCP, hooks and learned rules.Loading is a tool call rather than a shell
catso it passes thepre_toolgate, lands in the trace, and works whereshellis sandboxed or absent. A skill'stoolslist narrows the surface while loaded — union across loaded skills, never wider than the unrestricted surface, and enforced at dispatch rather than only in the tool list, because a shortened list is advisory to a model that remembers a tool from three turns ago. A loaded skill crosses a compaction verbatim, since a paraphrased procedure is a different procedure.
Changed
-
A staged message is reviewed as a message, not as its arguments.
mecha outbox showand the/outboxmodal led with provenance and then printed{"body_markdown": "Dear Dirk,\n\nThank…"}, so deciding whether to send a letter in your own name meant decoding escape sequences. Both now lead with the draft — headers, then the prose with its own newlines — with the taint warning still above everything and the provenance in grey below.mecha_core::outbox::DraftViewdoes the reshaping, keyed on well-known argument names likeheadlineso the store stays tool-agnostic and an unanticipated tool's fields land inotherrather than vanishing. Nothing is dropped: every argument appears in exactly one of headers, body or other, with a test on it, because a field the reviewer cannot see is a field they approved unread. The exact bytes are--jsonon the CLI andJin the modal — the check, not the read. A tainted send's confirmation is reshaped the same way: what is approved must be what was read. -
mecha outbox editopens the prose. Editing a letter inside a JSON string literal means typing\nfor a paragraph break and escaping every quote, in a file where one slip is a parse error that discards the whole edit — for the one action here whose purpose is changing the words. The scratch file is now a.mdholding the body, written back to the field it came from;--jsonis what it always did, and is the automatic fallback for a draft that is not prose. The learning capture is untouched:args_beforestill holds the draft andmecha reflectstill mines the difference. -
/outboxhides resolved items behindh. They stay on file forever — that is the record — but a decided draft is not work, and a queue where one pending item sits under twenty-eight resolved ones is a queue nobody reads to the bottom of. The count rides in the title so the filter is visible, and the toggle re-finds the row under the cursor by id, because the two lists have different lengths and an index carried across would name a different draft to a keypress that might bes. -
/mailshows its keys, andenteropens the thread. Eleven keys were going into a title bar that fit four and were replaced entirely by the first status message; they are a pinned strip now, built from the same table the key map is built from, with a test asserting the two agree, and?opens the full list.enterwas starting an MCP server, fetching a whole thread and printing its subject line into that title — the mail was downloaded and thrown away. It opens a scrollable reader instead, off the event loop on a watch, and the triage keys work from inside it so read-then-archive is one motion. The handle column is gone from the list: eight characters of base64 spending width the subject needed, kept on the reader's title bar where someone about to typemecha mail archive <handle>is looking.
0.1.7 - 2026-08-19
Two releases in one day, and they rhyme. mecha got a writable seat at the user's documents — with the grant chosen so a document nobody handed over is not reachable by any prompt — and it learned to file mail, with the reading of that mail quarantined so a subject line cannot reach a run holding the calendar. Both landed a verb in the same new capability quadrant, from opposite directions.
Upgrading
Both mail accounts must re-authenticate. The Google and Microsoft scopes
widened so mail can be filed rather than only read, and a widened scope does
not cover a grant issued under the old one. Run mecha-mail auth <account>
for each; mecha doctor names any account that cannot triage, with the
remedy. Nothing else in this release requires action.
Added
-
A run now records how it went, not only what it cost.
Record::Outcome(RunStats)lands one row per finished run in the session transcript, written by every front-end.RunOutcomecarries fifteen fields and the transcript kept two, so an interactive run was measurably less observable than an unattended one — the signal was computed and discarded at the end of every run a human was watching. Every field is a deterministic count and none is derived from the content of a tool result, which is what makes the corpus usable as an input to automated grading: a counter carries no instructions. -
mecha sessions healthreads that corpus back across the whole session store — stop causes, tool reliability, how often a run finished over a failure — split by model, because a corpus spanning two has no single rate worth quoting. A rate with no denominator prints as an em dash rather than as zero: "nothing went wrong" and "nothing happened" are different answers. -
mecha diagnosereads the corpus, folds indoctor's findings, and proposes one typed change with a falsifiable prediction. It does not measure and does not apply — it prints theeval --ab-configline that would falsify it. Automated failure attribution is right about which step failed roughly one time in seven, so the design target is that being wrong costs one measurement. The brief is built from counters and has no field for a transcript excerpt, and a proposal reproducing eight consecutive words from anything the diagnostician read is refused. -
mecha eval --ab-config KEY=VALUEruns the case set twice, differing only in the override, and judges the difference: paired by case, deterministic holdout, and a work guardrail that rejects a gain bought by attempting less. Overrides are a closed set of run options, so both arms are built by one code path. -
mecha doctorreads the population, not just the incident — a model finishing a fifth of its runs over a failed call, failing a quarter of its tool calls, or having a quarter of its runs cut short by a ceiling. Cancellations are excluded: a person pressing Ctrl-C is the system working. It also names a trigger failing a large share of its calls, and a trigger whose most recent run succeeded having done nothing at all. -
Mail triage.
mecha-mailgainedmail_triage— archive, read, unread, spam, trash — as a closed action enum over a whole thread. The write surface was previously send, reply and calendar CRUD, so a mailbox could be read and answered but never emptied. Gmail does all five as label arithmetic in one call; Graph has no thread resource at all, so each verb resolves the conversation to message ids and acts on each, reports how many it touched, and fails only if every message failed. -
mecha mail—classify,list,show,dismiss— over a new store at~/.mecha/mail-triage/, one typed verdict per thread.classifyreads recent mail through the MCP surface and hands each thread to a classifier with no tools, no history and no shared cache prefix, which returns a bucket (respond/notify/ignore), an urgency, a proposal, tags from a closed vocabulary, a deadline, and the request kind if it recognises one. Verified on fifty real threads: twenty-eight archivable, twenty-two needing attention, four request kinds recognised. -
A snippet-first pass with escalation. A preview settles the newsletters; the full body is read only when the verdict is
respondor names a request kind — the cases where the answer changes what happens. Measured at 25% of threads escalating. Deliberately not triggered by snippet length: a provider caps its preview, so nearly every real email looks truncated and escalating on that would escalate everything. -
A nightly sweep,
scripts/mecha-mail-classify.{service,timer}, 05:30 UTC and campus-only. A timer rather than amecha trigger, because a trigger's action is a prompt on purpose and this is a deterministic command. -
Google Docs, Sheets and Slides, as a fourth binary on
mecha-mail.mecha-docs authconsents once;mecha-docs pickopens Google's real file chooser to adopt a document that already existed;mecha-docs listreads the reachable set back from Drive rather than from a local index, since a listing under this scope returns exactly the in-scope files and a second copy could only disagree.mecha-docs serveis the MCP face. -
Eleven tools:
docs_list,docs_read,sheets_read,slides_read,docs_create,docs_append,docs_replace,sheets_create,sheets_write,slides_create,docs_trash.docs_replaceis the surgical edit — quote the text to change rather than index into it — and reports zero matches as a failure, because a model told "ok" there goes on to describe an edit that never happened. -
--paste, for a machine with no reachable browser. It prints the authorization URL and takes back the127.0.0.1address the browser lands on, which is displayed in full even though nothing is listening. No tunnel and no forwarded port. There is deliberately no device-code flow: Google's device flow refuses the client type the file chooser requires, and two clients cannot substitute, because a per-file grant belongs to a (user, client) pair and the two would hold disjoint sets of files. -
mecha-mail corpus— download a span of mail for analysis, envelope and snippet only, one JSON object per message. An operator command, deliberately not an MCP tool: the model has no business bulk-reading a year of mail, and a corpus verb on the tool surface would be one prompt away from being asked to. Deliberately unclassified, too — running a corpus through the classifier projects the existing tags onto it and confirms them by construction, so the taxonomy has to come from the mail rather than from the labels. Graph needed a new path (searchcaps at 100 with no cursor, and$searchforbids$orderby, so it walks@odata.nextLinkwith$orderbyalone, as the calendar has all along).
Security
-
The mail classifier sees the prose; nothing else does. Reading mail arms
untrusted_input, so a loop that reads fifty threads into one conversation arms the trifecta for all fifty and stages fifty tainted drafts — correct, and useless, because a warning that fires on everything has stopped being a warning. The front door's shape applies one directory over:Record::for_privileged_runreturns the typed verdict and the sender's address, and there is deliberately no argument that returns the subject, the sender's chosen display name, the classifier's own reasoning, or its one-line summary. That last one is the judgement call — short, and exactly what a summary wants — but it is model prose derived from attacker prose, which is the laundering path the front door withholdsreadingto close. -
mail_triagecarriesdestructiveHintalone. It mutates the user's own mailbox and reaches nobody, so it is notexternal_sendand must never appear in[outbox] tools— staging it would make triage review a queue in order to fill another queue. It is notreadOnlyHinteither, or apermission_mode = "read-only"trigger could empty an inbox unattended.assert_tool_surfacetakes a third slice asserting both negatives. This is the quadrantdocs_trashalso lands in. -
Tagging is not a provider operation. Gmail labels and Graph categories are different objects and a tag meaning different things per account fails at the one job tags have. A mecha tag lives on the triage record, costs no OAuth scope, and works identically on both providers; a test asserts the mail surface offers no
label/tag/categor*verb. -
Recognising a request kind is not routing it.
REQUEST_TYPESis what the classifier can name;ROUTABLE_TYPESis the subset a manifest exists for, and only that subset can be promoted to the front door. A recognised kind with no manifest keeps its name as evidence and loses only the promotion there would be nothing behind. Enforced in code, not asked of the model. -
The scope is the boundary.
drive.fileis the one non-sensitive scope in the family: no verification, no annual security assessment, and — on a published project — no seven-day token expiry. It also cannot reach a document that was not created by mecha or explicitly handed to it, which is a stronger guarantee than any of the wider scopes could be made to give. -
Writing a document counts as sending. Writing into a document a third party can read is exfiltration; it looks like a local edit and it is a publish. Every write carries
openWorldHintand belongs in[outbox] tools, so it stages for review rather than executing. Reads arereadOnlyHintand notopenWorldHint, withuntrusted_inputforced by config the way mail and the graph already are — a document comment is an injection vector invisible in the rendered page. -
docs_trashis in neither quadrant, carryingdestructiveHintalone: it reaches no third party, so staging it would make review circular, and it is not read-only, so an unattended read-only run must not reach it. The same quadrantmail_triageoccupies. There is no permanent-delete verb and no sharing or permissions verb; the scope would permit both, so the boundary is the tool surface and a test asserts the absence. -
Mail OAuth scopes widened, and a refresh stopped over-asking.
gmail.modifyreplacedgmail.readonly(stopping short ofhttps://mail.google.com/, whose only addition is irreversible deletion) andMail.ReadWritereplacedMail.Read. Both invalidate existing grants. Separately: Entra's refresh sent the whole scope list on every renewal, so widening it asked for a superset of what the stored grant consented to — refused asinvalid_grant, indistinguishable from a revocation, and classified as permanent. Every working Outlook account would have gone dark about an hour after install. A refresh now sends no scope at all, per RFC 6749 §6, and a test asserts the absence. -
A grant now says what it can do, and when it dies.
StoredCredentialsrecordsgranted_scopesverbatim andgranted_at(stamped at consent, never touched by a refresh).mecha doctorreports an account whose grant cannot support the triage verbs, and warns two days before a declaredgrant_lifetime_daysexpires — the Google Testing-mode seven-day clock that a marker written on failure could only ever report after the outage began. Absent reads as not covered and as unknown, never as safe. -
The grant lives under its own root (
~/.mecha/docs/<account>/), sharing the credential type with mail but not its namespace:mecha doctorreads everyoauth.jsonunder~/.mecha/mail/as a mail grant, so a documents credential there would be reported as a broken mail account — a finding naming the wrong subsystem.
Changed
-
The learned-rules budget is per domain, and a run carries only the domains it names.
MAX_ACTIVE_RULES_PER_DOMAIN15 → 25 withRULES_CHAR_BUDGET1600 → 2600 beside it, and the learner frames are handed the same constant rather than repeating it as prose — a frame saying "never exceed 15" while the gate admits 25 fails silently, because an over-consolidating learner looks like a well-behaved one.rules_prompt_block_for(RUN_DOMAINS)gives a runbehavior+writingand nothing else; new domains are opt-in andunrouted_domainswarns at startup about any holding rules no run carries. Everything reconstructing "what a run sees" moved with it, or the validation ledger would be keyed to a rule set no run ever had. -
The mail taxonomy is measured rather than guessed. A year of real mail said the request kinds were wrong in both directions.
student-advisingjoinsREQUEST_TYPESandadvisingjoinsTAGS— the largest category by a wide margin, and absent because it is the most routine thing that arrives.bookleaves: two threads in ten months, neither a request to write a book. A name on that list is a claim that the kind arrives. -
Mail no longer routes to the front door.
Proposed::Frontdoor,ROUTABLE_TYPESandis_routableare removed. The front door's[verification]block exists to prove a stranger controls an email address, and an email arrived from one — so the machinery being imported solved a problem mail does not have, while removing threads from the queue whose whole value is being the one place. Mail keeps its own request kinds; the surviving half of the idea becomes mail's ownneeds-info. Recognising a kind was already separate from routing it, which is why nothing else moved. -
mecha mail correct— say the classifier got a thread wrong, field by field. A misread bucket, a missed deadline and a wrong request kind are different errors with different fixes, so the correction names which. The verdict is fixed in place immediately and the before/after pair is kept on the record, because a learner shown only the right answer cannot see what to stop doing. A correction that agrees with the classifier records nothing. -
mecha mail score— score the live triage store against what actually happened. Behaviour (did a reply go out) and testimony (what you corrected) are reported apart, because a reply is one-sided evidence and a correction is not. Threads younger than 48 hours are excluded: most replies that ever happen land on the first day, so a same-day thread has no outcome yet and counting it would punish a rule for how recently the mail arrived. -
mecha mail eval— grade the classifier against a corpus whose outcome is known, with no human grading anything. Reports a false-ignorerate on the answered stratum and a volume on the unanswered one, never a blended accuracy;--outkeeps every graded verdict so a run can be re-read without being re-run.
Fixed
-
A pile of identical failures no longer rides in every request.
compact::collapse_repeated_failuresfolds repeated failures onto their newest member. Errors are exempt from supersession on purpose — a failed call says nothing about the target — and that rule is right for one failure and inverts for eight: a model is measurably likelier to fail a step when the context holds its own earlier errors, and the effect does not go away with model size. Nothing in the harness touched these before: eviction skips errors by construction and thinning only truncates long results, so a sixty-character failure message was untouchable by both. -
A run that answers over a failed call now says so.
RunOutcome::ended_on_failed_call, with anexpectcheck beside it. Nothing in the answer text or the stop reason distinguishes a run that recovered from one reporting success over a failure, and grading the claim needs a judge, which measures near chance at exactly this. -
A retired proposal no longer makes a triage record unreadable.
Proposed's derivedDeserializefailed the whole record on an unknown string, so removing a variant from a type written to an append-only store would have silently truncated it — five records in a live store carriedproposed: frontdooron the day it was removed. The hand-rolled impl degrades an unknown proposal tonone, which is what "a human decides" already meant. -
mecha-mail corpusno longer silently truncates Gmail. Each month was capped at 500 messages and a failed month wasunwrap_or_default()ed into an empty one, so a partial corpus and a quiet month were the same value. On a real year, most months hit the cap and the loss grew toward the present as volume grew — which does not add noise to an analysis, it adds a slope, and a slope is indistinguishable from a finding. The ceiling is now high enough to be theoretical, reported on stderr when reached, and errors propagate. -
A failed classification is retried instead of buried forever. The sweep skipped anything the store had heard of, and the store holds failures as well as verdicts — so a thread whose classification failed was skipped by every later sweep on the strength of a record saying it never happened. An outage on 2026-08-19 left 17 threads in that state, one of them a manuscript review invitation.
-
A classify run that did nothing now fails. It returned success however badly it went, so a run that classified 0 of 16 logged SUCCESS and every exit-code check —
OnFailure=,systemctl --failed, doctor's failed-unit scan — read a dead nightly as healthy. Partial failure stays a success deliberately. -
A retired learned rule survives a reworded re-derivation. Retirement was carried by exact text, so the same rule in different spelling or punctuation came back live. It now matches after normalising case, punctuation, spacing and
-ise/-ize, checked only against retired rules so two distinct rules cannot be merged by accident.
0.1.6 - 2026-08-16
The release where the knowledge graph became a sibling product: pkg went public as mecha-graph (three crates on crates.io), and mecha's side of the seam was renamed, un-prefixed, and folded into the install ritual.
Added
prefix_toolson[[mcp]]. Unset keeps the collision-proof<name>__<tool>default;falseregisters a server's tools under their raw names, for a server whose tools already carry their own namespace — the graph'skg_*family, wheregraph__kg_searchwas stutter the model typed in every call. Unprefixed is a promise of distinct names, and the promise is enforced: a collision with anything already registered fails startup loudly (a prefixed name always contains the__marker, so the two cases cannot be confused, and prefixed shadowing keeps its existing semantics).
Changed
-
The eval grades the surface that actually runs.
pkg-cases.jsonlis nowgraph-cases.jsonl, its fixturegraph_server.py, and the trace checks expect the barekg_*names production serves — an eval still measuringpkg__kg_*would grade a configuration that no longer exists anywhere. Scorecards across the rename are not comparable, which is the honest cost of any tool-surface change. Thedistill,vet, andcorroborateserver defaults follow the live alias tograph. -
The graph installs with mecha. The README offers
mecha-graph-mcpandmecha-graphbesidemecha-mail, the sample[[mcp]]block wires the installed binary (command = "mecha-graph-mcp",prefix_tools = false), and the update skill folds the graph into the same cargo-install ritual as every other binary.
Fixed
--mcp-filefixture paths survive the workspace. Relativeargswere joined against the file's directory but left relative, and since MCP servers began starting in the run's workspace that join resolved there instead — every fixture handshake failed regardless of names. The base directory is canonicalized, so the joined path means the same file from any working directory.
0.1.5 - 2026-08-16
Added
-
recall— the record is searchable after the summary. A tool over the session's recorded transcript: the union of every message ever recorded, including what a compaction rewrite replaced, so a run missing a dropped detail looks it up instead of re-running tools or re-living the stretch. Taint-neutral by construction (everything it returns entered this conversation once, and its arrival is what armed the interlock; the transcript path is fixed at registration, never model input). Registered by chat, the TUI, and resumed runs — deliberately not Slack (one shared registry across per-thread conversations would cross-wire transcripts) and not fresh one-shots or triggers (a per-run record is empty until run end). -
The cache lens: prompt-cache reuse is watched, not assumed. A pure per-run observer fed each request as actually sent plus the usage the provider reported. It names the reason when reuse legitimately breaks (tool/system surface changed, transcript rewritten by compaction), stays silent on providers that have never reported a cache figure (zeros are silence, not a miss), and warns on the one remaining shape — a large re-payment with nothing changed. Verdicts go to tracing only; the model and the loop never see them.
-
kind = "landlock": file confinement that needs no privilege. A fourth sandbox backend for machines wherebwrapfails even installed (Ubuntu 23.10+'s AppArmor userns switch): Landlock LSM rules applied in the child between fork and exec — ruleset built in the parent, the post-fork closure makes raw syscalls only, and a kernel reportingNotEnforcedfails the spawn rather than running unconfined. Priced honestly: Landlock cannot close the network (TCP denied on 6.7+ kernels, UDP unrestrictable at any ABI), so a landlockedshellnever earns the interlock relaxation — what it buys is the file story, hard-required at ABI 3 because older kernels cannot restrict truncate. Preflight proves the denial, not just the apply: it plants a file in the real home and requires the confined read to fail. -
Gossip adjudicates the queue instead of adding to it. After building deep context on an entity, a gossip probe now pulls the pending claims about that same entity and judges them under vet's existing
verificationmechanism — the one output that makes the review backlog smaller. Verdicts are opinions filed beside still-pending candidates;--adjudicatecaps the batch (default 25) so the owner node's ~1,800 claims cannot absorb a whole night.
Fixed
-
A run that compacts itself no longer loses its own head. Front-ends record at run end, so turns of the current run that a mid-run rewrite replaced were never anyone's to write. The states a rewrite replaces now ride on the
Conversation(rewritten, cleared at run start), andSession::record_run— which now takes the conversation, so a caller cannot record the destination while skipping the journey — walks snapshot → snapshot → final.load()still replays to the live state;recallreads back what the summary dropped; the taint timeline only gains rewrite records, which drop checkpoints in the over-tainting direction. -
A reader's searches no longer feed the probe selector. Gossip's
LensedSearchpassesprobe: truetokg_search, so pkg's Selector — which ranks probe targets by retrieval demand — stops counting a probe's own reads as the owner reaching for something. One probe had taken its target from 2 touches to 28 and re-elected the same person out of a pool of nine.
Changed
-
trusted_outputmust now name what it vouches for. A subagent profile settingtrusted_output = truewithout ananswer_shaperefuses to construct — the old semantics let one config line disarm the trifecta's untrusted leg for every answer the child ever gave, with nothing checking anything. Trust is now granted per answer: declareanswer_shape = "number","boolean", or a list of allowed strings, and an answer earns the vouch by parsing as that shape at return time. Prose never parses, a mismatch comes back marked untrusted with a note saying why, and the subagent's static capability now stays honest (untrusted_inputderives from the child's tools unconditionally — the loop'suntrusted_input && externalrule is what lets a shape-proven answer through clean). There is deliberately no bounded-string shape.docs/TRIFECTA.md(new) maps all four trifecta channels and which mechanism owns each. -
A trifecta refusal now names the durable fix, not just the workaround. Tools gained
denial_remedy()— the tool's own account of what would make a call like the refused one safe, relayed at the end of the interlock's message.shelluses it to point at the actual remedy ([sandbox]withnetwork = false, or just the network flag when the sandbox is already on), which ends the measured dead end where denials advised delegating to subagents that had no shell. The delegate suggestion itself now says it is for reading the outside world, so the model stops treating it as a route for local work. Security is unchanged: the same calls are refused, the refusal just routes somewhere real.
0.1.4 - 2026-08-15
Slack stops being a place to read about work and becomes a place to do it, and five stores that each knew something was wrong get something that reads across them.
Added
mecha doctor— one pass over every store, no network, no model, no tokens. The 2026-08-11 incident in one sentence: five stores each recorded a real failure correctly and the operator learned nothing, because nothing read across them. Doctor is that read — dead auth markers, releases that errored, drafts and requests waiting past a threshold, triggers whose slots stopped advancing, failedmecha-*units./doctorin the TUI, one keystroke from where the error surfaced. Findings frommecha doctor --jsonas a child process, broken before attention, a component's findings kept together. Acting on a finding dispatches on the remedy's shape: one whose surface is already a modal deep-links to it rather than spawning a nested CLI over the modal that is the review, and a remedy needing a terminal suspends the TUI and inherits the real one.- Executable actions from Slack. Buttons and modals that do the thing
rather than describe it — the button carries an id, the enum carries the
verb, so the prose that shares a context window with third-party text never
becomes the instruction. Trigger enable/disable, frontdoor close and
needs-info by modal, and a session-scoped
review now|later|autotaken only as the owner's exact command word, expiring with the thread state, with tainted drafts carded in every mode. - Tier-2 gossip and the vet mechanism (
mecha gossip --out,mecha vet) — corroboration and queue-verification over pkg's review queue, origin-family exclusion computed rather than reconstructed, batch runs that keep their evidence as JSONL with per-candidate error containment. - Recorded denials and structured corrections, mecha's half of pkg's D3: the graph can say "no", and the distiller can say "that was wrong".
- A start script for Qwen3.8-27B (
scripts/start-qwen38.sh) as a fourth local arm on port 8083 — dense rather than MoE, with the MTP draft model loaded separately because unsloth ships no MTP variant for 3.8. It carries the measurements and the two traps this model brings: llama.cpp#20837 is still open against theqwen35architecture it reuses, and its chat template rejects any system message that is not first.
Changed
- One release policy instead of two.
review_policy.rsholds the mode andauto_releases(mode, tainted, finished_clean), so the tainted exclusion and the early-stop exclusion are the function's signature rather than a check each surface has to remember. Slack auto no longer releases what a stopped run produced. - Nothing blocks the dispatch loop. A button press reads its one item file — id shape-checked before it touches a path — instead of parsing the whole outbox, twice, for tainted. The ledger append rides inside the spawned task, and the TUI's restart guard probes on a watched thread so a probe that never answers cannot hang the interface.
cargo fmt --all, which CI had been failing on for four commits. No semantic change; the drift spanned 20 files and predated the commits it was failing on.
Fixed
- Nothing fails silently: the staged jail, the unanswered card, the revoked
token. Three field failures with one shape — a component that knew
something was wrong and told no one. A staged call now records the jail its
tool really runs under, via
Tool::fixed_workspace.
0.1.3 - 2026-08-10
Dependency maintenance, and the one migration hiding among the bumps.
Changed
- The minimum supported Rust version is 1.89, raised from 1.88 to take
rustyline18, which usesfile_lock.CONTRIBUTING.mdnow carries the rule for when it may move again — with a dependency that needs it and never to use a new feature ourselves, raised in the same change as the dependency that forced it and naming it. The old number had no rationale attached, which is why every bump reopened the argument. - Nine dependency majors —
rand0.8 → 0.10,sha20.10 → 0.11,toml0.8 → 1.1,htmd0.1 → 0.5,base640.22 → 0.23,rustyline15 → 18 (the line editor behindmecha chat),clap4.6.6, andratatui0.29 → 0.30 withcrossterm0.28 → 0.29, which cannot move separately because ratatui pins it. The ratatui major was a migration rather than a bump: 0.30 makesBackend::Erroran associated type, soterminal.draw(..)?inside a function returninganyhow::Resultstops compiling — anyhow needs the error to beSend + Sync + 'staticand an opaque associated type promises none of it. Fixed with a bound,Terminal<impl Backend<Error: Send + Sync + 'static>>, rather than pinning the concreteio::Error: ratatui's ownTestBackendusesInfallible, and the concrete type would have locked every rendering test out of the functions it tests. - Google's PKCE verifier is filled in one call from
rand::rng(), the thread-local CSPRNG, rather than a byte at a time — the one code change therandmajor required. The function had no test at all, which is how a security-relevant generator gets its random source swapped with nothing watching; it has two now, the load-bearing one recomputing the challenge from the verifier exactly as the authorisation server does, because a wrong transform fails every sign-in with an error naming neither end. - Four GitHub Action majors —
checkout4 → 7,setup-node4 → 7,upload-pages-artifact3 → 5,deploy-pages4 → 5.
Added
- A test that an HTML-only email keeps its links, tables and lists.
htmdis a behavioural dependency rather than an API one — it decides what a mail body looks like to the model, and four minor versions can change that while every type still lines up. Asserted on content surviving rather than exact output: a golden-output test would break on every bump of a converter whose formatting is its own business, where "the link's URL is still in there" is what must never stop being true.
0.1.2 - 2026-08-10
Fixed
Reasoning, and the empty turns it turned out to be causing
A reasoning model's thinking arrives on its own channel, and this harness read
none of it. reasoning_content appeared nowhere in the tree, so on any local
model the reasoning toggle did nothing, the TUI showed silence where thinking
should stream, and the transcript kept no trace. Block::Thinking and
StreamEvent::ThinkingDelta had existed since the Anthropic backend was
written; the plumbing was all there and nothing was ever fed into it.
It was also, measurably, the cause of the empty turns this project has been
mitigating since 2026-08-07. Replaying the exact prefixes that went quiet
during the 2026-08-10 benchmark reproduced one, and its "reasoning" was a
complete, unparsed tool call — in one case 120 characters that were only a
tool call, with no deliberation at all. finish_reason: "stop", no content,
no tool_calls. The model had emitted its call before closing </think>, so
llama.cpp filed the whole turn as reasoning (upstream ggml-org/llama.cpp
#20837, #22684, #20809 — all unfixed, and the same failure is reported
against ollama). Note what that rules out: 120 characters is nowhere near a
limit, so --reasoning-budget, max_tokens and the context window were never
relevant. Every earlier mitigation aimed at "the model reasons too long" was
aimed at the wrong failure.
And half of it was ours. Because the history sent back stripped every
<think> block, the model was shown turn after turn of itself apparently
calling tools without thinking, and it obliged. Same server, same template,
same prompt, varying only whether the history carried reasoning: 6 of 6
empty turns without it, 0 of 6 with it (Fisher exact p ≈ 0.001, on a
reproducer that fails byte-identically).
reasoning_contentdecodes into aBlock::Thinkingand streams asThinkingDelta, so reasoning is visible live and recorded in the transcript. It is never output:produced_outputis the same definition the loop decidesproduced_nothingon, so a reasoning-only turn is still nudged rather than ending a run with an empty answer.- It rides back to the provider on the next request. Self-gating by
construction — a
Block::Thinkingexists on this path only because a server sent the field, so it returns only to servers that speak it and never to an endpoint that would reject an unknown one. No provider name is tested. Affordable because the prefix cache absorbs it: measured, turns with 5,000-token prompts prefill 16–211 tokens, better than 95% reuse. - An empty turn now records what actually arrived — its full trace at debug, with a tail and a family-agnostic lost-call marker at warn. Such a turn is in no transcript (the loop nudges and continues before pushing, and holds no session to record into), so this is its only durable record.
- A run that ends having only reasoned hands that reasoning back, labelled as deliberation rather than a committed answer, instead of reporting that the model said nothing.
usage.prompt_tokens_details.cached_tokensis read, so cache reuse is no longer invisible. The cached half is subtracted frominput_tokens: Anthropic reports the tiers beside the prompt count and OpenAI reports them inside it, andtotal_inputsums all three — carrying it over unchanged would report a prompt at nearly twice its size, and the compaction threshold reads exactly that number.
The benchmark adapter
- A task whose instruction opens with
-reaches mecha as a prompt, not a flag.terminal-bench/pytorch-model-recoverydescribes itself as a bulleted list, so clap read-as an argument and exited 2 before the run started; Harbor records that asNonZeroAgentExitCodeErrorand scores 0.0, indistinguishable from a model that tried and failed.shlex.quotenever helped — it makes the text one argv entry, and the problem is that entry's first character. Fixed with--, rather thanallow_hyphen_valueson the CLI, which would let a mistyped flag be swallowed as the prompt and run anyway.
0.1.1 - 2026-08-09
Added
Slack as a remote control
mecha slack—auth(tokens read from the environment rather than from flags, so neither reaches shell history orps),link(a one-time code binds this install to a workspace),connect(holds the socket open and drives runs from threads; this is what the systemd unit runs),threads,sweepandstatus. The transport is a fourth crate,mecha-slack, which joins the crates published to crates.io.- A Slack thread is a
Conversation, which hands the trifecta interlock the right granularity for free: a new thread is an honest clean slate, and a thread that fetched a hostile page on Monday still remembers on Tuesday. Everything per-thread — jail, budget, cancel token, steering queue, approver — rides onRunContext, because one agent serves every thread. - Unfurling is off on everything the model authors, and there is no parameter to turn it on. A model-emitted URL that unfurls becomes an outbound GET that no tool call made and no interlock saw, so it is a property of the transport rather than an argument some call site can forget.
- A private file download that comes back as a sign-in page is refused.
files.slack.comredirects to the workspace host, HTTP clients stripAuthorizationacross hosts, and Slack answers with an HTML login page at status 200 — so the header is sent explicitly, no redirect is followed,text/htmlis rejected even at 200, and the byte count is cross-checked against the size Slack reported. Without them a sign-in page reaches the model labelled as the user's screenshot. [slack]config —max_concurrent(threads with a run in flight at once; at the cap the connector refuses and says so rather than queueing),approval_timeout_secs,default_mode,max_turns,max_cost_usd,stream_flush_chars/stream_flush_ms,max_upload_mb, andtoolsto narrow the surface a Slack-driven run gets — measured on the first live run, the schemas of every wired MCP server cost ~7–8k input tokens per turn before any work happened.
Inter-agent messaging
mecha msg—send,list,show,dismissandagentsover a file-based mailbox between sessions (~/.mecha/messages/<recipient>/). One agent, or a person, leaves a short text for another by producer name; the recipient's run claims it at the top of a turn and folds it into the same user message that carries the tool results — the identical fold point as steering, because it is the identical problem. A recipient with no live run loses nothing: the message waits in the store until its producer next runs.- Taint travels with the message. A message is otherwise a laundering point,
since the receiving conversation's interlock never saw what the sender read.
The harness — never the model — stamps the sender's conversation taint onto
every message, and delivery merges it into the receiver's before the text
lands. A tainted overnight run can still report to
chat; the morning session then treats external sends exactly as if it had read the hostile page itself. - A message is structurally not the user. It cannot answer an approval
prompt, cannot change config, and arrives labelled as another agent's words;
the receiver's own permissions, hooks, outbox route and interlock govern
everything it provokes. A full mailbox refuses new sends, which is what
dismissis for.
Review, in the TUI
/outboxand/frontdoorare modals on the/triggerspattern: the store read for display, every mutation amecha …child process, and slow work — a release's MCP startup, an extraction, a triage run — spawned detached and watched, with the outcome reported as a notice when the store shows it. Every send confirms, and a draft staged with the trifecta armed is shown in red with its full arguments on screen./review now|later|autodecides what happens when a run stages drafts. It is set only by slash command and never inferred from the prompt, because release policy must not be decidable by anything sharing a context window with third-party text. Scope is an id-diff between submit and completion, so no mode touches items another session staged; tainted drafts never auto-release, and an errored or early-stopped run releases nothing./polls— the polls open on the public gate: tallies, close and export.
mecha-mail: availability and bookings
calendar_freebusymerges busy intervals across every configured account.mecha-mail freebusy --jsonis the same answer as data, for a scheduled pipeline with no model in it — and unlike the MCP surface, which reports a failed account beside the others' results, it fails when any account is unreadable: a mailbox that could not be read is not a mailbox with free time, and a booking page built from a partial answer offers strangers slots the user does not have.mecha-mail bookingsturns drained booking records into calendar events — the inbound sibling offreebusy, deterministic and with no model in it. The invite is the provider's own, from the user's real mailbox; a cancellation withdraws the event and the ledger remembers both directions; reminders fire once per tier. Idempotent against~/.mecha/mail/bookings.jsonlunder a lock, so re-running after a partial failure picks up exactly where it stopped and two sweeps cannot both create an event.
The work directory
~/.mecha/work/<producer>/is a run's workspace as well as its output directory, where a producer is a trigger's name, orchat, or a session id. It is stable across runs of the same producer, so yesterday's output is an ordinary file in today's run rather than something that has to be plumbed.mecha work—list(what each producer has generated, with counts, size, and the newest entry),path <producer>(forcd $(mecha work path x)), andcleanwith--keep,--producerand--dry-run. Retention is a policy rather than an intention: the last[work] keepentries per producer survive, the nightly job runs it, and it says exactly what it removed.cleannever removes anything a published bundle names as a source, and names the entries it kept for that reason instead of skipping them silently.[work] keepin config (default 10).
The public surface, outbound
- Publishing rides on the outbox. The factory's MCP tools render a bundle locally and stage the publish for review, so nothing reaches a public URL without a human releasing it.
[outbox] publish_toolsnames which routed tools are publications. An item now carries a kind (messageorpublish) that decides how it is reviewed:showleads with the rendered page rather than the arguments,editis refused with the real action named (edit the source, re-render, publish again), and the writing-reflection miner excludes publishes so a changed directory path can never be mined into a voice rule.- The kind is config's to declare, never the tool's. Anything unnamed is a
message, and a name inpublish_toolsthat is not intoolswarns on every start. - An item records the workspace it was drafted under, and a release rebuilds its tool surface rooted there. A batch release builds one surface per distinct workspace, lazily.
The public surface, inbound
-
mecha frontdoor—list,show,extractandnextover the requests drained from the public surface into~/.mecha/requests/. The whole layer serves one sentence: the privileged run sees the extraction, never the prose a stranger wrote. -
The extractor is issued a request with an empty tool list and no conversation, so there is nothing for an injected instruction to reach; an extraction failure routes to a human rather than passing the prose through.
-
Record::for_privileged_runis a function with no argument that returns the original text, so the boundary is structural rather than a rule to remember. -
triage,needs-infoandcloseare how a request reaches an answer, and the queue stops being somewhere requests only accumulate.triagedrafts a reply per extracted request into the outbox and refuses to run without the outbox route — unrouted, amail_sendthe model makes actually sends, and a stranger's inbox is not where you want to discover[outbox] toolswas unset. Each request gets a freshConversation, so flagged prose cannot arm the interlock for the request behind it.closerequires a reason. A rejected draft returns the request toextracted, never toclosed— "not this reply" is not "not this request".reconcilereads the outbox and updates the request store on its own rather than on a verb someone has to remember, and leaves a partly-resolved set alone, because some sent and some pending is a person mid-review.
Elsewhere
mecha outbox reviewwalks pending items one at a time, deciding each — the overnight-triage case, where nine drafts used to mean nine invocations and nine startups of every MCP server. Batching saves the invocations and never the reading. Ids may be given several at a time;--allis narrowed by--kindand--via;listis grouped by kind. A selection naming nothing, or a filter matching nothing, is an error rather than "everything".Tool::carried_statelets a tool hand state to a compaction to be carried verbatim. Thetodolist reached the model only through the echo in its last result — a message, and therefore exactly what a compaction summarises away. Exactly one copy survives a second compaction.
Fixed
The agent loop, after the 2026-08-07 Terminal-Bench diagnosis
Twenty-one trials of the arm64 subset broke down as 8 passes, 5 genuine model failures, and 8 deaths the harness owned some share of. Each fix below names the trial class that motivated it.
- Overflow recovery is no longer disabled by a summary that wasn't
worthwhile. The recovery arm's give-up flag was set when
compact()found nothing worth summarising — a normal answer for a short transcript already saved by thinning — and the flag gated the whole arm, so the next overflow propagated as a raw fatal 400 with eviction and thinning never attempted (path-tracing, dead at 45k tokens in a 32k window). Eviction and thinning now run on every overflow; only a summary request that failed stops further summary attempts. - The empty-turn retry allowance resets on any productive turn. It was
cumulative across the run, so a long run that recovered from silence early
was left one empty turn from death for the rest of its life — two trials
died
NoOutputmid-task that way, while two others recovered from a nudge and passed.max_turnsis what bounds the total; consecutive silence is still bounded by the same retry count. mecha runexits non-zero only when the run produced nothing. Every exhausted stop used to exit 3, which benchmark harnesses read as an agent crash: a MaxTurns trial was recorded as an agent error while its verifier scored the work 1.0. MaxTurns and budget stops with an answer now exit 0;--json'sstop_causestill says which ceiling stopped the run.- A crashed run keeps its transcript.
mecha runrecorded messages only after a successful return, so the trials that died mid-flight — the ones whose transcripts get read — left three-line session files. Messages and taint are now recorded before the error propagates in the unattended front-ends (run, triggers, frontdoor triage);chatand the TUI keep their deliberate interactive semantics — a failed turn is dropped so the prompt can be retried — but now restore from a snapshot instead of truncating a list whose length a mid-run compaction may have changed. - A compacted run's transcript records what actually happened. Recording
sliced "what the run added" off the end of the message list, but compaction
rewrites the list in place — the file kept the stale head, skipped the
rebuilt one (summary included), and a 28-turn trial recorded as 8 assistant
turns starting mid-conversation. A run that rewrote history now writes a
rewriterecord carrying the current state;loadreplaces, the taint timeline clamps stale positions (over-taint, never under), and the interactive fronts restore from a snapshot on error instead of truncating a list whose length compaction may have changed. - The per-turn tool-output budget derives from the context window when
[tools] output_budget_bytesis unset: an eighth of the window in tokens at ~3 bytes each, clamped to [6,000, 24,000] — 12,288 bytes at a 32k window, the old 24,000 at wide ones. The flat 24 KB was ~8–12k tokens of numeric data, larger than the gap between the compaction threshold and a 32k window, so one turn's results could leap from under the threshold straight past the window. - Benchmark trials capture mecha's stderr and tracing. Harbor recorded
stderr: None, discarding compaction notices and the loop's own account of every death; the bench adapter now redirects stderr to a file beside the session transcript and runs withMECHA_LOG=debug.
Jails, workspaces, and who is refusing
- A path jail rooted where the secrets live.
setupnow refuses any workspace that contains the mecha home, because$HOMEcontains~/.mecha/— the mail OAuth tokens, every session transcript, the learning store.mecha chatfrom a home directory was jailed over all of it, and an unattended trigger with no explicit workspace was worse: it fell through tocurrent_dir(), and the shipped systemd unit setsWorkingDirectory=%h. A workspace inside the mecha home is fine and is now the default. mecha trigger addwrites the workspace down rather than leaving it implicit, andtrigger showprints the resolved default — "where is this jailed" must not be answered by an omitted line.- An unconfined MCP server now starts in the run's workspace, like a confined one always did. It used to inherit mecha's own working directory, so a server resolving a relative path resolved it against wherever the user launched mecha.
- A refusal by the machine is no longer recorded as a correction by the
user.
Decisionis nowAllow | Deny | Blocked:Denyis a human saying no and is mined as a correction,Blockedis policy and never is. The prefix is chosen by the loop from the variant, never by the approver, so no wording a front-end picks can mislabel itself. This exposed a live bug —ModeApprover's own refusals (a read-only run's, an unattended run's "nothing is watching to answer") had been arriving as user denials, so every such run was feeding the learning miner corrections from a person who never spoke. - A trigger's
notifycommand runs in the run's workspace, as a hook already did. It inherited the daemon's directory, so the only way to put the answer somewhere was to spell out an absolute path — which is how the morning briefing came to end inmkdir -p ~/.mecha/briefings && cat > …, outside every path jail and unreadable by any later run.
0.1.0 - 2026-08-05
First public release. Everything is new, so the whole feature surface is listed under Added; later releases will record only what changed.
Added
The loop and the library
- Two crates —
mecha-coreis a plain Rust library that knows nothing about any CLI or application;mechais a thin binary over it. ImplementToolto add a native tool,Providerto add a backend,Approverto control what needs permission. A provider-agnostic message vocabulary means a transcript recorded against one backend can be replayed against another. - The agent loop — ask the model, run the tools it asked for, feed the results back, repeat until it stops calling tools. The loop never learns which provider is behind it or where a tool came from; both are trait objects.
- An Anthropic provider over raw HTTP, with adaptive thinking, thinking
blocks echoed across tool turns, and
stop_reason: "refusal"recognised as the HTTP 200 it arrives as. - An OpenAI-compatible provider covering llama-server, vLLM and Ollama, with
streamed tool-call reassembly across arbitrary chunk boundaries, parallel
calls interleaved by index, and tool calls that survive the
finish_reason: "stop"llama-server reports alongside them. - Prompt caching on Anthropic — a fixed breakpoint covering tools and system prompt plus a second moving breakpoint on the last message block, so an append-only transcript reads from cache instead of being re-sent uncached every turn.
- Classified failures, with transient ones retried — rate limits (honouring
Retry-Afterup to a cap), overload, server and transport errors back off and retry; auth, billing, invalid-request and context-overflow never do. A retry covers the send only, so it can never duplicate work already shown or acted on.[providers.X] fallbacksthen tries other configured providers on exhaustion, turn-local, each answering under its own model name. - A sampler you can pin —
temperatureandseedon the OpenAI-compatible provider, refused at startup on Anthropic rather than silently dropped, and both recorded in the session so a transcript says whether its run was repeatable. - Budgets —
max_turns,max_output_tokensandmax_cost_usd. All three end a run the same way: one final turn with the tools removed, so there is an answer rather than silence, andstop_causesays which ceiling fired. Cost prices cache reads and writes separately from fresh input, and reportsnullrather than a misleading zero where a provider has no prices configured. - Per-run context —
RunContextcarries the path jail, the approver, the budget, a cancellation token and a steering queue, so one agent with one provider connection can serve concurrent runs jailed to different directories under different permissions. - Cancellation that stops a run at the next safe point and keeps the partial answer, the partial assistant turn and the tokens already spent. Tools are never interrupted mid-call.
- Steering — text queued mid-run is folded into the message carrying the tool results, so the model reads the results and the new instruction as one user turn and keeps working, without being stopped and restarted.
- Subagents — an agent wrapped as a tool, given a rebuilt registry as an
allowlist rather than an inheritance, with optional per-profile model,
provider, turn limit and system prompt. A child's output is untrusted by
default;
trusted_outputoverrides that as a deliberate risk decision. - Layered TOML configuration — built-in defaults,
~/.mecha/config.toml, a project-local./mecha.toml, environment variables, then CLI flags, each level overriding only the fields it names. A global-only load exists for runs that no working directory should shape.
Interfaces
mecha run— one task, one answer, with--jsonfor machine-readable output,--resumeto continue a recorded session, and exit codes that distinguish success, error, refusal and turn exhaustion.mecha chat— a readline REPL with slash commands and input history saved after every accepted line, so a killed process keeps it.mecha tui— full-screen, with the input line live while the agent works: Enter starts a run when idle and steers one already going. Streaming output, scrollback that re-arms follow mode, session persistence and--resume.- TUI slash commands —
/help /tools /model /provider /mode /mcp /usage /todo /triggers /clear /session /exit, with modal pickers, name completion, and mid-session switching of model, provider, permission mode and individual MCP servers. A switch appends a configuration record, so a replay diffs against what actually ran. - TUI keys —
?for a full key reference,^Oto reveal tool output and reasoning retroactively,^Gto compose in$EDITOR,!commandto run a shell command locally with no model and no taint,@pathcompletion against the workspace, and Shift+Enter for a newline where the terminal supports the kitty keyboard protocol. - TUI rendering — a context fuel gauge that colours at 75% and 90%, a live todo pane, subagent work rendered nested under the call that spawned it (still correct when delegations run in parallel), atomic frame presentation, a tab title that says whether a run is in flight, and cached history cells so drawing no longer costs O(transcript) per streamed token.
mecha batch— the same agent over a JSONL file of prompts at bounded concurrency, results streamed to the output file as they finish and keyed by id, so a killed run leaves everything completed so far on disk. Every item gets its own conversation.mecha tools— the tool surface with no provider configured, including each tool's declared capabilities, the active sandbox, which MCP servers are unconfined, and--schemafor exactly what the model sees.mecha sessions list | show | path | statsandmecha config show | path | init— inspect saved transcripts, roll up tokens, turns and cost by provider and model over a window of days, and see what settings are in effect.
Tools
- Six built-in tools —
fs_read,fs_write,fs_edit,fs_list,shellandhttp_fetch— plustodo, a task list the model rewrites as it goes, kept outside the message history so it survives compaction intact. ask_user, registered only by front-ends that own a human, so the model can stop and ask instead of guessing at an under-specified task. Declining is a legitimate answer and returns a tool result, not a failed run.web_searchbehind aSearchBackendtrait with SearXNG, Exa and Tavily tried in order and falling through on failure, and adepthargument selecting a cheap round trip or a deep one.- An approval gate with
ask,allowandread-onlypermission modes, plus a planning phase that does not offer writing tools at all rather than offering them and refusing the call — enforced on both the advertised list and the dispatch path, and inherited by subagents. - A per-turn tool output budget divided across a turn's concurrent calls, so one runaway tool cannot starve its siblings. What gets cut is written to a spill file, and the marker names the path and the line the elision starts on, so recovering the rest is one read.
- An MCP stdio client that surfaces remote tools as the same
Tooltrait, namespaced<server>__<tool>so two servers can both expose asearch. It followsnextCursorpagination, accepts JSON-RPC ids in either numeric or string spelling, and routes a server's stderr through tracing instead of the terminal.
Security
- The path jail — every model-supplied path is canonicalized and proven to
sit inside the workspace before anything touches disk;
.., symlinks and absolute paths outside the root are refused. - The lethal-trifecta interlock — tools declare
private_data,untrusted_input,external_sendanddestructive; the loop tracks which have entered the conversation and refuses any sending tool once both private data and untrusted content are present. It sits ahead of the approver, because a human clicking yes is what an injection is trying to engineer.trifecta = "ask" | "allow"changes the policy deliberately and visibly. - Taint is a property of the conversation, not of one run, and it is recorded in the session file — so a new turn does not reset it, resuming does not launder it, and compaction does not summarise it away. A new conversation (a batch item, a subagent, an eval case, a trigger fire) starts clean.
block_sends_after_private— an opt-in second control aimed at ordinary privacy leaks rather than injection: any outbound tool is refused once private data is in context.- SSRF protections on
http_fetch— hostnames are resolved and loopback, private, link-local (including the cloud metadata endpoint) and CGNAT addresses refused; the connection is pinned to the addresses that passed, so a short-TTL DNS answer cannot swap them afterwards; redirects are not followed;allowed_domainsandblocked_domainsnarrow it further. - A sandbox for
shell—[sandbox] kind = "bwrap" | "docker" | "none". A confined command gets the workspace, a read-only system, no home directory, no environment beyond a named allowlist and by default no network. A configured sandbox that does not work stops the run at startup rather than degrading to unconfined execution. - MCP servers get the same treatment — the child environment is an allowlist
(
PATH,HOME,LANG,LC_ALL,TZ, plus whateverenv_passthroughnames andenvsets) rather than an inheritance, per-serversandbox = trueconfines the process, and per-servernetworkoverrides the global switch.[mcp.capabilities]can distrust a server further than its own annotations claim, never less. - Bounded output and owner-only stores —
shelldiscards beyond a per-stream cap as it arrives rather than buffering without bound and kills a command at its timeout; sessions, the outbox, the learning store, the spill directory and the mail credential directory are created at mode 0700, token files at 0600.
Hooks
[[hook]]commands atpre_tool,post_toolandsession_end, with the event as one JSON object on stdin, so policy, redaction and logging attach without editing the loop.- The dispatch order is interlock, then hook, then approver — a hook can
narrow policy and never loosen security, and a
pre_tooldenial never reaches the human. pre_toolfails closed — exit 0 allows, exit 2 denies, and an undefined exit code, a spawn failure or a timeout also deny.post_toolandsession_endare observers whose failures are swallowed. Subagents inherit the parent's hooks, and a typo'd event name is a startup error on every run rather than a warning only on the runs that needed it.
The outbox
[outbox] toolsnames tools whose calls are staged rather than executed: the loop intercepts the call, writes it to~/.mecha/outbox/, and tells the model it is a draft awaiting release. Draft-only becomes structural, so an email tool — including a third-party MCP server's — needs no knowledge of the outbox to be covered by it.mecha outbox list | show | edit | send | reject— review the queue, edit a draft in$EDITOR, release it (executing the real tool under the store lock so two sends cannot double-fire), or reject it with a reason.- Staging skips the interlock and the approver because nothing leaves the machine at stage time; the item records the conversation's taint snapshot, and review warns and confirms when a draft was written with the trifecta armed. A staging that fails returns an error to the model rather than falling through to execution. Subagents inherit the route, and a routed name matching no registered tool warns at every start.
Learning
mecha reflectmines recorded transcripts for the moments the user stepped in — a mid-run steer, a denied tool call, a corrective follow-up turn, an edited outbox draft — and asks a model for the reusable lesson behind each, appending it with the session id that proves it.mecha learnconsolidates unprocessed reflections intorules/<domain>.learned.tomlwithin a fixed budget and records which reflections it consumed.--holdoutkeeps a deterministic every-k-th slice out of the pass, so measurement has data the rules never saw.mecha validateprobes whether the rules change an answer. Follow-up probes re-ask the corrective turn and are judge-graded; steer and denial probes are counterfactual replays graded structurally on the trace — did the model do the steered thing without the steer, did it repeat the exact call the user refused.- A file-based learning store under
~/.mecha/learning/, which is a git repository:git logis the learning history andgit revertis the undo. User rules are never written by code, learned rules are freely editable, and an audit record per pass names the rules before and after. Learned rules ride in the system prompt inside the cached prefix, changing only at consolidation time;--no-learned-rulesopts out anywhere. - Provenance gates what may become a rule — every reflection carries an
origin classified by deterministic code from the transcript's recorded taint,
and
mecha learnexcludes anything not clean before a prompt is built. It fails closed: an unknown position, a torn transcript or a reflection written before the field existed all classify as untrusted. Excluded evidence stays in the archive. - Unattended learning never applies its own output —
mecha learn --proposemeasures a candidate rule set by counterfactual replay against the deployed one, rejects a candidate that regresses any probe before a human sees it, and stages the rest.mecha proposals list | show | accept | rejectis the review, and acceptance checks the live rules still match what the candidate was measured against. - Rule tenure — rules carry an id, sources and a creation time;
mecha validateappends every probe outcome to a validation ledger keyed to the exact rule set measured, and a regressed trace-graded probe bisects the active rules to name the one that flips it.mecha rulesfolds that ledger into per-rule tallies and stages retirement through the same proposal gate as any other rule change once a rule accumulates attributed regressions. Retirement is a flag, never a deletion: the rule stays as evidence, the learner is shown it as measured harmful, andrules restoreundoes it. A hard per-domain cap on the always-loaded block is warned about at startup and refused inmecha learn, so consolidation may shrink or rewrite an over-cap set but never grow past it. - A nightly cycle —
scripts/ruminate.shchains reflect, distill, validate, learn and the retirement scan, with a systemd user timer to fire it. Every stage is idempotent, a store writer lock serialises concurrent passes, and a night with the model server down defers entirely rather than half-running.
Distillation
mecha distillsummarises each closed session into an episode staged to a knowledge-graph MCP server through itskg_upserttool — evidence rather than belief, so the extracted facts wait in that graph's own review queue. It is idempotent at both ends: a local ledger under the learning store's writer lock, and the graph's own source key making a re-push an update.- A tainted session still distills, with its taint snapshot recorded on the episode instead, because losing the record of a real afternoon because a web page was open would gut the memory. Unknown taint is recorded as unknown.
Triggers
mecha trigger add | list | show | edit | rm | enable | disable | next | run | tick | daemon | cancel | runs— a prompt on a five-field cron schedule, run unattended.tickfires what is due and exits;daemonis a loop over it, so a crontab line or a systemd timer reaches the same answer andtick --dry-runis an honest preview rather than a second implementation of the schedule.scripts/mecha-triggers.serviceships the daemon as a systemd user unit.- A hand-rolled cron parser resolved in an IANA zone recorded on the trigger at authoring time, handling daylight saving in both directions: a job inside the spring-forward gap fires at the first instant that exists, and one inside the repeated autumn hour fires once.
- Missed slots collapse — a machine off for a week owes one run of each
trigger rather than a week's worth, and
--catch-up(always,never, or a duration) decides whether a stale slot still runs. A skip is written to the ledger, so "why did I not get my briefing" is answerable. - Triggers live in
~/.mecha/triggers/, never in the layered config, and a fire loads the global config only — a project file arrives with a cloned repository, and a scheduled agent run is not something a repository should be able to declare. Runs are read-only unless the file says otherwise, with outbox-routed calls still staging under read-only because staging executes nothing. - One run per trigger at a time via a flock the kernel releases if the
process dies, with the overlap skip recorded. The timeout, a daemon SIGTERM
and
trigger cancelall cancel rather than abort, so the partial answer and the ledger row survive. A manualtrigger runrecords a row with no slot, so testing a trigger cannot silently disarm its schedule. /triggersin the TUI — see, edit, enable, run and cancel what is scheduled, with the detail view reading the last answer back from the session transcript. Every action shells out tomecha trigger, so firing cannot freeze the interface and the TUI can do nothing the command line cannot.
Compaction
[agent] compact_at_tokenssummarises the middle of a transcript once the provider-reported prompt size passes it, keeping the task at the top and the recent turns verbatim. Off by default, and derived fromcontext_windowat two thirds where that is configured.- Superseded tool results are evicted first — when a later call covers the same target, the older result is replaced with a marker naming the recovery, so a write supersedes an earlier read of the file it changed. Errors neither supersede nor get evicted.
- Old tool results are then thinned — results are truncated from the head
with a marker saying so, while the calls themselves are left alone, which
keeps the sequence and therefore the agent's place in a traversal. Only if
that is not enough is a summary taken, and the cut is chosen so no
tool_resultis ever orphaned from itstool_use. - Summaries are validated before installing — one truncated by its own token limit is refused deterministically, and a second tool-less call reads the summary beside the transcript it replaces and names what is missing, triggering exactly one regeneration. An unusable verdict installs with a warning, because a run that needs to compact to survive must still compact.
- Overflow recovery — a prompt the provider refuses as too large is recognised across backends, compacted and retried once instead of ending the run.
- A loop guard armed by compaction — an identical call with an identical
result, repeated within a short window after a compaction, stops the run with
a distinct
loopstop cause rather than burning the turn budget re-living what the summary dropped. Polling never trips it.
Sessions and replay
- Append-only JSONL transcripts in
~/.mecha/sessions, recording messages, taint checkpoints, usage summaries and a configuration record per attach — provider, model, system prompt, tool list, effort, budgets, permission mode, sandbox, sampler, timezone — so a replay knows what shaped the run rather than diffing two variables at once. mecha replay <session>re-drives a recorded session against the current build using recorded tool results and a sequential loop, rebuilding the run from the recorded configuration rather than today's flags;-p/-mreplay against a different model. A structural divergence refuses the call and stops the replay, while an argument-only difference replays the recorded result and is reported for the caller to judge.scripts/replay-regression.shreplays pinned sessions and fails on any drift, turning recorded real work into standing regression cases; pins are machine-local, because transcripts are personal data.
The eval rig
mecha eval [cases.jsonl]scores a model on a case set, graded on the tool-call trace first and the text second, with a scorecard broken down by tag and--compareto put models side by side. It exits non-zero on failure, so it doubles as a regression gate on the harness.- Deterministic checks — which tools were called, in what order, with what arguments, and what the answer said. Two apply to every case whether it asks or not, because they disqualify a model regardless of the answer: malformed tool arguments and invented tool names.
expect.verifyruns a command in the case's workspace afterwards and grades the exit code, hashing the test file first so a model that edits the tests until they pass still fails.expect.judgegrades a rubric with a second model, for cases whose right answer is a judgement.- Run-metadata checks —
stop_cause,taint,blocked_sendsandmin_compactions, which are the only way to grade the harness rather than the model, since none of it appears in the answer text. - Per-case controls —
sandboxfor a private copy of the fixture with writes allowed,max_turnsfor a case that genuinely takes twenty steps,compact_at_tokensto force compaction for one case alone, and a list-valuedpromptfor several turns on one conversation. --runs krepeats every case k times and reports pass^k beside pass@k, with independent workspaces per run and a warning when a pinned seed at concurrency 1 would make the k samples one sample counted k times.--ab-rulesruns the set rules-free and then rules-on and reports the per-case flips as their own artifact rather than as a comparable scorecard.- Reproducibility by construction — eval forces MCP, hooks, learned rules, the outbox and provider fallback off, because a scorecard shaped by one machine's local configuration is not comparable to anyone else's.
- Generated fixtures and a second case set —
scripts/build-eval-fixtures.pyrewrites the workspace, prints the gold answers the cases must assert, and checks that each code kata fails as shipped and is solvable by a reference fix;eval/pkg-cases.jsonlruns against fixture MCP servers via--mcp-fileand grades the trifecta interlock end to end, offline.
mecha-mail
- A library plus three MCP binaries — Gmail and Google Calendar v3, Outlook
mail and calendar over Graph, both OAuth flows and the token lifecycle. The
library is what a GUI would depend on directly;
mecha-googleandmecha-outlookeach serve one provider with its own credential store. mecha-mail, the account-based surface deployments should wire: every account in~/.mecha/mail/behind one provider-neutral set of tools, so neither mecha nor the model knows Google or Microsoft exists. Account names are baked into every tool schema as an enum at startup, so the model picks from real names. Reads fan out across every mailbox and tag each row with its account; item operations name the account their id came from; creates use the configured default or ask the user. A failed account is reported beside the other accounts' results rather than sinking the whole call.mecha-mail auth <name> --provider ..., withimportto copy a legacy per-provider login in anddefault <name>to name the sending account. Microsoft signs in with device code, so it needs no redirect URI and no forwarded port, and works over SSH.- Unified replies —
mail_replytakes a thread id and answers the newest message, synthesizing Gmail's addressing (answer the sender, keep everyone on reply-all, never the user's own address) where Graph does it natively. - The token lifecycle in Rust — credentials at mode 0600, refresh ahead of expiry behind a lock so two concurrent tool calls cannot race, one forced refresh and retry on a 401, retry with backoff on 429 and 5xx, and an HTML-to-text fallback so an HTML-only email no longer reaches the model as an empty body.
- Capability labelling that matches the risk — reads are untrusted sources
but not send sinks, because a search query travels only to the party that
already custodies the mailbox; sends and calendar writes reach third parties,
are marked open-world, and are named in
[outbox] toolsso they stage rather than deliver. A draftedto,cc,bccorsubjectcontaining CR, LF or NUL is refused rather than stripped, so it cannot smuggle a hidden recipient into a raw message. - Timezone rendering —
[agent] timezoneis an IANA name that rides in the system prompt and reaches the mail servers asMECHA_TZ, so event times are rendered in the user's zone before the model ever sees them.
Testing, benchmarking and docs
- A three-layer test suite — unit tests for anything that is a function of
our own code (including a
ScriptedProviderthat exercises tool dispatch, denials, exhaustion and recovery with no network), integration tests for what is deterministic but needs real execution (docker actually confining a command, an MCP server actually receiving an environment), and eval cases for what only emerges with a model in the loop.MECHA_TEST_REQUIRE_BACKENDS=1turns every environment-based skip into a failure, because in CI a silently skipped test reads exactly like a passing one. - A Harbor adapter (
bench/) that installs themechabinary inside a Terminal-Bench task container and runs it there, so mecha's own loop, tools, path jail and budgets are what get measured, under the same no-MCP/no-hooks/no-outbox posture eval forces. - Research and design notes under
docs/covering context management, memory and rule lifecycle, verification, sandboxing, prior art, public benchmarks, the TUI survey, and a branching design recorded as a deliberate non-implementation.