Skip to content

feat(runtime): standalone runtime binary with stdio transport - #769

Merged
juliopolycarpo merged 14 commits into
mainfrom
feat/stdio-transport-runtime-binary
Jul 31, 2026
Merged

juliopolycarpo merged 14 commits into
mainfrom
feat/stdio-transport-runtime-binary

Conversation

@juliopolycarpo

@juliopolycarpo juliopolycarpo commented Jul 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Chat tools, workspaces, and Git can run in a separate mangostudio-runtime
process connected over NDJSON on stdin/stdout — the first execution target
the hub does not share memory with. Stdio environments spawn that binary
(sibling of the hub executable by default), handshake on a shared release
version, and reconnect on demand with exponential backoff instead of a
background timer. The Environments overview gains an Add Environment flow
for the stdio kind. Every distribution channel (platform archives, npm
platform packages, Docker images) now ships both binaries from one build
stamp; mango doctor warns when the sibling is missing or version-mismatched.

Changes

  • Add apps/runtime/src/cli.ts (--stdio, --version, --help) and
    transports/stdio.ts; route all console writers to stderr so stdout stays
    protocol-only; classify broken input pipes as hangup, not protocol errors.
  • Add hub spawn-runtime-child, stdio launcher (sanitized env, argv from
    discrete args, bounded stderr tail), and connection-manager reconnection
    (latch after five failures or protocol/release mismatch;
    requireMatchingRelease on stdio connects).
  • Fix crash-loop respawn, re-enable clearing latched failures, and shutdown
    waiting for child termination; shared teardown for disconnect/closeAll.
  • Refactor spawn path to accept a resolved argv (future WSL/SSH wrappers).
  • Add AddEnvironmentDialog with i18n (EN + pt-BR); omit blank optional
    fields; fix derived-id trimming and name-required validation.
  • Stop glob from falling back to hub process.cwd(); remove broken hub-side
    isGitAvailable retry; improve cwd vs binary launch error messages.
  • Ship runtime in scripts/build.ts, install script, npm pack, Docker stages,
    and distribution manifest (platformArchiveLayout keeps archive members in
    sync); smoke tests assert --version and stdio handshake.
  • mango doctor runtime probe handles unrunnable sibling paths without
    aborting; codec uses a cheap pending-buffer bound for large split frames.
  • Docs: hub-runtime architecture, CLI, releasing, agent playbooks.

Test Plan

  • bun run check passes (CI: Check / tooling + typecheck)
  • bun run test passes (CI: Unit & Integration Tests)
  • bun run build passes (CI: Build frontend + API)
  • New UI strings use @mangostudio/shared/i18n (no hardcoded strings)
  • Manually smoke-tested: login, chat creation, image generation

Screenshots / GIFs

N/A (no visual redesign beyond the add-environment dialog).

Notes

  • Behavior change: glob without cwd and no chat workdir now errors
    instead of searching the hub launch directory.
  • Release cost (linux-x64): ~+95 MB uncompressed per platform dir, +5s
    gzip per archive (
    +40s across the 8-target matrix); compile wall time
    unchanged when hub and runtime build concurrently.
  • Known gaps: Windows kill-tree (documented, no win32 CI); cross
    pathStyle targets still refuse connect by design.
  • No new environment variables for operators; child env strips connector keys
    and BETTER_AUTH_SECRET (covered in spawn integration tests).

Adds an NDJSON frame port and a `mangostudio-runtime` CLI entry so the
runtime can execute out of process. One JSON record per line in both
directions, with the shared codec enforcing the line cap and buffering
records torn across chunk boundaries.

A record the codec refuses tears the connection down rather than being
skipped: once the stream is desynchronised every later frame is a guess,
and a guess about a filesystem mutation is worse than a failure.

stdout is the protocol stream, so the stdio mode routes the stdout
console methods to stderr — one stray log line would otherwise inject a
record the peer has to reject.

Both spellings are accepted for every mode (`--stdio` and `stdio`),
leaving room for the dial-out and listen subcommands the remote
transports add without changing the flag the hub passes today.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
Registers the stdio transport so an environment can point at a runtime
process instead of the embedded one. The launcher builds argv from
discrete arguments, hands the child a sanitized environment with no
connector keys or auth secret, and keeps a bounded tail of its stderr so
a failed start explains itself.

Reconnection is a deadline rather than a timer. A runtime that dies now
reads as disconnected instead of broken — the target is usually still
there — and the next caller that needs it pays for the retry, backing
off exponentially and latching after five consecutive failures. Nothing
is scheduled, so a disabled, deleted, or simply unused environment never
respawns on its own and shutdown has no callback to forget. A protocol
mismatch latches immediately: a stale binary cannot fix itself by being
asked again.

