feat(app): version handshake — GUI restarts a stale running daemon

The daemon outlives the GUI, so after an update an OLD daemon can keep serving
the socket and the new GUI just connects to it (stale code — e.g. the missing
TERM fix). Both binaries are now stamped with the git build id (build.rs):
the daemon reports it in `health.build`, and on connect the bridge compares it
to the GUI's own SPACESH_BUILD; on mismatch it shuts the daemon down and lets
ensure_daemon respawn the bundled (matching) one. No-op for unstamped dev
builds or daemons too old to report a build. Build id is shown in Settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:39:46 +07:00
parent 8f431eaa40
commit cf7410b46a
6 changed files with 84 additions and 4 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ function DaemonSection({ health, onReload }: { health: DaemonHealth | null; onRe
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 8 }}>Daemon</div>
<div style={{ fontFamily: FONT.mono, fontSize: 12, color: COLORS.textSecondary, lineHeight: 1.7 }}>
{health ? (<>
<div>version {health.version} · pid {health.pid}</div>
<div>version {health.version}{health.build ? ` · ${health.build}` : ""} · pid {health.pid}</div>
<div>uptime {fmtUptime(health.started_at_ms)}</div>
</>) : <div>offline</div>}
</div>
+1 -1
View File
@@ -178,7 +178,7 @@ export async function closeSurfaceCmd(surfaceId: string): Promise<void> {
await invoke("close_surface", { surfaceId });
}
export interface DaemonHealth { version: string; pid: number; started_at_ms: number }
export interface DaemonHealth { version: string; build?: string; pid: number; started_at_ms: number }
export async function getHealth(): Promise<DaemonHealth> {
return await invoke<DaemonHealth>("health");