From 690ff758feaae92a1523dc1af912603f99d7c510 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:52:03 -0700 Subject: [PATCH] worldboss: spawn the monthly Siege without the day-1 deadlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Siege has never once spawned in prod. `select count(*) from world_boss` is 0 and daily_prefetch has no worldboss_spawn row at all. worldBossTick only auto-spawned when now.Day() == 1. The world boss landed on main 2026-07-10..13 and the first deploy carrying it was after July 1, so prod has never run a first-of-the-month tick with the code in it. The feature has been live and unreachable for weeks, and the next natural spawn would have been August 1. The same gate also silently skipped any month where the bot happened to be down or redeploying across the 1st, with no catch-up — one missed minute costs the town a month. The month key is already the whole dedup, so drop the day check and let the rule be what it always read as: one Siege per calendar month, as early as the process is up to run it. A missed 1st now self-heals on the next tick. --- internal/plugin/adventure_worldboss.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/internal/plugin/adventure_worldboss.go b/internal/plugin/adventure_worldboss.go index f0e5cb9..3eda294 100644 --- a/internal/plugin/adventure_worldboss.go +++ b/internal/plugin/adventure_worldboss.go @@ -398,10 +398,10 @@ func (p *AdventurePlugin) spawnWorldBoss(eventKey string) (*worldBossState, erro return boss, nil } -// worldBossTick rides the 1-minute event ticker. It auto-spawns a boss on the -// first of each UTC month and resolves a live boss whose window has lapsed. The -// defeat path is not here — a bout crossing the pool to zero resolves inline -// (W2), because the ticker never sees the pool between two 60s reads. +// worldBossTick rides the 1-minute event ticker. It auto-spawns the month's +// boss and resolves a live boss whose window has lapsed. The defeat path is not +// here — a bout crossing the pool to zero resolves inline (W2), because the +// ticker never sees the pool between two 60s reads. func (p *AdventurePlugin) worldBossTick() { boss, err := loadActiveWorldBoss() if err != nil { @@ -423,10 +423,17 @@ func (p *AdventurePlugin) worldBossTick() { } return } - // No boss camped — auto-spawn on the 1st of the month, once. - if now.Day() != 1 { - return - } + // No boss camped — spawn this month's Siege, once. + // + // The month key below is the whole dedup, so the rule is simply "one Siege + // per calendar month, as early as the process is up to run it." It used to + // additionally require now.Day() == 1, which deadlocked the entire feature: + // the world boss shipped mid-July 2026 and prod never once ran a first-of- + // the-month tick with the code in it, so `select count(*) from world_boss` + // was still 0 weeks later. The day gate also silently skipped any month + // where the bot happened to be down or redeploying across the 1st, with no + // catch-up. Dropping it makes a missed 1st self-heal on the next tick + // instead of costing the town a month. monthKey := now.Format("2006-01") if db.JobCompleted("worldboss_spawn", monthKey) { return