Skip to content

Commit 6bb648d

Browse files
committed
Merge upstream/main into plain approval prompts
2 parents 6f31345 + b6ce59d commit 6bb648d

929 files changed

Lines changed: 1217 additions & 349 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/gateway/heartbeat.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ On normal runs, `HEARTBEAT.md` is only injected when heartbeat guidance is enabl
387387

388388
On the native Codex harness, `HEARTBEAT.md` content is not injected into the turn. If the file exists and has non-whitespace content, the heartbeat collaboration-mode instructions point Codex at the file and tell it to read before proceeding.
389389

390-
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls. That skip is reported as `reason=empty-heartbeat-file`. If the file is missing, the heartbeat still runs and the model decides what to do.
390+
If `HEARTBEAT.md` exists but is effectively empty (only blank lines, Markdown/HTML comments, Markdown headings like `# Heading`, fence markers, or empty checklist stubs), OpenClaw skips the heartbeat run to save API calls. That skip is reported as `reason=empty-heartbeat-file`. If the file is missing, the heartbeat still runs and the model decides what to do.
391391

392392
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
393393

docs/gateway/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ Look for:
691691
- `cron: scheduler disabled; jobs will not run automatically` → cron disabled.
692692
- `cron: timer tick failed` → scheduler tick failed; check file/log/runtime errors.
693693
- `heartbeat skipped` with `reason=quiet-hours` → outside active hours window.
694-
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank lines / markdown headers, so OpenClaw skips the model call.
694+
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank, comment, header, fence, or empty-checklist scaffolding, so OpenClaw skips the model call.
695695
- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` contains a `tasks:` block, but none of the tasks are due on this tick.
696696
- `heartbeat: unknown accountId` → invalid account id for heartbeat delivery target.
697697
- `heartbeat skipped` with `reason=dm-blocked` → heartbeat target resolved to a DM-style destination while `agents.defaults.heartbeat.directPolicy` (or per-agent override) is set to `block`.

docs/help/faq-first-run.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ and troubleshooting see the main [FAQ](/help/faq).
6767
Common heartbeat skip reasons:
6868

6969
- `quiet-hours`: outside the configured active-hours window
70-
- `empty-heartbeat-file`: `HEARTBEAT.md` exists but only contains blank/header-only scaffolding
70+
- `empty-heartbeat-file`: `HEARTBEAT.md` exists but only contains blank, comment, header, fence, or empty-checklist scaffolding
7171
- `no-tasks-due`: `HEARTBEAT.md` task mode is active but none of the task intervals are due yet
7272
- `alerts-disabled`: all heartbeat visibility is disabled (`showOk`, `showAlerts`, and `useIndicator` are all off)
7373

docs/help/faq.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,8 +1350,9 @@ lives on the [First-run FAQ](/help/faq-first-run).
13501350
}
13511351
```
13521352

1353-
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
1354-
headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
1353+
If `HEARTBEAT.md` exists but is effectively empty (only blank lines,
1354+
Markdown/HTML comments, Markdown headings like `# Heading`, fence markers,
1355+
or empty checklist stubs), OpenClaw skips the heartbeat run to save API calls.
13551356
If the file is missing, the heartbeat still runs and the model decides what to do.
13561357

13571358
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).

docs/help/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ flowchart TD
364364

365365
- `cron: scheduler disabled; jobs will not run automatically` → cron is disabled.
366366
- `heartbeat skipped` with `reason=quiet-hours` → outside configured active hours.
367-
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank/header-only scaffolding.
367+
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank, comment, header, fence, or empty-checklist scaffolding.
368368
- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` task mode is active but none of the task intervals are due yet.
369369
- `heartbeat skipped` with `reason=alerts-disabled` → all heartbeat visibility is disabled (`showOk`, `showAlerts`, and `useIndicator` are all off).
370370
- `requests-in-flight` → main lane busy; heartbeat wake was deferred.

docs/start/openclaw.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ By default, OpenClaw runs a heartbeat every 30 minutes with the prompt:
172172
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
173173
Set `agents.defaults.heartbeat.every: "0m"` to disable.
174174

175-
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
175+
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines, Markdown/HTML comments, Markdown headings like `# Heading`, fence markers, or empty checklist stubs), OpenClaw skips the heartbeat run to save API calls.
176176
- If the file is missing, the heartbeat still runs and the model decides what to do.
177177
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), OpenClaw suppresses outbound delivery for that heartbeat.
178178
- By default, heartbeat delivery to DM-style `user:<id>` targets is allowed. Set `agents.defaults.heartbeat.directPolicy: "block"` to suppress direct-target delivery while keeping heartbeat runs active.

