From 0a7866873281f2710f747a4b3e03d945efbf68a6 Mon Sep 17 00:00:00 2001 From: Jonathan Borgwing Date: Wed, 22 Jul 2026 22:42:23 -0400 Subject: [PATCH 1/2] fix(ui): recover the bundled daemon after owner exit Supervise the app-owned daemon so updater, login, and relaunch overlap cannot leave a surviving menu bar UI permanently offline. Stop recovery during shutdown and preserve restored legacy-service ownership. --- apps/microbridge-ui/src-tauri/src/lib.rs | 74 ++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/apps/microbridge-ui/src-tauri/src/lib.rs b/apps/microbridge-ui/src-tauri/src/lib.rs index d97d70d..8173541 100644 --- a/apps/microbridge-ui/src-tauri/src/lib.rs +++ b/apps/microbridge-ui/src-tauri/src/lib.rs @@ -25,6 +25,8 @@ struct AppState { bus: BusHandle, snapshot: CachedSnapshot, bundled_daemon: StdMutex>, + supervise_bundled_daemon: bool, + shutting_down: AtomicBool, } fn daemon_socket_path() -> PathBuf { @@ -93,6 +95,54 @@ fn start_bundled_daemon() -> Option { spawn_owned_daemon(&binary) } +fn should_start_bundled_daemon( + daemon_reachable: bool, + owned_child_running: bool, + supervision_enabled: bool, + shutting_down: bool, +) -> bool { + supervision_enabled && !shutting_down && !daemon_reachable && !owned_child_running +} + +/// Keep an installed GUI usable if the daemon it initially discovered exits. +/// This matters during updater/login overlap: a new UI process may attach to +/// the old UI's child, then lose that daemon when the old process terminates. +fn spawn_bundled_daemon_supervisor(app: AppHandle) { + tauri::async_runtime::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(2)).await; + let Some(state) = app.try_state::() else { + return; + }; + if state.shutting_down.load(Ordering::Acquire) { + return; + } + if daemon_is_reachable() { + continue; + } + let Ok(mut child_slot) = state.bundled_daemon.lock() else { + continue; + }; + // Recheck after serializing with app shutdown/restart work. + let daemon_reachable = daemon_is_reachable(); + let owned_child_running = child_slot + .as_mut() + .is_some_and(|child| matches!(child.try_wait(), Ok(None))); + if !owned_child_running { + child_slot.take(); + } + if should_start_bundled_daemon( + daemon_reachable, + owned_child_running, + state.supervise_bundled_daemon, + state.shutting_down.load(Ordering::Acquire), + ) { + *child_slot = start_bundled_daemon(); + } + } + }); +} + const DAEMON_MIGRATION_MARKER: &str = "app-owned-daemon-v1"; fn daemon_migration_marker_path() -> PathBuf { @@ -338,6 +388,15 @@ mod daemon_migration_tests { ); } + #[test] + fn daemon_supervision_restarts_only_when_the_gui_should_own_recovery() { + assert!(should_start_bundled_daemon(false, false, true, false)); + assert!(!should_start_bundled_daemon(true, false, true, false)); + assert!(!should_start_bundled_daemon(false, true, true, false)); + assert!(!should_start_bundled_daemon(false, false, false, false)); + assert!(!should_start_bundled_daemon(false, false, true, true)); + } + #[test] fn successful_conversion_records_marker_then_removes_owned_plists() { let directory = test_directory("migration-success"); @@ -1631,8 +1690,7 @@ struct HardwareMenuPresentation { } fn is_physical_micro(device_name: &str) -> bool { - device_name.starts_with("codex-micro-") - || device_name.starts_with("creator-micro-v2-") + device_name.starts_with("codex-micro-") || device_name.starts_with("creator-micro-v2-") } fn hardware_menu_presentation_for( @@ -1982,12 +2040,12 @@ pub fn run() { } migrate_legacy_ui_login_item(); - let bundled_daemon = match migrate_legacy_daemon_to_app() { - Ok(Some(child)) => Some(child), - Ok(None) => start_bundled_daemon(), + let (bundled_daemon, supervise_bundled_daemon) = match migrate_legacy_daemon_to_app() { + Ok(Some(child)) => (Some(child), true), + Ok(None) => (start_bundled_daemon(), true), // The legacy service was restored; do not race it with a // second daemon if the migration could not be completed. - Err(()) => None, + Err(()) => (None, false), }; let (bus, mut event_rx) = spawn_bus_loop(); let snapshot: CachedSnapshot = Arc::new(Mutex::new(None)); @@ -2143,7 +2201,10 @@ pub fn run() { bus, snapshot, bundled_daemon: StdMutex::new(bundled_daemon), + supervise_bundled_daemon, + shutting_down: AtomicBool::new(false), }); + spawn_bundled_daemon_supervisor(app.handle().clone()); // Dedicated monochrome tray glyph (bridge silhouette), rendered as a // template image so macOS tints it for light/dark menu bars. The full @@ -2303,6 +2364,7 @@ pub fn run() { .run(|app, event| { if matches!(event, tauri::RunEvent::Exit) { if let Some(state) = app.try_state::() { + state.shutting_down.store(true, Ordering::Release); if let Ok(mut guard) = state.bundled_daemon.lock() { if let Some(mut child) = guard.take() { let _ = child.kill(); From 67a2680ebb802457ff13cafd65d31611cf0428fe Mon Sep 17 00:00:00 2001 From: Jonathan Borgwing Date: Wed, 22 Jul 2026 22:42:38 -0400 Subject: [PATCH 2/2] chore(release): prepare v0.3.10 Align app, daemon, protocol client, and package versions for the daemon recovery release. Keep clean macOS Tauri release builds deterministic by skipping the redundant debuginfo strip pass that corrupts proc-macro dylibs under the current toolchain. --- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- .../claude/hooks/microbridge-permission.mjs | 2 +- adapters/sdk/package.json | 2 +- apps/microbridge-ui/package-lock.json | 4 ++-- apps/microbridge-ui/package.json | 2 +- apps/microbridge-ui/src-tauri/Cargo.lock | 4 ++-- apps/microbridge-ui/src-tauri/Cargo.toml | 8 +++++++- apps/microbridge-ui/src-tauri/tauri.conf.json | 2 +- docs/releases/v0.3.10.md | 17 +++++++++++++++++ 10 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 docs/releases/v0.3.10.md diff --git a/Cargo.lock b/Cargo.lock index ba3694a..6b4ced2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,7 +616,7 @@ dependencies = [ [[package]] name = "mb-adapters" -version = "0.3.9" +version = "0.3.10" dependencies = [ "mb-protocol", "notify", @@ -628,7 +628,7 @@ dependencies = [ [[package]] name = "mb-device" -version = "0.3.9" +version = "0.3.10" dependencies = [ "hidapi", "mb-protocol", @@ -639,7 +639,7 @@ dependencies = [ [[package]] name = "mb-protocol" -version = "0.3.9" +version = "0.3.10" dependencies = [ "serde", "serde_json", @@ -653,7 +653,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "microbridgectl" -version = "0.3.9" +version = "0.3.10" dependencies = [ "mb-device", "mb-protocol", @@ -663,7 +663,7 @@ dependencies = [ [[package]] name = "microbridged" -version = "0.3.9" +version = "0.3.10" dependencies = [ "keyring", "mb-adapters", diff --git a/Cargo.toml b/Cargo.toml index 2288f96..b257c51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.3.9" +version = "0.3.10" edition = "2021" license = "MIT" repository = "https://github.com/DevVig/microbridge" diff --git a/adapters/claude/hooks/microbridge-permission.mjs b/adapters/claude/hooks/microbridge-permission.mjs index 3d9ac54..446bd3f 100644 --- a/adapters/claude/hooks/microbridge-permission.mjs +++ b/adapters/claude/hooks/microbridge-permission.mjs @@ -61,7 +61,7 @@ function ingestLifecycle(id, state) { adapter: "claude-hook", protocol_version: 0, role: "ui", - adapter_version: "0.3.9", + adapter_version: "0.3.10", capabilities: {}, }, { diff --git a/adapters/sdk/package.json b/adapters/sdk/package.json index 0b928b0..ecf67da 100644 --- a/adapters/sdk/package.json +++ b/adapters/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@microbridge/adapter-sdk", - "version": "0.3.9", + "version": "0.3.10", "description": "Zero-dependency SDK for publishing AI agent session states to Microbridge", "main": "index.mjs", "type": "module", diff --git a/apps/microbridge-ui/package-lock.json b/apps/microbridge-ui/package-lock.json index 51dc782..fc13121 100644 --- a/apps/microbridge-ui/package-lock.json +++ b/apps/microbridge-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "microbridge-ui", - "version": "0.3.9", + "version": "0.3.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "microbridge-ui", - "version": "0.3.9", + "version": "0.3.10", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", diff --git a/apps/microbridge-ui/package.json b/apps/microbridge-ui/package.json index 0c50b9f..0f48669 100644 --- a/apps/microbridge-ui/package.json +++ b/apps/microbridge-ui/package.json @@ -1,7 +1,7 @@ { "name": "microbridge-ui", "private": true, - "version": "0.3.9", + "version": "0.3.10", "type": "module", "scripts": { "dev": "vite", diff --git a/apps/microbridge-ui/src-tauri/Cargo.lock b/apps/microbridge-ui/src-tauri/Cargo.lock index 291e9b5..c7af031 100644 --- a/apps/microbridge-ui/src-tauri/Cargo.lock +++ b/apps/microbridge-ui/src-tauri/Cargo.lock @@ -1800,7 +1800,7 @@ dependencies = [ [[package]] name = "mb-protocol" -version = "0.3.9" +version = "0.3.10" dependencies = [ "serde", ] @@ -1822,7 +1822,7 @@ dependencies = [ [[package]] name = "microbridge-ui" -version = "0.3.9" +version = "0.3.10" dependencies = [ "mb-protocol", "objc2-service-management", diff --git a/apps/microbridge-ui/src-tauri/Cargo.toml b/apps/microbridge-ui/src-tauri/Cargo.toml index a447c36..e3f6f86 100644 --- a/apps/microbridge-ui/src-tauri/Cargo.toml +++ b/apps/microbridge-ui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "microbridge-ui" -version = "0.3.9" +version = "0.3.10" description = "Microbridge menu bar app (primary UI)" authors = ["Microbridge contributors"] edition = "2021" @@ -27,4 +27,10 @@ mb-protocol = { path = "../../../crates/mb-protocol" } [target.'cfg(target_os = "macos")'.dependencies] objc2-service-management = "0.3.2" +[profile.release] +# Cargo's macOS debuginfo strip pass can leave freshly built proc-macro dylibs +# unloadable before the app itself links. Release builds do not emit debug info, +# so skipping that redundant pass keeps clean Tauri builds deterministic. +strip = false + [workspace] diff --git a/apps/microbridge-ui/src-tauri/tauri.conf.json b/apps/microbridge-ui/src-tauri/tauri.conf.json index 08ab5a2..d1ec1d8 100644 --- a/apps/microbridge-ui/src-tauri/tauri.conf.json +++ b/apps/microbridge-ui/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Microbridge", - "version": "0.3.9", + "version": "0.3.10", "identifier": "ai.microbridge.ui", "build": { "beforeDevCommand": "npm run dev", diff --git a/docs/releases/v0.3.10.md b/docs/releases/v0.3.10.md new file mode 100644 index 0000000..b4e9e3f --- /dev/null +++ b/docs/releases/v0.3.10.md @@ -0,0 +1,17 @@ +# Microbridge v0.3.10 + +## Daemon recovery + +- Restore the bundled daemon automatically when the UI loses a daemon that was + owned by an older overlapping app process during login, update, or relaunch. +- Preserve explicit standalone/headless daemon ownership while it remains + reachable; the GUI only starts its bundled daemon after that service exits. +- Stop daemon recovery during clean app shutdown so no replacement child can be + orphaned as Microbridge exits. + +## Hardware status + +- Keep Codex Micro USB and Bluetooth detection available after daemon recovery, + including the native Claim, Retry, and Release actions added in v0.3.9. +- Continue requesting only Input Monitoring for hardware control. Open at Login + may separately require approval in macOS Login Items.