Shutdown closes every live connection, so runtime children are reaped
with the hub rather than orphaned, and `doctor` reports whether the
sibling binary is present and built from the same release.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
Two places treated the hub process's working directory as a filesystem
fact about the environment. That happened to hold while every runtime was
embedded; a spawned one need not share the hub's filesystem at all, and a
directory that does not exist there fails the call for the wrong reason.

`glob` now refuses a search it cannot anchor instead of falling back to
`process.cwd()`, matching what every other filesystem tool already does
with a relative path and no bound workdir.

`isGitAvailable` drops its `git --version` retry. The retry ran only when
the handshake had already failed, and it travelled through that same
connection — so it could not succeed where the manifest read had not,
and it needed a hub-side cwd to make the attempt at all.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
The hub resolves `mangostudio-runtime` as a sibling of its own
executable, so a release that ships one without the other silently
cannot start stdio environments. Every channel now carries both:
platform archives, npm platform packages, and the Docker images.

Both binaries take the same build stamp, which matters because the
protocol handshake refuses a version mismatch — a drifted pair fails at
connect rather than misbehaving. The runtime reads its version through a
literal `process.env` access, since that is the only form the compile
step substitutes; reading it through an injected record silently shipped
`dev` in the binary, which the smoke test caught.

Cost on linux-x64: the runtime compiles in ~0.6s alongside the hub's
~0.7s, so wall-clock compile time is unchanged, but the artifact adds
95 MB uncompressed and ~5s of gzip per platform archive — that lands on
the distribution+smoke critical path, roughly +40s across the matrix.

The smoke test now runs the binary the way the hub does: `--version`
must match the release, and `--stdio` must answer with a handshake frame
carrying a capability manifest.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
Ships the Add Environment dialog the entity work deferred until a
transport existed to create. It offers the one kind that can actually
connect today — the transports later plans add stay out until they can.

An identifier is derived from the name so the common path is one field,
and stops tracking it the moment someone types their own. Both advanced
overrides are omitted from the request when blank rather than sent
empty: an empty binary path is a path the launcher would try to spawn.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
Records the two facts that are easy to get wrong from the code alone:
the runtime child's stdout is protocol-only, and reconnection is a
backoff deadline checked on next use rather than a scheduled retry.

Also states that the two binaries are one unit — every distribution
channel ships both, because the hub resolves the runtime as a sibling of
its own executable and the handshake refuses a version mismatch.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
`spawnRuntimeChild` (was `launchStdioRuntime`) now receives an argv the
caller resolved rather than a stdio-shaped config, so a launcher that
reaches its target through a wrapper — a WSL distro, an SSH host —
supplies its own command and reuses the spawn, handshake, stderr
capture, and teardown unchanged instead of copying them.

Alongside it, three smaller cleanups:

- `environmentConfigFor` calls `assertEnvironmentConfig` instead of
  repeating its check and error message.
- `disconnect` and `closeAll` share one teardown, so the only difference
  between them is the publish they do or skip.
- `#markUnavailable` reads the latched deadline instead of re-deriving
  the attempt cap, which would drift the moment something else latches.

The frame decoder also stops measuring its pending buffer exactly on
every chunk. UTF-8 is never shorter than a string's UTF-16 length, so a
cheap bound catches an over-limit record while it accumulates; the exact
check still runs when the record completes. Before this, one large frame
split across many reads re-encoded everything received so far on each
one.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
@github-actions github-actions Bot added type: test Test coverage, test infrastructure, or test reliability work. area: frontend React, Vite, routes, UI components, hooks, or client behavior. area: api Elysia routes, API modules, middleware, or backend services. area: shared Shared contracts, schemas, types, or cross-workspace utilities. area: providers Gemini, OpenAI-compatible, Anthropic, DeepSeek, or provider adapters. area: tools Tool registry, built-in tools, tool settings, or tool-call rendering. area: i18n Translation keys, pt-BR/en dictionaries, or localization behavior. area: build Binary build, runtime paths, packaging, or standalone output. area: docs Documentation tree, guides, reference docs, or contributor docs. area: cli CLI entry points, command handlers, launcher, or detach/runtime processes. area: git Git repository detection, status parsing, initialization, or workspace repository UI. area: runtime Runtime host, execution services, or hub/runtime protocol. labels Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

PR head: 376a2fb • base: 266c9a0 • CI run

Commits — 14 commits

Base 266c9a0 → head 376a2fb, oldest first.

  • 1897a5e feat(runtime): serve the protocol over stdin and stdout
  • 3c81ff9 feat(environments): connect stdio environments to a spawned runtime
  • ef4d20c fix(tools): stop resolving target paths from the hub's own directory
  • 897a23b build: ship the runtime binary in every distribution channel
  • e5ba32d feat(environments): add environments from the overview
  • 335ba07 docs: describe the stdio transport and the runtime binary
  • ff359d9 refactor(runtime): take a resolved command into the runtime spawn path
  • dc72a70 fix(release): name the runtime binary in the distribution manifest
  • 3f8fc1a fix(runtime): keep every console writer off the protocol stream
  • 9589606 fix(runtime): report a broken input pipe as a hangup
  • 5fbc8f6 fix(cli): report an unrunnable runtime binary instead of aborting doctor
  • e9fe5e9 fix(environments): keep a derived id from blocking the add dialog
  • 71545a9 fix(environments): refuse a stale runtime and name the real launch failure
  • 376a2fb fix(environments): stop respawning a runtime that crashes on every start
