mirror of https://github.com/openclaw/openclaw.git
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:
parent
728aee277f
commit
d4c443bc1e
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue