From b2a97a84eb518296e2fffa701c19d3695c7df1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 18 Aug 2026 12:53:57 +0200 Subject: [PATCH] perf(ui): resync only after reconnects The first connected status from a newly started OpenCode stream waited for initial hydration and then repeated the same session, pending-request, catalog, metadata, and filesystem loads. This made every normal startup pay for a reconnect recovery pass. Track only genuine error or unexpected-disconnect transitions as requiring recovery. Initial connecting-to-connected startup and deliberate workspace shutdown no longer resync, while failed streams still receive one coalesced authoritative recovery after reconnection. Validated with 10 targeted connection and readiness tests, the UI typecheck, 537 UI tests, and git diff checks. --- .../src/stores/connection-resync-gate.test.ts | 23 +++++++++++++++++++ .../ui/src/stores/connection-resync-gate.ts | 18 +++++++++++++++ packages/ui/src/stores/instances.ts | 9 ++++++-- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/stores/connection-resync-gate.test.ts create mode 100644 packages/ui/src/stores/connection-resync-gate.ts diff --git a/packages/ui/src/stores/connection-resync-gate.test.ts b/packages/ui/src/stores/connection-resync-gate.test.ts new file mode 100644 index 00000000..aaa5fef1 --- /dev/null +++ b/packages/ui/src/stores/connection-resync-gate.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { ConnectionResyncGate } from "./connection-resync-gate.ts" + +describe("connection resync gate", () => { + it("skips initial connection and resyncs once after a stream failure", () => { + const gate = new ConnectionResyncGate() + assert.equal(gate.observe("instance", "connecting"), false) + assert.equal(gate.observe("instance", "connected"), false) + assert.equal(gate.observe("instance", "error"), false) + assert.equal(gate.observe("instance", "connected"), true) + assert.equal(gate.observe("instance", "connected"), false) + }) + + it("does not resync a deliberately stopped or cleared workspace", () => { + const gate = new ConnectionResyncGate() + assert.equal(gate.observe("instance", "disconnected", "workspace stopped"), false) + assert.equal(gate.observe("instance", "connected"), false) + gate.observe("instance", "disconnected") + gate.clear("instance") + assert.equal(gate.observe("instance", "connected"), false) + }) +}) diff --git a/packages/ui/src/stores/connection-resync-gate.ts b/packages/ui/src/stores/connection-resync-gate.ts new file mode 100644 index 00000000..88d30b96 --- /dev/null +++ b/packages/ui/src/stores/connection-resync-gate.ts @@ -0,0 +1,18 @@ +import type { InstanceStreamStatus } from "../../../server/src/api-types" + +export class ConnectionResyncGate { + private readonly pending = new Set() + + observe(instanceId: string, status: InstanceStreamStatus, reason?: string): boolean { + if (status === "error" || (status === "disconnected" && reason !== "workspace stopped")) { + this.pending.add(instanceId) + return false + } + if (status !== "connected") return false + return this.pending.delete(instanceId) + } + + clear(instanceId: string): void { + this.pending.delete(instanceId) + } +} diff --git a/packages/ui/src/stores/instances.ts b/packages/ui/src/stores/instances.ts index 271e5c94..79e73dc4 100644 --- a/packages/ui/src/stores/instances.ts +++ b/packages/ui/src/stores/instances.ts @@ -32,6 +32,7 @@ import { getRootClient } from "./opencode-client" import { buildV2RequestLocations } from "./request-locations" import { fetchCommands, clearCommands } from "./commands" import { getInstanceRefreshTargets, type InstanceRefreshTarget } from "./instance-invalidation" +import { ConnectionResyncGate } from "./connection-resync-gate" import { serverSettings } from "./preferences" import { reconcileSessionPendingState, @@ -271,6 +272,7 @@ const connectionResyncs = new TrailingResyncCoordinator( log.warn("Failed to resync sessions after instance connection", { instanceId, error }) }, ) +const connectionResyncGate = new ConnectionResyncGate() function resyncConnectedInstance(instanceId: string): void { void connectionResyncs.request(instanceId) @@ -313,11 +315,13 @@ function refreshVolatileInstanceState( } serverEvents.on("instance.eventStatus", (event) => { - if (event.type !== "instance.eventStatus" || event.status !== "connected") return + if (event.type !== "instance.eventStatus") return + const shouldResync = connectionResyncGate.observe(event.instanceId, event.status, event.reason) + if (event.status !== "connected") return if (disconnectedInstance()?.id === event.instanceId) { setDisconnectedInstance(null) } - resyncConnectedInstance(event.instanceId) + if (shouldResync) resyncConnectedInstance(event.instanceId) }) function createRestoreCreationRequestId(): string { @@ -1064,6 +1068,7 @@ function updateInstance(id: string, updates: Partial) { } function removeInstance(id: string, options: { authoritative?: boolean } = {}) { + connectionResyncGate.clear(id) const removedInstance = instances().get(id) const removedOccurrence = removedInstance ? Array.from(instances().values())