Full commit messages

1897a5e feat(runtime): serve the protocol over stdin and stdout

feat(runtime): serve the protocol over stdin and stdout

Adds an NDJSON frame port and a `mangostudio-runtime` CLI entry so the
runtime can execute out of process. One JSON record per line in both
directions, with the shared codec enforcing the line cap and buffering
records torn across chunk boundaries.

A record the codec refuses tears the connection down rather than being
skipped: once the stream is desynchronised every later frame is a guess,
and a guess about a filesystem mutation is worse than a failure.

stdout is the protocol stream, so the stdio mode routes the stdout
console methods to stderr — one stray log line would otherwise inject a
record the peer has to reject.

Both spellings are accepted for every mode (`--stdio` and `stdio`),
leaving room for the dial-out and listen subcommands the remote
transports add without changing the flag the hub passes today.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

3c81ff9 feat(environments): connect stdio environments to a spawned runtime

feat(environments): connect stdio environments to a spawned runtime

Registers the stdio transport so an environment can point at a runtime
process instead of the embedded one. The launcher builds argv from
discrete arguments, hands the child a sanitized environment with no
connector keys or auth secret, and keeps a bounded tail of its stderr so
a failed start explains itself.

Reconnection is a deadline rather than a timer. A runtime that dies now
reads as disconnected instead of broken — the target is usually still
there — and the next caller that needs it pays for the retry, backing
off exponentially and latching after five consecutive failures. Nothing
is scheduled, so a disabled, deleted, or simply unused environment never
respawns on its own and shutdown has no callback to forget. A protocol
mismatch latches immediately: a stale binary cannot fix itself by being
asked again.

Shutdown closes every live connection, so runtime children are reaped
with the hub rather than orphaned, and `doctor` reports whether the
sibling binary is present and built from the same release.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

ef4d20c fix(tools): stop resolving target paths from the hub's own directory

fix(tools): stop resolving target paths from the hub's own directory

Two places treated the hub process's working directory as a filesystem
fact about the environment. That happened to hold while every runtime was
embedded; a spawned one need not share the hub's filesystem at all, and a
directory that does not exist there fails the call for the wrong reason.

`glob` now refuses a search it cannot anchor instead of falling back to
`process.cwd()`, matching what every other filesystem tool already does
with a relative path and no bound workdir.

`isGitAvailable` drops its `git --version` retry. The retry ran only when
the handshake had already failed, and it travelled through that same
connection — so it could not succeed where the manifest read had not,
and it needed a hub-side cwd to make the attempt at all.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

897a23b build: ship the runtime binary in every distribution channel

build: ship the runtime binary in every distribution channel

The hub resolves `mangostudio-runtime` as a sibling of its own
executable, so a release that ships one without the other silently
cannot start stdio environments. Every channel now carries both:
platform archives, npm platform packages, and the Docker images.

Both binaries take the same build stamp, which matters because the
protocol handshake refuses a version mismatch — a drifted pair fails at
connect rather than misbehaving. The runtime reads its version through a
literal `process.env` access, since that is the only form the compile
step substitutes; reading it through an injected record silently shipped
`dev` in the binary, which the smoke test caught.

Cost on linux-x64: the runtime compiles in ~0.6s alongside the hub's
~0.7s, so wall-clock compile time is unchanged, but the artifact adds
95 MB uncompressed and ~5s of gzip per platform archive — that lands on
the distribution+smoke critical path, roughly +40s across the matrix.

The smoke test now runs the binary the way the hub does: `--version`
must match the release, and `--stdio` must answer with a handshake frame
carrying a capability manifest.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

e5ba32d feat(environments): add environments from the overview

feat(environments): add environments from the overview

Ships the Add Environment dialog the entity work deferred until a
transport existed to create. It offers the one kind that can actually
connect today — the transports later plans add stay out until they can.

An identifier is derived from the name so the common path is one field,
and stops tracking it the moment someone types their own. Both advanced
overrides are omitted from the request when blank rather than sent
empty: an empty binary path is a path the launcher would try to spawn.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

335ba07 docs: describe the stdio transport and the runtime binary

docs: describe the stdio transport and the runtime binary

Records the two facts that are easy to get wrong from the code alone:
the runtime child's stdout is protocol-only, and reconnection is a
backoff deadline checked on next use rather than a scheduled retry.