src/agents/tools/pdf-tool.model-config.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ describe("resolvePdfModelConfigForTool", () => {
180180
} as OpenClawConfig;
181181

182182
expect(resolvePdfModelConfigForTool({ cfg, agentDir: TEST_AGENT_DIR })).toEqual({
183-
primary: "openai/gpt-5.4-mini",
183+
primary: "openai/gpt-5.5",
184184
fallbacks: ["minimax/MiniMax-M2.7", "minimax-portal/MiniMax-M2.7"],
185185
});
186186
});

src/auto-reply/heartbeat.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,23 @@ describe("isHeartbeatContentEffectivelyEmpty", () => {
197197
"<!-- Heartbeat template; comments-only content prevents scheduled heartbeat API calls. -->",
198198
),
199199
).toBe(true);
200+
expect(
201+
isHeartbeatContentEffectivelyEmpty(`<!--
202+
Heartbeat template.
203+
Keep this comment-only file quiet.
204+
-->`),
205+
).toBe(true);
206+
expect(
207+
isHeartbeatContentEffectivelyEmpty(`<!--
208+
tasks:
209+
- name: inbox
210+
interval: 30m
211+
prompt: Check inbox
212+
-->`),
213+
).toBe(true);
214+
expect(isHeartbeatContentEffectivelyEmpty("<!-- One --> <!-- Two -->")).toBe(true);
215+
expect(isHeartbeatContentEffectivelyEmpty("<!-- One -->\n# Header")).toBe(true);
216+
expect(isHeartbeatContentEffectivelyEmpty("Reminder <!-- not scaffolding -->")).toBe(false);
200217
});
201218

202219
it("returns false when a template includes plain instructional prose", () => {
@@ -310,4 +327,15 @@ interval: should-not-bleed
310327
},
311328
]);
312329
});
330+
331+
it("ignores task blocks inside HTML comments", () => {
332+
const content = `<!--
333+
tasks:
334+
- name: inbox
335+
interval: 30m
336+
prompt: Check inbox
337+
-->
338+
`;
339+
expect(parseHeartbeatTasks(content)).toEqual([]);
340+
});
313341
});

src/auto-reply/heartbeat.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,42 @@ export const HEARTBEAT_TRANSCRIPT_PROMPT = "[OpenClaw heartbeat poll]";
2424
export const DEFAULT_HEARTBEAT_EVERY = "30m";
2525
export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300;
2626

