sipeed/picoclaw · warning

channel %s does not support reactions

Error message

channel %s does not support reactions

What it means

The resolved channel exists but does not implement channels.ReactionCapable (pkg/channels/interfaces.go:42), the interface with ReactToMessage(ctx, chatID, messageID). The reaction tool type-asserts the channel to this interface; only some channel types implement it. This is a permanent capability mismatch — retrying against the same channel will always fail.

Source

Thrown at pkg/agent/agent_init.go:233

				pubCtx, pubCancel := context.WithTimeout(context.Background(), 5*time.Second)
				defer pubCancel()
				return msgBus.PublishOutbound(pubCtx, outboundMessage)
			})
			agent.Tools.Register(messageTool)
		}
		if cfg.Tools.IsToolEnabled("reaction") {
			reactionTool := tools.NewReactionTool()
			reactionTool.SetReactionCallback(func(ctx context.Context, channel, chatID, messageID string) error {
				if al.channelManager == nil {
					return fmt.Errorf("channel manager not configured")
				}
				ch, ok := al.channelManager.GetChannel(channel)
				if !ok {
					return fmt.Errorf("channel %s not found", channel)
				}
				rc, ok := ch.(channels.ReactionCapable)
				if !ok {
					return fmt.Errorf("channel %s does not support reactions", channel)
				}
				_, err := rc.ReactToMessage(ctx, chatID, messageID)
				return err
			})
			agent.Tools.Register(reactionTool)
		}

		// Send file tool (outbound media via MediaStore — store injected later by SetMediaStore)
		if cfg.Tools.IsToolEnabled("send_file") {
			sendFileTool := tools.NewSendFileTool(
				agent.Workspace,
				cfg.Agents.Defaults.RestrictToWorkspace,
				cfg.Agents.Defaults.GetMaxMediaSize(),
				nil,
				allowReadPaths,
			)
			agent.Tools.Register(sendFileTool)
		}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Treat reactions as best-effort on this channel and instruct the model (system prompt) to only use reactions on channels that support them
  2. If it is your own channel, implement ReactToMessage returning an idempotent undo func (see interface contract)
  3. Disable the reaction tool if none of your channels support it
  4. Pick a reaction-capable channel (e.g. Telegram) for flows that rely on the 👀 acknowledgement

Example fix

// before — custom channel without reactions
type MyChannel struct{ /* ... */ }

// after — opt in to reactions
var _ channels.ReactionCapable = (*MyChannel)(nil)

func (c *MyChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) {
    if err := c.api.AddReaction(ctx, chatID, messageID, "👀"); err != nil {
        return nil, err
    }
    return func() { _ = c.api.RemoveReaction(ctx, chatID, messageID, "👀") }, nil // must be idempotent
}
Defensive patterns

Strategy: type-guard

Validate before calling

ch, ok := cm.GetChannel(name)
if !ok {
    return fmt.Errorf("channel %q not registered", name)
}
if _, capable := ch.(channels.ReactionCapable); !capable {
    // skip the reaction flow; this channel can never react
    return nil
}

Type guard

func supportsReactions(ch channels.Channel) bool {
    _, ok := ch.(channels.ReactionCapable)
    return ok
}

Prevention

When it happens

Trigger: Reaction tool invoked against a channel type that lacks ReactToMessage (console/IRC-style channels, test stubs, most custom channels), or a custom channel implementation that simply does not define the method.

Common situations: Writing a custom channel and assuming reaction support comes for free; enabling the reaction tool globally while some configured channels cannot react; the model choosing a non-reaction channel by mistake.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/7aed9b550505413b. Report an issue: GitHub.