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;