Also states that the two binaries are one unit — every distribution
channel ships both, because the hub resolves the runtime as a sibling of
its own executable and the handshake refuses a version mismatch.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

ff359d9 refactor(runtime): take a resolved command into the runtime spawn path

refactor(runtime): take a resolved command into the runtime spawn path

`spawnRuntimeChild` (was `launchStdioRuntime`) now receives an argv the
caller resolved rather than a stdio-shaped config, so a launcher that
reaches its target through a wrapper — a WSL distro, an SSH host —
supplies its own command and reuses the spawn, handshake, stderr
capture, and teardown unchanged instead of copying them.

Alongside it, three smaller cleanups:

- `environmentConfigFor` calls `assertEnvironmentConfig` instead of
  repeating its check and error message.
- `disconnect` and `closeAll` share one teardown, so the only difference
  between them is the publish they do or skip.
- `#markUnavailable` reads the latched deadline instead of re-deriving
  the attempt cap, which would drift the moment something else latches.

The frame decoder also stops measuring its pending buffer exactly on
every chunk. UTF-8 is never shorter than a string's UTF-16 length, so a
cheap bound catches an over-limit record while it accumulates; the exact
check still runs when the record completes. Before this, one large frame
split across many reads re-encoded everything received so far on each
one.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

dc72a70 fix(release): name the runtime binary in the distribution manifest

fix(release): name the runtime binary in the distribution manifest

The runtime was added to the release archives without being added to the
manifest's `archiveMembers`, which `extract-target.ts` asserts the extracted
layout equals exactly. Every target archive therefore carried an entry the
consumer refused, failing all ten binary smoke jobs before they materialized
a single binary.

Both lists now come from one `platformArchiveLayout` helper: the archive
scripts take the members relative to the build dir, the manifest takes those
plus the README that is archived from outside it. A future member cannot
reach one list without the other.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

3f8fc1a fix(runtime): keep every console writer off the protocol stream

fix(runtime): keep every console writer off the protocol stream

stdout carries NDJSON frames, so anything else written there desynchronises
the hub's decoder and takes the connection down. Only `log`, `info` and
`debug` were redirected, leaving `dir`, `table`, `group`, `count`, `timeEnd`
and Bun's own `write` still aimed at it.

Rather than enumerate them onto `error` — which would flatten the stateful
ones — the global console now takes its methods from a Console whose stream
pair is stderr on both sides, so each keeps its own behaviour. `write` is
replaced separately: it is Bun's, not part of the node Console surface.

The handshake test inherits stderr instead of piping it, since nothing read
that pipe and a runtime logging past its buffer would block on the write.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

9589606 fix(runtime): report a broken input pipe as a hangup

fix(runtime): report a broken input pipe as a hangup

Every `error` event on the input stream was classified `protocol-error`, but
EPIPE, ECONNRESET and EIO on the read side mean the peer went away — the same
thing the write side already reads as EOF. The classification is observable:
it decides the runtime's exit code and the reason the hub logs for the
disconnect, so a peer that simply died was reported as a protocol violation.

`protocol-error` now covers only records the codec refuses, which is where a
desynchronised stream actually shows up.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

5fbc8f6 fix(cli): report an unrunnable runtime binary instead of aborting doctor

fix(cli): report an unrunnable runtime binary instead of aborting doctor

`Bun.spawn` resolves the executable before it returns, so a sibling path that
exists but cannot be run — a directory, a file that lost its executable bit,
a foreign binary format — throws synchronously. That call sat outside the
guarded block, so `mango doctor` died on exactly the broken install it exists
to describe, rather than returning the probe's own `error` finding.

The probe now takes its path as a defaulted parameter so the cases can be
tested against real files instead of a resolver that returns null in a
source checkout.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

e9fe5e9 fix(environments): keep a derived id from blocking the add dialog

fix(environments): keep a derived id from blocking the add dialog

Two ways the form could refuse to submit without saying why. `suggestId`
trimmed dashes before slicing to the length limit, so a cut landing after an
internal dash left a trailing one — which the id pattern rejects, on a field
the user never touched. And `nameRequired` was defined in both locales but
never rendered, so a name of only whitespace just left submit greyed out.

The id is trimmed again after the slice, and the name field reports the
message the same way the id field already reported `idInvalid`.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

71545a9 fix(environments): refuse a stale runtime and name the real launch failure

fix(environments): refuse a stale runtime and name the real launch failure

Three ways the stdio launcher misread what went wrong.

The handshake compared only the protocol version, which moves when the wire
format does — so a runtime left behind by an older install handshook cleanly
and was trusted for method semantics it no longer shared. The client gains
`requireMatchingRelease`, set by the stdio launcher because the hub and the
runtime ship as one distribution; other transports may not want that rule.

A missing or unreadable `cwd` fails the spawn with the same ENOENT and EACCES
a bad executable does, so a mistyped working directory told the user to
reinstall MangoStudio. It is now blamed first when it is the thing missing.

