mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 23:52:53 +00:00
refactor: move task state to shared sqlite
Move task run, delivery, and flow registry persistence onto the shared OpenClaw state SQLite database.
Summary:
- Store task runs, delivery state, and flow runs in state/openclaw.sqlite via the generated Kysely schema.
- Migrate shipped task sidecars into the shared state DB and archive old sidecars, including invalid-config/read-only CLI paths.
- Keep startup migration lightweight for read-only status/tasks paths while still detecting known legacy state markers and custom session stores.
Verification:
- .agents/skills/autoreview/scripts/autoreview --mode local: clean after final fix
- pnpm test src/tasks/task-registry.store.test.ts src/tasks/task-flow-registry.store.test.ts src/commands/doctor-state-migrations.test.ts -- --reporter=verbose
- pnpm test src/commands/doctor-state-migrations.test.ts src/cli/program/config-guard.test.ts src/cli/route.test.ts src/cli/command-path-policy.test.ts -- --reporter=verbose
- pnpm test src/cli/program/config-guard.test.ts src/cli/route.test.ts src/cli/command-startup-policy.test.ts src/cli/command-path-policy.test.ts src/cli/command-execution-startup.test.ts -- --reporter=verbose
- pnpm test src/cli/program/config-guard.test.ts src/cli/argv.test.ts src/cli/route.test.ts src/commands/doctor-config-preflight.state-migration.test.ts -- --reporter=verbose
- pnpm test src/tasks/task-flow-registry.store.test.ts -- --reporter=verbose
- pnpm test test/scripts/lint-suppressions.test.ts -- --reporter=verbose
- pnpm db:kysely:check
- pnpm lint:kysely
- git diff --check HEAD
- pnpm test:startup:memory
- PR CI green on 2f7d76f0d5
This commit is contained in:
committed by
GitHub
parent
e9dee8dfe1
commit
d115fb4cf9
@@ -1,4 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { note } from "../../terminal/note.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import { ensureConfigReady, testApi } from "./config-guard.js";
|
||||
@@ -68,6 +71,10 @@ async function withCapturedStdout(run: () => Promise<void>): Promise<string> {
|
||||
|
||||
describe("ensureConfigReady", () => {
|
||||
const resetConfigGuardStateForTests = testApi.resetConfigGuardStateForTests;
|
||||
const originalHome = process.env.HOME;
|
||||
const originalOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
const originalOpenClawStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
async function runEnsureConfigReady(commandPath: string[], suppressDoctorStdout = false) {
|
||||
const runtime = makeRuntime();
|
||||
@@ -90,9 +97,38 @@ describe("ensureConfigReady", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function useTempOpenClawHome(): string {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-guard-"));
|
||||
tempRoots.push(root);
|
||||
process.env.OPENCLAW_HOME = root;
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeLegacyTaskSidecarMarker(root: string): void {
|
||||
const markerPath = path.join(root, ".openclaw", "tasks", "runs.sqlite");
|
||||
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
||||
fs.writeFileSync(markerPath, "");
|
||||
}
|
||||
|
||||
function writeStateMarker(root: string, relativePath: string): void {
|
||||
const markerPath = path.join(root, ".openclaw", relativePath);
|
||||
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
||||
fs.writeFileSync(markerPath, "{}");
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resetConfigGuardStateForTests();
|
||||
if (originalHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = originalHome;
|
||||
}
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
useTempOpenClawHome();
|
||||
readConfigFileSnapshotMock.mockResolvedValue(makeSnapshot());
|
||||
loadAndMaybeMigrateDoctorConfigMock.mockImplementation(async () => ({
|
||||
snapshot: makeSnapshot(),
|
||||
@@ -100,9 +136,30 @@ describe("ensureConfigReady", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = originalHome;
|
||||
}
|
||||
if (originalOpenClawHome === undefined) {
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
} else {
|
||||
process.env.OPENCLAW_HOME = originalOpenClawHome;
|
||||
}
|
||||
if (originalOpenClawStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = originalOpenClawStateDir;
|
||||
}
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "skips doctor flow for read-only fast path commands",
|
||||
name: "skips doctor flow for status task reads without legacy state",
|
||||
commandPath: ["status"],
|
||||
expectedDoctorCalls: 0,
|
||||
},
|
||||
@@ -112,7 +169,7 @@ describe("ensureConfigReady", () => {
|
||||
expectedDoctorCalls: 0,
|
||||
},
|
||||
{
|
||||
name: "runs doctor flow for commands that may mutate state",
|
||||
name: "runs doctor flow for commands that may mutate state without legacy state",
|
||||
commandPath: ["message"],
|
||||
expectedDoctorCalls: 1,
|
||||
},
|
||||
@@ -121,13 +178,82 @@ describe("ensureConfigReady", () => {
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledTimes(expectedDoctorCalls);
|
||||
if (expectedDoctorCalls > 0) {
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledWith({
|
||||
migrateState: false,
|
||||
migrateState: true,
|
||||
migrateLegacyConfig: false,
|
||||
invalidConfigNote: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("runs doctor flow when lightweight startup detection finds legacy state", async () => {
|
||||
const root = useTempOpenClawHome();
|
||||
writeLegacyTaskSidecarMarker(root);
|
||||
|
||||
await runEnsureConfigReady(["status"]);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledWith({
|
||||
migrateState: true,
|
||||
migrateLegacyConfig: false,
|
||||
invalidConfigNote: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("runs doctor flow for legacy sessions without task sidecars", async () => {
|
||||
const root = useTempOpenClawHome();
|
||||
fs.mkdirSync(path.join(root, ".openclaw", "sessions"), { recursive: true });
|
||||
|
||||
await runEnsureConfigReady(["status"]);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Discord model picker preferences", "discord/model-picker-preferences.json"],
|
||||
["Feishu dedupe sidecar", "feishu/dedup/default.json"],
|
||||
["Telegram bot info cache", "telegram/bot-info-default.json"],
|
||||
["Telegram pairing allowFrom", "credentials/telegram-allowFrom.json"],
|
||||
["WhatsApp root auth", "credentials/creds.json"],
|
||||
])("runs doctor flow for bundled channel legacy state: %s", async (_label, relativePath) => {
|
||||
const root = useTempOpenClawHome();
|
||||
writeStateMarker(root, relativePath);
|
||||
|
||||
await runEnsureConfigReady(["status"]);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("uses shared tilde expansion for OPENCLAW_HOME in the startup detector", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-guard-home-"));
|
||||
tempRoots.push(root);
|
||||
process.env.HOME = root;
|
||||
process.env.OPENCLAW_HOME = "~/svc";
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
writeLegacyTaskSidecarMarker(path.join(root, "svc"));
|
||||
|
||||
await runEnsureConfigReady(["status"]);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("runs doctor flow for read-only commands with configured custom session stores", async () => {
|
||||
const root = useTempOpenClawHome();
|
||||
const customStore = path.join(root, "sessions", "sessions.json");
|
||||
const snapshot = {
|
||||
...makeSnapshot(),
|
||||
config: { session: { store: customStore } },
|
||||
runtimeConfig: { session: { store: customStore } },
|
||||
};
|
||||
readConfigFileSnapshotMock.mockResolvedValue(snapshot);
|
||||
loadAndMaybeMigrateDoctorConfigMock.mockResolvedValue({
|
||||
snapshot,
|
||||
baseConfig: {},
|
||||
});
|
||||
|
||||
await runEnsureConfigReady(["status"]);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("pins a valid preflight snapshot for command code reuse", async () => {
|
||||
const snapshot = {
|
||||
...makeSnapshot(),
|
||||
@@ -137,7 +263,7 @@ describe("ensureConfigReady", () => {
|
||||
};
|
||||
readConfigFileSnapshotMock.mockResolvedValue(snapshot);
|
||||
|
||||
await runEnsureConfigReady(["status"]);
|
||||
await runEnsureConfigReady(["health"]);
|
||||
|
||||
expect(setRuntimeConfigSnapshotMock).toHaveBeenCalledWith(
|
||||
snapshot.runtimeConfig,
|
||||
@@ -155,9 +281,9 @@ describe("ensureConfigReady", () => {
|
||||
.mockResolvedValueOnce(recoveredSnapshot);
|
||||
|
||||
try {
|
||||
await expect(runEnsureConfigReady(["status"])).rejects.toThrow(transientError);
|
||||
await expect(runEnsureConfigReady(["status"])).resolves.toBeDefined();
|
||||
await expect(runEnsureConfigReady(["status"])).resolves.toBeDefined();
|
||||
await expect(runEnsureConfigReady(["health"])).rejects.toThrow(transientError);
|
||||
await expect(runEnsureConfigReady(["health"])).resolves.toBeDefined();
|
||||
await expect(runEnsureConfigReady(["health"])).resolves.toBeDefined();
|
||||
} finally {
|
||||
if (originalVitest === undefined) {
|
||||
delete process.env.VITEST;
|
||||
@@ -182,7 +308,7 @@ describe("ensureConfigReady", () => {
|
||||
"",
|
||||
`Fix: ${formatCliCommand("openclaw doctor --fix")}`,
|
||||
`Inspect: ${formatCliCommand("openclaw config validate")}`,
|
||||
"Status, health, logs, and doctor commands still run with invalid config.",
|
||||
"Status, health, logs, tasks list/audit, and doctor commands still run with invalid config.",
|
||||
]);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
@@ -227,6 +353,18 @@ describe("ensureConfigReady", () => {
|
||||
const gatewayRuntime = await runEnsureConfigReady(["gateway", "health"]);
|
||||
expect(gatewayRuntime.exit).not.toHaveBeenCalled();
|
||||
|
||||
const tasksListRuntime = await runEnsureConfigReady(["tasks", "list"]);
|
||||
expect(tasksListRuntime.exit).not.toHaveBeenCalled();
|
||||
|
||||
const tasksParentRuntime = await runEnsureConfigReady(["tasks"]);
|
||||
expect(tasksParentRuntime.exit).not.toHaveBeenCalled();
|
||||
|
||||
const tasksAuditRuntime = await runEnsureConfigReady(["tasks", "audit"]);
|
||||
expect(tasksAuditRuntime.exit).not.toHaveBeenCalled();
|
||||
|
||||
const tasksRunRuntime = await runEnsureConfigReady(["tasks", "run"]);
|
||||
expect(tasksRunRuntime.exit).toHaveBeenCalledWith(1);
|
||||
|
||||
const doctorRuntime = await runEnsureConfigReady(["doctor", "fix"]);
|
||||
expect(doctorRuntime.exit).not.toHaveBeenCalled();
|
||||
expect(doctorRuntime.error).toHaveBeenCalledWith(expect.stringContaining("agentRuntime"));
|
||||
@@ -244,6 +382,7 @@ describe("ensureConfigReady", () => {
|
||||
});
|
||||
|
||||
it("runs doctor migration flow only once per module instance", async () => {
|
||||
writeLegacyTaskSidecarMarker(useTempOpenClawHome());
|
||||
const runtimeA = makeRuntime();
|
||||
const runtimeB = makeRuntime();
|
||||
|
||||
@@ -253,11 +392,13 @@ describe("ensureConfigReady", () => {
|
||||
});
|
||||
|
||||
it("still runs doctor flow when stdout suppression is enabled", async () => {
|
||||
writeLegacyTaskSidecarMarker(useTempOpenClawHome());
|
||||
await runEnsureConfigReady(["message"], true);
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("prevents preflight note noise when suppression is enabled", async () => {
|
||||
writeLegacyTaskSidecarMarker(useTempOpenClawHome());
|
||||
loadAndMaybeMigrateDoctorConfigMock.mockImplementation(async () => {
|
||||
note("Doctor warnings", "Config warnings");
|
||||
return {
|
||||
@@ -272,6 +413,7 @@ describe("ensureConfigReady", () => {
|
||||
});
|
||||
|
||||
it("allows preflight note noise when suppression is not enabled", async () => {
|
||||
writeLegacyTaskSidecarMarker(useTempOpenClawHome());
|
||||
loadAndMaybeMigrateDoctorConfigMock.mockImplementation(async () => {
|
||||
note("Doctor warnings", "Config warnings");
|
||||
return {
|
||||
@@ -286,6 +428,7 @@ describe("ensureConfigReady", () => {
|
||||
});
|
||||
|
||||
it("does not suppress unrelated concurrent stdout writes while suppressing preflight notes", async () => {
|
||||
writeLegacyTaskSidecarMarker(useTempOpenClawHome());
|
||||
let releasePreflight: (() => void) | undefined;
|
||||
let preflightStarted: (() => void) | undefined;
|
||||
const preflightStartedPromise = new Promise<void>((resolve) => {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { readConfigFileSnapshot, setRuntimeConfigSnapshot } from "../../config/config.js";
|
||||
import { resolveLegacyStateDirs, resolveOAuthDir, resolveStateDir } from "../../config/paths.js";
|
||||
import { resolveRequiredHomeDir } from "../../infra/home-dir.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { withSuppressedNotes } from "../../terminal/note.js";
|
||||
import { shouldMigrateStateFromPath } from "../argv.js";
|
||||
@@ -17,6 +22,7 @@ const ALLOWED_INVALID_GATEWAY_SUBCOMMANDS = new Set([
|
||||
"stop",
|
||||
"restart",
|
||||
]);
|
||||
const ALLOWED_INVALID_TASK_SUBCOMMANDS = new Set(["list", "audit"]);
|
||||
let didRunDoctorConfigFlow = false;
|
||||
let configSnapshotPromise: Promise<Awaited<ReturnType<typeof readConfigFileSnapshot>>> | null =
|
||||
null;
|
||||
@@ -26,6 +32,91 @@ function resetConfigGuardStateForTests() {
|
||||
configSnapshotPromise = null;
|
||||
}
|
||||
|
||||
function fileOrDirExists(pathname: string): boolean {
|
||||
try {
|
||||
return fs.existsSync(pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function dirHasFile(dir: string, predicate: (name: string) => boolean): boolean {
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.some((entry) => entry.isFile() && predicate(entry.name));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isLegacyWhatsAppAuthFile(name: string): boolean {
|
||||
if (name === "creds.json" || name === "creds.json.bak") {
|
||||
return true;
|
||||
}
|
||||
return name.endsWith(".json") && /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
|
||||
}
|
||||
|
||||
function hasBundledChannelLegacyStateMigrationInputs(stateDir: string, oauthDir: string): boolean {
|
||||
if (fileOrDirExists(path.join(stateDir, "discord", "model-picker-preferences.json"))) {
|
||||
return true;
|
||||
}
|
||||
if (dirHasFile(path.join(stateDir, "feishu", "dedup"), (name) => name.endsWith(".json"))) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
fileOrDirExists(path.join(oauthDir, "telegram-allowFrom.json")) ||
|
||||
dirHasFile(
|
||||
path.join(stateDir, "telegram"),
|
||||
(name) => name.startsWith("bot-info-") && name.endsWith(".json"),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return dirHasFile(oauthDir, isLegacyWhatsAppAuthFile);
|
||||
}
|
||||
|
||||
function hasLegacyStateMigrationInputs(): boolean {
|
||||
const stateDir = resolveStateDir(process.env, os.homedir);
|
||||
const oauthDir = resolveOAuthDir(process.env, stateDir);
|
||||
if (
|
||||
!process.env.OPENCLAW_STATE_DIR?.trim() &&
|
||||
resolveLegacyStateDirs(() => resolveRequiredHomeDir(process.env, os.homedir)).some(
|
||||
fileOrDirExists,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
[
|
||||
path.join(stateDir, "agent"),
|
||||
path.join(stateDir, "agents"),
|
||||
path.join(stateDir, "flows", "registry.sqlite"),
|
||||
path.join(stateDir, "plugin-state", "state.sqlite"),
|
||||
path.join(stateDir, "sessions"),
|
||||
path.join(stateDir, "tasks", "runs.sqlite"),
|
||||
].some(fileOrDirExists) || hasBundledChannelLegacyStateMigrationInputs(stateDir, oauthDir)
|
||||
);
|
||||
}
|
||||
|
||||
function isReadOnlyStateMigrationCommand(commandPath: string[]): boolean {
|
||||
const commandName = commandPath[0];
|
||||
const subcommandName = commandPath[1];
|
||||
return (
|
||||
commandName === "status" ||
|
||||
(commandName === "tasks" &&
|
||||
(subcommandName === undefined || ALLOWED_INVALID_TASK_SUBCOMMANDS.has(subcommandName)))
|
||||
);
|
||||
}
|
||||
|
||||
function snapshotHasConfiguredSessionStore(
|
||||
snapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>,
|
||||
): boolean {
|
||||
const cfg = snapshot.runtimeConfig ?? snapshot.config;
|
||||
const store = cfg?.session?.store;
|
||||
return typeof store === "string" && store.trim().length > 0;
|
||||
}
|
||||
|
||||
async function getConfigSnapshot() {
|
||||
// Tests often mutate config fixtures; caching can make those flaky.
|
||||
if (process.env.VITEST === "true") {
|
||||
@@ -51,30 +142,51 @@ export async function ensureConfigReady(params: {
|
||||
}): Promise<void> {
|
||||
const commandPath = params.commandPath ?? [];
|
||||
let preflightSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>> | null = null;
|
||||
if (!didRunDoctorConfigFlow && shouldMigrateStateFromPath(commandPath)) {
|
||||
const shouldConsiderStateMigration = shouldMigrateStateFromPath(commandPath);
|
||||
const isReadOnlyMigrationCommand = isReadOnlyStateMigrationCommand(commandPath);
|
||||
const runStateMigrationPreflight = async () => {
|
||||
didRunDoctorConfigFlow = true;
|
||||
const runDoctorConfigPreflight = async () =>
|
||||
(await import("../../commands/doctor-config-preflight.js")).runDoctorConfigPreflight({
|
||||
// Keep ordinary CLI startup on the lightweight validation path.
|
||||
migrateState: false,
|
||||
migrateState: true,
|
||||
migrateLegacyConfig: false,
|
||||
invalidConfigNote: false,
|
||||
});
|
||||
if (!params.suppressDoctorStdout) {
|
||||
preflightSnapshot = (await runDoctorConfigPreflight()).snapshot;
|
||||
} else {
|
||||
preflightSnapshot = (await withSuppressedNotes(runDoctorConfigPreflight)).snapshot;
|
||||
}
|
||||
return !params.suppressDoctorStdout
|
||||
? (await runDoctorConfigPreflight()).snapshot
|
||||
: (await withSuppressedNotes(runDoctorConfigPreflight)).snapshot;
|
||||
};
|
||||
if (
|
||||
!didRunDoctorConfigFlow &&
|
||||
shouldConsiderStateMigration &&
|
||||
(!isReadOnlyMigrationCommand || hasLegacyStateMigrationInputs())
|
||||
) {
|
||||
preflightSnapshot = await runStateMigrationPreflight();
|
||||
}
|
||||
|
||||
const snapshot = preflightSnapshot ?? (await getConfigSnapshot());
|
||||
let snapshot = preflightSnapshot ?? (await getConfigSnapshot());
|
||||
if (
|
||||
!preflightSnapshot &&
|
||||
!didRunDoctorConfigFlow &&
|
||||
shouldConsiderStateMigration &&
|
||||
isReadOnlyMigrationCommand &&
|
||||
snapshot.valid &&
|
||||
snapshotHasConfiguredSessionStore(snapshot)
|
||||
) {
|
||||
preflightSnapshot = await runStateMigrationPreflight();
|
||||
snapshot = preflightSnapshot;
|
||||
}
|
||||
const commandName = commandPath[0];
|
||||
const subcommandName = commandPath[1];
|
||||
const isBareGatewayForegroundRun =
|
||||
commandName === "gateway" && (subcommandName === undefined || subcommandName.trim() === "");
|
||||
const isReadOnlyTaskStateCommand =
|
||||
commandName === "tasks" &&
|
||||
(subcommandName === undefined || ALLOWED_INVALID_TASK_SUBCOMMANDS.has(subcommandName));
|
||||
const allowInvalid = commandName
|
||||
? params.allowInvalid === true ||
|
||||
ALLOWED_INVALID_COMMANDS.has(commandName) ||
|
||||
isReadOnlyTaskStateCommand ||
|
||||
isBareGatewayForegroundRun ||
|
||||
(commandName === "gateway" &&
|
||||
subcommandName &&
|
||||
@@ -134,7 +246,9 @@ export async function ensureConfigReady(params: {
|
||||
`${muted("Inspect:")} ${commandText(formatCliCommand("openclaw config validate"))}`,
|
||||
);
|
||||
params.runtime.error(
|
||||
muted("Status, health, logs, and doctor commands still run with invalid config."),
|
||||
muted(
|
||||
"Status, health, logs, tasks list/audit, and doctor commands still run with invalid config.",
|
||||
),
|
||||
);
|
||||
if (!allowInvalid) {
|
||||
params.runtime.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user