Changelog
PlayerThree is pre-release; there are no versioned releases yet and the protocol changes without compatibility shims. This log summarises notable changes by date.
Unreleased
2026-11-11
- Website: playerthree.gg (
site/, Vite + React + Mantine) prerendersdocs/, the spec, this changelog, the package READMEs, a getting-started walkthrough and a TypeDoc API reference generated from the package exports, with Pagefind search;.github/workflows/pages.ymldeploys it frommaster. Root scriptssite:devandsite:build. - Docs audited against the code for the open-source launch: CLI flags, exit codes and bench outputs; MCP tool table, OAuth limits and the
playerthree://seat/status URI; DevTools status codes and player API; lobby phase, observation scope, commander log and ratings in DESIGN/DX; policy storage and Rust examples in POLICY_ABI; spec layer table, manifest/adapter rows and Implemented labels for §20.9–§20.11. Packages carryhomepage,repository,bugsand keywords. Nothing is on npm yet; docs give the from-checkout command beside eachnpxform. - Removed the unused
ClientMessagealias ofClientMsgfromplayerthreeandplayerthree/host. - Commander match log (SPEC §20.10.1): the host writes one append-only
playerthree.commanderLog/1entry per match × named commander (multi-lane commanders once) from host-observed facts — seats and plane share, lineup, opponents, controller intervals that equal a replay re-derivation, owner intents/tells, fallback and absent-policy ticks, policy versions withartifactHash, outcome/disposition (interrupted/abortedon release, never a draw), the replay digest — plus the game's verdict (commanderLog.verdict). Entries are hash-chained per commander; owner, name, declared metadata snapshot and self-reported usage sit in a salted, sealed part that deletion tombstones without breaking the chain. Stores: memory, JSONL underdataDir, PostgreSQLstorage.commanderLog(game)(playerthree_commander_log,playerthree_commander_log_verdictsin001_init.sql).CreateMatchOptions.logcarries the game's match facts;forfeit(team, {abandoned})marks abandons;PlayerThreeHost.commanderLogandcommanders.log(owner, id)read it. statsis removed from stored commanders:CommanderStatus.recordis derived from the log (decided, non-self-play entries). Stored commanders withstatsare refused. Policy versions and stored artifacts carryartifactHash(SHA-256 of the module bytes,policyArtifactHash). Reset required: commander stores (JSONcommanders.jsonand artifacts, or the PostgreSQL host tables) and a fresh001_init.sql(playerthree db reset).- Results signing:
commanderLog.signer(parseResultsSigner(text), Ed25519 PKCS#8 PEM or single-line base64/base64url DER; the private key stays inside the signer) makes a host official and signs every log entry and verdict amendment;keyIdisk_+ 16 hex of SHA-256 over the SPKI public key.verifyCommanderLog(entries, keys, sealed?)andverifyVerdictRow(row, keys)inplayerthree/resultscheck digests, chain order, key validity windows (rotation) and revocation;resultsPublicKey(signer, from, until)builds the published key. - Rating core:
rateMatch(env, lineups, ratings, weight?)(playerthree/results) is the single TrueSkill update (standingsuses it; its numbers are unchanged), with partial-play member weights and a blended match weight;blockBootstrapexposes the seed-block bootstrap. - One named commander is reserved and seated on one team only:
commanders.reservenow refuses a lane on a team other than its other reserved lanes (busy), asassignandcreateMatchalready refused its seats. New stickyPlayerThreeMatch.sameOwnerOpposed(andSessionRecord.sameOwnerOpposed): once one owner plays seats on two hostile teams, the match is its own practice and records no commander win/loss/draw or pool outcome. History records without the field are not loaded (data reset). playerthree db reset(andplanPostgresReset/resetPostgresStoragefrom the separateplayerthree/postgres/resetentry, never loaded by storage) resets a pre-release PostgreSQL database to the current schema. It readsDATABASE_URLonly and lists exactly the owned tables and ledger rows it drops:playerthree_*plus each given game scope's ledger-recorded tables. It requires--yesand the typed or--confirmed database name, drops them in one transaction and re-migrates. Any mismatch exits non-zero before dropping anything.- Benchmark campaigns score only model-caused authoring failures as zero. A missing key fails before any authoring; setup errors, provider failures before the first turn, 408/429/5xx and network outages and SIGINT/SIGTERM leave the run pending and retryable (at most
1 + maxInfraRetriesattempts, all in the privatecampaign.jsonledger; public runs carryauthoringAttempts). Per-run budgets are hard (an overshooting turn is charged, its tools never run);--max-campaign-usdis a cumulative ceiling. Interrupted evaluation closes the series runner (newclose(): interrupted matches becomeinfra_error/shutdownattempts, the series stays resumable) instead of aborting it. No further runs are authored while an earlier run can never complete. Anonymous exports dropexecutionDigest; publication ids derive from the public evidence, sopublishedAtis reused only for identical evidence. Stores validate each immutable record once per stored bytes/digest. - Trusted owner controls of a final team receive per-intent
game_overacks fromact/sequenceagain, as SPEC's per-team terminal rule requires; only retained seat leases are limited to terminal reads.
2026-11-10
- Commander kickoff and workflow guidance explicitly preserves top-level owner
messagesfrom every per-role wait/observe result before reducing its observation. Delivered messages are consumed, not replayed by the next observation. OMP setup explains that Streamable HTTP alone does not install a push-to-prompt extension; no protocol or message-routing change is required.
2026-11-09
- Commander policy libraries now retain independent active versions and role provenance (
activeByRole, versionrole). The one-time store migration restores unambiguous single-role records; ambiguous legacy multi-role versions remain unassigned and require role-specific redeployment, without discarding artifacts or guessing. Lobby probes honor configured host match options. Status awaits actual restoration and exposes activation failures; readiness still does not require a policy for direct agents. - Terminal delivery preserves already-claimed seat leases for read-only final waits/observations until disposal or expiry; explicit revocation remains immediate and eliminated seats cannot observe surviving play. Persistent commanders retain one private, non-consuming
previousMatchreceipt per role across reconnect/restart, bounded by history age. Interrupted sessions have unknown outcomes, not invented draws. Shutdown integrations can flush commander persistence withcommanders.flush(). - Policy status exposes successful decision counts and consecutive failures independently of historical error totals; a successful empty decision resets the failure streak without erasing
lastError. - Trusted intent provenance now reaches game adapters as
IntentContext.source(owner,agent,policy,bot,fallback) independently of the seat controller, including practice and replay execution. Direct adapter calls without context are explicit owner calls. Replay v2 records and validates the source for every intent; ambiguous v1 replays are rejected rather than silently reclassified. Arena/benchmark artifact metadata follows the recorded replay version. - Multi-role readiness guidance now consistently discovers authority from
game_briefing.commands, prepares and readies each seated role withgame_ready {role}, then monitors each withgame_wait {seconds,role}. Ready-but-still-lobby is explained as waiting for other participants or the host. Per-seat semantics, single-seat defaults, authorization and the shared static schema are unchanged; MCP integration coverage exercises isolated readiness and role-specific match entry. - Arena match/scenario configuration now permits up to 256 properties per nested object, including scheduled and downloadable resolved rules. Generic event metadata remains capped at 128; private-key rejection, finite JSON values, nesting and payload limits are unchanged. This fixes Series creation/export for games whose fully resolved rule groups exceed the metadata bound.
- Host transport parsing and named-argument dispatch now live in
host/messages.ts; the match retains authorization, rate limits, stream ownership and lifecycle.callMethodremains available fromplayerthree/host.ClientMsgnow describes already-supported omitted call arguments, and the Colyseus binding derives operation/channel vocabulary from the protocol instead of maintaining independent inbound and outbound unions/maps. Wire values, validation rules and public runtime contracts are unchanged. - Official identity cutover: repository
fraywing/player-three, core package/CLIplayerthree, companion packages@playerthree/*, publicPlayerThree*SDK symbols,playerthree:*transport messages and/playerthree/{devtools,player}routes. No old-name aliases are retained; Agent Command must switch in the same release. - Persistent namespaces also change: PostgreSQL tables and migration ledger, replay/results/series formats, policy detail sections, OAuth client identities, runner configuration paths, credential derivation and deterministic policy seed domains. Existing stores must not be reopened blindly: back up, then start from fresh stores (or
playerthree db reset), revoke and reissue commander credentials and OAuth connections, recompile WASM policies with theplayerthree.detailsection and regenerate replay fixtures and benchmark surface pins. No automatic data reset or conversion is provided.
2026-11-06
- Lobby phase: before a seat's match starts its agent is in phase
lobby—game_wait/game_observereturn the lobby state (seat, the game'smatchsummary frommanifest.lobby,you.ready,startsWhen,instruction); the newgame_ready(SeatControl.ready) marks it ready, andgame_waitreturns the first observation the moment the match starts. Agent readiness isabsent→online→ready. Game lobbies reserve commander lanes withcommanders.reserve(...)(CommanderStatus.reservations); held matches takecreateMatch({lobby})andsetLobbyStart. - Mid-match seat assignment:
PlayerThreeMatch.assign(assignment)hands a seat to a named commander (or another kind) before the start or mid-match, detaching the previous occupant; the commander's next wait returns its match entry with the first observation. - Observation scope: manifests may declare
observationScope { sections, defaults }; each seat observes its role's sections in full and the rest as the game's summaries.GameAdapter.observe(seat, sinceTick, detail),SeatControl.observe(since?, detail?), MCPgame_observe {detail}andop:'stream'detailwiden it ('all'for every section); policies declare a JS globaldetailor a wasmplayerthree.detailcustom section; built-in bots get every section; the core briefing listsobservationScope.full/summary. - Host: a human who hands their seat to an agent, policy, bot or hold keeps it (
mintHumanTokenno longer refuses withforbidden, which dropped them from the match at the next ticket renewal);renewHumanTokenextends a connected human credential in place so games renew without a rejoin. - Team signals (spec §22): a game declares a closed signal vocabulary (
manifest.signals: typed kinds with no text, per-role cards of what each role answers) and host limits (control.signals). Any seat — human, agent, policy, bot — sendssignaland answers withsignal_update(ack, decline, done, fail, cancel) through the ordinary intent pipeline. The host keeps one board per team, attachessignals: {inbox, sent}to every observation and the team board to every view, wakes agents (wake: "signal"), records accepted signals in replays and turns boards over after each step. Lifecycle and cards follow A2A tasks and agent-card skills.signalSchemas()generates the reserved members;adapter.signalLimits()narrows or disables them per match;SignalBoardserves a game's own headless runner. Thesignalack code and thesignalsbriefing topic are new; games declaring signals must re-pin their surface hash. - Bench: a failed series logs its cause with stack and exports it beside
durable_recording_failurein the results'completeness.missing, instead of failing silently.
2026-11-05
- Community files: Apache-2.0 licence and NOTICE, contributing guide (DCO), code of conduct, security and support policies, issue/PR templates, CI.
- MCP: serves 2025-era clients beside the 2026-07-28 protocol; seat leases claimed only after a validated first request; protocol version read from
params._meta. - Connect: recipes for OpenCode, Pi and oh-my-pi.
- Matches: per-match team subsets and named alliances, per-team outcomes and ranks.
- Observers: 20 Hz delayed playback from a delta ring buffer with shared per-frame encoding.
- Arena/bench: metadata-only series storage, streamed exports, ranked draws.
- Policies: the host enforces the 1 MiB UTF-8 output size and the 32-level output depth on every policy result (wasm included), so in-VM checks are no longer load-bearing.
- Security (OAuth/MCP): a redeemed authorization code is remembered for an hour so a late replay still revokes its connection; full pending-consent and DCR client tables evict the oldest (idle) entry instead of refusing every sign-in; MCP argument checks enforce
uniqueItemsanddependentRequired; JSON commander store (token-derivation key, policy code) written 0600 in 0700 directories; trace SSE subscribers that stop reading are dropped at 256 KiB backlog;mathjs(via ts-trueskill) overridden to ^15.2.0 (GHSA-29qv-4j9f-fjw5, GHSA-jvff-x2qm-6286). - Security (host/Colyseus): spectator watch changes rate limited per token (2/s, burst 4) across sockets; a policy restart whose sandbox cannot start stops that policy instead of failing the match tick; the policy sandbox PRNG is seeded one-way from the match seed so guest
Math.randomcannot reveal it.
2026-11-04
- Security: scoped transport leases across Colyseus and MCP, first-use-bound runner seat sessions, revocable authorization leases, hardened credential revocation.
- Policies: CPU budget measured on sandbox-thread time with a separate wall hang guard; provenance persisted.
- Storage: PoolStore/HistoryStore and a Postgres backend; guarded match bootstrap; lobby integration profile in the spec.
