From ecca66da586f01184573d6c25e4889f86bd282a3 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:35:37 -0700 Subject: [PATCH] Coop: split lock checks (per-minute) from resolution (daily) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locks were gated behind the same once-per-day 08:00 UTC tick as floor resolution. A run created after morningHour would wait until next day's tick to be eligible — meaning actual lock latency could be 24-48h, not the 24h advertised. Worse, a run created late on Day N missed the Day N+1 tick (too early) AND the Day N+2 tick happens 48h after creation. Fix: lock checks fire on every ticker minute (cheap timestamp scan over typically 0-5 open runs). Resolution stays daily, gated by JobCompleted. Real symptom that prompted this: Run #1 created 17:17 UTC, locked never fired because the morning tick at 08:00 UTC the next day saw the run as not-yet-eligible (only 15h elapsed), then marked the daily job complete before the actual 24h elapsed at 17:17 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/coop_dungeon_scheduler.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/internal/plugin/coop_dungeon_scheduler.go b/internal/plugin/coop_dungeon_scheduler.go index e9bfdc3..835855a 100644 --- a/internal/plugin/coop_dungeon_scheduler.go +++ b/internal/plugin/coop_dungeon_scheduler.go @@ -14,20 +14,24 @@ import ( "maunium.net/go/mautrix/id" ) -// coopTicker fires once per UTC day to: -// 1. Lock open invites whose 24h window has elapsed. -// 2. For each active run, resolve the day's floor and advance/complete. +// coopTicker has two cadences: // -// Reuses the daily_prefetch job-completion guard so a bot restart on the same -// UTC day does not double-process. +// 1. Lock checks fire every minute. They're cheap (one timestamp comparison +// per open run) and a 24h invite window is poorly served by a 24h tick — +// a run started after 08:00 UTC would otherwise wait 30-48h to lock. +// 2. Floor resolution fires once per UTC day at morningHour, gated by the +// daily_prefetch JobCompleted guard. Resolution is the heavy step that +// advances days, distributes rewards, and posts to the room. func (p *AdventurePlugin) coopTicker() { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for range ticker.C { + // Locks: every minute. + p.coopProcessLocks() + + // Resolutions: once per day at morningHour:00 UTC. now := time.Now().UTC() - // Run at the same minute as the morning DM (08:00 UTC) — same cadence as - // the rest of the adventure system. if now.Hour() != p.morningHour || now.Minute() != 0 { continue } @@ -36,8 +40,7 @@ func (p *AdventurePlugin) coopTicker() { if db.JobCompleted(jobName, dateKey) { continue } - slog.Info("coop: daily tick") - p.coopProcessLocks() + slog.Info("coop: daily tick — resolving active runs") p.coopProcessActiveRuns() db.MarkJobCompleted(jobName, dateKey) }