{"record":{"id":"7aed9b550505413b","repo":"sipeed/picoclaw","slug":"channel-s-does-not-support-reactions","errorCode":null,"errorMessage":"channel %s does not support reactions","messagePattern":"channel (.+?) does not support reactions","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/agent/agent_init.go","lineNumber":233,"sourceCode":"\t\t\t\tpubCtx, pubCancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\t\t\tdefer pubCancel()\n\t\t\t\treturn msgBus.PublishOutbound(pubCtx, outboundMessage)\n\t\t\t})\n\t\t\tagent.Tools.Register(messageTool)\n\t\t}\n\t\tif cfg.Tools.IsToolEnabled(\"reaction\") {\n\t\t\treactionTool := tools.NewReactionTool()\n\t\t\treactionTool.SetReactionCallback(func(ctx context.Context, channel, chatID, messageID string) error {\n\t\t\t\tif al.channelManager == nil {\n\t\t\t\t\treturn fmt.Errorf(\"channel manager not configured\")\n\t\t\t\t}\n\t\t\t\tch, ok := al.channelManager.GetChannel(channel)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"channel %s not found\", channel)\n\t\t\t\t}\n\t\t\t\trc, ok := ch.(channels.ReactionCapable)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"channel %s does not support reactions\", channel)\n\t\t\t\t}\n\t\t\t\t_, err := rc.ReactToMessage(ctx, chatID, messageID)\n\t\t\t\treturn err\n\t\t\t})\n\t\t\tagent.Tools.Register(reactionTool)\n\t\t}\n\n\t\t// Send file tool (outbound media via MediaStore — store injected later by SetMediaStore)\n\t\tif cfg.Tools.IsToolEnabled(\"send_file\") {\n\t\t\tsendFileTool := tools.NewSendFileTool(\n\t\t\t\tagent.Workspace,\n\t\t\t\tcfg.Agents.Defaults.RestrictToWorkspace,\n\t\t\t\tcfg.Agents.Defaults.GetMaxMediaSize(),\n\t\t\t\tnil,\n\t\t\t\tallowReadPaths,\n\t\t\t)\n\t\t\tagent.Tools.Register(sendFileTool)\n\t\t}","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/agent/agent_init.go#L215-L251","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Treat reactions as best-effort on this channel and instruct the model (system prompt) to only use reactions on channels that support them","If it is your own channel, implement ReactToMessage returning an idempotent undo func (see interface contract)","Disable the reaction tool if none of your channels support it","Pick a reaction-capable channel (e.g. Telegram) for flows that rely on the 👀 acknowledgement"],"exampleFix":"// before — custom channel without reactions\ntype MyChannel struct{ /* ... */ }\n\n// after — opt in to reactions\nvar _ channels.ReactionCapable = (*MyChannel)(nil)\n\nfunc (c *MyChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) {\n    if err := c.api.AddReaction(ctx, chatID, messageID, \"👀\"); err != nil {\n        return nil, err\n    }\n    return func() { _ = c.api.RemoveReaction(ctx, chatID, messageID, \"👀\") }, nil // must be idempotent\n}","handlingStrategy":"type-guard","validationCode":"ch, ok := cm.GetChannel(name)\nif !ok {\n    return fmt.Errorf(\"channel %q not registered\", name)\n}\nif _, capable := ch.(channels.ReactionCapable); !capable {\n    // skip the reaction flow; this channel can never react\n    return nil\n}","typeGuard":"func supportsReactions(ch channels.Channel) bool {\n    _, ok := ch.(channels.ReactionCapable)\n    return ok\n}","tryCatchPattern":null,"preventionTips":["Add compile-time assertions on custom channels: var _ channels.ReactionCapable = (*MyChannel)(nil)","Check the capability before enabling reaction affordances in UI/flows, not after the tool call fails","Remember the undo func returned by ReactToMessage must be idempotent — a broken undo breaks later mounts"],"tags":["go","picoclaw","channels","reaction-tool","type-assertion","capabilities"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}