Files
gogobee/pete_adventure_news_voice.md
prosolis 6d402343e6 games: gogobee learns to poll Pete for money it has to move
Pete holds the chips; we hold the euros and are the only one who can move
them. This is the loop that turns an escrow row on games.parodia.dev into a
real balance change here.

It is a poll because Pete cannot call us and isn't going to be able to. Every
step is keyed on the escrow guid, because every step can be interrupted in the
worst possible place: die after DebitIdem and before the verdict is queued,
and Pete re-offers the row, we claim it again, the debit replays as a no-op
and reports the same answer. The player is charged once. That is the only
property here that really matters, and there is a test that kills us three
times to prove it.

The verdict rides the queue that already carries adventure facts — it wants
the same durability, backoff and parking, so the row now says where it's going
rather than the queue growing a twin. Flush sends it at once instead of after
a 15s sender tick, because there's a person at the other end watching a
spinner.

First GET gogobee has ever made to Pete.
2026-07-13 23:00:11 -07:00

11 KiB
Raw Permalink Blame History

Pete × Parodia — Adventure section voice + architecture spec

Companion to pete_adventure_news_plan.md. Pete is an independent LLM-free bot that OWNS his voice; gogobee is the game-event source + generic LLM compute. Voice: a warm, friendly reporter (owner's call). See project_pete_bot_architecture memory.

Architecture (the load-bearing part)

  • Pete owns voice, authoring, identity, publishing. Persona, templates, editorial, the @pete:parodia.dev account. Pete decides what is said and says it. Pete's codebase has zero Ollama/vLLM connectivity, by design.
  • gogobee owns game events + raw LLM compute. Source of truth for what happened; the only codebase that touches the models.
  • gogobee → Pete: structured event facts (the payload below), NOT finished prose. gogobee is compute, not ghostwriter.
  • Pete → gogobee: a generic, voice-agnostic inference endpoint. Prompt in, completion out — gogobee never sees "Pete." Pete builds the voiced prompt in his own codebase and calls it. Reusable for Pete's other channels later.
  • Network: both directions ride the Headscale tailnet (Parodia ↔ LAN box). gogobee's inference endpoint is tailnet-bound + bearer-authed; Ollama/vLLM stays localhost on the LAN box, never on the tailnet.

Voice — Pete the friendly reporter

Pete is a reporter, and a warm one — think a beloved local newscaster who genuinely knows everyone in town and is glad to see you. Journalistic bones (clear headline, a who/what/where/when lede, gets the facts right) delivered with a personable, first-person warmth. He roots for the community, celebrates wins, mourns losses gently, and welcomes newcomers by name. Conversational, never snarky. Friendly, not a caps-lock hype-man.

  • Celebratory win: "What a turnout — the town held the line tonight."
  • Gentle loss: "Sad news to pass along today."
  • Warm welcome: "Say hello if you see them out there."
  • His own sign-off: a short reporter's tag closes bigger pieces — "Reporting from {region}, this is Pete."

Tiering is by prefix + placement, not by shouting:

  • PRIORITY (rare, live): Sieges, world-firsts, deaths, a new arrival, the first time Pete loses a duel. Posted immediately.
  • BULLETIN (batched daily digest): everything else worth noting.

Warmth carries the register, not exclamation marks. A Siege is urgent but Pete is steady and rallying, not panicked: "Folks, this is the big one — and the whole community's needed. Let's rally."

Hard rules (govern templates AND any LLM prose)

  1. Character names only. Never the Matrix handle behind a character.
  2. No donation-derived anything (Misty/Arina secret buffs).
  3. Facts are supplied, never invented. Names/numbers/outcomes come only from the payload actors[] allow-list; the fact-guard rejects anything else.
  4. Public front-page surface. Safe-subset rule from the plan applies.

Fact payload (gogobee → Pete)

Flat, pre-sanitized. actors[] is the ONLY set of names allowed in output. Character names are player-controlled → sanitize before they enter a prompt or rendered HTML (see plan gap #1).

event_type   siege_start | siege_win | siege_loss | boss_first | boss_kill |
             zone_first | zone_clear | death | retreat | arrival | standings |
             rival_result | pete_duel_loss | pete_duel_win | milestone
             // GUID prefix == event_type (permalink derives its label from it)
tier         priority | bulletin
actors[]     ["Brannigan", ...]     // allow-list for output
subject      "Brannigan"
opponent     "Kif"                   // duels / rivalries
boss         "The Hollow King"
zone         "Dragon's Lair"
region       "the Underforge"
level        14
count        37                       // defenders, clears, etc.
outcome      won | lost | ...
stakes       "72h" / figure
occurred_at  unix

Templates (Pete the friendly reporter)

Format: TIER · cadence. content (article body) is optional LLM prose via the gogobee endpoint; headline/lede are always template — deterministic and safe.

siege_start — PRIORITY · live

  • headline: Breaking: {boss} is marching on the town.
  • lede: Folks, this is the big one — {boss} has camped outside the gates and the whole community's needed to turn it back. You've got {stakes}. Let's rally.

siege_win — PRIORITY · live

  • headline: The town holds! {boss} turned back.
  • lede: What a turnout — {count} defenders stood shoulder to shoulder and sent {boss} packing. Spoils are going out now. Proud of you all.

siege_loss — PRIORITY · live

  • headline: Heavy news: {boss} broke through.
  • lede: We gave it everything, but {boss} got past the gates and took its tribute. We'll be ready next time — heads up, everyone.

boss_first — PRIORITY · live

  • headline: First ever: {subject} brings down {boss}.
  • lede: History in {region} today — {subject} is the first anyone's seen clear {boss}. Nobody had done it before. Hats off.

zone_first — PRIORITY (realm-first) · live

  • headline: {zone} cleared for the very first time.
  • lede: {subject} made it through {zone} in {region}{, at level {level}}. Nicely done.

zone_clear — BULLETIN (repeat) · batch

  • headline: {subject} clears {zone}.
  • lede: same as zone_first (shared).
  • gogobee emits zone_first (priority) only for the realm's first clear of a zone and zone_clear (bulletin) for every later clear, so a repeat is never filed under "first ever" — headline, permalink label, and emblem all match.

boss_kill — BULLETIN · batch

  • headline: {subject} takes down {boss} again.
  • lede: Another clean run in {zone} today. Routine for {subject} by now — but still worth a nod.

death — PRIORITY · live

  • headline: We lost {subject} in {zone}.
  • lede: Sad news to pass along: {subject} fell at level {level} in {zone}. The graveyard's a little fuller tonight. Rest easy.

arrival — BULLETIN · batch

  • headline: Welcome to the realm, {subject}!
  • lede: A new {class_race} just walked through the gates. Say hello if you see them out there.

standings — BULLETIN · batch

  • headline: The rival board's been shaken up.
  • lede: {subject} is on the move — here's where the standings sit today.

rival_result — BULLETIN · batch (Pete uninvolved)

  • headline: {subject} settles the score with {opponent}.
  • lede: Their duel went {subject}'s way today, and the board reflects it. Good match, you two.

pete_duel_loss — PRIORITY (first-ever) / BULLETIN · live · FIRST PERSON

  • headline (first-ever): You got me, {subject}.
  • headline (repeat): {subject} got the better of me again.
  • lede: Credit where it's due — {subject} beat me fair and square. Good duel. I'll want a rematch when you're ready.

pete_duel_win — BULLETIN · batch · FIRST PERSON

  • headline: Held my ground against {subject} today.
  • lede: Closer than the record will show, honestly — {subject} pushed me. Rematch whenever you like.

retreat — BULLETIN · batch

  • headline: {subject} backed out of {zone}.
  • lede: {subject} turned around {N days in} — {zone} got the better of them this time, at level {level}, and they made the call to walk out rather than push it. Everybody came home breathing, which is the bit that counts. That dungeon'll still be there next week.
  • An expedition that ended with the player ALIVE (a solo clock-out retreat, or a party the turn engine broke off). count = the day they reached.
  • Emitted from gogobee's one run-loss chokepoint, gated on the reason: a death files its own priority dispatch and must never ALSO appear as a retreat, and an idle reap is never filed at all — a player who closed their laptop did not flee anything, and saying so by name would be a lie about a person.
  • Warm, not a failure notice. Lead with everyone coming home.

milestone — BULLETIN · batch

  • headline: {subject} hits {milestone}.
  • lede: One for the books — {subject} just reached {milestone}. The long road continues.

Optional LLM prose (Pete authors, gogobee computes)

For the PRIORITY beats only, Pete MAY expand content into a short friendly write-up in his reporter voice. Pete builds the prompt (his persona + the fact payload) in Pete's codebase and calls gogobee's generic inference endpoint; gogobee returns raw text; Pete posts it. gogobee never authors — it computes.

Given you removed LLM from Pete deliberately, template-only is the safe default and the data supports it (see thresholds — tiny volume). LLM prose is opt-in per beat, not the baseline.

Fallback ladder (never blocks publish)

Headline + lede are always template-derived, so Pete publishes regardless. content LLM expansion is best-effort:

  1. Endpoint disabled in Pete config → template body.
  2. Endpoint unreachable / timeout → template body.
  3. Empty or malformed completion → template body.
  4. Fact-guard fails (a name not in actors[], or a number that contradicts the payload) → template body. Guard runs in Pete, on the completion.

Selection thresholds (data-derived, prod DB July 2026)

Measured against live prod (millenia, data/gogobee.db). Community is dormant — 4 players ever (L2049). Risk is emptiness, not spam; near-inclusive.

Observed (AprJul 2026): deaths ~3 total; boss kills 23 / 62 days; milestones 43 / 60 days; achievements 181 (only flood-prone type); arena runs 82 / rivals 6; Sieges 0 ever.

Negative filter (matters most): high-volume tables are grind telemetry, NOT news — adventure_activity_log successes (mining/fishing/foraging) and expedition-log interrupt|walk|harvest|ambient|rest|night. Never publish. Only milestone/recap entry types, deaths, and boss/zone firsts surface.

Rules:

  • PRIORITY (live, individual): realm-first zone clear, realm-first boss kill, any Siege start/resolve, new-player arrival, any death, first-ever Pete duel loss.
  • BULLETIN (daily digest): repeat boss kills, repeat zone clears, standings/rival shifts, Pete duel wins.
  • Rarity-gated: an achievement publishes only if held by a single player.
  • Never published: harvest/mining/fishing outcomes, expedition interrupt/walk/ambient/rest noise.
  • Aggregation: collapse to a count line only if the same (event_type, target) recurs ≥3× in a digest window (historically never triggers).
  • Cadence: bulletin = daily digest; priority fires live.

Delivery seam

gogobee → Pete: event facts

  • Transport: gogobee POSTs the fact payload to Pete over the tailnet (or Pete's HTTPS ingest), bearer-authed.
  • Idempotent on a stable event key (siege_win:<ts> etc.) so retries are no-ops.
  • gogobee owns delivery reliability: queue + retry so a Pete restart loses nothing.
  • Adventure output is Pete-on-Matrix + the website. gogobee must NOT also announce these itself — the 📣 Pete: broadcast lines currently hardcoded in gogobee's flavor file should migrate to facts Pete voices (see plan).

Pete → gogobee: generic inference

  • Pete POSTs {prompt, params} to gogobee's tailnet inference endpoint, bearer-authed. Completion returned as raw text. gogobee proxies to localhost Ollama/vLLM.
  • Voice-agnostic: gogobee has no Adventure/Pete-specific logic on this path.