fix(matrix): pass deviceId through health probe to prevent storage-meta overwrite (#61317) (#61581)

Merged via squash.

Prepared head SHA: b0495dc6ca
Co-authored-by: MoerAI <26067127+MoerAI@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
ToToKr 2026-04-06 13:42:22 +09:00 committed by GitHub
parent 728aee277f
commit d4c443bc1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 199 additions and 27 deletions

View File

@ -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

View File

@ -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",
});

View File

@ -498,6 +498,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
homeserver: auth.homeserver,
accessToken: auth.accessToken,
userId: auth.userId,
deviceId: auth.deviceId,
timeoutMs,
accountId: account.accountId,
allowPrivateNetwork: auth.allowPrivateNetwork,

View File

@ -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,
});
});
});

View File

@ -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,

View File

@ -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"),

View File

@ -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,