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.
11 KiB
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). Seeproject_pete_bot_architecturememory.
Architecture (the load-bearing part)
- Pete owns voice, authoring, identity, publishing. Persona, templates,
editorial, the
@pete:parodia.devaccount. 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
localhoston 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)
- Character names only. Never the Matrix handle behind a character.
- No donation-derived anything (Misty/Arina secret buffs).
- Facts are supplied, never invented. Names/numbers/outcomes come only from
the payload
actors[]allow-list; the fact-guard rejects anything else. - 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 andzone_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:
- Endpoint disabled in Pete config → template body.
- Endpoint unreachable / timeout → template body.
- Empty or malformed completion → template body.
- 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 (L20–49). Risk is emptiness, not spam;
near-inclusive.
Observed (Apr–Jul 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 tolocalhostOllama/vLLM. - Voice-agnostic: gogobee has no Adventure/Pete-specific logic on this path.