`terminate` resolves once the child is actually gone, so a caller can wait
for it rather than exiting mid-kill; bounded, so an unkillable child delays
a shutdown instead of holding it open.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

376a2fb fix(environments): stop respawning a runtime that crashes on every start

fix(environments): stop respawning a runtime that crashes on every start

A successful connect cleared the failure count, but a handshake only shows
the runtime started. One that died straight after each start recorded failure
1 every time, so it never reached the attempt cap and respawned for as long
as callers kept asking. The count is now cleared by `#markUnavailable` when
the connection actually lasted, not by the handshake that opened it.

Two related holes: re-enabling an environment did not clear the failures its
calls collected while it was disabled, which could already have latched it
until someone pressed Connect; and shutdown closed connections without
waiting, so the hub could exit before the SIGKILL escalation ran and leave a
runtime that ignored SIGTERM orphaned.

The backoff comment claimed a 16s window that the attempt cap makes
unreachable — the ladder is 1s, 2s, 4s, 8s, then latch.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>

📝 Changelog Preview

Entries this branch would add to the changelog on release:

[Unreleased]

🚀 Features

  • (environments) Add environments from the overview
  • (environments) Connect stdio environments to a spawned runtime
  • (runtime) Serve the protocol over stdin and stdout

🐛 Bug Fixes

  • (environments) Stop respawning a runtime that crashes on every start
  • (environments) Refuse a stale runtime and name the real launch failure
  • (environments) Keep a derived id from blocking the add dialog
  • (cli) Report an unrunnable runtime binary instead of aborting doctor
  • (runtime) Report a broken input pipe as a hangup
  • (runtime) Keep every console writer off the protocol stream
  • (release) Name the runtime binary in the distribution manifest
  • (tools) Stop resolving target paths from the hub's own directory

🏗️ Build

  • Ship the runtime binary in every distribution channel

♻️ Refactor

  • (runtime) Take a resolved command into the runtime spawn path

📚 Documentation

  • Describe the stdio transport and the runtime binary
CI duration (wall clock, critical path, slowest jobs)

CI Duration

Report-only timing from the Actions jobs API; runner variance does not affect the gate.
Comparisons cover the jobs both runs share, so main-only stages never skew the baseline.

Metric Base Head Δ
Wall clock (shared jobs) 6m 40s 6m 27s 🟢 ▼ -13s
Critical path Test / Unit & Integration Tests · 5m 12s Test / Unit & Integration Tests · 5m 7s 🟢 ▼ -5s

Since previous PR run: n/a — no previous successful CI run found for this pull request

Five slowest head jobs

Job Base Head Δ
Test / Unit & Integration Tests 5m 12s 5m 7s 🟢 ▼ -5s
Distribution / Build immutable distribution 1m 57s 2m 39s 🔴 ▲ +42s
QA Metrics / Collect 1m 11s 1m 4s 🟢 ▼ -7s
Smoke — Browser / Chromium smoke suite 52s 1m 1s 🔴 ▲ +9s
Check / Check (tooling + typecheck) 1m 4s 59s 🟢 ▼ -5s

QA Gate — Coverage & Quality

Base: 266c9a0 • Head: 17ca425 • generated 2026-07-31T11:23:33.449Z

✅ No attention signals — collected metrics look healthy against base.

LoC (code): 🔴 ▲ +1735 • Line coverage (all workspaces): 🟢 ▲ +0.04pp • Quick check: pass • Duplication: 🔴 ▲ +0pp • Bundle gzip: 🔴 ▲ +1.2 KiB • Locked deps: ⚪ ▲ = 0 • Tests passed: 🟢 ▲ +50

Metric details (coverage, LoC, bundle, dependencies, tests, duplication, tooling)

Coverage

API/shared/runtime branches and statements are source-derived from LCOV line hits because Bun LCOV does not emit branch or statement records.

Workspace Metric Base Head Δ
frontend lines 87.90% (10,465/11,905) 87.99% (10,555/11,996) 🟢 ▲ +0.09pp
frontend statements 77.43% (6,248/8,069) 77.53% (6,295/8,119) 🟢 ▲ +0.10pp
frontend functions 74.05% (1,938/2,617) 74.08% (1,949/2,631) 🟢 ▲ +0.03pp
frontend branches 68.40% (4,207/6,151) 68.51% (4,234/6,180) 🟢 ▲ +0.11pp
api lines 91.92% (52,805/57,445) 92.00% (53,224/57,853) 🟢 ▲ +0.08pp
api statements 90.77% (20,975/23,108) 90.92% (21,228/23,349) 🟢 ▲ +0.15pp
api functions 91.58% (5,418/5,916) 91.67% (5,470/5,967) 🟢 ▲ +0.09pp
api branches 57.57% (7,889/13,704) 57.65% (7,978/13,838) 🟢 ▲ +0.08pp
shared lines 99.66% (9,402/9,434) 99.66% (9,451/9,483) ⚪ ▲ = 0
shared statements 98.20% (1,310/1,334) 98.21% (1,314/1,338) 🟢 ▲ +0.01pp
shared functions 97.67% (210/215) 97.69% (211/216) 🟢 ▲ +0.02pp
shared branches 66.86% (341/510) 66.80% (342/512) 🔴 ▼ -0.06pp
runtime lines 63.02% (2,023/3,210) 63.72% (2,150/3,374) 🟢 ▲ +0.70pp
runtime statements 64.99% (1,151/1,771) 66.10% (1,250/1,891) 🟢 ▲ +1.11pp
runtime functions 74.47% (245/329) 75.50% (265/351) 🟢 ▲ +1.03pp
runtime branches 36.95% (310/839) 38.19% (338/885) 🟢 ▲ +1.24pp

