From 8cc80d970348c17bf73832ebd86ff767745e23c8 Mon Sep 17 00:00:00 2001 From: burakgon Date: Sun, 19 Jul 2026 01:41:15 +0300 Subject: [PATCH] fix: prevent managed relay enrollment race --- CHANGELOG.md | 7 +++++++ package.json | 2 +- packages/cli/package.json | 2 +- packages/server/package.json | 2 +- packages/web/package.json | 2 +- packages/web/src/App.test.tsx | 24 ++++++++++++++++++++++++ packages/web/src/App.tsx | 12 +++++++++++- 7 files changed, 46 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5459aa3f..962bf1bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ The format loosely follows [Keep a Changelog](https://keepachangelog.com/); date ## [Unreleased] +## [1.4.2] - 2026-07-19 + +### Fixed + +- Keep managed Node enrollment and the persistent terminal transport from opening competing relay connections, so a + successfully verified Node proceeds to its Sessions instead of falling into a **Couldn't reach the server** loop. + ## [1.4.1] - 2026-07-18 ### Fixed diff --git a/package.json b/package.json index 72dab2e4..4bcfce62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roamcode-workspace", - "version": "1.4.1", + "version": "1.4.2", "private": true, "type": "module", "packageManager": "pnpm@11.9.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 777f8870..b44ea263 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "roamcode", - "version": "1.4.1", + "version": "1.4.2", "description": "Operate Claude Code or Codex sessions remotely from an installable PWA", "homepage": "https://roamcode.ai", "bugs": "https://github.com/burakgon/roamcode/issues", diff --git a/packages/server/package.json b/packages/server/package.json index 336ab6c3..2c2e35de 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@roamcode.ai/server", - "version": "1.4.1", + "version": "1.4.2", "description": "Host-native server for RoamCode", "homepage": "https://roamcode.ai", "bugs": "https://github.com/burakgon/roamcode/issues", diff --git a/packages/web/package.json b/packages/web/package.json index 33c88d43..2c53af32 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@roamcode.ai/web", - "version": "1.4.1", + "version": "1.4.2", "description": "Installable web client bundled with RoamCode", "homepage": "https://roamcode.ai", "bugs": "https://github.com/burakgon/roamcode/issues", diff --git a/packages/web/src/App.test.tsx b/packages/web/src/App.test.tsx index 2dcf2360..1c1dcd0e 100644 --- a/packages/web/src/App.test.tsx +++ b/packages/web/src/App.test.tsx @@ -13,6 +13,25 @@ import { import { useStore } from "./store/store"; import type { SessionMeta } from "./types/server"; +const relayManagerSpies = vi.hoisted(() => ({ + clientFor: vi.fn(), + closeHost: vi.fn(), +})); + +vi.mock("./relay/host-client-manager", () => ({ + createRelayHostClientManager: () => ({ + resume: vi.fn(), + clientFor: relayManagerSpies.clientFor, + fetch: vi.fn(), + reconnect: vi.fn(() => false), + status: vi.fn(), + closeHost: relayManagerSpies.closeHost, + reconcile: vi.fn(), + subscribe: vi.fn(() => () => undefined), + close: vi.fn(), + }), +})); + // TerminalView bridges xterm.js (needs a real canvas / matchMedia), which jsdom lacks. These App-shell // tests only care about the rail/selection/landing chrome, not the terminal internals, so stub it. vi.mock("./chat/TerminalView", () => ({ @@ -52,6 +71,9 @@ beforeEach(() => { window.history.replaceState({}, "", "/"); // Reset the shared zustand singleton so tests don't leak state into each other. useStore.setState({ token: undefined, sessions: [], activeSessionId: undefined, lastActiveAt: {} }); + relayManagerSpies.clientFor.mockReset(); + relayManagerSpies.clientFor.mockRejectedValue(new Error("unexpected persistent relay client")); + relayManagerSpies.closeHost.mockReset(); fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); }); @@ -198,6 +220,8 @@ describe("App token validation on load", () => { expect(screen.queryByLabelText(/access token/i)).not.toBeInTheDocument(); expect(window.location.search).toBe(""); expect(sessionStorage.getItem("roamcode.managed-enrollment.pending.v1")).toContain(managedHostId); + expect(relayManagerSpies.closeHost).toHaveBeenCalledWith(registry.activeHostId); + expect(relayManagerSpies.clientFor).not.toHaveBeenCalled(); }, ); }); diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 198c7190..c661dcfe 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -703,6 +703,16 @@ export function App() { }, [activeDirectHost.baseUrl, activeDirectHost.id, activeDirectHost.relay]); useEffect(() => { + // A managed enrollment proves (or repairs) the saved device with its own short-lived relay client below. + // Starting the persistent client for that same route/device at the same time makes the broker supersede one + // of them. When the proof client then closes normally, the persistent client can remain fatally superseded + // and every API request falls through to the generic "Couldn't reach the server" retry state. Keep a single + // owner during enrollment; changing to "validating" re-runs this effect and creates the persistent client. + if (phase === "managed-enrollment") { + setActiveRelayTransport(undefined); + relayClientManager.closeHost(activeDirectHost.id); + return; + } const relay = activeDirectHost.relay; setActiveRelayTransport(undefined); setRelaySetupError(undefined); @@ -728,7 +738,7 @@ export function App() { return () => { disposed = true; }; - }, [activeDirectHost, relayAttempt, relayClientManager, token, tokenHostId]); + }, [activeDirectHost, phase, relayAttempt, relayClientManager, token, tokenHostId]); const activeConnection = useMemo(() => { const connection: ApiClientOptions & { hostId: string } = {