mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 08:41:40 +00:00
improve(sqlite): harden state lifecycle and snapshots (#104859)
* fix(sqlite): enable bounded vacuum and hot indexes * fix(sqlite): serialize shared schema upgrades * fix(sessions): reject stale transcript rewrites * fix(sqlite): surface busy checkpoints and refresh registry * fix(backup): verify sqlite snapshots * feat(doctor): compact shared sqlite state * test(sqlite): align hardening helpers * fix(backup): respect plugin sqlite ownership * fix(backup): snapshot plugin sqlite opaquely * fix(doctor): reject mixed sqlite actions * fix(backup): reject unsafe sqlite snapshots * fix(sessions): track unconditional transcript replacements * fix(backup): reject page-aligned sqlite truncation * fix(backup): reject state-root case aliases * docs: refresh sqlite maintenance map * fix(sqlite): register doctor compaction boundary * fix(sqlite): serialize fresh database initialization * fix(sqlite): prove main v5 agent upgrade * perf(sqlite): seek latest transcript messages * test(sessions): align terminal freshness fixtures with SQLite * docs: refresh generated map
This commit is contained in:
@@ -182,6 +182,63 @@ describe("registerMaintenanceCommands doctor action", () => {
|
||||
expect(runtime.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("passes shared-state sqlite compact mode and JSON output to doctor command", async () => {
|
||||
doctorCommand.mockResolvedValue(undefined);
|
||||
|
||||
await runMaintenanceCli(["doctor", "--state-sqlite", "compact", "--json"]);
|
||||
|
||||
expect(doctorCommand).toHaveBeenCalledTimes(1);
|
||||
const [, options] = commandCall(doctorCommand);
|
||||
expect(options.stateSqlite).toBe("compact");
|
||||
expect(options.json).toBe(true);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("rejects simultaneous shared-state and session SQLite modes", async () => {
|
||||
await runMaintenanceCli(["doctor", "--state-sqlite", "compact", "--session-sqlite", "compact"]);
|
||||
|
||||
expect(doctorCommand).not.toHaveBeenCalled();
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"doctor shared-state SQLite maintenance can only be combined with --json.",
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it("rejects shared-state SQLite maintenance combined with lint mode", async () => {
|
||||
await runMaintenanceCli(["doctor", "--state-sqlite", "compact", "--lint"]);
|
||||
|
||||
expect(doctorCommand).not.toHaveBeenCalled();
|
||||
expect(runDoctorLintCli).not.toHaveBeenCalled();
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"doctor shared-state SQLite maintenance can only be combined with --json.",
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["workspace suggestions", ["--no-workspace-suggestions"]],
|
||||
["yes mode", ["--yes"]],
|
||||
["repair mode", ["--repair"]],
|
||||
["fix mode", ["--fix"]],
|
||||
["force mode", ["--force"]],
|
||||
["non-interactive mode", ["--non-interactive"]],
|
||||
["gateway token generation", ["--generate-gateway-token"]],
|
||||
["exec secret resolution", ["--allow-exec"]],
|
||||
["deep scans", ["--deep"]],
|
||||
["post-upgrade mode", ["--post-upgrade"]],
|
||||
["session SQLite selectors", ["--session-sqlite-agent", "main"]],
|
||||
["lint selectors", ["--only", "core/example"]],
|
||||
])("rejects shared-state SQLite maintenance combined with %s", async (_label, args) => {
|
||||
await runMaintenanceCli(["doctor", "--state-sqlite", "compact", ...args]);
|
||||
|
||||
expect(doctorCommand).not.toHaveBeenCalled();
|
||||
expect(runDoctorLintCli).not.toHaveBeenCalled();
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"doctor shared-state SQLite maintenance can only be combined with --json.",
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it("rejects session sqlite selectors without session sqlite mode", async () => {
|
||||
await runMaintenanceCli(["doctor", "--session-sqlite-agent", "main"]);
|
||||
|
||||
|
||||
@@ -5,6 +5,30 @@ import { theme } from "../../../packages/terminal-core/src/theme.js";
|
||||
import { resolveDoctorCrossStateDirImports } from "../../commands/doctor-invocation.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { runCommandWithRuntime } from "../cli-utils.js";
|
||||
import { hasExplicitOptions } from "../command-options.js";
|
||||
|
||||
const STATE_SQLITE_CONFLICTING_OPTION_NAMES = [
|
||||
"workspaceSuggestions",
|
||||
"yes",
|
||||
"repair",
|
||||
"fix",
|
||||
"force",
|
||||
"nonInteractive",
|
||||
"generateGatewayToken",
|
||||
"allowExec",
|
||||
"deep",
|
||||
"lint",
|
||||
"postUpgrade",
|
||||
"sessionSqlite",
|
||||
"sessionSqliteStore",
|
||||
"sessionSqliteAgent",
|
||||
"sessionSqliteAllAgents",
|
||||
"githubIssue",
|
||||
"severityMin",
|
||||
"all",
|
||||
"skip",
|
||||
"only",
|
||||
] as const;
|
||||
|
||||
/** Register maintenance commands that inspect or mutate local OpenClaw state. */
|
||||
export function registerMaintenanceCommands(program: Command) {
|
||||
@@ -39,6 +63,7 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
"--session-sqlite <mode>",
|
||||
"Run session SQLite migration mode (dry-run|import|validate|inspect|compact|restore|recover)",
|
||||
)
|
||||
.option("--state-sqlite <mode>", "Run shared state SQLite maintenance mode (compact)")
|
||||
.option("--session-sqlite-store <path>", "With --session-sqlite: inspect one session store")
|
||||
.option("--session-sqlite-agent <id>", "With --session-sqlite: inspect one agent")
|
||||
.option(
|
||||
@@ -53,7 +78,7 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
)
|
||||
.option(
|
||||
"--json",
|
||||
"With --lint, --post-upgrade, or --session-sqlite: emit machine-readable JSON output",
|
||||
"With --lint, --post-upgrade, --state-sqlite, or --session-sqlite: emit machine-readable JSON output",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
@@ -73,7 +98,17 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
(v: string, prev: string[]) => [...prev, v],
|
||||
[],
|
||||
)
|
||||
.action(async (opts) => {
|
||||
.action(async (opts, command) => {
|
||||
if (
|
||||
typeof opts.stateSqlite === "string" &&
|
||||
hasExplicitOptions(command, STATE_SQLITE_CONFLICTING_OPTION_NAMES)
|
||||
) {
|
||||
defaultRuntime.error(
|
||||
"doctor shared-state SQLite maintenance can only be combined with --json.",
|
||||
);
|
||||
defaultRuntime.exit(2);
|
||||
return;
|
||||
}
|
||||
if (opts.lint === true) {
|
||||
await runCommandWithRuntime(
|
||||
defaultRuntime,
|
||||
@@ -113,6 +148,7 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
}
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
const { doctorCommand } = await import("../../commands/doctor.js");
|
||||
const stateSqlite = parseDoctorStateSqliteMode(opts.stateSqlite);
|
||||
const sessionSqlite = parseDoctorSessionSqliteMode(opts.sessionSqlite);
|
||||
await doctorCommand(defaultRuntime, {
|
||||
workspaceSuggestions: opts.workspaceSuggestions,
|
||||
@@ -124,6 +160,7 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
allowExec: Boolean(opts.allowExec),
|
||||
deep: Boolean(opts.deep),
|
||||
postUpgrade: Boolean(opts.postUpgrade),
|
||||
...(stateSqlite ? { stateSqlite } : {}),
|
||||
...(sessionSqlite ? { sessionSqlite } : {}),
|
||||
...(typeof opts.sessionSqliteStore === "string"
|
||||
? { sessionSqliteStore: opts.sessionSqliteStore }
|
||||
@@ -220,6 +257,7 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
function hasLintOnlyDoctorOptions(opts: {
|
||||
readonly json?: boolean;
|
||||
readonly postUpgrade?: boolean;
|
||||
readonly stateSqlite?: unknown;
|
||||
readonly sessionSqlite?: unknown;
|
||||
readonly severityMin?: unknown;
|
||||
readonly all?: boolean;
|
||||
@@ -227,7 +265,10 @@ function hasLintOnlyDoctorOptions(opts: {
|
||||
readonly only?: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
(opts.json === true && opts.postUpgrade !== true && typeof opts.sessionSqlite !== "string") ||
|
||||
(opts.json === true &&
|
||||
opts.postUpgrade !== true &&
|
||||
typeof opts.stateSqlite !== "string" &&
|
||||
typeof opts.sessionSqlite !== "string") ||
|
||||
typeof opts.severityMin === "string" ||
|
||||
opts.all === true ||
|
||||
(Array.isArray(opts.skip) && opts.skip.length > 0) ||
|
||||
@@ -251,6 +292,18 @@ function hasSessionSqliteOnlyDoctorOptions(opts: {
|
||||
);
|
||||
}
|
||||
|
||||
function parseDoctorStateSqliteMode(value: unknown): "compact" | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "compact") {
|
||||
return value;
|
||||
}
|
||||
defaultRuntime.error("Invalid --state-sqlite mode. Use compact.");
|
||||
defaultRuntime.exit(2);
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
function parseDoctorSessionSqliteMode(
|
||||
value: unknown,
|
||||
): "dry-run" | "import" | "validate" | "inspect" | "compact" | "restore" | "recover" | undefined {
|
||||
|
||||
Reference in New Issue
Block a user