diff --git a/CHANGELOG.md b/CHANGELOG.md index 479c7f111b5..4323dac22bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Docs: https://docs.openclaw.ai ## Unreleased +### Fixes + ## 2026.4.5 ### Breaking @@ -244,6 +246,7 @@ Docs: https://docs.openclaw.ai - Plugins/facades: resolve globally installed bundled-plugin runtime facades from registry roots so bundled channels like LINE still boot when the winning plugin install lives under the global extensions directory with an encoded scoped folder name. (#61297) Thanks @openperf. - Matrix: avoid failing startup when token auth already knows the user ID but still needs optional device metadata, retry transient auth bootstrap requests, and backfill missing device IDs after startup while keeping unknown-device storage reuse conservative until metadata is repaired. (#61383) Thanks @gumadeiras. - Agents/exec: stop streaming `tool_execution_update` events after an exec session backgrounds, preventing delayed background output from hitting a stale listener and crashing the gateway while keeping the output available through `process poll/log`. (#61627) Thanks @openperf. +- Matrix: pass configured `deviceId` through health probes and keep probe-only client setup out of durable Matrix storage, so health checks preserve the correct device identity without rewriting `storage-meta.json` or related probe state on disk. (#61581) Thanks @MoerAI. ## 2026.4.2 diff --git a/extensions/matrix/src/channel.account-paths.test.ts b/extensions/matrix/src/channel.account-paths.test.ts index bd9d13651ca..6e9681f3567 100644 --- a/extensions/matrix/src/channel.account-paths.test.ts +++ b/extensions/matrix/src/channel.account-paths.test.ts @@ -49,6 +49,7 @@ describe("matrix account path propagation", () => { homeserver: "https://matrix.example.org", userId: "@poe:example.org", accessToken: "poe-token", + deviceId: "POEDEVICE", }); }); @@ -66,7 +67,7 @@ describe("matrix account path propagation", () => { ); }); - it("forwards accountId to matrix probes", async () => { + it("forwards accountId and deviceId to matrix probes", async () => { await matrixPlugin.status!.probeAccount?.({ cfg: {} as never, timeoutMs: 500, @@ -83,6 +84,7 @@ describe("matrix account path propagation", () => { homeserver: "https://matrix.example.org", accessToken: "poe-token", userId: "@poe:example.org", + deviceId: "POEDEVICE", timeoutMs: 500, accountId: "poe", }); diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index bcbb80abefd..ed3073c268b 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -498,6 +498,7 @@ export const matrixPlugin: ChannelPlugin = homeserver: auth.homeserver, accessToken: auth.accessToken, userId: auth.userId, + deviceId: auth.deviceId, timeoutMs, accountId: account.accountId, allowPrivateNetwork: auth.allowPrivateNetwork, diff --git a/extensions/matrix/src/matrix/client/create-client.test.ts b/extensions/matrix/src/matrix/client/create-client.test.ts new file mode 100644 index 00000000000..4a2b28105a3 --- /dev/null +++ b/extensions/matrix/src/matrix/client/create-client.test.ts @@ -0,0 +1,115 @@ +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const ensureMatrixSdkLoggingConfiguredMock = vi.hoisted(() => vi.fn()); +const resolveValidatedMatrixHomeserverUrlMock = vi.hoisted(() => vi.fn()); +const maybeMigrateLegacyStorageMock = vi.hoisted(() => vi.fn(async () => undefined)); +const resolveMatrixStoragePathsMock = vi.hoisted(() => vi.fn()); +const writeStorageMetaMock = vi.hoisted(() => vi.fn()); +const MatrixClientMock = vi.hoisted(() => vi.fn()); + +vi.mock("./logging.js", () => ({ + ensureMatrixSdkLoggingConfigured: ensureMatrixSdkLoggingConfiguredMock, +})); + +vi.mock("./config.js", () => ({ + resolveValidatedMatrixHomeserverUrl: resolveValidatedMatrixHomeserverUrlMock, +})); + +vi.mock("./storage.js", () => ({ + maybeMigrateLegacyStorage: maybeMigrateLegacyStorageMock, + resolveMatrixStoragePaths: resolveMatrixStoragePathsMock, + writeStorageMeta: writeStorageMetaMock, +})); + +vi.mock("../sdk.js", () => ({ + MatrixClient: MatrixClientMock, +})); + +let createMatrixClient: typeof import("./create-client.js").createMatrixClient; + +describe("createMatrixClient", () => { + const storagePaths = { + rootDir: "/tmp/openclaw-matrix-create-client-test", + storagePath: "/tmp/openclaw-matrix-create-client-test/storage.json", + recoveryKeyPath: "/tmp/openclaw-matrix-create-client-test/recovery.key", + idbSnapshotPath: "/tmp/openclaw-matrix-create-client-test/idb.snapshot", + metaPath: "/tmp/openclaw-matrix-create-client-test/storage-meta.json", + accountKey: "default", + tokenHash: "token-hash", + }; + + beforeAll(async () => { + ({ createMatrixClient } = await import("./create-client.js")); + }); + + beforeEach(() => { + vi.clearAllMocks(); + ensureMatrixSdkLoggingConfiguredMock.mockReturnValue(undefined); + resolveValidatedMatrixHomeserverUrlMock.mockResolvedValue("https://matrix.example.org"); + resolveMatrixStoragePathsMock.mockReturnValue(storagePaths); + MatrixClientMock.mockImplementation(function MockMatrixClient() { + return { + stop: vi.fn(), + }; + }); + }); + + it("persists storage metadata by default", async () => { + await createMatrixClient({ + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + accessToken: "tok", + }); + + expect(writeStorageMetaMock).toHaveBeenCalledWith({ + storagePaths, + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + accountId: undefined, + deviceId: undefined, + }); + expect(resolveMatrixStoragePathsMock).toHaveBeenCalledTimes(1); + expect(MatrixClientMock).toHaveBeenCalledWith("https://matrix.example.org", "tok", { + userId: "@bot:example.org", + password: undefined, + deviceId: undefined, + encryption: undefined, + localTimeoutMs: undefined, + initialSyncLimit: undefined, + storagePath: storagePaths.storagePath, + recoveryKeyPath: storagePaths.recoveryKeyPath, + idbSnapshotPath: storagePaths.idbSnapshotPath, + cryptoDatabasePrefix: "openclaw-matrix-default-token-hash", + autoBootstrapCrypto: undefined, + ssrfPolicy: undefined, + dispatcherPolicy: undefined, + }); + }); + + it("skips persistent storage wiring when persistence is disabled", async () => { + await createMatrixClient({ + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + accessToken: "tok", + persistStorage: false, + }); + + expect(resolveMatrixStoragePathsMock).not.toHaveBeenCalled(); + expect(writeStorageMetaMock).not.toHaveBeenCalled(); + expect(MatrixClientMock).toHaveBeenCalledWith("https://matrix.example.org", "tok", { + userId: "@bot:example.org", + password: undefined, + deviceId: undefined, + encryption: undefined, + localTimeoutMs: undefined, + initialSyncLimit: undefined, + storagePath: undefined, + recoveryKeyPath: undefined, + idbSnapshotPath: undefined, + cryptoDatabasePrefix: undefined, + autoBootstrapCrypto: undefined, + ssrfPolicy: undefined, + dispatcherPolicy: undefined, + }); + }); +}); diff --git a/extensions/matrix/src/matrix/client/create-client.ts b/extensions/matrix/src/matrix/client/create-client.ts index cc53a835c05..009f727a943 100644 --- a/extensions/matrix/src/matrix/client/create-client.ts +++ b/extensions/matrix/src/matrix/client/create-client.ts @@ -33,6 +33,7 @@ export async function createMatrixClient(params: { accessToken: string; password?: string; deviceId?: string; + persistStorage?: boolean; encryption?: boolean; localTimeoutMs?: number; initialSyncLimit?: number; @@ -45,36 +46,41 @@ export async function createMatrixClient(params: { const { MatrixClient, ensureMatrixSdkLoggingConfigured } = await loadMatrixCreateClientRuntimeDeps(); ensureMatrixSdkLoggingConfigured(); - const env = process.env; const homeserver = await resolveValidatedMatrixHomeserverUrl(params.homeserver, { dangerouslyAllowPrivateNetwork: params.allowPrivateNetwork, }); const userId = params.userId?.trim() || "unknown"; const matrixClientUserId = params.userId?.trim() || undefined; + const persistStorage = params.persistStorage !== false; + const storagePaths = persistStorage + ? resolveMatrixStoragePaths({ + homeserver, + userId, + accessToken: params.accessToken, + accountId: params.accountId, + deviceId: params.deviceId, + env: process.env, + }) + : null; - const storagePaths = resolveMatrixStoragePaths({ - homeserver, - userId, - accessToken: params.accessToken, - accountId: params.accountId, - deviceId: params.deviceId, - env, - }); - await maybeMigrateLegacyStorage({ - storagePaths, - env, - }); - fs.mkdirSync(storagePaths.rootDir, { recursive: true }); + if (storagePaths) { + await maybeMigrateLegacyStorage({ + storagePaths, + env: process.env, + }); + fs.mkdirSync(storagePaths.rootDir, { recursive: true }); + writeStorageMeta({ + storagePaths, + homeserver, + userId, + accountId: params.accountId, + deviceId: params.deviceId, + }); + } - writeStorageMeta({ - storagePaths, - homeserver, - userId, - accountId: params.accountId, - deviceId: params.deviceId, - }); - - const cryptoDatabasePrefix = `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`; + const cryptoDatabasePrefix = storagePaths + ? `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}` + : undefined; return new MatrixClient(homeserver, params.accessToken, { userId: matrixClientUserId, @@ -83,9 +89,9 @@ export async function createMatrixClient(params: { encryption: params.encryption, localTimeoutMs: params.localTimeoutMs, initialSyncLimit: params.initialSyncLimit, - storagePath: storagePaths.storagePath, - recoveryKeyPath: storagePaths.recoveryKeyPath, - idbSnapshotPath: storagePaths.idbSnapshotPath, + storagePath: storagePaths?.storagePath, + recoveryKeyPath: storagePaths?.recoveryKeyPath, + idbSnapshotPath: storagePaths?.idbSnapshotPath, cryptoDatabasePrefix, autoBootstrapCrypto: params.autoBootstrapCrypto, ssrfPolicy: params.ssrfPolicy, diff --git a/extensions/matrix/src/matrix/probe.test.ts b/extensions/matrix/src/matrix/probe.test.ts index b01d78caee9..8ad2f708e93 100644 --- a/extensions/matrix/src/matrix/probe.test.ts +++ b/extensions/matrix/src/matrix/probe.test.ts @@ -34,6 +34,7 @@ describe("probeMatrix", () => { homeserver: "https://matrix.example.org", userId: undefined, accessToken: "tok", + persistStorage: false, localTimeoutMs: 1234, }); }); @@ -50,6 +51,7 @@ describe("probeMatrix", () => { homeserver: "https://matrix.example.org", userId: "@bot:example.org", accessToken: "tok", + persistStorage: false, localTimeoutMs: 500, }); }); @@ -67,6 +69,7 @@ describe("probeMatrix", () => { homeserver: "https://matrix.example.org", userId: "@bot:example.org", accessToken: "tok", + persistStorage: false, localTimeoutMs: 500, accountId: "ops", }); @@ -87,6 +90,7 @@ describe("probeMatrix", () => { homeserver: "https://matrix.example.org", userId: undefined, accessToken: "tok", + persistStorage: false, localTimeoutMs: 500, dispatcherPolicy: { mode: "explicit-proxy", @@ -95,6 +99,44 @@ describe("probeMatrix", () => { }); }); + it("passes deviceId through to client creation (#61317)", async () => { + await probeMatrix({ + homeserver: "https://matrix.example.org", + accessToken: "tok", + userId: "@bot:example.org", + deviceId: "ABCDEF", + timeoutMs: 500, + accountId: "ops", + }); + + expect(createMatrixClientMock).toHaveBeenCalledWith({ + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + accessToken: "tok", + deviceId: "ABCDEF", + persistStorage: false, + localTimeoutMs: 500, + accountId: "ops", + }); + }); + + it("omits deviceId when not provided", async () => { + await probeMatrix({ + homeserver: "https://matrix.example.org", + accessToken: "tok", + timeoutMs: 500, + }); + + expect(createMatrixClientMock).toHaveBeenCalledWith({ + homeserver: "https://matrix.example.org", + userId: undefined, + accessToken: "tok", + deviceId: undefined, + persistStorage: false, + localTimeoutMs: 500, + }); + }); + it("returns client validation errors for insecure public http homeservers", async () => { createMatrixClientMock.mockRejectedValue( new Error("Matrix homeserver must use https:// unless it targets a private or loopback host"), diff --git a/extensions/matrix/src/matrix/probe.ts b/extensions/matrix/src/matrix/probe.ts index f90cf5a8074..2ab83e49f75 100644 --- a/extensions/matrix/src/matrix/probe.ts +++ b/extensions/matrix/src/matrix/probe.ts @@ -24,6 +24,7 @@ export async function probeMatrix(params: { homeserver: string; accessToken: string; userId?: string; + deviceId?: string; timeoutMs: number; accountId?: string | null; allowPrivateNetwork?: boolean; @@ -65,6 +66,8 @@ export async function probeMatrix(params: { homeserver: params.homeserver, userId: inputUserId, accessToken: params.accessToken, + deviceId: params.deviceId, + persistStorage: false, localTimeoutMs: params.timeoutMs, accountId: params.accountId, allowPrivateNetwork: params.allowPrivateNetwork,