Repository navigation
1.3.1 — Steady Mercury
The release where Mercury stays out of your way. One process runs the fleet and your channels; the terminal just attaches to it — instantly. Bots do real work, deliver files you can find, stay online without burning tokens, and can be run from a phone. The 1.3.1 "Honest Mercury" line (chat that answers, "done" that means verified, security fixes) was never published on its own and ships here.
Upgrading
- Node.js 22 or newer is required for npm installs. Standalone binaries carry their own runtime — nothing to do. On npm: run
node -vfirst; on Node 20,nvm install 22 && nvm alias default 22, thennpm i -g @cosmicstack/mercury-agent.mercury upgradenow checks this before it removes anything, and the package refuses to install on an older Node with a readable message instead of installing something that will not start. - Your login service is re-pointed automatically the first time the new build launches (it used to keep starting the old install).
mercury doctorshows Login service: points at this install. - First start moves bot outputs from
~/.mercury/bots/outputs/to~/Documents/Mercury/<Bot name>/(one-way; the old folder gets a README saying where things went). - No config migrations.
~/.mercury(config, bots, memory, Telegram pairing) carries over untouched.
Known limitations
- Launching
mercurynow attaches to the daemon (ADR-024), and the attach TUI does not yet have bot threads (/bots open) or the/telegram,/discord,/slackmenus — use/bots send <id> <message>andmercury telegram ...from the shell, ormercury codefor the in-process TUI. Closing this gap is the first item of the next release. - Bots still share the daemon's thread with the main agent: bounded (turn caps, async tools) but not isolated.
/api/health.loopLagtells you if it ever matters.
One runtime (ADR-024)
- git tools no longer hold the runtime.
git_status/log/diff/add/commit/push(and/code diff) ran git synchronously — a push over a slow link froze Telegram, every bot and the attach TUI for up to 60 s. They now run async; errors and output are unchanged. - Windows service drift is detected too (Task Scheduler XML and the Run key are read back), so a stale autostart is re-pointed on every platform.
- The login service follows the install. After a reinstall (npm → binary, stable → dev, a new Node under nvm) the old LaunchAgent / systemd unit kept resurrecting the old daemon on every login, so the new build could never own its port. Mercury now checks what the service launches on every launch and re-points it (unloading the old job first);
mercury doctorshows Login service: points at this install / STALE, and the "port owned by another Mercury" message names the service when it is the cause. - The terminal no longer runs a second copy of Mercury. Launching
mercurywith no daemon used to spawn the daemon and boot a full runtime in the terminal — Telegram, the scheduler and every bot ran twice, and the second copy ran on the thread that paints your keystrokes. That was the typing lag, and the Telegram "terminated by other getUpdates" (409) conflicts. Now the terminal spawns the daemon and attaches to it; a foreground runtime beside a live daemon (mercury code, recovery) is a terminal for the main agent only — no bots, no channels, no web port. - Attach waits for a booting daemon (token not written yet, token rotated at bind) instead of failing on the first read.
- Event-loop stall monitor:
/api/healthreportsloopLag(p99 / worst stall over the last 10 s) and stalls ≥250 ms are logged with a warning, so lag is a number, not a feeling.
Instant attach
- Slash commands no longer show "Processing".
/memory,/bots,/helpand friends are answered without the model; the turn indicator and heartbeat are raised only when a command actually falls through to a model turn. (/memoryopens a menu that stays open as long as you use it — with the spinner up it looked like a hung agent.) mercuryattaches instantly to a running daemon. The daemon binds its web listener first at boot (channels and bots come after), an unauthenticated loopback/api/healthanswers with pid/version/booting, and the launcher polls every 250 ms instead of 750 ms. A port owned by another process (an old global install, another Mercury) is now reported in one line — Port 6174 is owned by another Mercury (PID …) — stop it or set web.port — instead of a 15-second wait followed by a pointless restart. The web server also retries a busy port for 5 s, so a restart right after a stop no longer loses the dashboard.
Bots in your pocket (ADR-023)
- Onboarding from Telegram.
/bots create <id> "Name" "Description"in Telegram (or the web chat) now runs the same flow as the TUI: your next message is the persona (keep as-is or convert to the template), then permissions tier, solo vs fleet, and the daily budget — each a tap on Telegram's native buttons that settle in place. Every reply lands in the chat that asked; nothing is lost to the TUI-only bot thread any more. - Telegram controls.
/botsand/botare in Telegram's command menu; the roster arrives with buttons — Stop/Start, Run and Runs per bot, plus Cost, Outputs, Doctor and Stop all for the fleet. A tap runs the slash command behind it as if you had typed it (same permissions, same reply)./bots outputscarries a 📎 button per delivered file. - Files, not paths. A task asked for from Telegram (or any remote chat) comes back with its delivered files attached.
/bots fetch <id> [name|latest]sends one on demand. /bots doctorruns the fleet health check from any chat;/bots helplists the commands. The TUI's slash completion, the busy-mode/help, and the docs now list every/botssubcommand.
Bots stay online
- Idle, never dead. A routine that keeps producing nothing is cooled down instead of parked: 6 hours, doubling to 24, then it resumes by itself (
/bots startresumes it now). Budget pauses lift at midnight and queued work is pumped then. - Declared cadence is honoured. A schedule in
bot.yamlruns as written; the 30-minute floor applies only to routines a bot creates for itself. - Doctor
stalecheck flags a bot whose routine has not fired in twice its cadence; the roster shows when a cooled-down routine resumes.
See what the bots cost and did (ADR-022)
/bots cost [days]— token spend per bot and per fleet: today against the cap, the window's runs, failures, deliveries and empty runs. AlsoGET /api/bots-cost./bots show <id> [runId]— what a run actually did: the tool trace, delivered files, the reply, and a flag when the reply claimed more than the trace shows. AlsoGET /api/bots/:id/runs[/:runId].- Kill switch —
/bots stop allhalts every bot and holds queued work;/bots start allresumes. AlsoPOST /api/bots-stop-all/-start-all. /bots dlq clear [id]— drop dead-lettered jobs (the journal keeps the runs). AlsoDELETE /api/bots-dlq.- Onboarding budget presets —
light(1M/day),standard(the default, 5M),heavy(20M), a number, ornone; the step also tells you where the bot's results will appear. GET /api/bots/:id/taskslists a lead's delegated tasks.
Fleets that coordinate (ADR-021)
Wave 2 of the bots work: how a lead hands out work and gets it back, how a bot remembers, and how a fleet's standard job runs without a lead turn between every step.
- Typed tasks with fan-in.
fleet_delegatehands several tasks to the crew in one call — each with a goal and what "done" means — and the lead is woken once with a digest (status, outcome, delivered files, summary per task) when the batch completes or its deadline passes (default 120 min; stragglers report once each).bot_send(task: true)is now a batch of one.fleet_taskslists and cancels;fleet_statusshows open tasks. Tasks are durable (tasks.json) and deduplicated, so a replayed delegation turn never double-dispatches. - Pipelines as data. A lead's standard job in
bot.yaml(pipeline.stages: name, bot, goal with{{input}}/{{previous}}, optionalfinal).fleet_pipelineruns it: stages chain themselves, each handing its deliverable to the next, with no lead turn in between; the lead is woken at the end; afinalstage's deliverable is promoted to the top of the fleet folder. - Bots remember. After a productive run, 0–3 durable facts (sources and their quality, decisions, where things are, what to avoid) are extracted into the bot's own memory (
memory.learn: falsedisables). Every turn also starts with a digest of the last five runs from the journal and the bot's own working-state note (bot_state, ≤1500 chars, replaced not appended) — the legitimate version of the status files bots used to write. - Cheaper crew.
bots.fleets.crewProviderroutes crew bots without a provider of their own to a cheaper lane; leads keep the default. /bots tasks <leadId>shows a lead's open and recent tasks from the TUI.
Bots that do work (ADR-020)
An audit of eleven days of real fleets found bots spending 617M input tokens (a median turn of 750k, the largest 6.6M), failing 38% of runs, and mostly writing notes about earlier runs. This wave gives every bot turn a governor, every routine a gate, every run an outcome verdict, and every owner a folder they can find.
- Per-turn caps. A turn stops at 300k tokens or 20 minutes (
autonomy.maxTokensPerTurn/maxTurnMinutes). A cut-off is journaled asturn_budget/turn_time— terminal, no dead-letter, no "needs you", one notice a day. - Context compaction. Tool results older than the last 8 are stubbed on every step; any single result is capped at 24KB. Prompts stop growing with the number of steps.
- Budgets on by default. Every bot has a daily cap (5M tokens unless
/bots budgetsays otherwise;none= unlimited) and the fleet has one (bots.fleetDailyTokenBudget, 20M). You are warned at 80%. - Routine gate. A cron tick is skipped — never stacked — while the bot is busy or within 30 minutes of its last run (
schedules[].minIntervalMinutes). Bot-created routines run at most every 30 minutes, at most 3 per bot; self-scheduled follow-ups are at least 10 minutes out. - Outcome verdict. The tool trace, not the reply, decides what a run did:
deliverable,action,delegated,messageornone. An unattended run that only wrote notes gets one nudge to deliver or admit it. A reply that claims delivery with no matching tool call is flagged. Three empty runs in a row pause the routine;/bots startresumes it. - Shared-folder budget. Writes to
_shared/are limited per run (warn at 2 files / 32KB, refuse at 6 / 128KB). The private workspace is unlimited. - Deliverables you can find. Results go to
~/Documents/Mercury/<Bot name>/(Windows: Documents; Termux: shared storage;bots.deliverablesDiroverrides) — finals on top, crew work underwork/, a README index, files namedYYYY-MM-DD <title>. Shown at onboarding, in the roster, and in every "delivered" message./bots outputs [id]lists them,/bots folder <id>opens the folder. The hiddenoutputs/zone is migrated once. - Honest failures. Network cuts ("terminated", socket closed, interrupted streams) and empty streams retry instead of dead-lettering; the provider's real error (an invalid key, a 429) reaches the journal instead of "No output generated".
- Journal v2. Real turn start, steps, tool calls, peak prompt size, outcome and delivered paths per run, plus a compact transcript per run (last 50 kept).
/bots journalshows them;mercury bots doctorflags bots whose unattended runs mostly produce nothing, paused routines, and write access to the whole home directory. - Bot file tools and shell commands now resolve relative paths inside the bot's workspace, never the daemon's working directory. The roster scan ignores profile copies in
sandbox/andoutputs/.
Ink 8
Mercury's terminal renderer moves from a heavily patched ink 5.2.1 to ink 8.0.0, which now does most of that work itself.
- Requires Node.js 22 or newer. Ink 7+ needs it, and Node 20 reached end of life in April 2026. On an older Node, Mercury now exits with a clear upgrade message instead of crashing at the first render.
- Smoother rendering — incremental rendering rewrites only the changed part of each line, and every frame is painted atomically (synchronized output).
- Upstream fixes Mercury lacked — CJK and emoji truncation, control characters stripped from rendered text, dropped keypresses when several keys arrive together, Backspace vs Delete, Windows stale frames, and many
<Static>fixes. - Smaller patch — four Mercury hunks remain (Static
itemKey, freeze gate, live-region guard, cursor anchor); the rest are upstream. Seedocs/ink-patch.mdand ADR-019. - React 19.3.
Honest Mercury — the earlier 1.3.1 work (never published separately)
The release where Mercury stops pretending. Chat answers instead of forcing tool rounds, "done" means a check actually passed, every channel shows the same evidence, and the terminal UI takes paste, Esc and IME like a real editor. Built from a full audit (ROADMAP.md); every change below is covered by tests, including a replay eval harness that drives the real agent loop.
Conversation and feedback
- Chat is no longer forced through tool rounds — the Mercury Code narration guard and verification gate ran in plain chat, so "make me a workout plan" could trigger up to ten tool-only rounds and end with "I couldn't get started". Both now run in execute mode only.
- Done means verified — a build/test/typecheck counts as evidence only when it exits clean (timeouts and kills are failures) and runs after the last edit; otherwise the task pauses with "the last check didn't pass" instead of "Task complete". Streamed turns now reach this gate too (they reported no finish reason and skipped it).
- One feedback contract on every channel (
TaskSurface) — begin, done-with-evidence, pause and fail behave the same on CLI, web, Telegram, Discord, Slack and Signal. Verification notes and a trace id reach messaging channels; pause and failure banners are persistent messages, never truncated status-card notices. /trace [id]— provider, tokens (incl. cached), steps, timing, verification and every tool call for a turn./newon every channel — fresh conversation, memory kept (#38).- Telegram prompts — timeouts say what they defaulted to, taps settle the card, keyboards are removed (#23).
Context, memory and speed
- Context window v2 — history is budgeted by tokens and includes a compact per-turn tool trace, so the next turn knows what was read and written. Retrieved memory goes into a per-turn system block instead of fake dialogue.
- Prompt caching — the stable system prompt carries an Anthropic cache marker and no longer changes every minute.
- Faster steps — per-step channel edits no longer block the agent loop; niche tool schemas (Spotify) are left out of unrelated turns; the cloud memory pool is skipped when local recall is strong.
- Providers — DeepSeek thinking for V4/reasoner models with reasoning passed back (#24); per-provider output ceilings; the configured default always leads (fallback is no longer sticky).
- Long builds —
run_commandreports progress so a 10-minute build is not killed as a stalled provider, and stops its child when the turn is interrupted. - Tool results — read from the AI SDK v6
outputfield, so failing-loop detection works.
Terminal UI
- Paste — multi-line and bracketed paste; Shift+Enter inserts a newline.
- Esc interrupts a turn; Ctrl+C clears input, double-tap to quit.
- Concurrent prompts queue instead of overwriting each other.
- Smoother streaming — fence-aligned tail rendering, one shared tick, async git header, correct repaint on resize; grapheme-aware cursor for emoji and CJK; history persists to
~/.mercury/history.
Platforms
- Windows — service install via Task Scheduler XML with an HKCU Run fallback (#13); graceful stop and upgrade; wizard no longer exits silently on closed stdin (#64); update check over HTTPS.
- Termux — detected on
android;mercury service installwrites a Termux:Boot script. - Bots — retried jobs can no longer run twice; a fleet lead's synthesis reaches its thread; "needs you" survives restarts.
Engineering
- Agent split — command handlers moved to
src/core/commands/; one step handler instead of four copies;agent.tsfrom 8,362 to about 5,000 lines. - Replay eval harness (
npm run eval) in CI. - CI — binary smoke tests per OS (built, run and discarded, never published), installer and script checks, Node 24, linux-arm64, ESLint ratchet (provisional baseline), vendored-Ink integrity check.
Security
- Argv-lane auto-approval (ADR-016) — auto-approved commands no longer go through a shell or
PATH: they must tokenize to plain argv, use an allowlisted program with an allowed flag set, stay inside the workspace, and run viaexecFileon a binary resolved from pinned system directories with a minimal environment (#103). Everything else is shown to the user for approval. Some read-only one-liners (pipelines,&&chains) now prompt; seeSECURITY.md. - Reads are authorised against the real file — symlink targets must be in scope, hard-linked files prompt, and
read_fileverifies the opened file is the one that was checked (#104). - Git helpers run argv, not a shell string —
git_status,git_logandgit_diffbuilt a command string from a model-chosenpathand ran it throughexecSyncwith no permission check. They now pass the path toexecFileSync('git', [...])as a single argv element after--(or-C <resolved>), and refuse any path that fails the same read-scope checkread_fileuses.git_pushrejects option-lookingremote/branchvalues (--receive-pack=…,--exec=…) and also uses argv. Regression tests:src/capabilities/git/git-helpers.test.ts. - Safe-read bypass fixes, now closed with regression tests — the read-only auto-approval lane in Ask Me mode denies:
findaction flags (-exec/-execdir/-ok/-okdir/-delete, since 1.2.2;-fprint/-fprintf/-files0-fromsince 1.3.0 — #71, #77, #101, #110); shell redirection (>,>>,2>,<,&>, since 1.2.2 — #72, #82, #109); and shell expansion ($VAR,${VAR},$(…), backticks,$'…',~, since 1.3.0 / PR #128 — #76, #80, #95). An explicitfind -execcase was added tosrc/capabilities/permissions.test.ts. - Sub-agent approval routing —
delegate_taskworkers no longer switch the shared permission manager into allow-all with a/write scope; their shell and filesystem calls go through the same Ask Me prompts as the parent (since 1.2.2 — #75, #99).allowedToolsis enforced at runtime, not just in the prompt (since 1.3.0 — #74, #97, #98). In 1.3.1 sub-agents only see and stop their own descendants and get orchestration tools only when granted (#74), and each agent runs under its own immutable permission context, so a concurrent internal-channel turn can no longer widen a worker's approvals (#75/#99 residual). - Symlink write canonicalisation —
create_file/write_file/edit_filecheck the resolved target of a symlink against the writable scopes, not only the lexical path (since 1.3.0 — #105). - SSRF guard — unchanged; see the 1.2.3 entry below (hardened in 1.3.0 by #121 and #124; response record in
docs/security/ssrf-108-response.md). - Disclosure process —
SECURITY.mdadded (private reporting, response targets, scope); issue-closure and advisory drafts indocs/security/2026-10-backlog-closure.md.
Fixed
- SQLite in the standalone binaries and on toolchain-less installs (ROADMAP P1.9, #96, ADR-018) — the binaries bundled better-sqlite3's JavaScript but never its native addon, so every SQLite probe failed inside them: Second Brain was silently disabled and bots/boards fell back to JSON. A shared driver (
src/utils/sqlite-driver.ts) now selects better-sqlite3 → bun:sqlite → node:sqlite → JSON: binaries use Bun's embedded engine, Node ≥ 22.13 installs work without a compiler vianode:sqlite, and the better-sqlite3 path is untouched. Second Brain, the bots queue, boards and the cloud pool-search cache all go through it; the pool cache now degrades to uncached instead of throwing when no engine loads. Newmercury doctor --storageprints the active backend (with why the others were rejected), every database file, its size and row counts, and warns loudly when storage is JSON-only.MERCURY_SQLITE_BACKEND=better-sqlite3|bun:sqlite|node:sqlite|jsonforces a backend. Contract tests run the same assertions against every engine the process can load (src/utils/sqlite-driver.test.ts). - Patched ink is bundled, not post-installed (ROADMAP P2.4, ADR-017) — the TUI's ink fixes no longer depend on
postinstall:vendor/ink(stock ink 5.2.1 + the patch set, regenerated byscripts/vendor-ink.cjs) is bundled intodist, so--ignore-scriptsinstalls, Termux and the standalone binaries all run the patched renderer.mercury doctorreports "Ink TUI patch: vendored". Hunk-by-hunk rationale:docs/ink-patch.md. - IME input lands on the input line (#41, #66) — the real terminal cursor is now parked on the input's cursor cell after every frame, so preedit text and candidate windows appear where you type, and terminals that suppress IME while the cursor is hidden compose again. It is hidden while a prompt or a non-input pane has focus.
MERCURY_HW_CURSOR=0opts out. - Narrow terminals (P2.8) — widths derive from the terminal's columns: below 60 columns the splash and side panels collapse, and the input header, hints and Mercury Code hint table truncate instead of wrapping into extra live-region rows.