Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
openclaw message send --dry-run correctly skips the actual delivery (regression fix from #66549 and #70977), but its pretty-text CLI output is identical to a successful send: ✅ Sent via Slack. Message ID: unknown. The dry-run-aware branch in the formatter (src/commands/message-format.ts:249-251) is dead code for the current send/poll path because executeSendAction returns handledBy: "core", never "dry-run". Only --json reveals that dryRun: true was honored.
Steps to reproduce
-
Install v2026.5.7 (today's npm-stable release; commit eeef4864494f859838fec1586bedbab1f8fa5702). Any working channel will do (Slack here).
-
Run a send with --dry-run to a real or invalid target — both behave the same:
pnpm openclaw message send --channel slack --target "channel:C00000FAKE000QA" --message "qa probe" --dry-run
-
Observe the CLI prints ✅ Sent via Slack. Message ID: unknown.
-
Re-run with --json and observe the structured output reports "dryRun": true and "handledBy": "core".
Captured live on v2026.5.7:
$ git describe --tags
v2026.5.7
$ pnpm openclaw message send --channel slack --target 'channel:C00000FAKE000QA' --message 'qa probe' --dry-run
✅ Sent via Slack. Message ID: unknown
$ pnpm openclaw message send --channel slack --target 'channel:C00000FAKE000QA' --message 'qa probe' --dry-run --json
{ "action": "send", "channel": "slack", "dryRun": true, "handledBy": "core",
"payload": { "channel": "slack", "to": "C00000FAKE000QA", "via": "direct",
"mediaUrl": null, "dryRun": true } }
No real delivery is needed — sendMessage() short-circuits dry-run before reaching any plugin (src/infra/outbound/message.ts:338-347), so even an obviously fake target produces the misleading "✅ Sent" output. Same output for valid and invalid Slack channel ids confirms no real Slack API call goes out.
Expected behavior
ame output the formatter already encodes for the dry-run case: a muted line such as [dry-run] would run send via slack, distinct from the success-state checkmark line. This is exactly what src/commands/message-format.ts:249-251 is written to produce; it just never gets reached on the current code path.
Actual behavior
Pretty-text CLI:
$ pnpm openclaw message send --channel slack --target "channel:C00000FAKE000" --message "qa probe" --dry-run
✅ Sent via Slack. Message ID: unknown
$ pnpm openclaw message send --channel slack --target "channel:invalid_target_for_qa" --message "test" --dry-run
✅ Sent via Slack. Message ID: unknown
Same command with --json shows the dry-run was honored:
{
"action": "send",
"channel": "slack",
"dryRun": true,
"handledBy": "core",
"payload": {
"channel": "slack",
"to": "C00000FAKE000",
"via": "direct",
"mediaUrl": null,
"dryRun": true
}
}
Tracing the bug in source (current origin/main f2458d8828):
src/commands/message-format.ts:249-251 has the dry-run formatter:
if (result.handledBy === "dry-run") {
return [muted(`[dry-run] would run ${result.action} via ${result.channel}`)];
}
src/infra/outbound/message-action-runner.ts:818-828 returns the message-send result with handledBy: send.handledBy plus a separate dryRun flag. send.handledBy comes from executeSendAction.
src/infra/outbound/outbound-send-service.ts:183-321 (executeSendAction) always returns handledBy: "plugin" | "core" — the type signature itself rules out "dry-run" on this path:
Promise<{
handledBy: "plugin" | "core";
payload: unknown;
...
}>
- So when a user passes
--dry-run, the result has dryRun: true and handledBy: "core". The formatter's handledBy === "dry-run" check never matches and execution falls through to:
// src/commands/message-format.ts:299-302
const label = resolveChannelLabel(result.channel);
const msgId = extractMessageId(result.payload);
return [ok(`✅ Sent via ${label}.${msgId ? ` Message ID: ${msgId}` : ""}`)];
which is the normal success line.
The actual delivery is not happening — sendMessage() returns early on params.dryRun (src/infra/outbound/message.ts:338-347). This is the relic of the #66549 / #70977 fixes, which moved the dry-run gate into sendMessage() but never updated the formatter to detect that the result it gets back is from a dry-run.
OpenClaw version
v2026.5.7
Operating system
Ubuntu 24.04
Install method
No response
Model
anthropic/claude-opus-4-7
Provider / routing chain
openclaw -> slack
Additional provider/model setup details
No response
Logs, screenshots, and evidence
Verbatim CLI:
$ pnpm openclaw message send --channel slack --target "channel:C00000FAKE000" --message "qa probe" --dry-run
> openclaw@2026.5.6 openclaw /home/orin/Gittensor/OpenCoven/openclaw-upstream
> node scripts/run-node.mjs message send --channel slack --target channel:C00000FAKE000 --message 'qa probe' --dry-run
✅ Sent via Slack. Message ID: unknown
$ pnpm openclaw message send --channel slack --target "channel:C00000FAKE000" --message "qa probe" --dry-run --json
> openclaw@2026.5.6 openclaw /home/orin/Gittensor/OpenCoven/openclaw-upstream
> node scripts/run-node.mjs message send --channel slack --target channel:C00000FAKE000 --message 'qa probe' --dry-run --json
{
"action": "send",
"channel": "slack",
"dryRun": true,
"handledBy": "core",
"payload": {
"channel": "slack",
"to": "C00000FAKE000",
"via": "direct",
"mediaUrl": null,
"dryRun": true
}
}
Source pointers:
- `src/commands/message-format.ts:249-251` — dead-code dry-run formatter branch (gated on `result.handledBy === "dry-run"`).
- `src/commands/message-format.ts:299-302` — fall-through branch that emits `✅ Sent via <label>. Message ID: <id|"unknown">`.
- `src/infra/outbound/message-action-runner.ts:773-828` — `executeSendAction` is invoked, then the runner forwards `send.handledBy` (always `"plugin" | "core"`) plus a separate `dryRun` flag.
- `src/infra/outbound/outbound-send-service.ts:183-201` — `executeSendAction` return type rules out `"dry-run"` on this path.
- `src/infra/outbound/message.ts:338-347` — `sendMessage` correctly short-circuits when `params.dryRun` is set, returning `dryRun: true`. So actual delivery is *not* happening; this is purely a CLI-output defect.
Confirmed via deliberately invalid Slack target (`channel:invalid_target_for_qa`): the same `✅ Sent via Slack. Message ID: unknown` line is produced, proving no real Slack API call goes out (otherwise Slack would error on the invalid channel). Output is identical for valid and invalid targets, which is itself diagnostic of the formatter not being aware of dry-run state.
Impact and severity
- Affected: anyone using
openclaw message send --dry-run for preflight checks — automation scripts, integration tests, manual sanity checks before broadcasts. Reproduces on every send/poll dry-run on any channel.
- Severity: Medium. Not data-leaking (delivery is correctly skipped) but actively misleads users into believing a real send happened. Erodes trust in
--dry-run as a safe validation mode.
- Frequency: Always, on every
--dry-run invocation of message send (and the equivalent path for message poll per src/commands/message-format.ts:304+).
- Consequence: Users debugging delivery problems see
✅ Sent via Slack and conclude "send worked, the recipient must be at fault" — losing time before realising they passed --dry-run. In automated pipelines, log-greppers that assert "Sent via" can falsely pass on dry-run-only runs. The opposite confusion is also possible: someone testing --dry-run for safety sees ✅ Sent and panics that real messages went out (the closed issues #66549 / #70977 show this exact panic-pattern was a real on-call cost when delivery was actually happening, and the residual misleading text reproduces the same panic).
Additional information
No response
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
openclaw message send --dry-runcorrectly skips the actual delivery (regression fix from#66549and#70977), but its pretty-text CLI output is identical to a successful send:✅ Sent via Slack. Message ID: unknown. The dry-run-aware branch in the formatter (src/commands/message-format.ts:249-251) is dead code for the current send/poll path becauseexecuteSendActionreturnshandledBy: "core", never"dry-run". Only--jsonreveals thatdryRun: truewas honored.Steps to reproduce
Install
v2026.5.7(today's npm-stable release; commiteeef4864494f859838fec1586bedbab1f8fa5702). Any working channel will do (Slack here).Run a send with
--dry-runto a real or invalid target — both behave the same:Observe the CLI prints
✅ Sent via Slack. Message ID: unknown.Re-run with
--jsonand observe the structured output reports"dryRun": trueand"handledBy": "core".Captured live on
v2026.5.7:No real delivery is needed —
sendMessage()short-circuits dry-run before reaching any plugin (src/infra/outbound/message.ts:338-347), so even an obviously fake target produces the misleading "✅ Sent" output. Same output for valid and invalid Slack channel ids confirms no real Slack API call goes out.Expected behavior
ame output the formatter already encodes for the dry-run case: a muted line such as
[dry-run] would run send via slack, distinct from the success-state checkmark line. This is exactly whatsrc/commands/message-format.ts:249-251is written to produce; it just never gets reached on the current code path.Actual behavior
Pretty-text CLI:
Same command with
--jsonshows the dry-run was honored:{ "action": "send", "channel": "slack", "dryRun": true, "handledBy": "core", "payload": { "channel": "slack", "to": "C00000FAKE000", "via": "direct", "mediaUrl": null, "dryRun": true } }Tracing the bug in source (current
origin/mainf2458d8828):src/commands/message-format.ts:249-251has the dry-run formatter:src/infra/outbound/message-action-runner.ts:818-828returns the message-send result withhandledBy: send.handledByplus a separatedryRunflag.send.handledBycomes fromexecuteSendAction.src/infra/outbound/outbound-send-service.ts:183-321(executeSendAction) always returnshandledBy: "plugin" | "core"— the type signature itself rules out"dry-run"on this path:--dry-run, the result hasdryRun: trueandhandledBy: "core". The formatter'shandledBy === "dry-run"check never matches and execution falls through to:The actual delivery is not happening —
sendMessage()returns early onparams.dryRun(src/infra/outbound/message.ts:338-347). This is the relic of the#66549/#70977fixes, which moved the dry-run gate intosendMessage()but never updated the formatter to detect that the result it gets back is from a dry-run.OpenClaw version
v2026.5.7
Operating system
Ubuntu 24.04
Install method
No response
Model
anthropic/claude-opus-4-7
Provider / routing chain
openclaw -> slack
Additional provider/model setup details
No response
Logs, screenshots, and evidence
Impact and severity
openclaw message send --dry-runfor preflight checks — automation scripts, integration tests, manual sanity checks before broadcasts. Reproduces on every send/poll dry-run on any channel.--dry-runas a safe validation mode.--dry-runinvocation ofmessage send(and the equivalent path formessage pollpersrc/commands/message-format.ts:304+).✅ Sent via Slackand conclude "send worked, the recipient must be at fault" — losing time before realising they passed--dry-run. In automated pipelines, log-greppers that assert "Sent via" can falsely pass on dry-run-only runs. The opposite confusion is also possible: someone testing--dry-runfor safety sees✅ Sentand panics that real messages went out (the closed issues#66549/#70977show this exact panic-pattern was a real on-call cost when delivery was actually happening, and the residual misleading text reproduces the same panic).Additional information
No response