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

@@ -9,6 +9,7 @@ import (
"time"
"gogobee/internal/db"
"gogobee/internal/peteclient"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
@@ -101,7 +102,7 @@ func (p *AchievementsPlugin) checkAndGrant(userID id.UserID) {
}
func (p *AchievementsPlugin) grant(d *sql.DB, userID id.UserID, achievementID string) {
_, err := d.Exec(
res, err := d.Exec(
`INSERT INTO achievements (user_id, achievement_id) VALUES (?, ?) ON CONFLICT DO NOTHING`,
string(userID), achievementID,
)
@@ -109,7 +110,43 @@ func (p *AchievementsPlugin) grant(d *sql.DB, userID id.UserID, achievementID st
slog.Error("achievements: grant", "user", userID, "achievement", achievementID, "err", err)
return
}
// Only a genuinely new grant is news; a re-check conflict (rows==0) is not.
if n, _ := res.RowsAffected(); n == 0 {
return
}
slog.Info("achievements: granted", "user", userID, "achievement", achievementID)
p.emitMilestoneNews(d, userID, achievementID)
}
// emitMilestoneNews files a BULLETIN milestone to Pete for RARE achievements
// only — rarity-gated to a single holder (per the news selection thresholds) so
// routine unlocks don't flood the feed. Character name only; no-op unless the
// seam is enabled.
func (p *AchievementsPlugin) emitMilestoneNews(d *sql.DB, userID id.UserID, achievementID string) {
var holders int
if err := d.QueryRow(`SELECT count(*) FROM achievements WHERE achievement_id = ?`, achievementID).Scan(&holders); err != nil || holders != 1 {
return
}
name := charName(userID)
if name == "" {
return
}
label := achievementID
for _, a := range p.achievements {
if a.ID == achievementID {
label = a.Name
break
}
}
ts := nowUnix()
emitFact(peteclient.Fact{
GUID: fmt.Sprintf("achv:%s:%s", userHash(userID), achievementID),
EventType: "milestone",
Tier: "bulletin",
Subject: name,
Milestone: label,
OccurredAt: ts,
}, userID, "")
}
func (p *AchievementsPlugin) handleAchievements(ctx MessageContext) error {