Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion adapters/claude/hooks/microbridge-permission.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
},
{
Expand Down
2 changes: 1 addition & 1 deletion adapters/sdk/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/microbridge-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/microbridge-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "microbridge-ui",
"private": true,
"version": "0.3.9",
"version": "0.3.10",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
4 changes: 2 additions & 2 deletions apps/microbridge-ui/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion apps/microbridge-ui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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]
74 changes: 68 additions & 6 deletions apps/microbridge-ui/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ struct AppState {
bus: BusHandle,
snapshot: CachedSnapshot,
bundled_daemon: StdMutex<Option<Child>>,
supervise_bundled_daemon: bool,
shutting_down: AtomicBool,
}

fn daemon_socket_path() -> PathBuf {
Expand Down Expand Up @@ -93,6 +95,54 @@ fn start_bundled_daemon() -> Option<Child> {
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::<AppState>() else {
return;
};
if state.shutting_down.load(Ordering::Acquire) {
return;
}
if daemon_is_reachable() {
continue;
}
Comment on lines +114 to +122
let Ok(mut child_slot) = state.bundled_daemon.lock() else {
continue;
};
Comment on lines +123 to +125
// 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 {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2303,6 +2364,7 @@ pub fn run() {
.run(|app, event| {
if matches!(event, tauri::RunEvent::Exit) {
if let Some(state) = app.try_state::<AppState>() {
state.shutting_down.store(true, Ordering::Release);
if let Ok(mut guard) = state.bundled_daemon.lock() {
Comment on lines 2364 to 2368
if let Some(mut child) = guard.take() {
let _ = child.kill();
Expand Down
2 changes: 1 addition & 1 deletion apps/microbridge-ui/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
17 changes: 17 additions & 0 deletions docs/releases/v0.3.10.md
Original file line number Diff line number Diff line change
@@ -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.
Loading