fix(heartbeat): return false for zero-width active-hours window (#21408)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 993860bd03
Co-authored-by: adhitShet <131381638+adhitShet@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
adhitShet
2026-02-20 05:03:57 +04:00
committed by GitHub
parent 57f0ac21e9
commit ae4907ce6e
4 changed files with 16 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
- Heartbeat/Cron: restore interval heartbeat behavior so missing `HEARTBEAT.md` no longer suppresses runs (only effectively empty files skip), preserving prompt-driven and tagged-cron execution paths.
- Heartbeat/Active hours: constrain active-hours `24` sentinel parsing to `24:00` in time validation so invalid values like `24:30` are rejected early. (#21410) thanks @adhitShet.
- Heartbeat: treat `activeHours` windows with identical `start`/`end` times as zero-width (always outside the window) instead of always-active. (#21408) thanks @adhitShet.
- Gateway/Pairing: tolerate legacy paired devices missing `roles`/`scopes` metadata in websocket upgrade checks and backfill metadata on reconnect. (#21447, fixes #21236) Thanks @joshavant.
- Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek.
- Provider/HTTP: treat HTTP 503 as failover-eligible for LLM provider errors. (#21086) Thanks @Protocol-zero-0.

View File

@@ -163,6 +163,16 @@ Restrict heartbeats to business hours in a specific timezone:
Outside this window (before 9am or after 10pm Eastern), heartbeats are skipped. The next scheduled tick inside the window will run normally.
### 24/7 setup
If you want heartbeats to run all day, use one of these patterns:
- Omit `activeHours` entirely (no time-window restriction; this is the default behavior).
- Set a full-day window: `activeHours: { start: "00:00", end: "24:00" }`.
Do not set the same `start` and `end` time (for example `08:00` to `08:00`).
That is treated as a zero-width window, so heartbeats are always skipped.
### Multi account example
Use `accountId` to target a specific account on multi-account channels like Telegram:
@@ -210,10 +220,11 @@ Use `accountId` to target a specific account on multi-account channels like Tele
- `prompt`: overrides the default prompt body (not merged).
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery.
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
- `activeHours`: restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.
- `activeHours`: restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive; use `00:00` for start-of-day), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.
- Omitted or `"user"`: uses your `agents.defaults.userTimezone` if set, otherwise falls back to the host system timezone.
- `"local"`: always uses the host system timezone.
- Any IANA identifier (e.g. `America/New_York`): used directly; if invalid, falls back to the `"user"` behavior above.
- `start` and `end` must not be equal for an active window; equal values are treated as zero-width (always outside the window).
- Outside the active window, heartbeats are skipped until the next tick inside the window.
## Delivery behavior

View File

@@ -39,7 +39,7 @@ describe("isWithinActiveHours", () => {
).toBe(true);
});
it("returns true when activeHours start equals end", () => {
it("returns false when activeHours start equals end", () => {
const cfg = cfgWithUserTimezone("UTC");
expect(
isWithinActiveHours(
@@ -47,7 +47,7 @@ describe("isWithinActiveHours", () => {
heartbeatWindow("08:00", "08:00", "UTC"),
Date.UTC(2025, 0, 1, 12, 0, 0),
),
).toBe(true);
).toBe(false);
});
it("respects user timezone windows for normal ranges", () => {

View File

@@ -83,7 +83,7 @@ export function isWithinActiveHours(
return true;
}
if (startMin === endMin) {
return true;
return false;
}
const timeZone = resolveActiveHoursTimezone(cfg, active.timezone);