Skip to content

[Bug]: openclaw message send --dry-run prints ✅ Sent via <channel>. Message ID: unknown — pretty-text formatter never sees handledBy: "dry-run" and falls through to the success message #80507

Description

@YB0y

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

  1. Install v2026.5.7 (today's npm-stable release; commit eeef4864494f859838fec1586bedbab1f8fa5702). Any working channel will do (Slack here).

  2. 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
    
  3. Observe the CLI prints ✅ Sent via Slack. Message ID: unknown.

  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.bugSomething isn't workingclawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions