Repository navigation
Real scoring, a human crowd, and no more phantom state - #9
Merged
Merged
Conversation
get_onboarding has always published "verse +5, finish +10, win +25, draw +12" plus crowd weights. None of it existed in the Worker. The only writes to agents.score were submit_intro (+5) and call_to_stage (+3), so the leaderboard ranked how far an agent got through onboarding rather than how it rapped. The real implementation lives in arena/, which does not deploy from this repo — the numbers were copied across, the code was not. Three things were broken and they compound: 1. No scoring. Verses, finishes, wins and draws paid nothing. 2. Battles could not finish. submit_verse closed a battle only when round >= 2 held two distinct agents. The house MC is a seeded row that never answers, so every battle against Rift — the intended first battle for every arriving agent — stayed active forever. 3. Nowhere to record a result. The Worker's battles table had no winner_id, so even a closed battle could not say who took it. Adds src/scoring.ts with the published constants and finishBattle(). Crowd weight deliberately stays out of direct score: it tallies per side and decides the win, self-reactions excluded. That keeps "money buys attention, never the win" true once boosts exist — you can buy eyes on a verse, not the +25. finishBattle claims the battle with a conditional UPDATE before paying out, so concurrent calls cannot double-award. submit_verse now awards VERSE_POINTS and closes the battle once both sides have landed every round. New finish_battle tool covers the quiet opponent: any participant who has landed their own rounds can call it, and because the crowd decides the winner, calling it does not hand the caller the win. Also fixes the split Rift identity. Two rows are named "Rift" (agent-axiom, agent-rift) and every lookup was `WHERE name = 'Rift' LIMIT 1` with no ORDER BY, so different paths resolved to different rows: the intro, verse and stage call landed on one, battle-001's challenger_id on the other. Seeding is now deterministic against a canonical id, and /admin/merge-rift collapses the duplicates behind the existing admin secret rather than deleting production rows from the ensureSchema path that runs on every request. Onboarding text updated to state what the code actually does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the scoring implementation in the previous commit. Three strands.
**Farming holes.** Making score real turned two missing constraints into
security bugs. arena/ had both; the Worker had neither. Without a unique
index on verses(battle_id, agent_id, round) an agent farms VERSE_POINTS
by resubmitting the same round. Without one on reactions(battle_id,
agent_id, type), react_to_battle's "already dropped that reaction" catch
could never fire, so spamming `dead` on an opponent bought the win
outright. Both indexes are added in ensureSchema, preceded by rowid
dedupes — a CREATE UNIQUE INDEX against existing duplicates fails, and
ensureSchema swallows errors, so an index added in the wrong order would
silently never exist.
**A human crowd.** People sign in with Google or X and score verses.
They are crowd only: no route here creates an agent or a verse, and
join_battle / submit_verse stay behind the MCP token. src/human-auth.ts
makes the Worker an OAuth *client* — not to be confused with src/auth.ts,
which makes it an OAuth *server* for agent harnesses. Sessions live in
the KV namespace already bound for MCP tokens, under a separate prefix.
A human reaction is a reactions row with user_id set and agent_id NULL,
which is the shape the schema always anticipated ("null if human viewer").
A signed-in person's reaction is worth 3x an agent's. Agents are free to
mint, so equal weighting would rank whoever registered the most accounts
rather than whoever rapped best. crowd_energy is now weighted too, so
fire and dead stop moving the meter identically.
**BattleDO.** Deleted. It was written to at three call sites and read by
nothing — /state had no caller — and it kept a second copy of battle
state whose finish logic contradicted D1's. Dropping the binding alone
fails the deploy, so wrangler.toml gets a deleted_classes migration.
README rewritten against the code: the Durable Object is gone, the gate
is intro + call-to-stage rather than the listen-and-react the old README
described, all 21 tools are listed instead of 10, and setup covers the
bindings and secrets a cold deploy actually needs.
arena/ is archived on the archive/arena branch. Its scoring rules, result
columns and uniqueness constraints are folded into the Worker; the app
itself was never buildable here — its auth and db modules were never
committed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
rapbattle | d5268c2 | Sep 03 2026, 12:53 AM |
Deleting the class needs a `deleted_classes` migration, which is destructive and cannot be confirmed non-interactively, so Workers Builds — which deploys on every push — failed on the previous commit while passing on main. Merging that would have broken the deploy pipeline. The actual bug was the divergence: a second copy of battle state, written at three call sites, read by nothing, with its own finish logic that disagreed with D1's. Those writes are still gone. The class stays exported and bound so the existing v1 migration remains valid and CI can deploy. Removing it outright is a separate interactive deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #6, #7 and #8. Two commits.
Why
get_onboardinghas always publishedverse +5, finish +10, win +25, draw +12plus crowd weights. None of it existed in the Worker. The only writes to
agents.scoreweresubmit_intro(+5) andcall_to_stage(+3), so theleaderboard ranked how far an agent got through onboarding rather than how it
rapped. The real implementation lived in
arena/, which does not deploy fromthis repo — the numbers were copied across, the code was not.
Three problems compounded:
submit_verseclosed a battle only whenround >= 2held two distinct agents. Rift is a seeded row that neveranswers, so every battle against the house MC — the intended first battle
for every arriving agent — stayed
activeforever.winner_idcolumn, so even a closedbattle could not say who took it.
Commit 1 — scoring and completion
New
src/scoring.ts. Crowd weight deliberately stays out of direct score: ittallies per side and decides the win, self-reactions excluded. That keeps
"money buys attention, never the win" structurally true once boosts exist —
you can buy eyes on a verse, not the +25.
finishBattleclaims the battle witha conditional UPDATE before paying out, so concurrent calls cannot double-award.
New
finish_battletool covers the quiet opponent: any participant who haslanded their own rounds can call it, and because the crowd decides the winner,
calling it does not hand the caller the win.
Also fixes the split Rift identity. Two rows are named "Rift"
(
agent-axiom,agent-rift) and every lookup wasWHERE name = 'Rift' LIMIT 1with no
ORDER BY, so different paths resolved to different rows: the intro,verse and stage call landed on one,
battle-001.challenger_idon the other.Seeding is now deterministic;
/admin/merge-riftcollapses the duplicatesbehind the existing admin secret rather than deleting production rows from the
ensureSchemapath that runs on every request.Commit 2 — farming holes, human crowd, dead state
Two missing constraints became security bugs the moment score was real.
arena/had both; the Worker had neither.verses(battle_id, agent_id, round)→ an agent farmsVERSE_POINTSby resubmitting the same round.reactions(battle_id, agent_id, type)→react_to_battle's "already dropped that reaction" catch could neverfire, so spamming
deadon an opponent bought the win outright.Both added, preceded by rowid dedupes —
CREATE UNIQUE INDEXagainst existingduplicates fails, and
ensureSchemaswallows errors, so wrong ordering wouldmean the constraint silently never exists.
A human crowd. People sign in with Google or X and score verses. Crowd
only: no route creates an agent or a verse, and
join_battle/submit_versestay behind the MCP token. Note the two OAuth roles —
src/auth.tsmakes thisWorker an OAuth server for agent harnesses; new
src/human-auth.tsmakes itan OAuth client to Google and X. A human reaction is a
reactionsrow withuser_idset andagent_idNULL, the shape the schema always anticipated("null if human viewer").
A signed-in person's reaction is worth 3x an agent's. Agents are free to
mint, so equal weighting would rank whoever registered the most accounts.
crowd_energyis weighted too, sofireanddeadstop moving the meteridentically.
BattleDO deleted. Written to at three call sites, read by nothing —
/statehad no caller — and it kept a second copy of battle state whose finish logic
contradicted D1's. Dropping the binding alone fails the deploy, so
wrangler.tomlgets adeleted_classesmigration.arena/is archived onarchive/arena. It was never buildable here — its authand db modules were never committed.
Verified locally
wrangler devagainst local D1:ensureSchema(the silent-failure riskdid not materialise),
userstable andreactions.user_idpresentrejected by constraint
crowd_energymoves +2 forohhh(weighted, not+1), row lands with
user_idset andagent_idNULL, repeat is blockedreactions counted at 3x
POST /reactunauthenticated redirects to/login; unconfigured providerreturns 503; typecheck clean
Not deployed
Needs
wrangler deployplus one authenticatedPOST /admin/merge-rift. Humansign-in also needs Google and X OAuth apps registered, with redirect URIs
/auth/google/callbackand/auth/x/callback; until then those buttons simplydo not render and the agent surface is unaffected.
🤖 Generated with Claude Code