diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md
index 8106c21c94..f71d568e5f 100644
--- a/docs/automation/cron-jobs.md
+++ b/docs/automation/cron-jobs.md
@@ -43,6 +43,8 @@ together`, and similar hints) and no descendant subagent run is still
responsible for the final answer, OpenClaw re-prompts once for the actual
result before delivery.
+
+
Task reconciliation for cron is runtime-owned: an active cron task stays live while the
cron runtime still tracks that job as running, even if an old child session row still exists.
Once the runtime stops owning the job and the 5-minute grace window expires, maintenance can
diff --git a/docs/automation/hooks.md b/docs/automation/hooks.md
index 71a239eb51..6220b25e34 100644
--- a/docs/automation/hooks.md
+++ b/docs/automation/hooks.md
@@ -164,10 +164,14 @@ Enable any bundled hook:
openclaw hooks enable
```
+
+
### session-memory details
Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `/memory/YYYY-MM-DD-slug.md`. Requires `workspace.dir` to be configured.
+
+
### bootstrap-extra-files config
```json
@@ -187,6 +191,18 @@ Extracts the last 15 user/assistant messages, generates a descriptive filename s
Paths resolve relative to workspace. Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`).
+
+
+### command-logger details
+
+Logs every slash command to `~/.openclaw/logs/commands.log`.
+
+
+
+### boot-md details
+
+Runs `BOOT.md` from the active workspace when the gateway starts.
+
## Plugin hooks
Plugins can register hooks through the Plugin SDK for deeper integration: intercepting tool calls, modifying prompts, controlling message flow, and more. The Plugin SDK exposes 28 hooks covering model resolution, agent lifecycle, message flow, tool execution, subagent coordination, and gateway lifecycle.
diff --git a/docs/cli/agents.md b/docs/cli/agents.md
index 7110732338..d43993628d 100644
--- a/docs/cli/agents.md
+++ b/docs/cli/agents.md
@@ -37,7 +37,7 @@ Use routing bindings to pin inbound channel traffic to a specific agent.
If you also want different visible skills per agent, configure
`agents.defaults.skills` and `agents.list[].skills` in `openclaw.json`. See
[Skills config](/tools/skills-config) and
-[Configuration Reference](/gateway/configuration-reference#agentsdefaultsskills).
+[Configuration Reference](/gateway/configuration-reference#agents-defaults-skills).
List bindings:
diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md
index fd53242fe7..9f0ea09099 100644
--- a/docs/gateway/configuration.md
+++ b/docs/gateway/configuration.md
@@ -224,7 +224,7 @@ When validation fails:
- Omit `agents.list[].skills` to inherit the defaults.
- Set `agents.list[].skills: []` for no skills.
- See [Skills](/tools/skills), [Skills config](/tools/skills-config), and
- the [Configuration Reference](/gateway/configuration-reference#agentsdefaultsskills).
+ the [Configuration Reference](/gateway/configuration-reference#agents-defaults-skills).
diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md
index 338ca9a7b3..699ad3730a 100644
--- a/docs/gateway/security/index.md
+++ b/docs/gateway/security/index.md
@@ -13,7 +13,7 @@ OpenClaw is **not** a hostile multi-tenant security boundary for multiple advers
If you need mixed-trust or adversarial-user operation, split trust boundaries (separate gateway + credentials, ideally separate OS users/hosts).
-**On this page:** [Trust model](#scope-first-personal-assistant-security-model) | [Quick audit](#quick-check-openclaw-security-audit) | [Hardened baseline](#hardened-baseline-in-60-seconds) | [DM access model](#dm-access-model-pairing--allowlist--open--disabled) | [Configuration hardening](#configuration-hardening-examples) | [Incident response](#incident-response)
+**On this page:** [Trust model](#scope-first-personal-assistant-security-model) | [Quick audit](#quick-check-openclaw-security-audit) | [Hardened baseline](#hardened-baseline-in-60-seconds) | [DM access model](#dm-access-model-pairing-allowlist-open-disabled) | [Configuration hardening](#configuration-hardening-examples) | [Incident response](#incident-response)
## Scope first: personal assistant security model
@@ -187,7 +187,7 @@ Allowlists gate triggers and command authorization. The `contextVisibility` sett
- `contextVisibility: "allowlist"` filters supplemental context to senders allowed by the active allowlist checks.
- `contextVisibility: "allowlist_quote"` behaves like `allowlist`, but still keeps one explicit quoted reply.
-Set `contextVisibility` per channel or per room/conversation. See [Group Chats](/channels/groups#context-visibility) for setup details.
+Set `contextVisibility` per channel or per room/conversation. See [Group Chats](/channels/groups#context-visibility-and-allowlists) for setup details.
Advisory triage guidance:
@@ -579,6 +579,8 @@ Plugins run **in-process** with the Gateway. Treat them as trusted code:
Details: [Plugins](/tools/plugin)
+
+
## DM access model (pairing / allowlist / open / disabled)
All current DM-capable channels support a DM policy (`dmPolicy` or `*.dm.policy`) that gates inbound DMs **before** the message is processed:
diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md
index a910d774dc..87e6bc92f6 100644
--- a/docs/gateway/troubleshooting.md
+++ b/docs/gateway/troubleshooting.md
@@ -111,7 +111,7 @@ Fix options:
Related:
- [/gateway/local-models](/gateway/local-models)
-- [/gateway/configuration#models](/gateway/configuration#models)
+- [/gateway/configuration](/gateway/configuration)
- [/gateway/configuration-reference#openai-compatible-endpoints](/gateway/configuration-reference#openai-compatible-endpoints)
## No replies
diff --git a/docs/help/troubleshooting.md b/docs/help/troubleshooting.md
index ec1ede3131..49e02780a4 100644
--- a/docs/help/troubleshooting.md
+++ b/docs/help/troubleshooting.md
@@ -251,18 +251,19 @@ flowchart TD
Common log signatures:
-- `cron: scheduler disabled; jobs will not run automatically` → cron is disabled.
-- `heartbeat skipped` with `reason=quiet-hours` → outside configured active hours.
-- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank/header-only scaffolding.
-- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` task mode is active but none of the task intervals are due yet.
-- `heartbeat skipped` with `reason=alerts-disabled` → all heartbeat visibility is disabled (`showOk`, `showAlerts`, and `useIndicator` are all off).
-- `requests-in-flight` → main lane busy; heartbeat wake was deferred. - `unknown accountId` → heartbeat delivery target account does not exist.
+ - `cron: scheduler disabled; jobs will not run automatically` → cron is disabled.
+ - `heartbeat skipped` with `reason=quiet-hours` → outside configured active hours.
+ - `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank/header-only scaffolding.
+ - `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` task mode is active but none of the task intervals are due yet.
+ - `heartbeat skipped` with `reason=alerts-disabled` → all heartbeat visibility is disabled (`showOk`, `showAlerts`, and `useIndicator` are all off).
+ - `requests-in-flight` → main lane busy; heartbeat wake was deferred.
+ - `unknown accountId` → heartbeat delivery target account does not exist.
- Deep pages:
+ Deep pages:
- - [/gateway/troubleshooting#cron-and-heartbeat-delivery](/gateway/troubleshooting#cron-and-heartbeat-delivery)
- - [/automation/cron-jobs#troubleshooting](/automation/cron-jobs#troubleshooting)
- - [/gateway/heartbeat](/gateway/heartbeat)
+ - [/gateway/troubleshooting#cron-and-heartbeat-delivery](/gateway/troubleshooting#cron-and-heartbeat-delivery)
+ - [/automation/cron-jobs#troubleshooting](/automation/cron-jobs#troubleshooting)
+ - [/gateway/heartbeat](/gateway/heartbeat)
@@ -338,7 +339,7 @@ flowchart TD
- [/tools/exec](/tools/exec)
- [/tools/exec-approvals](/tools/exec-approvals)
- - [/gateway/security#runtime-expectation-drift](/gateway/security#runtime-expectation-drift)
+ - [/gateway/security#what-the-audit-checks-high-level](/gateway/security#what-the-audit-checks-high-level)
@@ -376,6 +377,7 @@ flowchart TD
- [/tools/browser-wsl2-windows-remote-cdp-troubleshooting](/tools/browser-wsl2-windows-remote-cdp-troubleshooting)
+
## Related
diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md
index 3455f3cc42..04042a6530 100644
--- a/docs/plugins/sdk-channel-plugins.md
+++ b/docs/plugins/sdk-channel-plugins.md
@@ -256,7 +256,7 @@ should use `resolveInboundMentionDecision({ facts, policy })`.
Create the standard plugin files. The `channel` field in `package.json` is
what makes this a channel plugin. For the full package-metadata surface,
- see [Plugin Setup and Config](/plugins/sdk-setup#openclawchannel):
+ see [Plugin Setup and Config](/plugins/sdk-setup#openclaw-channel):
```json package.json
diff --git a/docs/tools/reactions.md b/docs/tools/reactions.md
index 26456ecdd3..98bd1d96cf 100644
--- a/docs/tools/reactions.md
+++ b/docs/tools/reactions.md
@@ -68,7 +68,7 @@ tool with the `react` action. Reaction behavior varies by channel.
Per-channel `reactionLevel` config controls how broadly the agent uses reactions. Values are typically `off`, `ack`, `minimal`, or `extensive`.
- [Telegram reactionLevel](/channels/telegram#reaction-notifications) — `channels.telegram.reactionLevel`
-- [WhatsApp reactionLevel](/channels/whatsapp#reactions) — `channels.whatsapp.reactionLevel`
+- [WhatsApp reactionLevel](/channels/whatsapp#reaction-level) — `channels.whatsapp.reactionLevel`
Set `reactionLevel` on individual channels to tune how actively the agent reacts to messages on each platform.
diff --git a/scripts/docs-link-audit.mjs b/scripts/docs-link-audit.mjs
index 1c1e899c1b..d6e121dce1 100644
--- a/scripts/docs-link-audit.mjs
+++ b/scripts/docs-link-audit.mjs
@@ -9,6 +9,8 @@ import { pathToFileURL } from "node:url";
const ROOT = process.cwd();
const DOCS_DIR = path.join(ROOT, "docs");
const DOCS_JSON_PATH = path.join(DOCS_DIR, "docs.json");
+const MINTLIFY_BROKEN_LINKS_ARGS = ["dlx", "mint", "broken-links", "--check-anchors"];
+const NODE_25_UNSUPPORTED_BY_MINTLIFY = 25;
if (!fs.existsSync(DOCS_DIR) || !fs.statSync(DOCS_DIR).isDirectory()) {
console.error("docs:check-links: missing docs directory; run from repo root.");
@@ -263,6 +265,56 @@ export function prepareAnchorAuditDocsDir(sourceDir = DOCS_DIR) {
return tempDir;
}
+/** @param {string} version */
+function parseNodeMajor(version) {
+ const major = Number.parseInt(version.split(".")[0] ?? "", 10);
+ return Number.isFinite(major) ? major : 0;
+}
+
+/**
+ * Mintlify currently rejects Node 25+. If the repo script itself is running
+ * under a too-new experimental Node, probe common local version managers and
+ * use their Node 22 wrapper for only the Mintlify child process.
+ *
+ * @param {{
+ * cwd: string;
+ * nodeVersion?: string;
+ * spawnSyncImpl: typeof spawnSync;
+ * }} params
+ */
+export function resolveMintlifyAnchorAuditInvocation(params) {
+ const nodeVersion = params.nodeVersion ?? process.versions.node;
+ if (parseNodeMajor(nodeVersion) < NODE_25_UNSUPPORTED_BY_MINTLIFY) {
+ return { command: "pnpm", args: MINTLIFY_BROKEN_LINKS_ARGS };
+ }
+
+ const node22Probe = "process.exit(Number(process.versions.node.split('.')[0]) === 22 ? 0 : 1)";
+ const candidates = [
+ {
+ command: "fnm",
+ probeArgs: ["exec", "--using=22", "node", "-e", node22Probe],
+ args: ["exec", "--using=22", "pnpm", ...MINTLIFY_BROKEN_LINKS_ARGS],
+ },
+ {
+ command: "mise",
+ probeArgs: ["exec", "node@22", "--", "node", "-e", node22Probe],
+ args: ["exec", "node@22", "--", "pnpm", ...MINTLIFY_BROKEN_LINKS_ARGS],
+ },
+ ];
+
+ for (const candidate of candidates) {
+ const probe = params.spawnSyncImpl(candidate.command, candidate.probeArgs, {
+ cwd: params.cwd,
+ stdio: "ignore",
+ });
+ if (probe.status === 0) {
+ return { command: candidate.command, args: candidate.args };
+ }
+ }
+
+ return { command: "pnpm", args: MINTLIFY_BROKEN_LINKS_ARGS };
+}
+
export function auditDocsLinks() {
/** @type {{file: string; line: number; link: string; reason: string}[]} */
const broken = [];
@@ -386,6 +438,7 @@ export function auditDocsLinks() {
/**
* @param {{
* args?: string[];
+ * nodeVersion?: string;
* spawnSyncImpl?: typeof spawnSync;
* prepareAnchorAuditDocsDirImpl?: (sourceDir?: string) => string;
* cleanupAnchorAuditDocsDirImpl?: (dir: string) => void;
@@ -403,19 +456,19 @@ export function runDocsLinkAuditCli(options = {}) {
const anchorDocsDir = prepareAnchorAuditDocsDirImpl(DOCS_DIR);
try {
- const result = spawnSyncImpl("mint", ["broken-links", "--check-anchors"], {
+ // Use the npm Mintlify package explicitly. Some developer machines also
+ // have the Swift Package Manager tool named `mint` on PATH, and that
+ // binary exits with "command 'broken-links' not found".
+ const invocation = resolveMintlifyAnchorAuditInvocation({
+ cwd: anchorDocsDir,
+ nodeVersion: options.nodeVersion,
+ spawnSyncImpl,
+ });
+ const result = spawnSyncImpl(invocation.command, invocation.args, {
cwd: anchorDocsDir,
stdio: "inherit",
});
- if (result.error?.code === "ENOENT") {
- const fallback = spawnSyncImpl("pnpm", ["dlx", "mint", "broken-links", "--check-anchors"], {
- cwd: anchorDocsDir,
- stdio: "inherit",
- });
- return fallback.status ?? 1;
- }
-
return result.status ?? 1;
} finally {
cleanupAnchorAuditDocsDirImpl(anchorDocsDir);
diff --git a/src/scripts/docs-link-audit.test.ts b/src/scripts/docs-link-audit.test.ts
index 7d9737cc89..a8615b0c0f 100644
--- a/src/scripts/docs-link-audit.test.ts
+++ b/src/scripts/docs-link-audit.test.ts
@@ -19,6 +19,7 @@ const {
) => { ok: boolean; terminal: string; loop?: boolean };
runDocsLinkAuditCli: (options?: {
args?: string[];
+ nodeVersion?: string;
spawnSyncImpl?: (
command: string,
args: string[],
@@ -146,7 +147,7 @@ describe("docs-link-audit", () => {
}
});
- it("prefers a local mint binary for anchor validation", () => {
+ it("uses Mintlify through pnpm dlx for anchor validation", () => {
let invocation:
| {
command: string;
@@ -159,6 +160,7 @@ describe("docs-link-audit", () => {
const exitCode = runDocsLinkAuditCli({
args: ["--anchors"],
+ nodeVersion: "22.21.1",
prepareAnchorAuditDocsDirImpl() {
return anchorDocsDir;
},
@@ -173,14 +175,14 @@ describe("docs-link-audit", () => {
expect(exitCode).toBe(0);
expect(invocation).toBeDefined();
- expect(invocation?.command).toBe("mint");
- expect(invocation?.args).toEqual(["broken-links", "--check-anchors"]);
+ expect(invocation?.command).toBe("pnpm");
+ expect(invocation?.args).toEqual(["dlx", "mint", "broken-links", "--check-anchors"]);
expect(invocation?.options.stdio).toBe("inherit");
expect(invocation?.options.cwd).toBe(anchorDocsDir);
expect(cleanedDir).toBe(anchorDocsDir);
});
- it("falls back to pnpm dlx when mint is not on PATH", () => {
+ it("wraps Mintlify with Node 22 when the current Node is too new", () => {
const invocations: Array<{
command: string;
args: string[];
@@ -191,6 +193,7 @@ describe("docs-link-audit", () => {
const exitCode = runDocsLinkAuditCli({
args: ["--anchors"],
+ nodeVersion: "25.3.0",
prepareAnchorAuditDocsDirImpl() {
return anchorDocsDir;
},
@@ -199,9 +202,6 @@ describe("docs-link-audit", () => {
},
spawnSyncImpl(command, args, options) {
invocations.push({ command, args, options });
- if (command === "mint") {
- return { status: null, error: { code: "ENOENT" } };
- }
return { status: 0 };
},
});
@@ -209,13 +209,19 @@ describe("docs-link-audit", () => {
expect(exitCode).toBe(0);
expect(invocations).toHaveLength(2);
expect(invocations[0]).toMatchObject({
- command: "mint",
- args: ["broken-links", "--check-anchors"],
- options: { stdio: "inherit" },
+ command: "fnm",
+ args: [
+ "exec",
+ "--using=22",
+ "node",
+ "-e",
+ "process.exit(Number(process.versions.node.split('.')[0]) === 22 ? 0 : 1)",
+ ],
+ options: { stdio: "ignore" },
});
expect(invocations[1]).toMatchObject({
- command: "pnpm",
- args: ["dlx", "mint", "broken-links", "--check-anchors"],
+ command: "fnm",
+ args: ["exec", "--using=22", "pnpm", "dlx", "mint", "broken-links", "--check-anchors"],
options: { stdio: "inherit" },
});
expect(invocations[0]?.options.cwd).toBe(anchorDocsDir);