{"record":{"id":"ad9bd3af9fe974cd","repo":"sipeed/picoclaw","slug":"no-target-channel-chat-available","errorCode":null,"errorMessage":"no target channel/chat available","messagePattern":"no target channel/chat available","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/audio/tts/tts.go","lineNumber":105,"sourceCode":"\n// SynthesizeAndStore synthesizes text to speech and registers it in the media store, returning the media reference.\nfunc SynthesizeAndStore(\n\tctx context.Context,\n\tprovider TTSProvider,\n\tstore media.MediaStore,\n\ttext string,\n\tfilename string,\n\tchannel string,\n\tchatID string,\n) (string, error) {\n\tif provider == nil {\n\t\treturn \"\", fmt.Errorf(\"tts provider is not configured\")\n\t}\n\tif store == nil {\n\t\treturn \"\", fmt.Errorf(\"media store not configured\")\n\t}\n\tif channel == \"\" || chatID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no target channel/chat available\")\n\t}\n\tif strings.TrimSpace(text) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"text is required\")\n\t}\n\n\tstream, err := provider.Synthesize(ctx, text)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"tts synthesize failed: %w\", err)\n\t}\n\tdefer stream.Close()\n\n\terr = os.MkdirAll(media.TempDir(), 0o700)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create media temp dir: %w\", err)\n\t}\n\n\tfileExt := \".ogg\"\n\tcontentType := \"audio/ogg\"","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/audio/tts/tts.go#L87-L123","documentation":"Returned by tts.SynthesizeAndStore when channel or chatID is empty. These identify where the audio will be registered — they feed the media store scope 'tool:send_tts:<channel>:<chatID>:<timestamp>' — so without them the store cannot namespace the media.","triggerScenarios":"Invoking TTS from a context without chat routing: scheduled jobs, CLI commands, webhooks that carry no channel; passing zero-value strings from a message struct that was never populated.","commonSituations":"New integrations (cron announcements, test harnesses) that bypass the chat message path.","solutions":["Derive channel/chatID from the originating message before calling SynthesizeAndStore","Default them to explicit values (e.g. 'console'/'local') for non-chat contexts","Skip voice output when the calling context has no target"],"exampleFix":"// before\nref, err := tts.SynthesizeAndStore(ctx, p, s, text, name, ``, ``)\n// after\nif channel == `` {\n    channel = `console`\n}\nif chatID == `` {\n    chatID = `local`\n}\nref, err := tts.SynthesizeAndStore(ctx, p, s, text, name, channel, chatID)","handlingStrategy":"validation","validationCode":"if channel == `` || chatID == `` {\n    return errors.New(`tts requires a target channel and chat id; derive them from the originating message`)\n}","typeGuard":"func isNoTargetError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), `no target channel/chat`)\n}","tryCatchPattern":"if err != nil && isNoTargetError(err) {\n    // context has no chat routing: skip voice output or supply explicit defaults\n}","preventionTips":["Thread channel/chatID from the originating message into every TTS call","Give non-chat contexts (cron, CLI) explicit default identifiers","Validate routing fields before entering the voice pipeline"],"tags":["validation","tts","routing"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}