diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed468bd1c6..9bc983a78b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Doctor/WhatsApp: warn when Linux crontabs still run the legacy `ensure-whatsapp.sh` health check, which can misreport `Gateway inactive` when cron lacks the systemd user-bus environment. Fixes #60204. Thanks @mySebbe. - Slack/setup: print the generated app manifest as plain JSON instead of embedding it inside the framed setup note, so it can be copied into Slack without deleting border characters. Fixes #65751. Thanks @theDanielJLewis. - Channels/WhatsApp: route CLI logout through the live Gateway and stop runtime-backed listeners before channel removal, so removing a WhatsApp account does not leave the old socket replying until restart. Fixes #67746. Thanks @123Mismail. - Agents/Codex: stop prompting message-tool-only source turns to finish with `NO_REPLY`, so quiet turns are represented by not calling the visible message tool instead of conflicting final-text instructions. Thanks @pashpashpash. diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index 572b041159e..5a9662bd3a6 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -553,6 +553,14 @@ Behavior notes: openclaw logs --follow ``` + If `~/.openclaw/logs/whatsapp-health.log` says `Gateway inactive` but + `openclaw gateway status` and `openclaw channels status --probe` show the + gateway and WhatsApp are healthy, run `openclaw doctor`. On Linux, doctor + warns about legacy crontab entries that still invoke + `~/.openclaw/bin/ensure-whatsapp.sh`; remove those stale entries with + `crontab -e` because cron can lack the systemd user-bus environment and + make that old script misreport gateway health. + If needed, re-link with `channels login`. diff --git a/docs/cli/doctor.md b/docs/cli/doctor.md index 301bdd85266..e63933e443c 100644 --- a/docs/cli/doctor.md +++ b/docs/cli/doctor.md @@ -44,6 +44,7 @@ Notes: - `doctor --fix --non-interactive` reports missing or stale gateway service definitions but does not install or rewrite them outside update repair mode. Run `openclaw gateway install` for a missing service, or `openclaw gateway install --force` when you intentionally want to replace the launcher. - State integrity checks now detect orphan transcript files in the sessions directory. Archiving them as `.deleted.` 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. +- On Linux, doctor warns when the user's crontab still runs legacy `~/.openclaw/bin/ensure-whatsapp.sh`; that script is no longer maintained and can log false WhatsApp gateway outages when cron lacks the systemd user-bus environment. - Doctor cleans legacy plugin dependency staging state created by older OpenClaw versions. It also repairs missing configured downloadable plugins when the registry can resolve them. - 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. - Doctor quarantines invalid plugin config by disabling the affected `plugins.entries.` entry and removing its invalid `config` payload. Gateway startup already skips only that bad plugin so other plugins and channels can keep running. diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index bce5e33868f..10192a2e838 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -298,6 +298,8 @@ That stages grounded durable candidates into the short-term dreaming store while Doctor only auto-migrates `notify: true` jobs when it can do so without changing behavior. If a job combines legacy notify fallback with an existing non-webhook delivery mode, doctor warns and leaves that job for manual review. + On Linux, doctor also warns when the user's crontab still invokes legacy `~/.openclaw/bin/ensure-whatsapp.sh`. That host-local script is not maintained by current OpenClaw and can write false `Gateway inactive` messages to `~/.openclaw/logs/whatsapp-health.log` when cron cannot reach the systemd user bus. Remove the stale crontab entry with `crontab -e`; use `openclaw channels status --probe`, `openclaw doctor`, and `openclaw gateway status` for current health checks. + Doctor scans every agent session directory for stale write-lock files — files left behind when a session exited abnormally. For each lock file found it reports: the path, PID, whether the PID is still alive, lock age, and whether it is considered stale (dead PID or older than 30 minutes). In `--fix` / `--repair` mode it removes stale lock files automatically; otherwise it prints a note and instructs you to rerun with `--fix`. diff --git a/src/commands/doctor-cron.test.ts b/src/commands/doctor-cron.test.ts index 5eb0a9e4bc2..e2f949fb346 100644 --- a/src/commands/doctor-cron.test.ts +++ b/src/commands/doctor-cron.test.ts @@ -3,7 +3,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { maybeRepairLegacyCronStore } from "./doctor-cron.js"; +import { maybeRepairLegacyCronStore, noteLegacyWhatsAppCrontabHealthCheck } from "./doctor-cron.js"; type TerminalNote = (message: string, title?: string) => void; @@ -385,3 +385,46 @@ describe("maybeRepairLegacyCronStore", () => { ); }); }); + +describe("noteLegacyWhatsAppCrontabHealthCheck", () => { + it("warns about legacy ensure-whatsapp crontab entries on Linux", async () => { + await noteLegacyWhatsAppCrontabHealthCheck({ + platform: "linux", + readCrontab: async () => ({ + stdout: [ + "# keep comments ignored", + "*/5 * * * * ~/.openclaw/bin/ensure-whatsapp.sh >> ~/.openclaw/logs/whatsapp-health.log 2>&1", + "0 9 * * * /usr/bin/true", + "", + ].join("\n"), + }), + }); + + expect(noteMock).toHaveBeenCalledWith( + expect.stringContaining("Legacy WhatsApp crontab health check detected"), + "Cron", + ); + expect(noteMock).toHaveBeenCalledWith( + expect.stringContaining("systemd user bus environment is missing"), + "Cron", + ); + expect(noteMock).toHaveBeenCalledWith(expect.stringContaining("Matched 1 entry"), "Cron"); + }); + + it("ignores missing crontab support and non-Linux hosts", async () => { + await noteLegacyWhatsAppCrontabHealthCheck({ + platform: "darwin", + readCrontab: async () => { + throw new Error("should not read crontab on non-Linux"); + }, + }); + await noteLegacyWhatsAppCrontabHealthCheck({ + platform: "linux", + readCrontab: async () => { + throw Object.assign(new Error("crontab missing"), { code: "ENOENT" }); + }, + }); + + expect(noteMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/doctor-cron.ts b/src/commands/doctor-cron.ts index e85686fb65f..f907abc8d87 100644 --- a/src/commands/doctor-cron.ts +++ b/src/commands/doctor-cron.ts @@ -1,3 +1,5 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveCronStorePath, loadCronStore, saveCronStore } from "../cron/store.js"; @@ -20,6 +22,12 @@ type CronDoctorOutcome = { warnings: string[]; }; +type CrontabReader = () => Promise<{ stdout: string; stderr?: string }>; + +const execFileAsync = promisify(execFile); +const LEGACY_WHATSAPP_HEALTH_SCRIPT_RE = + /(?:^|\s)(?:"[^"]*ensure-whatsapp\.sh"|'[^']*ensure-whatsapp\.sh'|[^\s#;|&]*ensure-whatsapp\.sh)\b/u; + function pluralize(count: number, noun: string) { return `${count} ${noun}${count === 1 ? "" : "s"}`; } @@ -129,6 +137,58 @@ function migrateLegacyNotifyFallback(params: { return { changed, warnings }; } +async function readUserCrontab(): Promise<{ stdout: string; stderr?: string }> { + const result = await execFileAsync("crontab", ["-l"], { + encoding: "utf8", + windowsHide: true, + }); + return { + stdout: result.stdout, + stderr: result.stderr, + }; +} + +function findLegacyWhatsAppHealthCrontabLines(crontab: string): string[] { + return crontab + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith("#")) + .filter((line) => LEGACY_WHATSAPP_HEALTH_SCRIPT_RE.test(line)); +} + +export async function noteLegacyWhatsAppCrontabHealthCheck( + params: { + platform?: NodeJS.Platform; + readCrontab?: CrontabReader; + } = {}, +): Promise { + if ((params.platform ?? process.platform) !== "linux") { + return; + } + + let crontab: string; + try { + crontab = (await (params.readCrontab ?? readUserCrontab)()).stdout; + } catch { + return; + } + + const legacyLines = findLegacyWhatsAppHealthCrontabLines(crontab); + if (legacyLines.length === 0) { + return; + } + + note( + [ + "Legacy WhatsApp crontab health check detected.", + "`~/.openclaw/bin/ensure-whatsapp.sh` is not maintained by current OpenClaw and can misreport `Gateway inactive` from cron when the systemd user bus environment is missing.", + `Remove the stale crontab entry with ${formatCliCommand("crontab -e")}; use ${formatCliCommand("openclaw channels status --probe")}, ${formatCliCommand("openclaw doctor")}, and ${formatCliCommand("openclaw gateway status")} for current health checks.`, + `Matched ${pluralize(legacyLines.length, "entry")}.`, + ].join("\n"), + "Cron", + ); +} + export async function maybeRepairLegacyCronStore(params: { cfg: OpenClawConfig; options: DoctorOptions; diff --git a/src/flows/doctor-health-contributions.ts b/src/flows/doctor-health-contributions.ts index 7f157dd6d23..8ddef04d820 100644 --- a/src/flows/doctor-health-contributions.ts +++ b/src/flows/doctor-health-contributions.ts @@ -284,7 +284,9 @@ async function runSessionTranscriptsHealth(ctx: DoctorHealthFlowContext): Promis } async function runLegacyCronHealth(ctx: DoctorHealthFlowContext): Promise { - const { maybeRepairLegacyCronStore } = await import("../commands/doctor-cron.js"); + const { maybeRepairLegacyCronStore, noteLegacyWhatsAppCrontabHealthCheck } = + await import("../commands/doctor-cron.js"); + await noteLegacyWhatsAppCrontabHealthCheck(); await maybeRepairLegacyCronStore({ cfg: ctx.cfg, options: ctx.options,