openclaw - ✅(Solved) Fix [Bug]: `cron add` / `cron edit` with invalid `--cron` expression logs gateway error and returns `UNAVAILABLE` instead of `INVALID_REQUEST` (raw croner TypeError/RangeError leaks to CLI + log) [5 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#74066Fetched 2026-04-30 06:29:07
View on GitHub
Comments
1
Participants
2
Timeline
12
Reactions
0
Author
Timeline (top)
cross-referenced ×7closed ×1commented ×1labeled ×1

Server-side cron pattern validation is missing in the gateway handlers for cron.add and cron.update. When a user passes an invalid --cron expression to openclaw cron add or openclaw cron edit, the croner library throws a raw TypeError (bad shape) or RangeError (out-of-range field) from inside context.cron.add(...) / context.cron.update(...) (after the handler's INVALID_REQUEST guards have already passed). The throw escapes the handler and is caught by the WS dispatcher's catch-all, which (a) logs it at error level as request handler failed: TypeError|RangeError: CronPattern: ... and (b) maps it onto ErrorCodes.UNAVAILABLE (a code that means "service unavailable, retry later") instead of INVALID_REQUEST. Operators get false-positive ERROR-level entries that look like real gateway crashes, and the CLI displays a raw GatewayClientRequestError: TypeError: ... that leaks JS error class names. The matching --every value path is validated cleanly client-side and produces Error: Invalid --every; use e.g. 10m, 1h, 1d, so this is a server-side gap, not a CLI parser gap.

Error Message

$ pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping" GatewayClientRequestError: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required. ELIFECYCLE Command failed with exit code 1.

$ pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping" GatewayClientRequestError: RangeError: CronPattern: Invalid value for minute: 99 ELIFECYCLE Command failed with exit code 1.

$ pnpm openclaw cron edit <id> --cron "* * * 13 *" GatewayClientRequestError: RangeError: CronPattern: Invalid value for month: 12 ELIFECYCLE Command failed with exit code 1.

Root Cause

Server-side cron pattern validation is missing in the gateway handlers for cron.add and cron.update. When a user passes an invalid --cron expression to openclaw cron add or openclaw cron edit, the croner library throws a raw TypeError (bad shape) or RangeError (out-of-range field) from inside context.cron.add(...) / context.cron.update(...) (after the handler's INVALID_REQUEST guards have already passed). The throw escapes the handler and is caught by the WS dispatcher's catch-all, which (a) logs it at error level as request handler failed: TypeError|RangeError: CronPattern: ... and (b) maps it onto ErrorCodes.UNAVAILABLE (a code that means "service unavailable, retry later") instead of INVALID_REQUEST. Operators get false-positive ERROR-level entries that look like real gateway crashes, and the CLI displays a raw GatewayClientRequestError: TypeError: ... that leaks JS error class names. The matching --every value path is validated cleanly client-side and produces Error: Invalid --every; use e.g. 10m, 1h, 1d, so this is a server-side gap, not a CLI parser gap.

Fix Action

Fix / Workaround

Server-side cron pattern validation is missing in the gateway handlers for cron.add and cron.update. When a user passes an invalid --cron expression to openclaw cron add or openclaw cron edit, the croner library throws a raw TypeError (bad shape) or RangeError (out-of-range field) from inside context.cron.add(...) / context.cron.update(...) (after the handler's INVALID_REQUEST guards have already passed). The throw escapes the handler and is caught by the WS dispatcher's catch-all, which (a) logs it at error level as request handler failed: TypeError|RangeError: CronPattern: ... and (b) maps it onto ErrorCodes.UNAVAILABLE (a code that means "service unavailable, retry later") instead of INVALID_REQUEST. Operators get false-positive ERROR-level entries that look like real gateway crashes, and the CLI displays a raw GatewayClientRequestError: TypeError: ... that leaks JS error class names. The matching --every value path is validated cleanly client-side and produces Error: Invalid --every; use e.g. 10m, 1h, 1d, so this is a server-side gap, not a CLI parser gap.

(a) Gateway should classify cron-pattern parse failures as user-input rejections, not handler crashes. The handler must return ErrorCodes.INVALID_REQUEST with a human-readable message such as "invalid --cron: <croner message>". Concrete grounded reference: the sibling INVALID_REQUEST guards already in the same file at src/gateway/server-methods/cron.ts:200-208 (normalizeCronJobCreate), :211-219 (validateCronAddParams), :225-230 (validateScheduleTimestamp), :234-243 (assertValidCronCreateDelivery), :253-262, :267-275, :297-303, :313-322 — they all wrap throwing validators in try/catch and respond with INVALID_REQUEST. The remaining call to context.cron.add(jobCreate) on line 245 and context.cron.update(jobId, patch) on line 324 are not wrapped, so any throw from croner escapes.

Defect 1 — server-side validation gap. src/gateway/server-methods/cron.ts:188-247 ("cron.add") guards normalizeCronJobCreate, validateCronAddParams, validateScheduleTimestamp, and assertValidCronCreateDelivery with try/catch + INVALID_REQUEST. But the final call await context.cron.add(jobCreate) at line 245 is NOT wrapped. The croner library throws TypeError/RangeError during pattern parsing inside that call, and the throw escapes the handler. Same gap on line 324 for await context.cron.update(jobId, patch) (cron.update handler).

PR fix notes

PR #74068: test(cron): assert live disabled schedule rollback

Description (problem / solution / changelog)

Summary

  • Draft test-only follow-up after #74459 was fixed on main by #74720.
  • Adds one narrow assertion to the existing cron service regression test: after a rejected invalid disabled-job cron edit, the live in-memory job still keeps the previous valid schedule.
  • No production code changes.

Verification

  • pnpm test src/cron/service.issue-regressions.test.ts -- --reporter=verbose
  • git diff --check openclaw/main...HEAD

Scope

Draft only. This is not intended to replace #74720.

Changed files

  • src/cron/service.issue-regressions.test.ts (modified, +5/-0)

PR #74101: fix(cron): catch croner TypeError/RangeError in add/update and return INVALID_REQUEST

Description (problem / solution / changelog)

Fix #74066: Wrap context.cron.add() and context.cron.update() in try-catch to intercept croner library throws and map them to ErrorCodes.INVALID_REQUEST instead of letting them escape as UNAVAILABLE.

Changed files

  • src/gateway/server-methods/cron.ts (modified, +22/-2)

PR #74193: fix(cron): catch croner parse errors in cron.add and cron.update handlers

Description (problem / solution / changelog)

Summary

Fixes #74066

  • Wrap context.cron.add() and context.cron.update() in .catch() handlers so croner TypeError/RangeError from invalid cron expressions are returned as INVALID_REQUEST errors instead of crashing the gateway RPC handler
  • Follows existing adjacent guard pattern (assertValidCronCreateDelivery)
  • Adds two new tests covering croner parse errors on both add and update paths

Changes

  • src/gateway/server-methods/cron.ts: .catch() around context.cron.add() (line ~244) and context.cron.update() (line ~323), returning errorShape(ErrorCodes.INVALID_REQUEST, ...) with formatted message
  • src/gateway/server-methods/cron.validation.test.ts: Two new test cases for TypeError (add) and RangeError (update) from croner

Changed files

  • src/gateway/server-methods/cron.ts (modified, +34/-2)
  • src/gateway/server-methods/cron.validation.test.ts (modified, +85/-0)

PR #74252: fix(logs): only report rotation when the log file actually shrank

Description (problem / solution / changelog)

PR title: fix(logs): only report rotation when the log file actually shrank


Summary

  • Problem: openclaw logs --follow prints Log cursor reset (file rotated). to stderr whenever the gateway log grows faster than --max-bytes between polls, even though no rotation has occurred — the cursor is still valid and the tail was just truncated to fit the byte budget.
  • Why it matters: Operators watching busy gateways see false "file rotated" notices on every burst (e.g. during a noisy startup or while a chatty channel pumps logs). The accompanying Log tail truncated (increase --max-bytes). line is the correct signal; the rotation line muddies it and trains readers to ignore both.
  • What changed: In src/logging/log-tail.ts::readLogSlice, only set reset = true in the true rotation case (cursor > size — the file shrank below the previous cursor). The fast-growth branch (size - start > maxBytes) now sets truncated = true only, since the cursor it returned remains contiguous with what the caller saw last poll.
  • What did NOT change (scope boundary): The LogTailPayload shape, the rotation-case behavior, the Log tail truncated message, and the byte-budget arithmetic are all unchanged. No CLI strings, schema, or downstream consumers were modified — src/logging/diagnostic-support-export.ts and src/cli/logs-cli.ts continue to work with the same fields.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration (logging is shared infra)
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX (stderr notice text)
  • CI/CD / infra

Linked Issue/PR

  • No existing issue. Found while reading sibling cron handler code around #74066.
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: readLogSlice conflated two situations into a single reset flag:
    1. cursor > size — the previously-returned cursor is past EOF; the file was rotated/truncated, so the cursor is meaningless and we must restart from the new tail.
    2. size - start > maxBytes — the file grew faster than the per-poll byte budget; the cursor is still valid, we just can't read everything that was appended in one poll. Only case (1) is a real reset/rotation. Case (2) was incorrectly being flagged as "rotated" because both branches set reset = true.
  • Missing detection / guardrail: log-tail.test.ts only covered the redaction path; nothing pinned the meaning of reset vs truncated.
  • Contributing context: The CLI side (src/cli/logs-cli.ts:402) hard-codes Log cursor reset (file rotated). for payload.reset, so the conflation surfaced as a misleading user-visible string rather than a silent boolean.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/logging/log-tail.test.ts
  • Scenarios the test locks in:
    1. File grows by >> maxBytes between two polls with the same valid cursor → truncated: true, reset: false, lines returned.
    2. File shrinks below the previous cursor (real rotation) → reset: true.
  • Why this is the smallest reliable guardrail: the bug is at the readLogSlice boundary; both scenarios can be exercised end-to-end against a real temp log file using the existing setLoggerOverride test seam.
  • Existing test that already covers this (if any): none.
  • If no new test is added, why not: N/A.

User-visible / Behavior Changes

openclaw logs --follow (and JSON-mode --json) no longer emits the Log cursor reset (file rotated). notice during high-throughput writes. The existing Log tail truncated (increase --max-bytes). notice still fires in that case, which is the actionable signal. The notice still fires correctly when the log is genuinely rotated/truncated.

Diagram (if applicable)

N/A

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • N/A

Repro + Verification

Environment

  • OS: Ubuntu 24.04 (WSL) / Windows 11 host
  • Runtime/container: Node 22 (pnpm dev)
  • Model/provider: N/A
  • Integration/channel (if any): N/A — gateway logs only
  • Relevant config (redacted): default loggerSettings

Steps (to reproduce the original misleading notice on main)

  1. Start a gateway with anything that writes a steady stream of log lines (e.g. a chatty channel or openclaw status --watch against a busy gateway).
  2. In another terminal: openclaw logs --follow --max-bytes 4000.
  3. Wait for a write burst that exceeds 4 KB between two polls.

Expected

Only Log tail truncated (increase --max-bytes). is printed to stderr while the burst is being caught up.

Actual (before this PR)

Both Log tail truncated (increase --max-bytes). and Log cursor reset (file rotated). are printed, even though the file was never rotated.

After this PR

Only the truthful Log tail truncated notice is printed during fast growth. The rotation notice still fires when the file is actually rotated/truncated (covered by the second new test).

Evidence

  • Failing test/log before + passing after (see new tests in src/logging/log-tail.test.ts)
  • Trace/log snippets (see "Repro + Verification")
  • Screenshot/recording
  • Perf numbers (if relevant)

Local verification:

# Targeted unit lane
pnpm test src/logging/log-tail.test.ts
# Expect: 3 tests pass (1 existing + 2 new)

Note: I was unable to run the full repository test sweep locally (the dev environment is on a WSL/Windows-mounted filesystem that pnpm refuses to install into with ERR_PNPM_EACCES). The fix is a 5-line local change inside readLogSlice with no schema, no public-API, and no other call-site impact, and the new tests exercise the exact two paths affected. CI in this repo runs the full lane.

Human Verification (required)

  • Verified scenarios: fast file growth between polls → no rotation message; real rotation (file shrunk) → rotation message still emitted.
  • Edge cases checked: initial poll with no cursor still uses start = max(0, size - maxBytes) and reports truncated only (unchanged); empty file still returns cursor: size, lines: [] (unchanged); cursor inside [start, size] window with size - start <= maxBytes is unchanged.
  • What I did NOT verify: live CLI output against a running daemon during a real burst (will rely on CI + reviewer spot-check).

Review Conversations

  • I will reply to or resolve every bot review conversation I address in this PR.
  • I will leave unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • N/A

Risks and Mitigations

  • Risk: a downstream consumer relies on reset === true to mean "skipped some lines" rather than "the cursor is no longer meaningful".
    • Mitigation: I grepped the repo for payload.reset / LogTailPayload. The only consumers are src/cli/logs-cli.ts (renders the user-visible string this PR is correcting) and src/logging/diagnostic-support-export.ts::sanitizeLogTail (passes the boolean through verbatim into the support bundle). Both are display-only; neither uses reset to decide whether lines were skipped — they use the existing truncated flag for that.

AI/Vibe-Coded PRs Welcome! 🤖

  • AI-assisted PR. Drafted with help from Claude (Opus 4.7) via the GitHub Copilot CLI agent harness.
  • Degree of testing: lightly tested — new unit tests added; full repo lane not run locally (see note above).
  • Prompts/session logs: available on request.
  • I understand exactly what the code does (5-line semantic narrowing of when reset is asserted in readLogSlice).
  • codex review --base origin/main not run locally (no Codex access in this environment); happy to address Codex bot comments after the PR opens.
  • Will resolve bot review conversations after addressing them.

Changed files

  • src/logging/log-tail.test.ts (modified, +48/-0)
  • src/logging/log-tail.ts (modified, +6/-1)

PR #74306: fix: catch croner errors in cron gateway handlers and fix false-positive allowlist error

Description (problem / solution / changelog)

Problem

#74066 — Invalid cron expressions return UNAVAILABLE instead of INVALID_REQUEST

When a user passes an invalid --cron expression (e.g. "not-a-cron-expr" or "99 * * * *"), the croner library throws a raw TypeError (bad shape) or RangeError (out-of-range field) from inside context.cron.add(...) / context.cron.update(...). The throw escapes the handler, is caught by the WS dispatcher's catch-all, and mapped to ErrorCodes.UNAVAILABLE — a false-positive ERROR-level log that looks like a real gateway crash.

The same gap exists in cron.remove, cron.list, and cron.status.

#74019 — Lobster llm-task fails with "No callable tools remain"

When tools.alsoAllow is configured (e.g. ["lobster", "llm-task"]) but the caller sets disableTools: true (as the llm-task plugin does), the allowlist guard falsely reports an error because config-level allowlists exist but all tools are intentionally disabled.

Fix

Cron handlers (src/gateway/server-methods/cron.ts)

Wraps context.cron.add/update/remove/list/status calls in try/catch blocks, converting any thrown errors to INVALID_REQUEST responses with descriptive error messages.

Tool allowlist guard (src/agents/tool-allowlist-guard.ts)

When disableTools: true and no runtime toolsAllow is passed, the empty tool set is intentional — the guard now returns null instead of an error, even when config-level allowlists exist.

Tests

  • tool-allowlist-guard.test.ts: Added test case for disableTools without runtime toolsAllow

Changed files

  • src/agents/tool-allowlist-guard.test.ts (modified, +12/-0)
  • src/agents/tool-allowlist-guard.ts (modified, +10/-0)
  • src/cli/capability-cli.test.ts (modified, +23/-0)
  • src/cli/capability-cli.ts (modified, +5/-1)
  • src/gateway/server-methods/cron.ts (modified, +75/-13)

Code Example

$ pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping"
GatewayClientRequestError: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
ELIFECYCLE  Command failed with exit code 1.

$ pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping"
GatewayClientRequestError: RangeError: CronPattern: Invalid value for minute: 99
ELIFECYCLE  Command failed with exit code 1.

$ pnpm openclaw cron edit <id> --cron "* * * 13 *"
GatewayClientRequestError: RangeError: CronPattern: Invalid value for month: 12
ELIFECYCLE  Command failed with exit code 1.

---

$ pnpm openclaw logs --max-bytes 60000 | grep -E "request handler failed|errorCode=UNAVAILABLE.*cron"
2026-04-29T03:23:31.476Z error gateway request handler failed: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:31.480Z info gateway/ws ⇄ res ✗ cron.add 22ms errorCode=UNAVAILABLE errorMessage=TypeError: CronPattern: invalid configuration format ...
2026-04-29T03:23:37.686Z error gateway request handler failed: RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:23:37.693Z info gateway/ws ⇄ res ✗ cron.add 11ms errorCode=UNAVAILABLE errorMessage=RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:19:12.760Z error gateway request handler failed: RangeError: CronPattern: Invalid value for month: 12
2026-04-29T03:19:12.764Z info gateway/ws ⇄ res ✗ cron.update 7ms errorCode=UNAVAILABLE errorMessage=RangeError: CronPattern: Invalid value for month: 12

---

$ pnpm openclaw cron add --name probetest --every "BogusDuration" --message "ping"
Error: Invalid --every; use e.g. 10m, 1h, 1d
ELIFECYCLE  Command failed with exit code 1.

### OpenClaw version

2026.4.27 (bfdee7c)

### Operating system

Ubuntu 24.04.4 LTS (Linux 6.8.0-110-generic)

### Install method

pnpm dev (repo checkout, `pnpm openclaw ...`)

### Model

anthropic/claude-opus-4-7

### Provider / routing chain

N/A

### Additional provider/model setup details

_No response_

### Logs, screenshots, and evidence
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Server-side cron pattern validation is missing in the gateway handlers for cron.add and cron.update. When a user passes an invalid --cron expression to openclaw cron add or openclaw cron edit, the croner library throws a raw TypeError (bad shape) or RangeError (out-of-range field) from inside context.cron.add(...) / context.cron.update(...) (after the handler's INVALID_REQUEST guards have already passed). The throw escapes the handler and is caught by the WS dispatcher's catch-all, which (a) logs it at error level as request handler failed: TypeError|RangeError: CronPattern: ... and (b) maps it onto ErrorCodes.UNAVAILABLE (a code that means "service unavailable, retry later") instead of INVALID_REQUEST. Operators get false-positive ERROR-level entries that look like real gateway crashes, and the CLI displays a raw GatewayClientRequestError: TypeError: ... that leaks JS error class names. The matching --every value path is validated cleanly client-side and produces Error: Invalid --every; use e.g. 10m, 1h, 1d, so this is a server-side gap, not a CLI parser gap.

Steps to reproduce

  1. Have an OpenClaw gateway running locally (the default loopback gateway is fine; no live channels/providers needed): pnpm openclaw status | grep "Gateway " | head -1 → "Gateway | local · ws://127.0.0.1:18789 ..."

  2. Reproduce both shapes of croner rejection (one per error class):

    • TypeError shape (wrong field count): pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping"
    • RangeError shape (out-of-range minute): pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping"
    • RangeError on edit too: pnpm openclaw cron add --name probetest --every 1h --message "ping" --json (capture id); pnpm openclaw cron edit <id> --cron "* * * 13 *" (invalid month)
  3. Inspect the gateway log: pnpm openclaw logs --max-bytes 60000 | grep -E "request handler failed|errorCode=UNAVAILABLE.*cron|GatewayClientRequestError.*Cron"

  4. Compare with the parallel --every path which IS validated cleanly: pnpm openclaw cron add --name probetest --every "BogusDuration" --message "ping" → "Error: Invalid --every; use e.g. 10m, 1h, 1d" (No gateway log entry, exit 1, clean.)

Expected behavior

Bad --cron syntax should land on the same UX as bad --every:

(a) Gateway should classify cron-pattern parse failures as user-input rejections, not handler crashes. The handler must return ErrorCodes.INVALID_REQUEST with a human-readable message such as "invalid --cron: <croner message>". Concrete grounded reference: the sibling INVALID_REQUEST guards already in the same file at src/gateway/server-methods/cron.ts:200-208 (normalizeCronJobCreate), :211-219 (validateCronAddParams), :225-230 (validateScheduleTimestamp), :234-243 (assertValidCronCreateDelivery), :253-262, :267-275, :297-303, :313-322 — they all wrap throwing validators in try/catch and respond with INVALID_REQUEST. The remaining call to context.cron.add(jobCreate) on line 245 and context.cron.update(jobId, patch) on line 324 are not wrapped, so any throw from croner escapes.

(b) Gateway should log such rejections at info (or at most warn) on the cron subsystem, not at error on the gateway subsystem. The current catch-all in src/gateway/server/ws-connection/message-handler.ts:1586-1589 treats every uncaught handler throw as error + UNAVAILABLE, which is correct for true unexpected failures but wrong for known-class user-input throws.

(c) CLI should never display GatewayClientRequestError: TypeError: ... / GatewayClientRequestError: RangeError: ... — those error class names leak runtime internals. The matching --every path produces a clean Error: Invalid --every; use e.g. 10m, 1h, 1d. --cron should produce something equivalent, e.g. Error: Invalid --cron: <pattern>: <reason>.

The errorCode in the WS protocol response should be INVALID_REQUEST per the existing enum at src/gateway/protocol/schema/error-codes.ts:7, not UNAVAILABLE (which means "service unavailable" with retry semantics).

Actual behavior

Three layered defects, all triggered by a single bad --cron value.

Defect 1 — server-side validation gap. src/gateway/server-methods/cron.ts:188-247 ("cron.add") guards normalizeCronJobCreate, validateCronAddParams, validateScheduleTimestamp, and assertValidCronCreateDelivery with try/catch + INVALID_REQUEST. But the final call await context.cron.add(jobCreate) at line 245 is NOT wrapped. The croner library throws TypeError/RangeError during pattern parsing inside that call, and the throw escapes the handler. Same gap on line 324 for await context.cron.update(jobId, patch) (cron.update handler).

Defect 2 — wrong protocol error code. The WS dispatcher catch-all at src/gateway/server/ws-connection/message-handler.ts:1586-1589 maps every uncaught handler throw to: logGateway.error(request handler failed: ${formatForLog(err)}); respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err))); So invalid user input ends up as errorCode=UNAVAILABLE, which per src/gateway/protocol/schema/error-codes.ts is reserved for service-unavailable states with retry semantics — not user input rejection.

Defect 3 — log noise + class-name leak in CLI output. The same dispatcher logs at error on the gateway subsystem with "request handler failed: TypeError|RangeError" — exactly the line shape operators scan for to spot real handler crashes. The CLI relays the raw error string back as GatewayClientRequestError: TypeError: CronPattern: ..., leaking JS class names.

Live evidence (2026-04-29, on commit bfdee7c8):

$ pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping"
GatewayClientRequestError: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
ELIFECYCLE  Command failed with exit code 1.

$ pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping"
GatewayClientRequestError: RangeError: CronPattern: Invalid value for minute: 99
ELIFECYCLE  Command failed with exit code 1.

$ pnpm openclaw cron edit <id> --cron "* * * 13 *"
GatewayClientRequestError: RangeError: CronPattern: Invalid value for month: 12
ELIFECYCLE  Command failed with exit code 1.
$ pnpm openclaw logs --max-bytes 60000 | grep -E "request handler failed|errorCode=UNAVAILABLE.*cron"
2026-04-29T03:23:31.476Z error gateway request handler failed: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:31.480Z info gateway/ws ⇄ res ✗ cron.add 22ms errorCode=UNAVAILABLE errorMessage=TypeError: CronPattern: invalid configuration format ...
2026-04-29T03:23:37.686Z error gateway request handler failed: RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:23:37.693Z info gateway/ws ⇄ res ✗ cron.add 11ms errorCode=UNAVAILABLE errorMessage=RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:19:12.760Z error gateway request handler failed: RangeError: CronPattern: Invalid value for month: 12
2026-04-29T03:19:12.764Z info gateway/ws ⇄ res ✗ cron.update 7ms errorCode=UNAVAILABLE errorMessage=RangeError: CronPattern: Invalid value for month: 12

Compare: parallel --every value path validated cleanly client-side; no gateway log entry produced.

$ pnpm openclaw cron add --name probetest --every "BogusDuration" --message "ping"
Error: Invalid --every; use e.g. 10m, 1h, 1d
ELIFECYCLE  Command failed with exit code 1.

### OpenClaw version

2026.4.27 (bfdee7c)

### Operating system

Ubuntu 24.04.4 LTS (Linux 6.8.0-110-generic)

### Install method

pnpm dev (repo checkout, `pnpm openclaw ...`)

### Model

anthropic/claude-opus-4-7

### Provider / routing chain

N/A

### Additional provider/model setup details

_No response_

### Logs, screenshots, and evidence

```shell
$ pnpm openclaw --version
> [email protected] openclaw /home/orin/Gittensor/Test/openclaw
> node scripts/run-node.mjs --version
OpenClaw 2026.4.27 (bfdee7c)



$ pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping"
> node scripts/run-node.mjs cron add --name probetest --cron not-a-cron-expr --message ping
GatewayClientRequestError: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
 ELIFECYCLE  Command failed with exit code 1.



$ pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping"
> node scripts/run-node.mjs cron add --name probetest --cron '99 * * * *' --message ping
GatewayClientRequestError: RangeError: CronPattern: Invalid value for minute: 99
 ELIFECYCLE  Command failed with exit code 1.


Server-side log entries (one INFO + two ERRORs per bad input, on every attempt):

$ pnpm openclaw logs --max-bytes 60000 | grep -E "TypeError.*CronPattern|RangeError.*CronPattern|errorCode=UNAVAILABLE.*cron"
2026-04-29T03:23:31.476Z error gateway request handler failed: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:31.480Z info  gateway/ws ⇄ res ✗ cron.add 22ms errorCode=UNAVAILABLE errorMessage=TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:31.496Z error GatewayClientRequestError: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:37.686Z error gateway request handler failed: RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:23:37.693Z info  gateway/ws ⇄ res ✗ cron.add 11ms errorCode=UNAVAILABLE errorMessage=RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:23:37.721Z error GatewayClientRequestError: RangeError: CronPattern: Invalid value for minute: 99


Parallel clean path for comparison (--every is client-side validated, no gateway log noise, no class-name leak):

$ pnpm openclaw cron add --name probetest --every "BogusDuration" --message "ping"
Error: Invalid --every; use e.g. 10m, 1h, 1d
 ELIFECYCLE  Command failed with exit code 1.

Impact and severity

Affected users/systems/channels: every operator using openclaw cron add or openclaw cron edit with a --cron expression. Also affects any non-CLI client (Control UI, MCP, RPC clients) that calls cron.add/cron.update with a malformed pattern. Not platform- or channel-specific.

Severity: reliability / observability degradation. Each bad-pattern attempt adds two error gateway request handler failed: TypeError|RangeError entries to gateway.log — exactly the line shape operators search for to spot true handler crashes. In support triage, this means false positives crowd out signal. The wrong errorCode=UNAVAILABLE also misleads any client that implements retry-on-UNAVAILABLE: the client will retry a permanently malformed pattern indefinitely. The CLI-side class-name leak (GatewayClientRequestError: TypeError: ...) breaks the principle that user-facing error text should not expose JS internals.

Frequency: always. Deterministic on every malformed --cron value to cron.add or cron.update.

Consequence:

  • Log signal/noise erosion in gateway.log (operators with many cron consumers see this regularly during onboarding and skill setup).
  • Any RPC client implementing UNAVAILABLE-retry semantics will spin on permanent user errors.
  • Inconsistent UX between --every (clean) and --cron (raw class name) for the same conceptual user error class.
  • Support burden: maintainers reading "request handler failed: TypeError" cannot distinguish real handler crashes from user input rejections without inspecting each line.

Additional information

Cross-references (related but distinct):

  • src/gateway/server-methods/cron.ts:188-247 (cron.add handler) and :249-326 (cron.update handler) — handler shape + try/catch coverage.
  • src/gateway/server/ws-connection/message-handler.ts:1586-1589 — the catch-all that emits error gateway request handler failed: ... and maps to UNAVAILABLE. This is correct for true handler crashes but is the wrong destination for cron-pattern user errors that escape the handler.
  • src/gateway/protocol/schema/error-codes.ts — INVALID_REQUEST is the intended code for user input rejection; UNAVAILABLE is reserved for service-unavailable state.
  • src/cli/cron-cli/schedule-options.ts:114 — the parallel --every client-side validator with the clean Error: Invalid --every; use e.g. 10m, 1h, 1d message that establishes the UX baseline.

Suggested fix shape (anchor for reviewers, not prescribing the design):

  1. In src/gateway/server-methods/cron.ts, wrap the final context.cron.add(jobCreate) (line 245) and context.cron.update(jobId, patch) (line 324) in try/catch. On error class === TypeError || RangeError, respond with INVALID_REQUEST and a normalized message ("invalid --cron: <croner message>"). Re-throw any other error class so it stays in the legitimate "handler failed" log path.
  2. Consider adding a server-side cron-pattern validator alongside validateScheduleTimestamp so the rejection happens at the same layer as the existing INVALID_REQUEST guards instead of mid-store-write.
  3. On the CLI side, mirror --every's client-side validation: parse and reject malformed --cron patterns in src/cli/cron-cli/schedule-options.ts before sending to the gateway. Defense in depth, but does not replace the server-side fix (Control UI / MCP / RPC clients won't go through the CLI parser).

Not a duplicate of:

  • #71605, #62981 — about gateway timeouts + embedded fallback contention (different surface).
  • #52563 (closed) — "Cron task configuration error causes Gateway crash" — about a real crash, not a malformed-input log/error-code mismatch.
  • #68221 (closed) — "Cron job manual execution fails with TypeError" — that was a bug inside cron run for a stored job; here the issue is at add/edit time before the job is ever stored.

A regression test that asserts cron.add and cron.update respond with INVALID_REQUEST (not UNAVAILABLE) and do NOT log error gateway request handler failed for a known-bad pattern would pin the fix.

extent analysis

TL;DR

The most likely fix involves wrapping the context.cron.add and context.cron.update calls in try/catch blocks to catch TypeError and RangeError exceptions, and responding with INVALID_REQUEST and a normalized error message.

Guidance

  1. Wrap context.cron.add and context.cron.update in try/catch blocks: In src/gateway/server-methods/cron.ts, add try/catch blocks around the context.cron.add (line 245) and context.cron.update (line 324) calls to catch TypeError and RangeError exceptions.
  2. Respond with INVALID_REQUEST on cron pattern errors: When catching TypeError or RangeError, respond with INVALID_REQUEST and a normalized error message, such as "invalid --cron: <croner message>".
  3. Consider adding server-side cron pattern validation: Add a server-side cron pattern validator alongside validateScheduleTimestamp to reject malformed patterns at the same layer as existing INVALID_REQUEST guards.

Example

try {
  await context.cron.add(jobCreate);
} catch (error) {
  if (error instanceof TypeError || error instanceof RangeError) {
    return { errorCode: ErrorCodes.INVALID_REQUEST, errorMessage: `invalid --cron: ${error.message}` };
  } else {
    throw error;
  }
}

Notes

  • The suggested fix focuses on handling cron pattern errors at the server-side, but consider adding client-side validation for --cron patterns in src/cli/cron-cli/schedule-options.ts for defense in depth.
  • A regression test should be added to assert that cron.add and cron.update respond with INVALID_REQUEST (not UNAVAILABLE) and do not log error gateway request handler failed for known-bad patterns.

Recommendation

Apply the suggested fix by wrapping the context.cron.add and `context.cron

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Bad --cron syntax should land on the same UX as bad --every:

(a) Gateway should classify cron-pattern parse failures as user-input rejections, not handler crashes. The handler must return ErrorCodes.INVALID_REQUEST with a human-readable message such as "invalid --cron: <croner message>". Concrete grounded reference: the sibling INVALID_REQUEST guards already in the same file at src/gateway/server-methods/cron.ts:200-208 (normalizeCronJobCreate), :211-219 (validateCronAddParams), :225-230 (validateScheduleTimestamp), :234-243 (assertValidCronCreateDelivery), :253-262, :267-275, :297-303, :313-322 — they all wrap throwing validators in try/catch and respond with INVALID_REQUEST. The remaining call to context.cron.add(jobCreate) on line 245 and context.cron.update(jobId, patch) on line 324 are not wrapped, so any throw from croner escapes.

(b) Gateway should log such rejections at info (or at most warn) on the cron subsystem, not at error on the gateway subsystem. The current catch-all in src/gateway/server/ws-connection/message-handler.ts:1586-1589 treats every uncaught handler throw as error + UNAVAILABLE, which is correct for true unexpected failures but wrong for known-class user-input throws.

(c) CLI should never display GatewayClientRequestError: TypeError: ... / GatewayClientRequestError: RangeError: ... — those error class names leak runtime internals. The matching --every path produces a clean Error: Invalid --every; use e.g. 10m, 1h, 1d. --cron should produce something equivalent, e.g. Error: Invalid --cron: <pattern>: <reason>.

The errorCode in the WS protocol response should be INVALID_REQUEST per the existing enum at src/gateway/protocol/schema/error-codes.ts:7, not UNAVAILABLE (which means "service unavailable" with retry semantics).

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: `cron add` / `cron edit` with invalid `--cron` expression logs gateway error and returns `UNAVAILABLE` instead of `INVALID_REQUEST` (raw croner TypeError/RangeError leaks to CLI + log) [5 pull requests, 1 comments, 2 participants]