package channelbridge import ( "context" "fmt" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) // bubbleSender is the per-bubble outbound call the shared IM dispatcher uses. A // reply exits through the adapter's SendMessage (UnifiedResponse) path, a proactive // push through its SendProactive path — both collapse to "dispatch one chat bubble to // the platform", which is what lets the reply and the proactive push share a single // sanitize/split/gap pipeline instead of each re-implementing it. type bubbleSender func(ctx context.Context, msg adapter.UnifiedMessage) error // sendIMBubbles is the single IM outbound exit shared by sendReply (reply path) and // SendProactive (proactive push). It turns content into the plain chat bubbles an IM // channel should receive: role-play/action/narration markup (rendered as muted italic // on the Web) is stripped and each

paragraph is delivered as its own // bubble/message (design/17 §2), with a multi-bubble burst spacing its sends 0.5-2s // apart so it does not flood the channel. The Web frontend is never affected — it // renders the persisted stream directly and does not route through here. // // target carries the platform/channel targeting fields and is copied onto every // bubble (only Content/ContentType/Timestamp differ per bubble); sender is the // adapter's outbound call. A content that strips to nothing (a pure action line) is // dropped without a send, and a per-bubble failure is wrapped with its index. func (b *Bridge) sendIMBubbles(ctx context.Context, target adapter.UnifiedMessage, sender bubbleSender, content string) error { // splitBubbles applies stripNarration per bubble (so / and the // star-drift role-play forms are removed for IM) and splits on

. It is the // single sanitize entry point, shared with the old reply path. bubbles := splitBubbles(content) if len(bubbles) == 0 { b.log.Debug("im outbound has no plain text, skipped", "platform", target.Platform, "channel", target.ChannelID) return nil } for i, bubble := range bubbles { if i > 0 { time.Sleep(b.bubbleGapFn()) } out := target out.Content = bubble out.ContentType = adapter.ContentText out.Timestamp = time.Now().UTC() // Each bubble is its own send so the gap lands between messages. if err := sender(ctx, out); err != nil { return fmt.Errorf("channelbridge: send bubble #%d of %d: %w", i+1, len(bubbles), err) } } return nil }