Matrix: improve migration startup warnings

This commit is contained in:
Gustavo Madeira Santana
2026-03-08 22:12:03 -04:00
parent b57488d1ff
commit e5fbedf012
7 changed files with 209 additions and 30 deletions

View File

@@ -59,8 +59,7 @@ OpenClaw cannot automatically recover:
Current warning scope:
- stale custom Matrix plugin path installs are surfaced by `openclaw doctor` today
- gateway startup does not currently emit a separate Matrix-specific custom-path warning
- stale custom Matrix plugin path installs are surfaced by both gateway startup and `openclaw doctor`
If your old installation had local-only encrypted history that was never backed up, some older encrypted messages may remain unreadable after the upgrade.

View File

@@ -26,6 +26,10 @@ import {
isTrustedSafeBinPath,
normalizeTrustedSafeBinDirs,
} from "../infra/exec-safe-bin-trust.js";
import {
detectMatrixInstallPathIssue,
formatMatrixInstallPathIssue,
} from "../infra/matrix-install-path-warnings.js";
import {
autoPrepareLegacyMatrixCrypto,
detectLegacyMatrixCrypto,
@@ -300,6 +304,7 @@ function formatMatrixLegacyStatePreview(
"- Matrix plugin upgraded in place.",
`- Legacy sync store: ${detection.legacyStoragePath} -> ${detection.targetStoragePath}`,
`- Legacy crypto store: ${detection.legacyCryptoPath} -> ${detection.targetCryptoPath}`,
...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
'- Run "openclaw doctor --fix" to migrate this Matrix state now.',
].join("\n");
}
@@ -326,33 +331,14 @@ function formatMatrixLegacyCryptoPreview(
}
async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise<string[]> {
const install = cfg.plugins?.installs?.matrix;
if (!install || install.source !== "path") {
const issue = await detectMatrixInstallPathIssue(cfg);
if (!issue) {
return [];
}
const candidatePaths = [install.sourcePath, install.installPath]
.map((value) => (typeof value === "string" ? value.trim() : ""))
.filter(Boolean);
if (candidatePaths.length === 0) {
return [];
}
for (const candidatePath of candidatePaths) {
try {
await fs.access(path.resolve(candidatePath));
return [];
} catch {
// keep checking remaining candidates
}
}
const missingPath = candidatePaths[0] ?? "(unknown)";
return [
`- Matrix is installed from a custom path that no longer exists: ${missingPath}`,
`- Reinstall with "${formatCliCommand("openclaw plugins install @openclaw/matrix")}".`,
`- If you are running from a repo checkout, you can also use "${formatCliCommand("openclaw plugins install ./extensions/matrix")}".`,
];
return formatMatrixInstallPathIssue({
issue,
formatCommand: formatCliCommand,
}).map((entry) => `- ${entry}`);
}
function scanTelegramAllowFromUsernameEntries(cfg: OpenClawConfig): TelegramAllowFromUsernameHit[] {

View File

@@ -34,6 +34,10 @@ import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js";
import { getMachineDisplayName } from "../infra/machine-name.js";
import {
detectMatrixInstallPathIssue,
formatMatrixInstallPathIssue,
} from "../infra/matrix-install-path-warnings.js";
import { autoPrepareLegacyMatrixCrypto } from "../infra/matrix-legacy-crypto.js";
import { autoMigrateLegacyMatrixState } from "../infra/matrix-legacy-state.js";
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
@@ -343,6 +347,18 @@ export async function startGatewayServer(
env: process.env,
log,
});
const matrixInstallPathIssue = await detectMatrixInstallPathIssue(
autoEnable.changes.length > 0 ? autoEnable.config : configSnapshot.config,
);
if (matrixInstallPathIssue) {
const lines = formatMatrixInstallPathIssue({
issue: matrixInstallPathIssue,
formatCommand: formatCliCommand,
});
log.warn(
`gateway: matrix install path warning:\n${lines.map((entry) => `- ${entry}`).join("\n")}`,
);
}
const emitSecretsStateEvent = (
code: "SECRETS_RELOADER_DEGRADED" | "SECRETS_RELOADER_RECOVERED",
message: string,

View File

@@ -0,0 +1,58 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { withTempHome } from "../../test/helpers/temp-home.js";
import type { OpenClawConfig } from "../config/config.js";
import {
detectMatrixInstallPathIssue,
formatMatrixInstallPathIssue,
} from "./matrix-install-path-warnings.js";
describe("matrix install path warnings", () => {
it("detects stale custom Matrix plugin paths", async () => {
const cfg: OpenClawConfig = {
plugins: {
installs: {
matrix: {
source: "path",
sourcePath: "/tmp/openclaw-matrix-missing",
installPath: "/tmp/openclaw-matrix-missing",
},
},
},
};
const issue = await detectMatrixInstallPathIssue(cfg);
expect(issue).toEqual({ missingPath: "/tmp/openclaw-matrix-missing" });
expect(
formatMatrixInstallPathIssue({
issue: issue!,
}),
).toEqual([
"Matrix is installed from a custom path that no longer exists: /tmp/openclaw-matrix-missing",
'Reinstall with "openclaw plugins install @openclaw/matrix".',
'If you are running from a repo checkout, you can also use "openclaw plugins install ./extensions/matrix".',
]);
});
it("skips warnings when the configured custom path exists", async () => {
await withTempHome(async (home) => {
const pluginPath = path.join(home, "matrix-plugin");
await fs.mkdir(pluginPath, { recursive: true });
const cfg: OpenClawConfig = {
plugins: {
installs: {
matrix: {
source: "path",
sourcePath: pluginPath,
installPath: pluginPath,
},
},
},
};
await expect(detectMatrixInstallPathIssue(cfg)).resolves.toBeNull();
});
});
});

View File

@@ -0,0 +1,52 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
export type MatrixInstallPathIssue = {
missingPath: string;
};
function resolveMatrixInstallCandidatePaths(cfg: OpenClawConfig): string[] {
const install = cfg.plugins?.installs?.matrix;
if (!install || install.source !== "path") {
return [];
}
return [install.sourcePath, install.installPath]
.map((value) => (typeof value === "string" ? value.trim() : ""))
.filter(Boolean);
}
export async function detectMatrixInstallPathIssue(
cfg: OpenClawConfig,
): Promise<MatrixInstallPathIssue | null> {
const candidatePaths = resolveMatrixInstallCandidatePaths(cfg);
if (candidatePaths.length === 0) {
return null;
}
for (const candidatePath of candidatePaths) {
try {
await fs.access(path.resolve(candidatePath));
return null;
} catch {
// keep checking remaining candidates
}
}
return {
missingPath: candidatePaths[0] ?? "(unknown)",
};
}
export function formatMatrixInstallPathIssue(params: {
issue: MatrixInstallPathIssue;
formatCommand?: (command: string) => string;
}): string[] {
const formatCommand = params.formatCommand ?? ((command: string) => command);
return [
`Matrix is installed from a custom path that no longer exists: ${params.issue.missingPath}`,
`Reinstall with "${formatCommand("openclaw plugins install @openclaw/matrix")}".`,
`If you are running from a repo checkout, you can also use "${formatCommand("openclaw plugins install ./extensions/matrix")}".`,
];
}

View File

@@ -83,4 +83,40 @@ describe("matrix legacy state migration", () => {
expect(fs.existsSync(detection.targetStoragePath)).toBe(true);
});
});
it("records which account receives a flat legacy store when multiple Matrix accounts exist", async () => {
await withTempHome(async (home) => {
const stateDir = path.join(home, ".openclaw");
writeFile(path.join(stateDir, "matrix", "bot-storage.json"), '{"next_batch":"s1"}');
const cfg: OpenClawConfig = {
channels: {
matrix: {
defaultAccount: "work",
accounts: {
work: {
homeserver: "https://matrix.example.org",
userId: "@work-bot:example.org",
accessToken: "tok-work",
},
alerts: {
homeserver: "https://matrix.example.org",
userId: "@alerts-bot:example.org",
accessToken: "tok-alerts",
},
},
},
},
};
const detection = detectLegacyMatrixState({ cfg, env: process.env });
expect(detection && "warning" in detection).toBe(false);
if (!detection || "warning" in detection) {
throw new Error("expected a migratable Matrix legacy state plan");
}
expect(detection.accountId).toBe("work");
expect(detection.selectionNote).toContain('account "work"');
});
});
});

