{"record":{"id":"c20abc30bfe2631c","repo":"sipeed/picoclaw","slug":"channel-manager-not-configured","errorCode":null,"errorMessage":"channel manager not configured","messagePattern":"channel manager not configured","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/agent/agent_init.go","lineNumber":225,"sourceCode":"\t\t\t\t\tSessionKey:       outboundSessionKey,\n\t\t\t\t\tScope:            outboundScope,\n\t\t\t\t\tContent:          content,\n\t\t\t\t\tReplyToMessageID: replyToMessageID,\n\t\t\t\t}\n\t\t\t\tif al.channelManager != nil && channel != \"\" {\n\t\t\t\t\treturn al.channelManager.SendMessage(ctx, outboundMessage)\n\t\t\t\t}\n\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(","sourceCodeStart":207,"sourceCodeEnd":243,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/agent/agent_init.go#L207-L243","documentation":"Returned by the reaction tool's callback when the AgentLoop has no channels.Manager wired in. picoclaw injects the manager after construction via al.SetChannelManager (pkg/agent/agent_inject.go:22), but the reaction tool is registered from config regardless, so the callback guards on al.channelManager == nil. If this fires, the 'reaction' tool was enabled but the embedding process never attached a channel manager.","triggerScenarios":"Config enables the 'reaction' tool (cfg.Tools.IsToolEnabled(\"reaction\")) and the model invokes it while SetChannelManager was never called (or called with nil) — e.g. running the loop headless with only the message bus, or starting dispatch before channel setup finished.","commonSituations":"Embedding picoclaw in direct/CLI mode without channels but keeping the default tool list; tests constructing an AgentLoop by hand and forgetting the inject calls; startup-ordering bugs where al.Run starts before channels are registered.","solutions":["Create and start a channels.Manager (channels.NewManager + RegisterChannel + StartAll) and pass it via al.SetChannelManager(cm) before al.Run(ctx)","If channels are intentionally unused, remove 'reaction' from the enabled tools in config so the tool is never registered","Fix startup ordering: build manager -> register/start channels -> SetChannelManager -> Run","Add a startup smoke test that dispatches a reaction to a scratch channel and asserts no error"],"exampleFix":"// before\nal := agent.NewAgentLoop(cfg, ...)\ngo al.Run(ctx) // reaction tool enabled in cfg, no channel manager\n\n// after\nal := agent.NewAgentLoop(cfg, ...)\ncm, err := channels.NewManager(cfg, msgBus, mediaStore)\nif err != nil {\n    return err\n}\ncm.RegisterChannel(\"telegram\", telegramChannel)\nif err := cm.StartAll(ctx); err != nil {\n    return err\n}\nal.SetChannelManager(cm)\ngo al.Run(ctx)","handlingStrategy":"validation","validationCode":"cm, err := channels.NewManager(cfg, msgBus, mediaStore)\nif err != nil {\n    return fmt.Errorf(\"create channel manager: %w\", err)\n}\nif err := cm.StartAll(ctx); err != nil {\n    return fmt.Errorf(\"start channels: %w\", err)\n}\nal.SetChannelManager(cm) // must happen before al.Run when channel tools are enabled\nif cfg.Tools.IsToolEnabled(\"reaction\") || cfg.Tools.IsToolEnabled(\"message\") {\n    if cm == nil {\n        return fmt.Errorf(\"channel-bound tools enabled but no channel manager wired\")\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always call SetChannelManager before Run when channel-bound tools (reaction, message, send_file) are enabled","In headless/direct mode, strip channel-bound tools from the config so the guard can never trip","Add a startup assertion that fails fast on nil channel manager instead of failing at first tool call"],"tags":["go","picoclaw","channels","reaction-tool","dependency-injection"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}