{"record":{"id":"d8a4edbc987d820f","repo":"sipeed/picoclaw","slug":"matrix-room-id-is-empty","errorCode":null,"errorMessage":"matrix room ID is empty","messagePattern":"matrix room ID is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/channels/matrix/matrix.go","lineNumber":587,"sourceCode":"\t\t}\n\t}\n\n\tif hasTrackedMsg {\n\t\tc.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)\n\t}\n\n\treturn eventIDs, nil\n}\n\n// StartTyping implements channels.TypingCapable.\nfunc (c *MatrixChannel) StartTyping(ctx context.Context, chatID string) (func(), error) {\n\tif !c.IsRunning() {\n\t\treturn func() {}, nil\n\t}\n\n\troomID := id.RoomID(strings.TrimSpace(chatID))\n\tif roomID == \"\" {\n\t\treturn func() {}, fmt.Errorf(\"matrix room ID is empty\")\n\t}\n\n\tsession := newTypingSession()\n\n\tc.typingMu.Lock()\n\tif prev := c.typingSessions[chatID]; prev != nil {\n\t\tprev.stop()\n\t}\n\tc.typingSessions[chatID] = session\n\tc.typingMu.Unlock()\n\n\tparent := c.baseContext()\n\tgo c.typingLoop(parent, roomID, session)\n\n\tvar once sync.Once\n\tstop := func() {\n\t\tonce.Do(func() {\n\t\t\tsession.stop()","sourceCodeStart":569,"sourceCodeEnd":605,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/matrix/matrix.go#L569-L605","documentation":"Thrown by MatrixChannel.StartTyping when chatID is empty after trimming (pkg/channels/matrix/matrix.go:587). It returns a no-op stop function alongside the error, so callers that ignore the error still get a safe closure. Note the distinct behavior one branch above: when the channel is not running, StartTyping returns a no-op with NO error — only a blank room ID errors. Typing is cosmetic, so failures here should never break message flow.","triggerScenarios":"Calling StartTyping with an empty/whitespace chat ID from a UI/handler that lost its conversation binding; calling it before the chat ID is established (e.g. during onboarding when no inbound Matrix event exists yet).","commonSituations":"Frontend handlers invoking typing on selection of a not-yet-opened conversation; relays synthesizing typing notifications without a mapped room; races where typing is requested while a conversation is being created.","solutions":["Skip the typing call when chatID is blank — degrade silently rather than surfacing an error","Fix the caller to pass the same trimmed room ID used for Send","Treat any error from StartTyping as non-fatal: log at debug level and continue"],"exampleFix":"// before\n stop, err := ch.StartTyping(ctx, chatID)\n if err != nil { return err } // typing failure aborts the flow\n\n// after\n stop, err := ch.StartTyping(ctx, chatID)\n if err != nil { logger.DebugC(\"matrix\", \"typing indicator skipped: \" + err.Error()) }\n defer stop() // no-op closure is safe either way","handlingStrategy":"validation","validationCode":"// typing is cosmetic: skip silently when unsendable\nvar stop func() = func() {}\nif id := strings.TrimSpace(chatID); id != \"\" {\n\ts, err := matrixCh.StartTyping(ctx, id)\n\tif err == nil { stop = s }\n}","typeGuard":"func canShowTyping(chatID string) bool {\n\treturn strings.TrimSpace(chatID) != \"\"\n}","tryCatchPattern":"stop, err := matrixCh.StartTyping(ctx, chatID)\nif err != nil {\n\tlog.Debug(\"typing indicator unavailable: \", err) // never propagate: UI degrades gracefully\n}\ndefer stop() // safe: returns a no-op closure on every error path","preventionTips":["Never let typing errors bubble into user flows — they are cosmetic only","Always defer the returned stop function; it is a safe no-op on error","Pass the exact trimmed room ID you use for Send"],"tags":["go","matrix","validation","typing","ux-indicator"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}