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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "roamcode-workspace",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"type": "module",
"packageManager": "pnpm@11.9.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
24 changes: 24 additions & 0 deletions packages/web/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => ({
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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();
},
);
});
Expand Down
12 changes: 11 additions & 1 deletion packages/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -728,7 +738,7 @@ export function App() {
return () => {
disposed = true;
};
}, [activeDirectHost, relayAttempt, relayClientManager, token, tokenHostId]);
}, [activeDirectHost, phase, relayAttempt, relayClientManager, token, tokenHostId]);

const activeConnection = useMemo<ApiClientOptions & { hostId: string }>(() => {
const connection: ApiClientOptions & { hostId: string } = {
Expand Down