Lines of Code

Workspace Base Head Δ
frontend 499 files / 53,787 lines 501 files / 54,091 lines files 🔴 ▲ +2 • code 🔴 ▲ +304
api 859 files / 116,185 lines 864 files / 117,046 lines files 🔴 ▲ +5 • code 🔴 ▲ +861
shared 137 files / 15,954 lines 137 files / 16,009 lines files ⚪ ▲ = 0 • code 🔴 ▲ +55
runtime 44 files / 5,579 lines 48 files / 6,094 lines files 🔴 ▲ +4 • code 🔴 ▲ +515
total 1,539 files / 191,505 lines 1,550 files / 193,240 lines files 🔴 ▲ +11 • code 🔴 ▲ +1735

Frontend Bundle

Metric Base Head Δ
gzip total 749.4 KiB 750.6 KiB 🔴 ▲ +1.2 KiB
gzip JavaScript 733.8 KiB 735.1 KiB 🔴 ▲ +1.2 KiB
gzip CSS 14.6 KiB 14.6 KiB ⚪ ▲ = 0
gzip HTML 946 B 943 B 🟢 ▼ -3 B
tracked files 35 35 ⚪ ▲ = 0

Dependencies

Metric Base Head Δ
locked packages 858 858 ⚪ ▲ = 0
direct dependencies 40 40 ⚪ ▲ = 0
direct devDependencies 41 41 ⚪ ▲ = 0
workspace manifests 5 5 ⚪ ▲ = 0

Tests

Single full-suite pass (unit + integration, from the coverage run).

Base Head Δ passed
4,041 passed (root 4,041 / frontend 0 / api 0 / shared 0 / runtime 0) · exit 0 · 290s 4,091 passed (root 4,091 / frontend 0 / api 0 / shared 0 / runtime 0) · exit 0 · 275s 🟢 ▲ +50

Code Duplication (jscpd)

Metric Base Head Δ
clones 826 836 🔴 ▲ +10
duplicated lines 9,427 9,530 🔴 ▲ +103
percentage 4.13% 4.13% 🔴 ▲ +0pp

Repo Tooling

Metric Base Head Δ
Full repo check pass pass ⚪ ▲ = 0
TS errors (total) 0 0 ⚪ ▲ = 0
Circular dependencies 0 0 ⚪ ▲ = 0

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff359d91a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/lib/release-assets.ts Resolved
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added support for local runtime processes, including stdio communication, automatic reconnection, and graceful shutdown.
    • Added an interface for creating execution environments with runtime path and working-directory settings.
    • Platform releases and container images now include the companion runtime executable.
    • Added runtime version and availability diagnostics to the doctor command.
  • Bug Fixes

    • User-initiated environment connections now retry immediately.
    • Restricted glob searches no longer fall back to an unsafe working directory.
    • Improved runtime error handling and bounded diagnostic output.
  • Documentation

    • Documented runtime installation, packaging, compatibility checks, and stdio operation.

Walkthrough

This PR adds the mangostudio-runtime companion binary and NDJSON stdio transport. The API can launch, reconnect, and shut down runtime child processes. The doctor command validates runtime presence and version compatibility. The frontend adds local stdio environment creation. Build, release, npm, Docker, and installer workflows now distribute both binaries. Tests cover protocol handling, lifecycle behavior, UI validation, packaging, and smoke execution.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant EnvironmentDialog
  participant API
  participant RuntimeConnectionManager
  participant mangostudio-runtime
  User->>EnvironmentDialog: configure and submit environment
  EnvironmentDialog->>API: create stdio environment
  API->>RuntimeConnectionManager: connect to environment
  RuntimeConnectionManager->>mangostudio-runtime: spawn with --stdio
  mangostudio-runtime-->>RuntimeConnectionManager: return protocol handshake
  RuntimeConnectionManager-->>API: expose runtime connection
Loading

