From 9aca6b202dec58f6b8aff9ea4cb015f3d7573fd2 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Fri, 15 May 2026 08:03:42 -0700 Subject: [PATCH] Phase D: presence intent comment + sendZoneCombatMessages contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D1: single block comment above the presence.go best-effort QueryRow cluster (lines 149-198). Zero-value fallbacks are correct for new users with no activity rows yet. D2: contract comment on sendZoneCombatMessages — returns a done channel, callers chaining post-flush work MUST consume it; in-flow handlers with nothing to chain MUST explicitly discard. Blocking on flush would stall command handlers behind the 2-3s/message pacing, defeating streaming. Two fire-and-forget sites (adventure_arena.go round-advance, dnd_zone_cmd.go streamFlow) now use `_ =` + comment. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/adventure_arena.go | 5 ++++- internal/plugin/combat_bridge.go | 7 +++++++ internal/plugin/dnd_zone_cmd.go | 5 ++++- internal/plugin/presence.go | 5 +++++ 4 files changed, 20 insertions(+), 2 deletions(-) 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(