diff --git a/src/gateway/server/ws-connection.ts b/src/gateway/server/ws-connection.ts index c052a8f3c2..d1483ee076 100644 --- a/src/gateway/server/ws-connection.ts +++ b/src/gateway/server/ws-connection.ts @@ -6,6 +6,7 @@ import type { ResolvedGatewayAuth } from "../auth.js"; import type { GatewayRequestContext, GatewayRequestHandlers } from "../server-methods/types.js"; import type { GatewayWsClient } from "./ws-types.js"; import { resolveCanvasHostUrl } from "../../infra/canvas-host-url.js"; +import { removeRemoteNodeInfo } from "../../infra/skills-remote.js"; import { listSystemPresence, upsertPresence } from "../../infra/system-presence.js"; import { truncateUtf16Safe } from "../../utils.js"; import { isWebchatClient } from "../../utils/message-channel.js"; @@ -243,6 +244,7 @@ export function attachGatewayWsConnectionHandler(params: { const context = buildRequestContext(); const nodeId = context.nodeRegistry.unregister(connId); if (nodeId) { + removeRemoteNodeInfo(nodeId); context.nodeUnsubscribeAll(nodeId); } } diff --git a/src/infra/skills-remote.test.ts b/src/infra/skills-remote.test.ts new file mode 100644 index 0000000000..5aecf39a3b --- /dev/null +++ b/src/infra/skills-remote.test.ts @@ -0,0 +1,36 @@ +import { randomUUID } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { + getRemoteSkillEligibility, + recordRemoteNodeBins, + recordRemoteNodeInfo, + removeRemoteNodeInfo, +} from "./skills-remote.js"; + +describe("skills-remote", () => { + it("removes disconnected nodes from remote skill eligibility", () => { + const nodeId = `node-${randomUUID()}`; + const bin = `bin-${randomUUID()}`; + recordRemoteNodeInfo({ + nodeId, + displayName: "Remote Mac", + platform: "darwin", + commands: ["system.run"], + }); + recordRemoteNodeBins(nodeId, [bin]); + + expect(getRemoteSkillEligibility()?.hasBin(bin)).toBe(true); + + removeRemoteNodeInfo(nodeId); + + expect(getRemoteSkillEligibility()?.hasBin(bin) ?? false).toBe(false); + }); + + it("supports idempotent remote node removal", () => { + const nodeId = `node-${randomUUID()}`; + expect(() => { + removeRemoteNodeInfo(nodeId); + removeRemoteNodeInfo(nodeId); + }).not.toThrow(); + }); +}); diff --git a/src/infra/skills-remote.ts b/src/infra/skills-remote.ts index 5854810d36..01d49804b3 100644 --- a/src/infra/skills-remote.ts +++ b/src/infra/skills-remote.ts @@ -168,6 +168,10 @@ export function recordRemoteNodeBins(nodeId: string, bins: string[]) { upsertNode({ nodeId, bins }); } +export function removeRemoteNodeInfo(nodeId: string) { + remoteNodes.delete(nodeId); +} + function listWorkspaceDirs(cfg: OpenClawConfig): string[] { const dirs = new Set(); const list = cfg.agents?.list;