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:
Vincent Koc
2026-07-12 11:35:22 +08:00
committed by GitHub
parent a74fb334a7
commit 350bac4e0e
32 changed files with 3241 additions and 232 deletions

View File

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

View File

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