mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:00:43 +00:00
fix(doctor): require confirmation for transcript archive
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/models: keep per-agent primary models strict when `fallbacks` is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.
|
||||
- Gateway/models: add `models.pricing.enabled` so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.
|
||||
- Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale `OPENCLAW_GATEWAY_TOKEN` or `OPENCLAW_GATEWAY_PASSWORD` values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.
|
||||
- Doctor/state: require an interactive confirmation before archiving orphan transcript files, so `openclaw doctor --fix` no longer silently renames recoverable session history after upgrades regenerate `sessions.json`. Fixes #73106. Thanks @scottgl9.
|
||||
- Cron/Telegram: preserve explicit `:topic:` delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.
|
||||
- Build/runtime: write the runtime-postbuild stamp after `pnpm build` writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.
|
||||
- Build/runtime: preserve staged bundled-plugin runtime dependency caches across source-checkout tsdown rebuilds, so local CLI and gateway-watch rebuilds no longer recreate large plugin dependency trees before starting. Refs #73205. Thanks @SymbolStar.
|
||||
|
||||
@@ -41,7 +41,7 @@ Notes:
|
||||
- Interactive prompts (like keychain/OAuth fixes) only run when stdin is a TTY and `--non-interactive` is **not** set. Headless runs (cron, Telegram, no terminal) will skip prompts.
|
||||
- Performance: non-interactive `doctor` runs skip eager plugin loading so headless health checks stay fast. Interactive sessions still fully load plugins when a check needs their contribution.
|
||||
- `--fix` (alias for `--repair`) writes a backup to `~/.openclaw/openclaw.json.bak` and drops unknown config keys, listing each removal.
|
||||
- State integrity checks now detect orphan transcript files in the sessions directory and can archive them as `.deleted.<timestamp>` to reclaim space safely.
|
||||
- State integrity checks now detect orphan transcript files in the sessions directory. Archiving them as `.deleted.<timestamp>` requires an interactive confirmation; `--fix`, `--yes`, and headless runs leave them in place.
|
||||
- Doctor also scans `~/.openclaw/cron/jobs.json` (or `cron.store`) for legacy cron job shapes and can rewrite them in place before the scheduler has to auto-normalize them at runtime.
|
||||
- Doctor repairs missing bundled plugin runtime dependencies without writing into packaged global installs. For root-owned npm installs or hardened systemd units, set `OPENCLAW_PLUGIN_STAGE_DIR` to a writable directory such as `/var/lib/openclaw/plugin-runtime-deps`; it can also be a path-list such as `/opt/openclaw/plugin-runtime-deps:/var/lib/openclaw/plugin-runtime-deps`, where earlier roots are read-only lookup layers and the final root is the repair target.
|
||||
- Doctor repairs stale plugin config by removing missing plugin ids from `plugins.allow`/`plugins.entries`, plus matching dangling channel config, heartbeat targets, and channel model overrides when plugin discovery is healthy.
|
||||
|
||||
@@ -85,6 +85,42 @@ describe("createDoctorPrompter", () => {
|
||||
expect(confirmMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not auto-accept runtime repairs that require interactive confirmation", async () => {
|
||||
const prompter = createRepairPrompter();
|
||||
|
||||
await expect(
|
||||
prompter.confirmRuntimeRepair({
|
||||
message: "Archive orphan transcripts?",
|
||||
initialValue: false,
|
||||
requiresInteractiveConfirmation: true,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
expect(confirmMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not accept interactive-only runtime repairs through --yes defaults", async () => {
|
||||
setNonInteractiveTerminal();
|
||||
const prompter = createDoctorPrompter({
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
},
|
||||
options: {
|
||||
yes: true,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
prompter.confirmRuntimeRepair({
|
||||
message: "Archive orphan transcripts?",
|
||||
initialValue: true,
|
||||
requiresInteractiveConfirmation: true,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
expect(confirmMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps skip-in-non-interactive prompts disabled during update-mode repairs", async () => {
|
||||
process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1";
|
||||
const prompter = createRepairPrompter();
|
||||
|
||||
@@ -11,11 +11,16 @@ import { guardCancel } from "./onboard-helpers.js";
|
||||
|
||||
export type { DoctorOptions } from "./doctor.types.js";
|
||||
|
||||
type DoctorConfirmParams = Parameters<typeof confirm>[0];
|
||||
type DoctorRuntimeRepairConfirmParams = DoctorConfirmParams & {
|
||||
requiresInteractiveConfirmation?: boolean;
|
||||
};
|
||||
|
||||
export type DoctorPrompter = {
|
||||
confirm: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmAutoFix: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmAggressiveAutoFix: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmRuntimeRepair: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmRuntimeRepair: (params: DoctorRuntimeRepairConfirmParams) => Promise<boolean>;
|
||||
select: <T>(params: Parameters<typeof select>[0], fallback: T) => Promise<T>;
|
||||
shouldRepair: boolean;
|
||||
shouldForce: boolean;
|
||||
@@ -71,19 +76,26 @@ export function createDoctorPrompter(params: {
|
||||
);
|
||||
},
|
||||
confirmRuntimeRepair: async (p) => {
|
||||
if (shouldAutoApproveDoctorFix(repairMode, { blockDuringUpdate: true })) {
|
||||
const { requiresInteractiveConfirmation, ...confirmParams } = p;
|
||||
if (
|
||||
requiresInteractiveConfirmation !== true &&
|
||||
shouldAutoApproveDoctorFix(repairMode, { blockDuringUpdate: true })
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (requiresInteractiveConfirmation === true && !repairMode.canPrompt) {
|
||||
return false;
|
||||
}
|
||||
if (repairMode.nonInteractive) {
|
||||
return false;
|
||||
}
|
||||
if (!repairMode.canPrompt) {
|
||||
return p.initialValue ?? false;
|
||||
return confirmParams.initialValue ?? false;
|
||||
}
|
||||
return guardCancel(
|
||||
await confirm({
|
||||
...p,
|
||||
message: stylePromptMessage(p.message),
|
||||
...confirmParams,
|
||||
message: stylePromptMessage(confirmParams.message),
|
||||
}),
|
||||
params.runtime,
|
||||
);
|
||||
|
||||
@@ -296,12 +296,35 @@ describe("doctor state integrity oauth dir checks", () => {
|
||||
expect(confirmRuntimeRepair).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("This only renames them to *.deleted.<timestamp>."),
|
||||
requiresInteractiveConfirmation: true,
|
||||
}),
|
||||
);
|
||||
const files = fs.readdirSync(sessionsDir);
|
||||
expect(files.some((name) => name.startsWith("orphan-session.jsonl.deleted."))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not auto-archive orphan transcripts from non-interactive repair mode", async () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
setupSessionState(cfg, process.env, process.env.HOME ?? "");
|
||||
const sessionsDir = resolveSessionTranscriptsDirForAgent("main", process.env, () => tempHome);
|
||||
fs.writeFileSync(path.join(sessionsDir, "orphan-session.jsonl"), '{"type":"session"}\n');
|
||||
const confirmRuntimeRepair = vi.fn(
|
||||
async (params: { initialValue?: boolean; requiresInteractiveConfirmation?: boolean }) =>
|
||||
params.requiresInteractiveConfirmation !== true,
|
||||
);
|
||||
await noteStateIntegrity(cfg, { confirmRuntimeRepair, note: noteMock });
|
||||
|
||||
expect(confirmRuntimeRepair).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialValue: false,
|
||||
requiresInteractiveConfirmation: true,
|
||||
}),
|
||||
);
|
||||
const files = fs.readdirSync(sessionsDir);
|
||||
expect(files).toContain("orphan-session.jsonl");
|
||||
expect(files.some((name) => name.startsWith("orphan-session.jsonl.deleted."))).toBe(false);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"does not archive referenced transcripts when the state dir path resolves through a symlink",
|
||||
async () => {
|
||||
|
||||
@@ -28,7 +28,11 @@ import { note } from "../terminal/note.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
|
||||
type DoctorPrompterLike = {
|
||||
confirmRuntimeRepair: (params: { message: string; initialValue?: boolean }) => Promise<boolean>;
|
||||
confirmRuntimeRepair: (params: {
|
||||
message: string;
|
||||
initialValue?: boolean;
|
||||
requiresInteractiveConfirmation?: boolean;
|
||||
}) => Promise<boolean>;
|
||||
note?: typeof note;
|
||||
};
|
||||
|
||||
@@ -921,6 +925,7 @@ export async function noteStateIntegrity(
|
||||
const archiveOrphans = await prompter.confirmRuntimeRepair({
|
||||
message: `Archive ${orphanCount} in ${displaySessionsDir}? This only renames them to *.deleted.<timestamp>.`,
|
||||
initialValue: false,
|
||||
requiresInteractiveConfirmation: true,
|
||||
});
|
||||
if (archiveOrphans) {
|
||||
let archived = 0;
|
||||
|
||||
Reference in New Issue
Block a user