From a107f8a0792c3add90d398602998c55c3ebaef27 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 24 Sep 2026 22:36:51 -0700 Subject: [PATCH] fix(clients): a preview app no longer knocks the desktop's own server offline (#13577) Co-authored-by: Claude Opus 5.5 (1M context) --- .../client-runtime/src/connection/layer.ts | 7 ++ .../src/connection/registry.test.ts | 96 ++++++++++++++++++- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/packages/client-runtime/src/connection/layer.ts b/packages/client-runtime/src/connection/layer.ts index 46dcdcf569b9e2b7ecfb1d2bec364fcec2325743..b64e58c4fef179b01d2ba2d64210dbbffcb90f1f 100644 --- a/packages/client-runtime/src/connection/layer.ts +++ b/packages/client-runtime/src/connection/layer.ts @@ -32,11 +32,18 @@ export const watchDiscoveredCompatibility = Effect.fn("connection.watchDiscovere if (!current.environments.has(environmentId)) seenChecks.delete(environmentId); } } + const registered = yield* SubscriptionRef.get(registry.entries); for (const entry of current.environments.values()) { const status = Option.getOrNull(entry.status); const descriptor = status?.descriptor; if (status === null || descriptor === undefined) continue; const environmentId = entry.environment.environmentId; + // Discovery describes the server behind the relay route. A direct + // connection (the desktop's own server, a saved URL, SSH) can reach + // a different server with the same environment id, such as a + // preview app that shares the home directory. Its socket handshake + // already checks the protocol. + if (registered.get(environmentId)?.target._tag !== "RelayConnectionTarget") continue; const previous = seenChecks.get(environmentId); const fresh = previous?.checkedAt !== status.checkedAt || diff --git a/packages/client-runtime/src/connection/registry.test.ts b/packages/client-runtime/src/connection/registry.test.ts index c07a15101f605ffad2e3a460f8ff1d7f718f8e4f..8a71344ad1734128561130257988df830eb3d5c6 100644 --- a/packages/client-runtime/src/connection/registry.test.ts +++ b/packages/client-runtime/src/connection/registry.test.ts @@ -683,7 +683,7 @@ describe("EnvironmentRegistry", () => { it.effect("only a fresh health check for the rejected environment unlocks it", () => Effect.gen(function* () { - const harness = yield* makeHarness([RELAY_TARGET], [], [], { + const harness = yield* makeHarness([RELAY_TARGET, SECOND_RELAY_TARGET], [], [], { initialDisabled: [RELAY_TARGET.environmentId], }); const descriptor = (environmentId: EnvironmentId): ExecutionEnvironmentDescriptor => ({ @@ -778,8 +778,8 @@ describe("EnvironmentRegistry", () => { yield* SubscriptionRef.update(discoveryState, (state) => ({ ...state, environments: new Map(state.environments).set( - SECOND_TARGET.environmentId, - discovered(descriptor(SECOND_TARGET.environmentId)), + SECOND_RELAY_TARGET.environmentId, + discovered(descriptor(SECOND_RELAY_TARGET.environmentId)), ), })); yield* Deferred.await(unrelated); @@ -798,8 +798,8 @@ describe("EnvironmentRegistry", () => { environments: new Map([ [RELAY_TARGET.environmentId, discovered(descriptor(RELAY_TARGET.environmentId))], [ - SECOND_TARGET.environmentId, - discovered(descriptor(SECOND_TARGET.environmentId), "2026-09-15T00:01:00Z"), + SECOND_RELAY_TARGET.environmentId, + discovered(descriptor(SECOND_RELAY_TARGET.environmentId), "2026-09-15T00:01:00Z"), ], ]), })); @@ -827,6 +827,92 @@ describe("EnvironmentRegistry", () => { }), ); + it.effect("discovery leaves direct connections that share an environment id alone", () => + Effect.gen(function* () { + const harness = yield* makeHarness([TARGET, RELAY_TARGET]); + const discovered = (environmentId: EnvironmentId) => { + const endpoint = { + httpBaseUrl: "https://relay.example.test", + wsBaseUrl: "wss://relay.example.test", + providerKind: "manual" as const, + }; + return { + environment: { + environmentId, + label: "Preview server", + endpoint, + linkedAt: "2026-09-25T00:00:00Z", + }, + availability: "online" as const, + status: Option.some({ + environmentId, + endpoint, + status: "online", + checkedAt: "2026-09-25T00:00:00Z", + descriptor: { + environmentId, + label: "Preview server", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "2.0.0", + orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1, + capabilities: { repositoryIdentity: true }, + }, + }), + error: Option.none(), + }; + }; + yield* Effect.gen(function* () { + const registry = yield* EnvironmentRegistry.EnvironmentRegistry; + yield* registry.start; + yield* awaitConnectionState( + registry, + TARGET.environmentId, + (state) => state.phase === "connected", + ); + const relayChecked = yield* Deferred.make(); + yield* watchDiscoveredCompatibility().pipe( + Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, { + ...registry, + setCompatibility: (environmentId, error) => + registry + .setCompatibility(environmentId, error) + .pipe( + Effect.andThen( + environmentId === RELAY_TARGET.environmentId + ? Deferred.succeed(relayChecked, undefined) + : Effect.void, + ), + ), + }), + Effect.provideService( + RelayEnvironmentDiscovery.RelayEnvironmentDiscovery, + RelayEnvironmentDiscovery.RelayEnvironmentDiscovery.of({ + state: + yield* SubscriptionRef.make( + { + ...RelayEnvironmentDiscovery.EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE, + environments: new Map([ + [TARGET.environmentId, discovered(TARGET.environmentId)], + [RELAY_TARGET.environmentId, discovered(RELAY_TARGET.environmentId)], + ]), + }, + ), + refresh: Effect.void, + }), + ), + Effect.forkScoped, + ); + yield* Deferred.await(relayChecked); + + const entries = yield* SubscriptionRef.get(registry.entries); + expect(entries.get(RELAY_TARGET.environmentId)).toMatchObject({ enabled: false }); + expect(entries.get(TARGET.environmentId)).toMatchObject({ enabled: true }); + expect(entries.get(TARGET.environmentId)?.unsupportedReason).toBeUndefined(); + expect((yield* registry.state(TARGET.environmentId)).phase).toBe("connected"); + }).pipe(Effect.provide(harness.layer), Effect.scoped); + }), + ); + it.effect("discovery keeps unsupported environments off until compatibility changes", () => Effect.gen(function* () { const harness = yield* makeHarness([RELAY_TARGET], [], [], { -- 2.54.0