27+
function stripLeadingHtmlCommentScaffolding(
28+
line: string,
29+
state: { inHtmlComment: boolean },
30+
): string {
31+
let remaining = line;
32+
while (state.inHtmlComment || remaining.trimStart().startsWith("<!--")) {
33+
const searchText = state.inHtmlComment ? remaining : remaining.trimStart();
34+
const commentEnd = searchText.indexOf("-->");
35+
if (commentEnd === -1) {
36+
state.inHtmlComment = true;
37+
return "";
38+
}
39+
40+
state.inHtmlComment = false;
41+
if (searchText === remaining) {
42+
remaining = remaining.slice(commentEnd + 3);
43+
} else {
44+
const leadingWidth = remaining.length - searchText.length;
45+
remaining = remaining.slice(0, leadingWidth) + searchText.slice(commentEnd + 3);
46+
}
47+
}
48+
return remaining;
49+
}
50+
51+
function stripHeartbeatHtmlComments(content: string): string[] {
52+
const state = { inHtmlComment: false };
53+
return content.split("\n").map((line) => stripLeadingHtmlCommentScaffolding(line, state));
54+
}
55+
2756
/**
2857
* Check if HEARTBEAT.md content is "effectively empty" - meaning it has no actionable tasks.
2958
* This allows skipping heartbeat API calls when no tasks are configured.
3059
*
3160
* A file is considered effectively empty if it contains only:
3261
* - Whitespace / empty lines
62+
* - Markdown/HTML comments
3363
* - Markdown ATX headers (`#`, `##`, ...)
3464
* - Single-line HTML comments (`<!-- ... -->`)
3565
* - Markdown fence markers such as ``` or ```markdown
@@ -46,7 +76,7 @@ export function isHeartbeatContentEffectivelyEmpty(content: string | undefined |
4676
return false;
4777
}
4878

49-
const lines = content.split("\n");
79+
const lines = stripHeartbeatHtmlComments(content);
5080
for (const line of lines) {
5181
const trimmed = line.trim();
5282
// Skip empty lines
@@ -229,7 +259,7 @@ export function stripHeartbeatToken(
229259
*/
230260
export function parseHeartbeatTasks(content: string): HeartbeatTask[] {
231261
const tasks: HeartbeatTask[] = [];
232-
const lines = content.split("\n");
262+
const lines = stripHeartbeatHtmlComments(content);
233263
let inTasksBlock = false;
234264

235265
for (let i = 0; i < lines.length; i++) {

src/auto-reply/reply.test-harness.ts

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
33
import os from "node:os";
44
import path from "node:path";
55
import { afterAll, beforeAll, vi, type Mock } from "vitest";
6-
import { captureEnv } from "../test-utils/env.js";
6+
import { withEnvAsync } from "../test-utils/env.js";
77
import { withFastReplyConfig } from "./reply/get-reply-fast-path.js";
88

99
type ReplyRuntimeMocks = {
@@ -121,15 +121,6 @@ vi.mock("./reply/agent-runner.runtime.js", () => ({
121121
},
122122
}));
123123

124-
const HOME_ENV_KEYS = [
125-
"HOME",
126-
"USERPROFILE",
127-
"HOMEDRIVE",
128-
"HOMEPATH",
129-
"OPENCLAW_STATE_DIR",
130-
"OPENCLAW_AGENT_DIR",
131-
] as const;
132-
133124
/** Creates a per-test HOME fixture and restores environment variables after each case. */
134125
export function createTempHomeHarness(options: { prefix: string; beforeEachCase?: () => void }) {
135126
let fixtureRoot = "";
@@ -149,26 +140,24 @@ export function createTempHomeHarness(options: { prefix: string; beforeEachCase?
149140
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
150141
const home = path.join(fixtureRoot, `case-${++caseId}`);
151142
await fs.mkdir(path.join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true });
152-
const envSnapshot = captureEnv([...HOME_ENV_KEYS]);
153-
process.env.HOME = home;
154-
process.env.USERPROFILE = home;
155-
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
156-
process.env.OPENCLAW_AGENT_DIR = path.join(home, ".openclaw", "agent");
157-
143+
const env: Record<string, string | undefined> = {
144+
HOME: home,
145+
USERPROFILE: home,
146+
OPENCLAW_STATE_DIR: path.join(home, ".openclaw"),
147+
OPENCLAW_AGENT_DIR: path.join(home, ".openclaw", "agent"),
148+
};
158149
if (process.platform === "win32") {
159150
const match = home.match(/^([A-Za-z]:)(.*)$/);
160151
if (match) {
161-
process.env.HOMEDRIVE = match[1];
162-
process.env.HOMEPATH = match[2] || "\\";
152+
env.HOMEDRIVE = match[1];
153+
env.HOMEPATH = match[2] || "\\";
163154
}
164155
}
165156

166-
try {
157+
return await withEnvAsync(env, async () => {
167158
options.beforeEachCase?.();
168159
return await fn(home);
169-
} finally {
170-
envSnapshot.restore();
171-
}
160+
});
172161
}
173162

174163
return { withTempHome };

0 commit comments

Comments
 (0)