View File

@@ -33,6 +33,7 @@ type MatrixLegacyStatePlan = {
targetRootDir: string;
targetStoragePath: string;
targetCryptoPath: string;
selectionNote?: string;
};
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -128,6 +129,32 @@ function resolveMatrixTargetAccountId(cfg: OpenClawConfig): string {
return DEFAULT_ACCOUNT_ID;
}
function resolveMatrixFlatStoreSelectionNote(params: {
channel: Record<string, unknown>;
accountId: string;
}): string | undefined {
const accounts = isRecord(params.channel.accounts) ? params.channel.accounts : null;
if (!accounts) {
return undefined;
}
const configuredAccounts = Array.from(
new Set(
Object.keys(accounts)
.map((accountId) => normalizeAccountId(accountId))
.filter(Boolean),
),
);
if (configuredAccounts.length <= 1) {
return undefined;
}
return (
`Legacy Matrix flat store uses one shared on-disk state, so it will be migrated into ` +
`account "${params.accountId}".`
);
}
function resolveMatrixMigrationPlan(params: {
cfg: OpenClawConfig;
env: NodeJS.ProcessEnv;
@@ -149,6 +176,7 @@ function resolveMatrixMigrationPlan(params: {
const accountId = resolveMatrixTargetAccountId(params.cfg);
const account = resolveMatrixAccountConfig(params.cfg, accountId);
const stored = loadStoredMatrixCredentials(params.env, accountId);
const selectionNote = resolveMatrixFlatStoreSelectionNote({ channel, accountId });
const homeserver = typeof account.homeserver === "string" ? account.homeserver.trim() : "";
const configUserId = typeof account.userId === "string" ? account.userId.trim() : "";
@@ -191,6 +219,7 @@ function resolveMatrixMigrationPlan(params: {
targetRootDir: rootDir,
targetStoragePath: path.join(rootDir, "bot-storage.json"),
targetCryptoPath: path.join(rootDir, "crypto"),
selectionNote,
};
}
@@ -266,10 +295,13 @@ export async function autoMigrateLegacyMatrixState(params: {
});
if (changes.length > 0) {
const details = [
...changes.map((entry) => `- ${entry}`),
...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
"- No user action required.",
];
params.log?.info?.(
`matrix: plugin upgraded in place for account "${detection.accountId}".\n${changes
.map((entry) => `- ${entry}`)
.join("\n")}\n- No user action required.`,
`matrix: plugin upgraded in place for account "${detection.accountId}".\n${details.join("\n")}`,
);
}
if (warnings.length > 0) {