openclaw - ✅(Solved) Fix Define one canonical outbound message.send path that is hook-bearing, deliverable, and auditable [1 pull requests, 1 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#63011Fetched 2026-04-09 07:59:32
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1

Error Message

  • GatewayClientRequestError: Error: No delivery result

Root Cause

Without one canonical path that is hook-bearing, deliverable, and auditable, downstream integrations can truthfully say only that message.send is real at the hook boundary, not that it is live-proven end to end on a standard-local runtime.

PR fix notes

PR #63596: fix(gateway): canonicalize outbound message send semantics

Description (problem / solution / changelog)

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: direct message.send and gateway send did not share one canonical outbound contract. A message_sending hook cancellation surfaced as an implicit empty result in one path and as No delivery result in another.
  • Why it matters: blocked sends were not auditable or semantically consistent across CLI and gateway entrypoints, and gateway send still duplicated core delivery interpretation.
  • What changed: introduced an explicit delivery-layer blocked status, routed canonical direct-send results through sendResolvedDirectMessage(), and aligned gateway send plus CLI formatting to use the same success-vs-blocked outcome semantics.
  • What did NOT change (scope boundary): no protocol schema changes, no new gateway methods, and no broad outbound refactor outside message.send/gateway send semantics.

Change Type (select all)

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

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #63011
  • Related #61373
  • This PR fixes a bug or regression

Root Cause (if applicable)

For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write N/A. If the cause is unclear, write Unknown.

  • Root cause: gateway send interpreted raw delivery output independently instead of consuming the same canonical outbound send result used by direct message.send.
  • Missing detection / guardrail: the delivery layer exposed only results[], so blocked sends were inferred heuristically and gateway-specific tests did not enforce a shared blocked contract.
  • Contributing context (if known): deliverOutboundPayloads() treats message_sending cancellation as a skipped payload rather than an exception, which made empty-result handling drift between entrypoints.

Regression Test Plan (if applicable)

For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write N/A.

  • 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/infra/outbound/deliver.test.ts
    • src/infra/outbound/message.test.ts
    • src/gateway/server-methods/send.test.ts
    • src/commands/message-format.test.ts
  • Scenario the test should lock in: message_sending cancellation yields an explicit blocked producer signal, direct message.send returns a blocked result, gateway send maps that to a structured invalid request response, and CLI output shows blocked guidance instead of a fake success.
  • Why this is the smallest reliable guardrail: the behavior spans the delivery producer, the canonical direct-send wrapper, the gateway consumer, and the CLI formatter; these four tests cover each seam directly.
  • Existing test that already covers this (if any): src/infra/outbound/deliver.test.ts -t "short-circuits lower-priority message_sending hooks after cancel=true" already covered the hook-cancel baseline but not the explicit producer contract.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

Blocked outbound sends now surface consistently: direct CLI message.send prints 🚫 Send blocked ..., and gateway send returns an explicit structured blocked error instead of No delivery result.

Diagram (if applicable)

For UI changes or non-trivial logic flows, include a small ASCII diagram reviewers can scan quickly. Otherwise write N/A.

Before:
CLI message.send -> sendMessage -> deliverOutboundPayloads -> [] => implicit/ambiguous
Gateway send     -> deliverOutboundPayloads -> [] => "No delivery result"

After:
Producer (deliver) -> { results, blockedByHook }
Canonical send     -> { result | blocked }
Gateway/CLI        -> consume the same canonical outcome

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)
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS (local dev)
  • Runtime/container: Node 22 + pnpm
  • Model/provider: N/A
  • Integration/channel (if any): direct CLI message.send and gateway send
  • Relevant config (redacted): hook-enabled outbound delivery with message_sending cancel path

Steps

  1. Trigger an outbound message.send path where a message_sending hook cancels delivery.
  2. Trigger the gateway send path for the same blocked-delivery scenario.
  3. Observe the returned outcome / CLI output.

Expected

  • Delivery producer reports an explicit blocked signal.
  • Direct message.send returns a blocked result instead of silently producing result: undefined.
  • Gateway send returns a structured blocked error instead of No delivery result.
  • CLI blocked sends show explicit blocked guidance.

Actual

  • Targeted tests and manual CLI verification confirmed all three surfaces now share consistent blocked-send semantics.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Trace/log snippets:

  • 🚫 Send blocked via Telegram. blocked by message_sending hook

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • pnpm test -- src/infra/outbound/message.test.ts src/gateway/server-methods/send.test.ts src/commands/message-format.test.ts
    • pnpm test -- src/infra/outbound/deliver.test.ts -t "short-circuits lower-priority message_sending hooks after cancel=true"
    • pnpm test -- src/infra/outbound/deliver.test.ts -t "reports explicit blockedByHook when message_sending cancels delivery"
    • manual CLI formatter check printing 🚫 Send blocked via Telegram. blocked by message_sending hook
  • Edge cases checked:
    • blocked-by-hook direct send keeps agentId/sessionKey/requestId
    • gateway blocked send returns a structured invalid request error instead of No delivery result
    • successful sends still keep existing success payload formatting and tests
  • What you did not verify:
    • full pnpm check as a clean branch-local gate, because latest origin/main still has unrelated pre-existing failures in extensions/msteams and src/agents/skills*

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps:

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk: any other future caller that bypasses sendMessage() and reads raw results[] directly could still drift semantically.
    • Mitigation: this PR makes the producer contract explicit and keeps gateway + direct send on the shared canonical wrapper; follow-up call sites can adopt the same status-aware helper incrementally.
  • Risk: CLI/gateway now treat blocked sends as explicit outcomes, which may surface user-facing behavior differences in scripts that previously tolerated ambiguous success output.
    • Mitigation: the new behavior is more truthful and is covered by focused regression tests.

Changed files

  • extensions/memory-wiki/index.test.ts (modified, +3/-0)
  • src/commands/message-format.test.ts (added, +28/-0)
  • src/commands/message-format.ts (modified, +37/-2)
  • src/gateway/server-methods/send.test.ts (modified, +72/-26)
  • src/gateway/server-methods/send.ts (modified, +49/-7)
  • src/infra/outbound/deliver.test.ts (modified, +33/-1)
  • src/infra/outbound/deliver.ts (modified, +20/-4)
  • src/infra/outbound/message.test.ts (modified, +38/-0)
  • src/infra/outbound/message.ts (modified, +114/-24)
  • src/infra/outbound/target-resolver.test.ts (modified, +8/-0)
  • src/plugins/contracts/boundary-invariants.test.ts (modified, +1/-0)
  • test/extension-test-boundary.test.ts (modified, +1/-0)
  • test/helpers/plugins/public-artifacts.ts (modified, +1/-0)

Code Example

openclaw message send --channel telegram --target 5434311066 --message "..." --json

---

{
  "action": "send",
  "channel": "telegram",
  "dryRun": false,
  "handledBy": "plugin",
  "payload": {
    "ok": true,
    "messageId": "942",
    "chatId": "5434311066"
  }
}

---

openclaw gateway call send --json --params "{...}"
RAW_BUFFERClick to expand / collapse

This issue is about establishing one canonical outbound message.send path in OpenClaw that can be used for honest end-to-end guard proof and downstream audit correlation.

Problem

Right now there is no single outbound send path that simultaneously:

  • routes through the active runtime's message_sending hook
  • returns a real delivery result on success
  • surfaces explicit guard denial on blocked sends
  • preserves enough request/session identity for downstream audit correlation

That leaves the direct CLI send path and the gateway send path semantically split in a way that makes guarded outbound proof ambiguous.

Current observed split

Runtime and channel used:

  • runtime: standard local OpenClaw at ~/.openclaw
  • gateway: ws://127.0.0.1:18789
  • channel: Telegram default
  • target: 5434311066

Primary direct path tested:

openclaw message send --channel telegram --target 5434311066 --message "..." --json

Observed result:

{
  "action": "send",
  "channel": "telegram",
  "dryRun": false,
  "handledBy": "plugin",
  "payload": {
    "ok": true,
    "messageId": "942",
    "chatId": "5434311066"
  }
}

But with the Tessera guard plugin loaded in that runtime and local credentials cleared to { "agents": {} }:

  • delivery still succeeded
  • no Tessera audit artifact was written

Alternate gateway path checked:

openclaw gateway call send --json --params "{...}"

Observed result:

  • GatewayClientRequestError: Error: No delivery result
  • no Tessera audit artifact written

Requested outcome

Please choose or define one canonical outbound send path instead of leaving both paths semantically ambiguous. The canonical path should satisfy all of the following:

  • uses the loaded runtime's message_sending hook
  • returns a real provider delivery result on success
  • surfaces explicit guarded cancellation / denial on blocked sends
  • preserves agentId plus session/request identity when available
  • exposes enough context for downstream guard/audit layers to correlate deny/allow and delivery outcomes

If the direct openclaw message send path is intended to be canonical, it should reliably cross the runtime hook and make the guarded outcome observable.

If the gateway send path is intended to be canonical, it needs to stop failing with No delivery result in the same setup.

Acceptance contract

One path should support this exact sequence:

  1. no grant -> blocked before delivery, with explicit guarded denial
  2. valid message.send grant -> real provider delivery result is returned
  3. revoked or cleared grant -> blocked again

All three cases should run through the same runtime path and expose enough context for audit correlation, including:

  • agentId
  • session/request identity when available
  • channel/target metadata
  • allow/deny reason
  • delivery result or blocked result

Why this matters

Without one canonical path that is hook-bearing, deliverable, and auditable, downstream integrations can truthfully say only that message.send is real at the hook boundary, not that it is live-proven end to end on a standard-local runtime.

extent analysis

TL;DR

  • Establish a single canonical outbound message.send path in OpenClaw that satisfies all requirements, including routing through the active runtime's message_sending hook, returning a real delivery result, and preserving request/session identity for audit correlation.

Guidance

  • Identify and modify the direct openclaw message send path to reliably cross the runtime hook and make the guarded outcome observable, ensuring it returns a real provider delivery result and surfaces explicit guarded cancellation/denial on blocked sends.
  • Alternatively, fix the gateway send path to stop failing with No delivery result and ensure it meets all the required conditions, including using the loaded runtime's message_sending hook and preserving agentId plus session/request identity.
  • Verify that the chosen canonical path supports the exact sequence outlined in the acceptance contract, including handling no grant, valid grant, and revoked/cleared grant scenarios.
  • Ensure the canonical path exposes enough context for downstream guard/audit layers to correlate deny/allow and delivery outcomes, including agentId, session/request identity, channel/target metadata, allow/deny reason, and delivery result or blocked result.

Example

  • No specific code snippet can be provided without modifying the existing OpenClaw codebase, but the solution involves ensuring that the message_sending hook is properly integrated into the chosen canonical path and that all required information is preserved and returned.

Notes

  • The solution requires careful consideration of the OpenClaw architecture and the specific requirements outlined in the issue, including the need for a single canonical outbound message.send path that meets all conditions.
  • The chosen canonical path must be thoroughly tested to ensure it satisfies all the requirements and scenarios outlined in the acceptance contract.

Recommendation

  • Apply workaround: Modify the direct openclaw message send path to meet all the required conditions, as it seems to be the more straightforward approach, and then test it thoroughly to ensure it satisfies all the requirements and scenarios outlined in the acceptance contract.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING