fix(doctor): warn and continue when cron job store is unreadable (#86384)

Catch non-ENOENT load failures inside maybeRepairLegacyCronStore so an
unreadable ~/.openclaw/cron/jobs.json (e.g. root-owned 0600 inside
Docker) no longer aborts the rest of the doctor health checks. The
scheduler-side loadCronStore keeps its strict throw-on-read-failure
contract.

Closes #86102

Co-authored-by: 1052326311 <1052326311@users.noreply.github.com>
This commit is contained in:
xin zhuang
2026-05-25 19:20:52 +08:00
committed by GitHub
parent 90caa3b610
commit c637944707
3 changed files with 39 additions and 1 deletions

View File

@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
- Docker E2E: dedupe scheduler lane resources so npm/service package lanes are not over-counted and serialized unnecessarily.
- Crabbox: bootstrap Git metadata for sparse remote changed gates so raw synced workspaces can run `pnpm check:changed` from the intended diff.
- xAI/LM Studio: avoid buffering ordinary bracketed or `final` prose until stream completion while watching for plain-text tool-call fallbacks.
- Doctor: warn and continue when the cron job store exists but cannot be read (e.g. owned by another user with `0600` mode inside Docker) so later health checks still run. (#86102)
- Discord: suppress a bot's previous reply body and referenced media from prompt context when a user replies to that bot message, while keeping reply metadata for routing. (#86238) Thanks @fuller-stack-dev.
- Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs.
- Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that `pnpm build` includes `ui:build`.

View File

@@ -566,6 +566,29 @@ describe("maybeRepairLegacyCronStore", () => {
expectNoteContaining("managed dreaming job", "Cron");
expectNoteContaining("Rewrote 1 managed dreaming job", "Doctor changes");
});
it("warns and continues when the cron job store cannot be read", async () => {
const storePath = await makeTempStorePath();
// Force loadCronStore to throw a non-ENOENT read error by placing a
// directory where the cron job store file would be. This mirrors the
// Docker-on-root permission failure reported in #86102 without depending
// on the test runner's effective uid (root bypasses chmod gates).
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.mkdir(storePath);
const prompter = makePrompter(true);
await expect(
maybeRepairLegacyCronStore({
cfg: { cron: { store: storePath } },
options: {},
prompter,
}),
).resolves.toBeUndefined();
expect(prompter.confirm).not.toHaveBeenCalled();
expectNoteContaining("Unable to read cron job store at", "Cron");
expectNoteContaining("later health checks will continue", "Cron");
});
});
describe("legacy WhatsApp crontab health check", () => {

View File

@@ -335,7 +335,21 @@ export async function maybeRepairLegacyCronStore(params: {
prompter: Pick<DoctorPrompter, "confirm">;
}) {
const storePath = resolveCronStorePath(params.cfg.cron?.store);
const store = await loadCronStore(storePath);
let store: Awaited<ReturnType<typeof loadCronStore>>;
try {
store = await loadCronStore(storePath);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
note(
[
`Unable to read cron job store at ${shortenHomePath(storePath)}.`,
`- ${reason}`,
`Fix the file's permissions or contents and re-run ${formatCliCommand("openclaw doctor")}; later health checks will continue.`,
].join("\n"),
"Cron",
);
return;
}
const rawJobs = (store.jobs ?? []) as unknown as Array<Record<string, unknown>>;
if (rawJobs.length === 0) {
return;