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) prerenders docs/, 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.yml deploys it from master. Root scripts site:dev and site: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 carry homepage, repository, bugs and keywords. Nothing is on npm yet; docs give the from-checkout command beside each npx form.
  • Removed the unused ClientMessage alias of ClientMsg from playerthree and playerthree/host.
  • Commander match log (SPEC §20.10.1): the host writes one append-only playerthree.commanderLog/1 entry 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 with artifactHash, outcome/disposition (interrupted/aborted on 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 under dataDir, PostgreSQL storage.commanderLog(game) (playerthree_commander_log, playerthree_commander_log_verdicts in 001_init.sql). CreateMatchOptions.log carries the game's match facts; forfeit(team, {abandoned}) marks abandons; PlayerThreeHost.commanderLog and commanders.log(owner, id) read it.
  • stats is removed from stored commanders: CommanderStatus.record is derived from the log (decided, non-self-play entries). Stored commanders with stats are refused. Policy versions and stored artifacts carry artifactHash (SHA-256 of the module bytes, policyArtifactHash). Reset required: commander stores (JSON commanders.json and artifacts, or the PostgreSQL host tables) and a fresh 001_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; keyId is k_ + 16 hex of SHA-256 over the SPKI public key. verifyCommanderLog(entries, keys, sealed?) and verifyVerdictRow(row, keys) in playerthree/results check 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 (standings uses it; its numbers are unchanged), with partial-play member weights and a blended match weight; blockBootstrap exposes the seed-block bootstrap.
  • One named commander is reserved and seated on one team only: commanders.reserve now refuses a lane on a team other than its other reserved lanes (busy), as assign and createMatch already refused its seats. New sticky PlayerThreeMatch.sameOwnerOpposed (and SessionRecord.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 (and planPostgresReset / resetPostgresStorage from the separate playerthree/postgres/reset entry, never loaded by storage) resets a pre-release PostgreSQL database to the current schema. It reads DATABASE_URL only and lists exactly the owned tables and ledger rows it drops: playerthree_* plus each given game scope's ledger-recorded tables. It requires --yes and 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 + maxInfraRetries attempts, all in the private campaign.json ledger; public runs carry authoringAttempts). Per-run budgets are hard (an overshooting turn is charged, its tools never run); --max-campaign-usd is a cumulative ceiling. Interrupted evaluation closes the series runner (new close(): interrupted matches become infra_error/shutdown attempts, the series stays resumable) instead of aborting it. No further runs are authored while an earlier run can never complete. Anonymous exports drop executionDigest; publication ids derive from the public evidence, so publishedAt is 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_over acks from act/sequence again, 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 messages from 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, version role). 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 previousMatch receipt per role across reconnect/restart, bounded by history age. Interrupted sessions have unknown outcomes, not invented draws. Shutdown integrations can flush commander persistence with commanders.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 with game_ready {role}, then monitors each with game_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. callMethod remains available from playerthree/host. ClientMsg now 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/CLI playerthree, companion packages @playerthree/*, public PlayerThree* 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 the playerthree.detail section 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_observe return the lobby state (seat, the game's match summary from manifest.lobby, you.ready, startsWhen, instruction); the new game_ready (SeatControl.ready) marks it ready, and game_wait returns the first observation the moment the match starts. Agent readiness is absent → online → ready. Game lobbies reserve commander lanes with commanders.reserve(...) (CommanderStatus.reservations); held matches take createMatch({lobby}) and setLobbyStart.
  • 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?), MCP game_observe {detail} and op:'stream' detail widen it ('all' for every section); policies declare a JS global detail or a wasm playerthree.detail custom section; built-in bots get every section; the core briefing lists observationScope.full/summary.
  • Host: a human who hands their seat to an agent, policy, bot or hold keeps it (mintHumanToken no longer refuses with forbidden, which dropped them from the match at the next ticket renewal); renewHumanToken extends 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 — sends signal and answers with signal_update (ack, decline, done, fail, cancel) through the ordinary intent pipeline. The host keeps one board per team, attaches signals: {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; SignalBoard serves a game's own headless runner. The signal ack code and the signals briefing 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_failure in 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 uniqueItems and dependentRequired; 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.random cannot 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.