From c637944707b07810eacc768ac8fb0d04897dda70 Mon Sep 17 00:00:00 2001 From: xin zhuang <65798732+1052326311@users.noreply.github.com> Date: Mon, 25 May 2026 19:20:52 +0800 Subject: [PATCH] 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> --- CHANGELOG.md | 1 + src/commands/doctor-cron.test.ts | 23 +++++++++++++++++++++++ src/commands/doctor-cron.ts | 16 +++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9479a12d5823..522c5826b93d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/src/commands/doctor-cron.test.ts b/src/commands/doctor-cron.test.ts index bdc32b942813..325e1bc0fafd 100644 --- a/src/commands/doctor-cron.test.ts +++ b/src/commands/doctor-cron.test.ts @@ -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", () => { diff --git a/src/commands/doctor-cron.ts b/src/commands/doctor-cron.ts index b0342f6d8220..970b2a0d69b8 100644 --- a/src/commands/doctor-cron.ts +++ b/src/commands/doctor-cron.ts @@ -335,7 +335,21 @@ export async function maybeRepairLegacyCronStore(params: { prompter: Pick; }) { const storePath = resolveCronStorePath(params.cfg.cron?.store); - const store = await loadCronStore(storePath); + let store: Awaited>; + 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>; if (rawJobs.length === 0) { return;