Skip to content

Plugin slash commands aren't dispatched in openclaw agent (--local and gateway) or TUI #78347

@Shreyanshsingh23

Description

@Shreyanshsingh23

Summary

Plugin-registered slash commands (api.registerCommand({ name: "remember", ... })) are correctly persisted to the central registry but never fire when the user runs openclaw agent --message "/remember ...". Both the --local (embedded) and gateway agent paths take the message verbatim and hand it to the LLM without ever calling the plugin command dispatcher. The TUI behaves the same way.

This is an OpenClaw core gap, not a plugin gap. Slash commands work fine in the channels that wire dispatch up themselves (Telegram, Discord, and any channel routed through the auto-reply pipeline), but the agent CLI surface is missing the equivalent hook.

Reproduction

With any plugin that registers a slash command (e.g. @maximem/memory-plugin exposes /remember and /recall):

$ openclaw agent --local --message "/recall favorite color" --to "+15555550999"

Expected: the /recall handler fires, returns its reply.
Actual: "/recall favorite color" is sent to the LLM as a regular user prompt. The slash never matches.

Confirmed at the source level on origin/main:

$ git grep -nE "executePluginCommand|matchPluginCommand" -- "*.ts"
extensions/discord/src/monitor/native-command.runtime.ts   ← Discord channel
src/telegram/bot-native-commands.ts                         ← Telegram channel
src/auto-reply/reply/commands-plugin.ts                     ← generic auto-reply
src/plugins/commands.ts                                     ← (declarations only)

Three call sites, all in channel-handling code. Neither src/commands/agent.ts (--local path → agentCommand) nor src/commands/agent-via-gateway.ts (gateway path → agentViaGatewayCommand) reference handleCommands, handlePluginCommand, matchPluginCommand, or executePluginCommand:

$ git grep -nE "handleCommands|handlePluginCommand|matchPluginCommand|executePluginCommand" \
    src/commands/agent.ts src/commands/agent-via-gateway.ts
(no matches)

So when the user types --message "/<cmd>", the dispatcher is loaded in memory but nobody calls it on this path.

Why it matters

  1. Plugin authors can't dev-loop locally. Building any plugin that registers slash commands requires hooking up a real Telegram/Discord/etc. channel just to verify the command runs. There is no terminal-driven equivalent today.
  2. CI / smoke tests can't exercise slash flows. Verifying a plugin command via openclaw agent --local is the natural minimum-viable integration test, but it can't observe the slash path.
  3. Inconsistent surface. TUI and the agent CLI silently send /<cmd> to the LLM, which is surprising. Users reasonably expect a /-prefixed message to behave the same way it does in their channels.

Proposed fix

Call the existing handleCommands wrapper (the same one the auto-reply pipeline uses) in both agent paths, before the message is forwarded to the LLM. The wrapper already returns null when no plugin command matches, so non-slash messages flow through unchanged.

Sketch (pseudo-diff against src/commands/agent.ts near where body is sent into the agent runner):

import { handleCommands } from "../auto-reply/reply/commands.js";

// Inside agentCommand, after `body` is normalised:
const commandResult = await handleCommands({
  command: {
    commandBodyNormalized: body,
    senderId: opts.to ?? "cli",
    channel: opts.channel ?? "cli",
    isAuthorizedSender: true,
  },
  cfg,
}, /* allowTextCommands */ true);

if (commandResult && !commandResult.shouldContinue) {
  // Print/return the plugin command's reply and exit early.
  runtime.log?.(commandResult.reply.text ?? JSON.stringify(commandResult.reply));
  return;
}
// ...existing flow: hand `body` to the embedded agent runner.

The same insertion point applies in agentViaGatewayCommand — either dispatch locally before sending to the gateway, or have the gateway invoke handleCommands on inbound agent requests (cleaner, but a bigger change). Local dispatch keeps --local and gateway behaviourally aligned with channels that already do this themselves.

The same hook is also worth adding to the TUI input path so /<cmd> typed in the TUI fires the same handlers.

Acceptance

  • openclaw agent --local --message "/<registered-cmd>" invokes the registered handler and returns its reply.
  • openclaw agent --message "/<registered-cmd>" (gateway path) does the same.
  • Messages that don't match any plugin command flow through to the LLM unchanged (verified by passing a plain prompt and confirming the LLM is invoked).
  • openclaw tui slash input fires plugin commands.

Happy to follow up with a PR if there's interest in the local-dispatch shape proposed above. The Telegram and Discord call sites are clean precedents to mirror.

References

  • src/commands/agent.tsagentCommand (the --local entry)
  • src/commands/agent-via-gateway.tsagentViaGatewayCommand (gateway entry)
  • src/auto-reply/reply/commands.ts — exports handleCommands
  • src/auto-reply/reply/commands-plugin.tshandlePluginCommand implementation
  • src/plugins/commands.tsmatchPluginCommand / executePluginCommand declarations
  • src/plugins/registry.ts:422 — confirms plugin registrations land in the central registry correctly; the gap is only on the dispatch side.

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleMarked as stale due to inactivity

    Type

    No type
    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