Releases: Open330/muxa
v0.5.0 — push-based watch + stuck-Working sweep
Minor release.
muxa watchSTATE column now updates in milliseconds instead of waiting for the 500 ms polling tick, and a new opt-in reconciler sweep recovers from missedStop/TurnStoppedhook firings.
What changed
User reported intermittent stale STATE in muxa watch. Investigation surfaced two distinct root causes — both fixed in this release.
Push-based updates via streaming Subscribe
The watch was polling on a hard 500 ms tick. New IPC `Subscribe` RPC: client opens a long-lived socket, daemon streams one JSON-encoded `Transition` per state change. Watch races the stream against a much slower fallback poll (5 s).
- Hot path: state hits the screen in ~ms (was up to 500 ms)
- Idle path: CPU drops to ~0 (was constant 2 Hz polling)
- Fallback:
Laggeddrops or dead stream cleanly degrade to historical 500 ms polling - Back-compat: older daemons → silent fallback to polling, no client-visible breakage
Stuck-Working sweep
Push doesn't help when the agent never fires the event in the first place (Claude Code's Stop hook can drop on context-limit hits, mid-turn crashes, etc). New `[reconciler] stuck_working_timeout_secs` config: every reconciler tick auto-flips agents stuck in Working past the threshold to Idle and broadcasts a synthetic Transition so push subscribers see the correction live.
[reconciler]
stuck_working_timeout_secs = 300 # 5 min — reasonable for interactive use
# 0 (default) = disabled, preserves "state changes only on explicit events" guaranteePerformance
Push-based isn't a perf hit — it's a perf win on every axis except first-connection setup:
| Polling (≤ v0.4.5) | Push (v0.5.0) | |
|---|---|---|
| agent CLI hook latency | unchanged | unchanged |
| daemon CPU (idle) | constant 2 Hz request handling | 0 (broadcast subscriber blocked on recv) |
| watch CPU (idle) | 2 Hz wake + snapshot | 5 s wake (push handles change cases) |
| state→screen latency | up to 500 ms | ~5 ms |
Upgrading
git pull
cargo install --path crates/muxad --locked
cargo install --path crates/muxa-cli --lockedIf you want to enable the stuck-Working sweep, add to `~/.config/muxa/config.toml`:
[reconciler]
stuck_working_timeout_secs = 300PR: #23
v0.4.5 — launchd bootstrap race + fast path
Patch release. Fixes the
Bootstrap failed: 5: Input/output errorreported on a macOS host where v0.4.2 had already wired the launchd plist and the user re-ran the wizard.
Fixed
muxa initno longer hits EIO from launchctl bootstrap when re-run against an already-installed launchd agent (#22). Two orthogonal changes:- 1500 ms sleep between
bootoutandbootstrap. launchd needs ~1-2 s to reap the previous agent's state before it'll accept a re-bootstrap into the same slot. The fresh-install path doesn't hit this delay because the new fast path below short-circuits. - Fast-path skip when the agent is already loaded. If
launchctl print gui/<uid>/<label>succeeds, justkickstart -kto pick up any binary/config changes and return. The common idempotent re-run case is now both faster and entirely bypasses the EIO surface.
- 1500 ms sleep between
Upgrading
git pull
cargo install --path crates/muxad --locked
cargo install --path crates/muxa-cli --locked
muxa init --component muxad-launchd --yes # idempotent, safe to re-runIf you previously hit EIO and manually bootstrapped the agent (the workaround that was floating around), v0.4.5's wizard now does the right thing on its own — no manual intervention needed.
PR: #22
v0.4.4 — defer to service manager + clearer preflight label
Patch release. Fixes the auto-restart gap reported on macOS during v0.4.3 testing —
muxa initsucceeded butpkill -9 muxadleft muxad gone with no auto-restart, because our--start-daemonstep was racing launchd's spawn and producing an unsupervised orphan.
Fixed
--start-daemondefers to the service manager (#21).StartDaemonIfNeededis now suppressed whenmuxad-systemdormuxad-launchdis in the plan. Previously both fired during apply and ournohup muxad &won the socket-bind race against the manager's child —muxa statusworked, butlaunchctl printrevealedactive count = 0 / state = spawn scheduled, andKeepAlivenever kicked in. The shellrc-only path (where no real manager owns the lifecycle) keeps the action somuxa initleaves muxad responsive in the same session.- Pre-flight label —
· muxad already runningwas emitted even when muxad was not running (only the leading glyph flipped). Now state-dependent:✔ muxad respondingvs· muxad not running (will be started on apply).
Upgrading
git pull
cargo install --path crates/muxad --locked
cargo install --path crates/muxa-cli --lockedIf you ran v0.4.2 / v0.4.3 and your auto-restart never kicked in:
pkill -9 muxad # kill the orphan
muxa init --component muxad-launchd --yes # macOS — re-bootstraps launchd
# or `muxad-systemd` on LinuxVerify auto-restart:
pkill -9 muxad ; sleep 12 ; muxa status # should respond again
launchctl print "gui/$(id -u)/dev.open330.muxad" | grep -E "active count|state"
# active count = 1, state = runningPR: #21
v0.4.3 — IPC liveness + polling spawn
Patch release. Fixes the same failure mode as the v0.4.0 incident, but caught at the wizard's daemon-liveness check rather than after the fact.
Fixed
- Daemon liveness now probes the IPC socket, not
pgrep(#20). Both pre-flight detection and the--start-daemonaction's already-up short-circuit. The pgrep approach returnedtruefor zombie processes whose listen socket had died — exactly the v0.4.0 stale-pid case where the wizard claimed muxad was running butmuxa statusstill failed withdaemon not reachable. Socket-connect captures the only thing that matters: "is the daemon answering." - Replaced the 300 ms post-spawn sleep with bounded polling (3 s timeout, 20 ms interval). systemd's
enable --nowand launchd'sbootstrapreturn as soon as the spawn is initiated, not when the socket is bound — 300 ms worked on hot hardware and silently raced on slow VMs / CI / cold caches. Polling adapts: hot path completes in <20 ms, slow boots get the grace window. locate_muxadnow also checks/opt/homebrew/bin/muxadand/usr/local/bin/muxadafter the cargo bin fallback, so a brew-installed muxad lands at the correct path on first install.
Internal
- New
init::utilmodule hostsuid_string()(deduplicated fromdetect.rs+files/launchd.rs) plus the newdefault_muxad_socket()/muxad_responsive()/wait_for_muxad()helpers. Six new unit tests against a realUnixListenercover both the immediate-return and timeout paths with elapsed-time bounds.
Upgrading
git pull
cargo install --path crates/muxad --locked
cargo install --path crates/muxa-cli --lockedIf you'd hit the v0.4.0/v0.4.2 false-positive pgrep case (wizard finished but muxa status failed), v0.4.3's muxa init will correctly detect the stale state and re-spawn muxad.
PR: #20
v0.4.2 — macOS launchd + shellrc fallback + --start-daemon
End-to-end install on every platform. v0.4.1 wired tmux/agent configs, but on macOS / containers / WSL1 the wizard finished with
muxadnot actually running. v0.4.2 closes that gap with a launchd component, a cross-platform shellrc fallback, and an explicit--start-daemonstep.
TL;DR
muxa init # interactive wizard, picks the right manager for your OS
muxa init --preset standard --yes # one-shot, leaves muxad running
muxa init --start-daemon=false # skip the auto-spawn (dotfile bootstrap)Added
muxad-launchd— macOS analogue ofmuxad-systemd. Writes~/Library/LaunchAgents/dev.open330.muxad.plist, runslaunchctl bootstrap gui/<uid>+kickstart -k. Uninstall does the inverse.muxad-shellrc— cross-platform fallback for hosts with no service manager (containers, BSDs, WSL1, minimal Linux, CI sandboxes). Auto-detects$SHELLto pick~/.zshrc/~/.bashrc/ fish config /~/.profile, drops a marker block that lazy-starts muxad from the next interactive shell.--start-daemonflag (defaulttrue) — final action that spawns muxad detached ifpgrep -x muxaddoesn't find it. Belt-and-suspenders alongside the manager components: even ifsystemctl enable --noworlaunchctl bootstrapsilently no-ops, the explicit start guarantees a responsive muxad when the wizard finishes.
Changed
- Smart daemon-manager selection.
Preset::Standard/Preset::Fulland the wizard's multi-select now show exactly one daemon-manager candidate, picked fromstd::env::consts::OS. macOS users no longer seemuxad-systemdin the picker; Linux users no longer seemuxad-launchd.Detection::recommended_daemon_manager()further degrades tomuxad-shellrcwhen systemctl/launchctl is missing on a host that'd normally support them — the "Standard preset = working install" invariant holds even in stripped-down envs.
Upgrading
git pull
cargo install --path crates/muxad --locked
cargo install --path crates/muxa-cli --locked
muxa init # picks the right manager automaticallyIf you already ran v0.4.1 and the daemon was never running, just rerun muxa init — the new --start-daemon step boots it.
PR: #19
v0.4.1 — muxa init wizard (safe release)
v0.4.1 is the canonical release of the
muxa initinstall wizard. It bundles the new feature shipped in v0.4.0 with a critical safety fix. v0.4.0 is unsafe — do not use.
TL;DR
A single command replaces the previous five-step manual install:
# Existing checkout
muxa init # interactive wizard
muxa init --preset standard --yes # CI / dotfile bootstrap
muxa init --dry-run # preview, write nothing
muxa init --uninstall # surgical reverse
# Fresh machine
curl -fsSL https://raw.githubusercontent.com/Open330/muxa/main/scripts/install.sh | shWhat's in this release
✨ Added — muxa init install wizard (originally v0.4.0, PR #17)
Three entry points sharing one core:
- Tier 1 —
curl … | shbootstrap (scripts/install.sh) builds binaries, hands off to the wizard. - Tier 2 — Interactive cliclack wizard (clack.cc-styled boxed UI: pre-flight ▸ multi-select ▸ review diff ▸ apply ▸ verify).
- Tier 3 —
--preset {minimal,standard,full},--yes,--component,--no <id>,--dry-run,--uninstall, withMUXA_INIT_*/CI=trueenv-var fallbacks.
7 selectable components: tmux-popup, tmux-statusline, claude-hooks, codex-hooks, gemini-hooks, muxad-systemd, dashboard. Each owns either a # >>> muxa managed (id) >>> marker block (line-based files) or a command-prefix match (JSON/TOML), so --uninstall is a surgical reverse — hand-edited config around our blocks stays intact.
Best-practice baseline:
- Auto-detect → smart defaults (Claude config present →
claude-hookspre-checked) <file>.muxa-backup-<unix_ts>before any write- Atomic temp-file + rename writes
- Idempotent — install/uninstall round-trip is lossless
- Same visual structure in non-interactive mode for CI logs
- Custom Claude statusLine (e.g. ccstatusline) detected → warning, file left alone
🐛 Fixed — muxa init no longer kills running tmux sessions (PR #18)
v0.4.0's post-apply verify step shelled out to:
tmux -f ~/.tmux.conf start-server \; kill-server
intending to spin up a throwaway tmux server, parse the conf, then tear it down. The -f flag scopes the config file but NOT the socket — start-server attached to the user's default socket and the trailing kill-server killed every running session there.
v0.4.1 removes the check entirely. Hardening with -L muxa-init-verify would patch the immediate bug, but the value/risk math doesn't justify keeping it: the check ran after we wrote the conf (couldn't prevent bad output), marker-block content is fixed in the binary (we control it), the user already approves the diff in the "Review changes" step before apply, and tmux source-file (which apply.rs runs) surfaces any real syntax error itself.
If you ran v0.4.0 and lost tmux sessions: sessions can't be recovered without tmux-resurrect / tmux-continuum. Apologies. The *.muxa-backup-<ts> files preserve your pre-init configs — restore from those if anything else looks off.
Upgrading from v0.3.x
git pull
cargo install --path crates/muxad --locked
cargo install --path crates/muxa-cli --locked
muxa init # opt in via the wizardNumbers
- 15 new files, ~3.0 kLOC under
crates/muxa-cli/src/init/ - +146 unit tests (workspace 215 → 361)
- Clippy clean with
-D warnings, rustfmt clean - 4 platforms:
x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-apple-darwin,aarch64-apple-darwin
v0.4.0 (broken — use v0.4.1)
⚠️ DO NOT USE — UPGRADE TO v0.4.1This release contains a critical bug: a post-apply verification step in
muxa initkilled every running tmux session on the host. The fix in
v0.4.1 removes the
dangerous code path. Same wizard, no session loss.
Original v0.4.0 — muxa init install wizard
A single command replaces the previous five-step manual install (cargo install + hand-edit ~/.tmux.conf + jq-merge ~/.claude/settings.json + ~/.codex/config.toml + ~/.gemini/settings.json + systemd unit).
Three entry points sharing one core:
- Tier 1 —
curl -fsSL https://raw.githubusercontent.com/Open330/muxa/main/scripts/install.sh | shbootsmuxa initafter building binaries. - Tier 2 —
muxa initinteractive wizard via cliclack (clack.cc-style flow: pre-flight ▸ multi-select ▸ review diff ▸ apply ▸ verify). - Tier 3 —
--preset {minimal,standard,full},--yes,--component,--no <id>,--dry-run,--uninstall, withMUXA_INIT_*/CI=trueenv-var fallbacks for headless installs.
Components (7 selectable)
tmux-popup, tmux-statusline, claude-hooks, codex-hooks, gemini-hooks, muxad-systemd, dashboard. Each owns either a # >>> muxa managed (id) >>> marker block (line-based files) or a command-prefix match (JSON/TOML), so --uninstall is a surgical reverse — hand-edited config around our blocks stays intact.
Best-practice baseline
- Auto-detect → smart defaults (Claude config present →
claude-hookspre-checked) <file>.muxa-backup-<unix_ts>before any write- Atomic temp-file + rename writes
- Idempotent — install/uninstall round-trip is lossless
- Same visual structure in non-interactive mode for CI logs
- Custom Claude statusLine (e.g. ccstatusline) detected → warning, file left alone
PR: #17
v0.3.2
Release notes are populated from CHANGELOG.md after the matrix uploads finish.
v0.3.1
Release notes are populated from CHANGELOG.md after the matrix uploads finish.
v0.3.0
Release notes are populated from CHANGELOG.md after the matrix uploads finish.