diff --git a/internal/plugin/adventure_arena.go b/internal/plugin/adventure_arena.go index 6e21f63..485cef4 100644 --- a/internal/plugin/adventure_arena.go +++ b/internal/plugin/adventure_arena.go @@ -429,7 +429,10 @@ func (p *AdventurePlugin) resolveArenaSurvival(ctx MessageContext, run *ArenaRun finalMessage += "`!arena fight` — Face this opponent" } - p.sendZoneCombatMessages(ctx.Sender, phaseMessages, finalMessage) + // Fire-and-forget: no post-flush work; blocking would stall the + // round-advance handler behind streamed pacing. Contract is honored + // by the explicit discard — see sendZoneCombatMessages comment. + _ = p.sendZoneCombatMessages(ctx.Sender, phaseMessages, finalMessage) return nil } diff --git a/internal/plugin/combat_bridge.go b/internal/plugin/combat_bridge.go index 88d948c..55c949a 100644 --- a/internal/plugin/combat_bridge.go +++ b/internal/plugin/combat_bridge.go @@ -244,6 +244,13 @@ func (p *AdventurePlugin) sendCombatMessages(userID id.UserID, phaseMessages []s // sendZoneCombatMessages is the zone/expedition variant — same staging as // arena, but tighter pacing (2–3s) so dungeon advances feel snappier. +// +// Contract: returns a done channel that closes after the final DM has +// been sent. Callers that need to chain post-flush work (death follow-ups, +// hospital ads, tier countdowns) MUST consume it. Callers in the middle +// of a command-handler flow that have no post-flush work should explicitly +// discard with `_ = ...` — blocking on flush would stall the handler +// behind the 2–3s/message pacing, which is the whole point of streaming. func (p *AdventurePlugin) sendZoneCombatMessages(userID id.UserID, phaseMessages []string, finalMessage string) <-chan struct{} { return p.sendCombatMessagesWithDelay(userID, phaseMessages, finalMessage, 2, 2) // 2–3s } diff --git a/internal/plugin/dnd_zone_cmd.go b/internal/plugin/dnd_zone_cmd.go index 65c2126..8898e00 100644 --- a/internal/plugin/dnd_zone_cmd.go +++ b/internal/plugin/dnd_zone_cmd.go @@ -652,7 +652,10 @@ func (p *AdventurePlugin) streamFlow(userID id.UserID, phaseMessages []string, f if len(phaseMessages) == 0 { return p.SendDM(userID, finalMessage) } - p.sendZoneCombatMessages(userID, phaseMessages, finalMessage) + // Fire-and-forget: streamFlow's whole job is to hand off to the + // streamer and return — no post-flush chaining. Explicit discard + // honors the sendZoneCombatMessages contract. + _ = p.sendZoneCombatMessages(userID, phaseMessages, finalMessage) return nil } diff --git a/internal/plugin/presence.go b/internal/plugin/presence.go index f1a1a77..a9025f6 100644 --- a/internal/plugin/presence.go +++ b/internal/plugin/presence.go @@ -146,6 +146,11 @@ func (p *PresencePlugin) handleWhois(ctx MessageContext) error { displayName = string(targetUser) } + // best-effort: activity tracking is non-critical; missing rows are + // expected for new users. The cluster of `_ = QueryRow(...).Scan(...)` + // calls below all share this rationale — zero values are the correct + // fallback (no messages yet, no streak, no rep, no presence row). + // Get stats var totalMessages, totalWords int _ = db.Get().QueryRow(