Pete Adventure News: emit seam, !news command, cold-start backfill

gogobee's game-side of Pete's push-based Adventure news feed. Emits
structured event *facts* (not prose) to Pete's ingest endpoint over a
durable queue; Pete owns voice/authoring/publishing.

- internal/peteclient: durable pete_emit_queue + retry sender; Fact
  payload; FEATURE_PETE_NEWS master switch.
- pete.go: emitFact enforces runtime kill-switch + per-player opt-out
  (anonymize, never drop); zone-clear + realm-first tiering; !news
  command (player optout/optin, admin on/off).
- bootstrap_pete_news.go: cold-start backfill replays deaths +
  single-holder achievements + zone realm-firsts, backdated + NoPush,
  idempotent, fires only once the seam is live; seeds news_realm_firsts.
- Emit sites: death, zone_first, rival_result, milestone, arrival.
- db.go: pete_emit_queue, news_optout (+opted_out_at), news_realm_firsts.

Unshipped. Deploy Pete first, then set FEATURE_PETE_NEWS + ingest env.

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
prosolis
2026-07-11 00:53:25 -07:00
parent 0c4c4757d3
commit b42beec348
13 changed files with 1062 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ import (
"time"
"gogobee/internal/db"
"gogobee/internal/peteclient"
"github.com/google/uuid"
"maunium.net/go/mautrix/id"
@@ -613,6 +614,21 @@ func (p *AdventurePlugin) settleDuel(ctx MessageContext, ch *advDuelChallenge, c
upsertRivalRecord(winnerID, loserID, true)
upsertRivalRecord(loserID, winnerID, false)
// One BULLETIN dispatch per settled duel (from the winner's side, so it
// fires once). Character names only; no-op unless the seam is enabled.
if wn, ln := charName(winnerID), charName(loserID); wn != "" && ln != "" {
ts := nowUnix()
emitFact(peteclient.Fact{
GUID: fmt.Sprintf("rival:%s:%s:%d", userHash(winnerID), userHash(loserID), ts),
EventType: "rival_result",
Tier: "bulletin",
Subject: wn,
Opponent: ln,
Outcome: "won",
OccurredAt: ts,
}, winnerID, loserID)
}
p.announceDuel(ctx, winnerID, loserID, ch.Stake, winnerShare, potShare, res.decisive)
return nil
}