Possibly related PRs


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (2)
apps/frontend/src/features/environments/components/EnvironmentEntitiesOverview.tsx (1)

27-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore focus to the trigger button when the dialog closes.

No ref tracks the "Add environment" button, so keyboard focus is not returned to it when AddEnvironmentDialog closes. Keyboard and screen-reader users lose their place in the page after closing the dialog.

Store a ref to the trigger button and focus it in onClose.

♻️ Proposed refactor
+  const triggerRef = useRef<HTMLButtonElement>(null);
   const [adding, setAdding] = useState(false);
+
+  const closeDialog = () => {
+    setAdding(false);
+    triggerRef.current?.focus();
+  };

   return (
     ...
         <button
           type="button"
+          ref={triggerRef}
           onClick={() => setAdding(true)}
           className="..."
         >
           <Plus size={13} />
           {t.environments.entities.add.trigger}
         </button>
       </div>

-      {adding ? <AddEnvironmentDialog onClose={() => setAdding(false)} /> : null}
+      {adding ? <AddEnvironmentDialog onClose={closeDialog} /> : null}
apps/runtime/src/transports/stdio.ts (1)

107-114: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Contain listener exceptions inside #deliver.

A synchronous onFrame listener can throw from the input stream data handler. Catch each listener exception and tear down the port with protocol-error so the runtime reports closure instead of raising an uncaught exception.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c466dae6-6af3-4ea7-9b5b-ccd3cde47f50

📥 Commits

Reviewing files that changed from the base of the PR and between 266c9a0 and ff359d9.

📒 Files selected for processing (56)
  • Dockerfile
  • Dockerfile.alpine
  • apps/api/src/cli/commands/doctor.ts
  • apps/api/src/cli/doctor-checks.ts
  • apps/api/src/cli/runtime-binary-probe.ts
  • apps/api/src/lib/bounded-tail.ts
  • apps/api/src/lib/runtime-paths.ts
  • apps/api/src/modules/environments/application/environment-service.ts
  • apps/api/src/modules/environments/domain/environment-config.ts
  • apps/api/src/modules/git/infrastructure/git-cli.ts
  • apps/api/src/server/start-server.ts
  • apps/api/src/services/providers/core/node-sidecar/spawn-sidecar.ts
  • apps/api/src/services/providers/cursor/sidecar-process.ts
  • apps/api/src/services/runtime-client/runtime-connection-manager.ts
  • apps/api/src/services/runtime-client/spawn-runtime-child.ts
  • apps/api/src/services/tools/builtin/glob.ts
  • apps/api/tests/integration/services/spawn-runtime-child.integration.test.ts
  • apps/api/tests/unit/cli/commands/doctor.test.ts
  • apps/api/tests/unit/cli/doctor-checks.test.ts
  • apps/api/tests/unit/services/runtime-connection-manager.test.ts
  • apps/api/tests/unit/services/tools/glob-tool.test.ts
  • apps/frontend/src/features/environments/components/AddEnvironmentDialog.tsx
  • apps/frontend/src/features/environments/components/EnvironmentEntitiesOverview.tsx
  • apps/frontend/src/features/environments/queries.ts
  • apps/frontend/tests/unit/features/environments/add-environment-dialog.test.tsx
  • apps/runtime/AGENTS.md
  • apps/runtime/src/cli.ts
  • apps/runtime/src/config.ts
  • apps/runtime/src/index.ts
  • apps/runtime/src/transports/stdio.ts
  • apps/runtime/tests/unit/cli.test.ts
  • apps/runtime/tests/unit/transports/stdio.test.ts
  • apps/shared/src/i18n/en.ts
  • apps/shared/src/i18n/pt-BR.ts
  • apps/shared/src/runtime-protocol/codec.ts
  • apps/shared/tests/unit/runtime-protocol.test.ts
  • docs/architecture/hub-runtime.md
  • docs/reference/agent-playbooks.md
  • docs/reference/cli.md
  • docs/reference/releasing.md
  • knip.json
  • scripts/build.ts
  • scripts/install/install.sh
  • scripts/lib/docker-stage.ts
  • scripts/lib/npm-pack.ts
  • scripts/lib/npm-package-validation.ts
  • scripts/lib/release-assets.ts
  • scripts/lib/release-targets.ts
  • scripts/release/archive-assets.ts
  • scripts/release/pack-npm.ts
  • scripts/release/stage-docker-ctx.ts
  • scripts/test-build.ts
  • scripts/tests/archive-assets.unit.test.ts
  • scripts/tests/npm-pack.unit.test.ts
  • scripts/tests/npm-package-validation.unit.test.ts
  • scripts/tests/release-assets.unit.test.ts

Comment thread apps/api/src/server/start-server.ts Resolved
Comment thread apps/runtime/src/cli.ts Resolved
Comment thread apps/runtime/src/transports/stdio.ts Resolved
Comment thread apps/runtime/tests/unit/cli.test.ts Resolved
@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Reviews (1): Last reviewed commit: "refactor(runtime): take a resolved comma..." | Re-trigger Greptile

The runtime was added to the release archives without being added to the
manifest's `archiveMembers`, which `extract-target.ts` asserts the extracted
layout equals exactly. Every target archive therefore carried an entry the
consumer refused, failing all ten binary smoke jobs before they materialized
a single binary.

Both lists now come from one `platformArchiveLayout` helper: the archive
scripts take the members relative to the build dir, the manifest takes those
plus the README that is archived from outside it. A future member cannot
reach one list without the other.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
stdout carries NDJSON frames, so anything else written there desynchronises
the hub's decoder and takes the connection down. Only `log`, `info` and
`debug` were redirected, leaving `dir`, `table`, `group`, `count`, `timeEnd`
and Bun's own `write` still aimed at it.

Rather than enumerate them onto `error` — which would flatten the stateful
ones — the global console now takes its methods from a Console whose stream
pair is stderr on both sides, so each keeps its own behaviour. `write` is
replaced separately: it is Bun's, not part of the node Console surface.

The handshake test inherits stderr instead of piping it, since nothing read
that pipe and a runtime logging past its buffer would block on the write.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
Every `error` event on the input stream was classified `protocol-error`, but
EPIPE, ECONNRESET and EIO on the read side mean the peer went away — the same
thing the write side already reads as EOF. The classification is observable:
it decides the runtime's exit code and the reason the hub logs for the
disconnect, so a peer that simply died was reported as a protocol violation.

`protocol-error` now covers only records the codec refuses, which is where a
desynchronised stream actually shows up.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
`Bun.spawn` resolves the executable before it returns, so a sibling path that
exists but cannot be run — a directory, a file that lost its executable bit,
a foreign binary format — throws synchronously. That call sat outside the
guarded block, so `mango doctor` died on exactly the broken install it exists
to describe, rather than returning the probe's own `error` finding.

The probe now takes its path as a defaulted parameter so the cases can be
tested against real files instead of a resolver that returns null in a
source checkout.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
Two ways the form could refuse to submit without saying why. `suggestId`
trimmed dashes before slicing to the length limit, so a cut landing after an
internal dash left a trailing one — which the id pattern rejects, on a field
the user never touched. And `nameRequired` was defined in both locales but
never rendered, so a name of only whitespace just left submit greyed out.

The id is trimmed again after the slice, and the name field reports the
message the same way the id field already reported `idInvalid`.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
…ilure

Three ways the stdio launcher misread what went wrong.

The handshake compared only the protocol version, which moves when the wire
format does — so a runtime left behind by an older install handshook cleanly
and was trusted for method semantics it no longer shared. The client gains
`requireMatchingRelease`, set by the stdio launcher because the hub and the
runtime ship as one distribution; other transports may not want that rule.

A missing or unreadable `cwd` fails the spawn with the same ENOENT and EACCES
a bad executable does, so a mistyped working directory told the user to
reinstall MangoStudio. It is now blamed first when it is the thing missing.

`terminate` resolves once the child is actually gone, so a caller can wait
for it rather than exiting mid-kill; bounded, so an unkillable child delays
a shutdown instead of holding it open.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
A successful connect cleared the failure count, but a handshake only shows
the runtime started. One that died straight after each start recorded failure
1 every time, so it never reached the attempt cap and respawned for as long
as callers kept asking. The count is now cleared by `#markUnavailable` when
the connection actually lasted, not by the handshake that opened it.

Two related holes: re-enabling an environment did not clear the failures its
calls collected while it was disabled, which could already have latched it
until someone pressed Connect; and shutdown closed connections without
waiting, so the hub could exit before the SIGKILL escalation ran and leave a
runtime that ignored SIGTERM orphaned.

The backoff comment claimed a 16s window that the attempt cap makes
unreachable — the ladder is 1s, 2s, 4s, 8s, then latch.

Signed-off-by: Julio Polycarpo <julio@polycarpo.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: api Elysia routes, API modules, middleware, or backend services. area: build Binary build, runtime paths, packaging, or standalone output. area: cli CLI entry points, command handlers, launcher, or detach/runtime processes. area: docs Documentation tree, guides, reference docs, or contributor docs. area: frontend React, Vite, routes, UI components, hooks, or client behavior. area: git Git repository detection, status parsing, initialization, or workspace repository UI. area: i18n Translation keys, pt-BR/en dictionaries, or localization behavior. area: providers Gemini, OpenAI-compatible, Anthropic, DeepSeek, or provider adapters. area: runtime Runtime host, execution services, or hub/runtime protocol. area: shared Shared contracts, schemas, types, or cross-workspace utilities. area: tools Tool registry, built-in tools, tool settings, or tool-call rendering. type: test Test coverage, test infrastructure, or test reliability work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant