openclaw - 💡(How to fix) Fix message tool: `message` body param schema-optional but runtime-required, causing silent LLM field-drop on send [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#69650Fetched 2026-04-22 07:49:43
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
closed ×1

message.send has a latent schema/runtime mismatch that causes models to omit the body text on otherwise valid calls. The tool returns message required and the agent has no way to recover without an out-of-band hint.

Root Cause

The body field name (message) collides with the tool name (message). When we ask an LLM to call message(action: \"send\", ..., message: \"hello\"), the model regularly drops the inner message field — producing { action: \"send\", channel: ..., target: ... } with no body. Because the schema marks that field optional, there is no validator signal to push the model to include it; the failure only surfaces at runtime.

Fix Action

Fix / Workaround

Every puppet / shared-session deployment that uses message.send for plain text hits this. Workarounds today are either patching the runner downstream or prompt-engineering the model to always include message:, neither of which scales.

Code Example

function buildSendSchema(options: { includeInteractive: boolean }) {
  const props: Record<string, TSchema> = {
    message: Type.Optional(Type.String()),
    ...
  };
}

---

let message =
  readStringParam(params, "message", {
    required:
      !mediaHint && !hasButtons && !hasCard && !hasComponents && !hasInteractive && !hasBlocks,
    allowEmpty: true,
  }) ?? "";

---

message(action: \"send\", channel: \"discorduser\", target: \"channel:<id>\", message: \"test\", accountId: \"alice\")

---

message(action: \"send\", channel: \"discorduser\", target: \"channel:<id>\", accountId: \"alice\")
RAW_BUFFERClick to expand / collapse

Summary

message.send has a latent schema/runtime mismatch that causes models to omit the body text on otherwise valid calls. The tool returns message required and the agent has no way to recover without an out-of-band hint.

Mismatch

Schema declares the body optionalsrc/agents/tools/message-tool.ts:88:

function buildSendSchema(options: { includeInteractive: boolean }) {
  const props: Record<string, TSchema> = {
    message: Type.Optional(Type.String()),
    ...
  };
}

Runtime requires itsrc/infra/outbound/message-action-runner.ts:484:

let message =
  readStringParam(params, "message", {
    required:
      !mediaHint && !hasButtons && !hasCard && !hasComponents && !hasInteractive && !hasBlocks,
    allowEmpty: true,
  }) ?? "";

So a text-only send call with the message field missing is schema-valid but runtime-rejected.

Why this bites on real calls

The body field name (message) collides with the tool name (message). When we ask an LLM to call message(action: \"send\", ..., message: \"hello\"), the model regularly drops the inner message field — producing { action: \"send\", channel: ..., target: ... } with no body. Because the schema marks that field optional, there is no validator signal to push the model to include it; the failure only surfaces at runtime.

We see this on Claude, but the shape of the problem is provider-agnostic — it's a tool-schema design issue.

Reproduction

Anthropic claude-opus-4-7, discorduser plugin, text-only send:

message(action: \"send\", channel: \"discorduser\", target: \"channel:<id>\", message: \"test\", accountId: \"alice\")

~50% of the time the model emits:

message(action: \"send\", channel: \"discorduser\", target: \"channel:<id>\", accountId: \"alice\")

Runtime returns message required. Retrying in the same turn doesn't reliably fix it because the model has already committed the schema it thinks the tool wants.

Suggested fixes (either/or)

A. Tighten the schema to match runtime. Use a discriminated union on action so send without any media/card/components/interactive/blocks actually requires message. This is the right fix because it gives LLMs a correctness signal they can use during call generation.

B. Accept a disambiguating alias. Teach readStringParam (or the send runner specifically) to fall back to body, text, or content if message is missing. This follows the same pattern already used in buildReactionSchema at lines 151–157, where messageId is aliased as message_id explicitly for "tool-schema discoverability in LLMs." A similar note there says:

Intentional duplicate alias for tool-schema discoverability in LLMs.

An alias is cheaper to ship and backwards-compatible. I'd advocate doing both — alias now, discriminated-union later.

Impact

Every puppet / shared-session deployment that uses message.send for plain text hits this. Workarounds today are either patching the runner downstream or prompt-engineering the model to always include message:, neither of which scales.

Happy to send a PR for the alias fix if the direction sounds right.


Environment: openclaw v2026.3.1, discorduser plugin, claude-opus-4-7

extent analysis

TL;DR

The most likely fix is to tighten the schema to match the runtime requirements by making the message field required for the send action when no other media or interactive elements are present.

Guidance

  • Review the buildSendSchema function in message-tool.ts to ensure it accurately reflects the runtime requirements for the send action.
  • Consider adding a discriminated union to the action field in the schema to require message when no other media or interactive elements are present.
  • As a temporary workaround, teach readStringParam to accept a disambiguating alias such as body, text, or content if message is missing.
  • Verify the fix by testing the message.send action with and without the message field to ensure it behaves as expected.

Example

function buildSendSchema(options: { includeInteractive: boolean }) {
  const props: Record<string, TSchema> = {
    message: Type.String(), // Make message required
    ...
  };
  // or use a discriminated union
  const props: Record<string, TSchema> = {
    action: Type.Union(
      Type.Object({
        send: Type.Object({
          message: Type.String(), // require message for send action
        }),
      }),
      // other actions
    ),
  };
}

Notes

The provided information suggests that the issue is specific to the message.send action and the collision between the message field and the tool name. The suggested fixes should address this specific issue, but further testing may be required to ensure no other edge cases are introduced.

Recommendation

Apply the workaround by teaching readStringParam to accept a disambiguating alias, and then upgrade to a tighter schema that matches the runtime requirements. This approach allows for a quick fix while also planning for a more robust solution.

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

openclaw - 💡(How to fix) Fix message tool: `message` body param schema-optional but runtime-required, causing silent LLM field-drop on send [1 participants]