All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Corp-ship combat now preempts normal task work by waking active TaskAgents into a combat goal or starting an idle ship task through the normal task path, while preserving the existing combat prompt and doctrine injection flow.
- BYOA harnesses can now customize active combat wake behavior with an
@app.on_combat_wakehook.
start_tasknow uses Pipecat 1.3'swatch_workers(...)API and disables duplicate auto-watch registration when adding local TaskAgents, unblocking player/corp task startup on the released package.TaskAgent._inject_steeringnow injects steer text as a single<event name="task.steered">user message with a short priority line followed by the raw steer text inline. Same event-xml shape astask.progress/task.cancelledthe agent already reads — without it, the steer was just another peer instruction to the original task description (which is itself a plain user message). Orchestrator side drops its now-redundant"Steering instruction: "text prefix.start_taskandsteer_tasknow pre-flight the target's finishing state. If the in-process TaskAgent has already calledfinished(or been cancelled), the steer would race the terminal turn and silently drop.start_taskhandles this transparently: the public entrypoint waits up toTASK_STEER_CLOSING_WAIT_SECONDS(default 5s) for the slot to free, then retries the start cleanly — the LLM never sees the intermediate state. Explicitsteer_taskreturnserror: "task_closing"for the LLM to chainstart_taskitself. Personal and corp in-process ships symmetric; BYOA skips the check.- Voice agent prompt: removed the contradictory "do NOT call start_task if all slots are occupied" line; added a short note that for steered instructions the agent should read the
task.completedmessage and mention any unfulfilled intent to the commander.
- Bumped
pipecat-aito 1.3.0 (subagents now in-tree; dropped thepipecat-ai-subagentspackage). - Centralized runtime configuration in
gradientbang.configusingpydantic-settings. - Repository structure tidy-up and runtime module reorganization under
src/gradientbang/runtime/. - Migrated to the latest Pipecat SubAgents spec —
PipelineWorker-hostedOrchestrator, bus-basedTaskAgentworkers, and the universal wake-up handshake aligned to the currentpipecat-ai-subagentsAPI. - New-player onboarding now keeps the generated mega-port route as session-scoped private TaskAgent startup context for player-ship tasks, controlled by
BOT_NEW_PLAYER_ONBOARDING. - Local player-ship TaskAgents are now one-task workers like local corp-ship TaskAgents; completed player tasks end and remove their worker instead of reusing an idle pipeline.
- Text input now waits on Pipecat user-mute events via
app_resources, matching voice turn sequencing.
- Map UI: numeric
map_zoom_levelfrom the UI agent is now honored as an absolute zoom (4–50 scale) instead of being downgraded to a single step when no center/highlight/fit is set.map_zoom_directionstill steps gently for vague "zoom in/out" requests.
list_known_portsno longer builds an overlong PostgREST.in(...)URL for long-lived characters; ports are now loaded via a direct Postgres query against only the BFS-searched sectors, unblocking session startup for explored players.- Port commodity sell trades now emit realized FIFO net profit instead of gross sale revenue, keeping tutorial trade-profit progress aligned with the voice agent.
- Tightened voice-agent prompt and
create_corporationschema to discourage auto-founding a corp on cost/info questions ("how much for a corp?") and to nudge the agent toward asking for a name first. - Added a corp-ship-purchase worked example to the voice-agent prompt and a rule against deferring ship purchases to the UI, to reduce cases where the agent refuses voice purchases.
- BigMap: follow/freeroam mode with a Recenter button after pan or zoom gestures.
- Map: garrisons render in mode-specific colors (defensive/offensive/toll) for both own and enemy garrisons.
- Map: corp-mate ships render in a distinct color from self-owned corp ships.
local_map_regionresponse carries garrisonmodeand activecombatper visited sector, so first-paint renders are complete without waiting for follow-up events.- Ship payload includes
owner_character_idso the client can classify self vs corp-mate without a separate lookup.
- Internal: BYOA wake, presence, broker auth, and registry handling extracted from
VoiceAgentinto a newByoaCoordinatorcollaborator (pipecat_server/byoa_coordinator.py). No behavior change;voice_agent.pyshrinks by ~12%. - BigMap zoom controls now share a store-backed zoom value with mouse-wheel zoom, keeping the slider, buttons, and canvas aligned.
- Course plot framing now runs only while BigMap is in follow mode; route animation continues in freeroam.
- Removed the course plot zoom toggle from map controls.
- Client map mutations consolidated behind a single
applyMapDelta(delta)switch inmapSlice. ReplacesupdateMapSectorsand the separatecombat_sectorsrecord; combat is now a flag on the sector node.
- EventRelay and TaskAgent now process game events through ordered mailboxes.
- BigMap freeroam no longer snaps back from delayed map fetch, fallback, or route-fit responses.
- Task-agent event inference drain grace is configurable via
TASK_AGENT_EVENT_DRAIN_GRACE_SECONDS. - Map sector icons (port, mega-port, garrison) reliably render on first paint after moving sector — previously required a hover to refresh. Memo equality now covers
garrison.modeandcombat.
- Combat screen crash on missing result participant payloads.
- Corp ship task-response cleanup now releases session locks even if task-output notification fails.
ship.destroyedclient handling no longer creates a synthetic corporation ship entry for sector-visible destruction events that do not belong to the player's corporation.
- Gameplay
EVENT_TRANSPORT=pubsubnow uses online session-scoped PGMQ queues with heartbeat expiry and scheduled DB cleanup.
- Per-character corporation ship cap with BYOA and destroyed-ship exemptions
- Gradium is now supported as the default TTS provider, with Cartesia still selectable via
TTS_PROVIDER=cartesia
- BYOA wake dispatch always calls
wake_agent. The previousfresh_presenceskip-wake optimization was racy — the just-exited harness'sonline=Falsebroadcast could lag the next task dispatch, leaving the cache stale and the second task silently timing out - Pubsub event delivery hardened to a required boundary.
pgmq_publishandrecord_event_with_recipientsno longer swallow publish failures, andpurge_backlogkeeps the per-character queue present (purges messages instead of dropping the table) so bootstrap-RPC events stop hitting silentundefined_tableno-ops. Bot-side double-purge bracketssession_initso bootstrap echoes never leak into the LLM context via EventRelay pgBuildLocalMapRegionincludesplayer: { id: characterId }in its return so bootstrapmap.localpayloads pass the client's session-payload checkDockerfile.bot: installgitsouvcan clone the pinnedpipecat-ai-subagentsrev; drop the deadCOPY src/gradientbang/subagents/line- Deno edge server warms the adjacency cache at startup before serving traffic; bumps the local pg pool 3→8 to absorb bursty bootstrap traffic
purge_event_backlogresolver:PGMQ_URLfalls back toLOCAL_API_POSTGRES_URLwhen unset (both point at the same admin DSN in cloud deploys)
- BYOA (Bring-Your-Own-Agent) — corp members can claim a corp ship and run their own task agent for it. Operators deploy the BYOA harness (local dev via
uv run byoa --serve, or production via a Vercel Function the operator owns). The bot wakes the operator's receiver over HTTPS per task; the operator's runtime spawnsuv run byoawith the wake env. BYOA ships are owner-only: only the BYOA owner can start tasks on them; any corp member can force-cancel - Per-ship wake config via
ship_byoa_configure { action: 'set', source_url, wake_secret }. The wake bearer is stored encrypted at rest (byoa_wake_secret_enc, pgcrypto) and used per-ship — no shared env-var bearer - BYOA online/offline + waking state on corp ship cards. Cold starts show a fuel-colored
Wakingbadge until the firsttask_output - Subagent bus is transport-pluggable via
SUBAGENT_BUS_TRANSPORT(localdefault;pgmqenables BYOA). SetSUBAGENT_BUS_DATABASE_URLwhen runningpgmq. Channels are server-allocated UUID-128 strings (gb_<32hex>) minted per voice session — knowledge of the channel name is the bus capability TASK_AGENT_TIMEOUT(bot env, default1800s) is now the per-task hard upper bound. Bot cancels and clears its local ship lock on expiry. Not operator-overridablecurrent_task_actorandbyoablocks onlist_user_shipsandcorporation_infoship-list payloads (character IDs truncated to 12 hex chars)- Universal wake-up handshake (
BusAgentHelloRequest/Response) before task dispatch. Times out perBYOA_AGENT_WAKE_TIMEOUT_SECONDS(default 30s) — generous enough for a Vercel-Sandbox cold start - Idle teardown for warm corp-ship / BYOA agents after
BYOA_AGENT_IDLE_TEARDOWN_SECONDS(default 300s) of no activity. Player-ship agents are reused across tasks and excluded
- Ship-task lock is per-bot in memory (
VoiceAgent._locked_ships). BYOA presence heartbeats (10s cadence over the bus) are the crash signal — ~30s stale presence clears the local lock and emitstask.cancel task_lifecycle event_type=startandtask_cancelno longer touch a DB lock; they emit lifecycle events only. Still returns403 byoa_private_not_ownerwhen a non-owner tries to start a task on a private BYOA ship.task_cancel { force: true }lets any corp member cancel a BYOA-owned task when the owner is unreachablerecord_event_with_recipientsowns pubsub delivery from SQL, so SQL-only quest events reach pgmq without JS dual-writesquest.reward_claimedevents reframed as payout, not progress- TaskAgent drops its dedicated
AsyncGameClient— VoiceAgent's single player-bound client services every TaskAgent via per-call identity overrides. Concurrent calls disambiguatetask_idvia aContextVarinstead of mutating shared client state - Game RPCs, lifecycle events, corp queries, and combat doctrine fetches all flow through typed bus messages to VoiceAgent's broker. Broker treats envelope
character_id/actor_character_idas authoritative
- Onboarding mega-port route stays in Federation Space.
findRouteToNearestaccepts a traversable predicate;joinfilters byfedspace_sectors; worldgen'sselect_fedspacegrows the fedspace region as a connected subgraph so every mega-port is reachable without crossing Neutral. Existing universes must be regenerated to pick up the new layout - Task-agent mega-port verification: read the
MEGA/STDprefix on the port string inmovement.complete/status.snapshotinstead of stacking commodity filters onlist_known_ports, which produced false negatives
- DB ship-lock columns and RPCs (
acquire_ship_task_lock,release_ship_task_lock,force_release_ship_task_lock,refresh_ship_task_heartbeats), thetask_heartbeatedge function, andAsyncGameClient.task_heartbeat - BYOA token surface:
byoa_tokenstable,verify_byoa_tokenSQL function, and thebyoa_token_mint/byoa_token_revokeedge functions. Channel-as-capability replaced the per-op token check - Env vars:
BYOA_TOKEN,BYOA_HEARTBEAT_INTERVAL_SECONDS,TASK_LOCK_HEARTBEAT_STALE_SECONDS,TASK_LOCK_HARD_TTL_MINUTES,BYOA_SERVER_LOCK_STALE_SECONDS,BYOA_SERVER_LOCK_HARD_TTL_MINUTES
EVENT_TRANSPORTnow defaults topubsub(waspolling). The polling adapter is still selectable viaEVENT_TRANSPORT=polling. Edge-function test runner and CI no longer run both transports in parallel by default — only pubsub runs; passTRANSPORT=pollingtorun_tests.shfor a polling-only regression pass.
- Per-character authentication on every gameplay edge function: the caller's Supabase Auth JWT is verified server-side and
canActOnCharacter()enforces direct ownership or corp-ship access via corp membership. - Optional pgmq-backed event delivery (
EVENT_TRANSPORT=pubsub) — long-polls per-character queues via auth-gated SQL functions, eliminating the busy-poll loop. Polling remains the default; both modes are wire-compatible. Broadcast events (chat, gm/system) fan out to pubsub subscribers via Postgres LISTEN/NOTIFY - Admin-only credential path:
combat_tick,test_reset, andeval_webhooknow strictly require an admin token.corporation_info's member-only payload is gated behind ownership — non-members get the public view
- Bumped to Pipecat 1.0 (
pipecat-ai1.1.0,pipecatcloud0.6.0). Replaced the in-repo subagents framework fork with the publishedpipecat-ai-subagents0.4.0 - Bot edge function calls now require BOTH the user JWT (
X-API-Token) andEDGE_API_TOKEN(newX-Edge-Authheader). A bare user JWT is rejected withadmin_token_required. Auth contexts are nowadmin(X-Edge-Auth only),bot(both headers), andbyoa(future, third-party agents) - Bot's
/startnow requires a Supabase Authaccess_tokenin the body so per-character credentials flow into pubsub and ownership checks; dev workflows can useBOT_TEST_ACCESS_TOKEN - Supabase Auth JWT expiry bumped from 1h to 24h so a session covers any plausible gameplay length without a refresh path
- Local-dev admin bypass now requires explicit
ALLOW_AUTH_BYPASS_FOR_LOCAL_DEV=1— closes a prod foot-gun where missingEDGE_API_TOKENcould silently grant admin context
- Combat round outcomes are now persisted to
ship_instancesafter every round, not just at end of combat — DB-sourced reads (my_corporation,corporation_info,status.update, voice/task tools) reflect live fighter and shield counts during a fight instead of pre-combat values - Destroyed players exit combat immediately: escape-pod conversion, personalized
combat.ended, and unblocked movement / trade / bank actions all happen the moment the ship dies — no more sitting through the rest of someone else's fight - Destroyed corp ships clean up immediately when they die (pseudo-character +
corporation_shipsrow removed mid-round) instead of waiting for the encounter to end - Destroyed combatants stay in the participant list flagged
destruction_handled = trueso observers and the LLM see "destroyed in round N" persistently across the rest of the encounter, not a vague absence
- Salvage from a ship destroyed mid-combat now drops to the sector only when combat ends — preventing in-fight loots and sector passers-by from collecting wrecks while the battle is still going
combat_actionrejects all actions from destroyed or escape-pod participants (was: only rejectedflee); attacks on a destroyed target are rejected at submission with a clear error- VoiceAgent task.completed narration is now driven by a generic deferred-update queue instead of a per-event blocking gate. Close-together completions batch into one announcement (settle window of 2s, capped at 8s); narrations defer politely around the user's turn (won't front-load ahead of an answer to a question, won't fire while the user is speaking); a same-ship follow-up command silently folds the prior arrival into context with no narration; and once the conversation has moved 5+ turns past, queued events are appended silently. Voice agent prompt also restricts task.completed reports to the ships in the most recent event block — no more volunteering stale status on still-in-flight ships
- Voice agent task.completed reports no longer include ship status (shields, warp, cargo, ports) — the "unless commander asks" caveat was never used in practice
- Bot audio volume slider now actually controls remote bot audio: a local
PipecatClientAudiobinds the media element'svolumeto the store'sremoteAudioVolumesetting (the default component from@pipecat-ai/client-reactignored it) - Sector map edge feather is now applied after the zoomed scene render, so it fades the final composed viewport instead of being painted over by ships, course plot, and badges
corporation.dataevents no longer leak into the voice agent's LLM context — every internalmy_corporationRPC (start_task ship resolution, kick member resolution, client refreshes) was dumping the full corp roster, ship list, and destroyed-ship history into voice context. TaskAgents and the web UI still receive the event via the bus broadcast and realtime push; the voice agent gets corp info on demand via thecorporation_infotool's summary
- Ship combat strategies: per-ship doctrine (
balanced/offensive/defensive) plus optional custom prompt; voice + UI tools to set/get/clear; auto-injected at round-1 combat - Combat strategies panel for selecting doctrine and editing the custom prompt
garrison.destroyedevent with voice narration for the owner; silent context for corp and sector observers- Per-viewer combat POV (DIRECT / corp-ship observer / garrison observer / sector-only) shapes both summary lines and XML envelope attrs on every encounter event
- Round-1 fan-out: combat-start broadcasts to all stakeholders (corp members, absent garrison owners, sector observers); rounds 2+ stay participant-only
- Successful flee now emits the full movement cascade to the fleer (
movement.start,character.moveddepart,movement.complete,map.local,character.movedarrive) plus a personalizedcombat.endedso the client unsticks from combat state immediately, even when the fight continues for the others has_fled/fled_to_sectorfields on participants in combat round payloads — distinguishes fled combatants from destroyed ones- Standalone combat sim client for offline strategy experimentation
- Map marker for a destroyed garrison is cleared and logged to the activity feed
- Per-garrison fighter cap of 32,000; deploys that would exceed are rejected with a clear
max additional = Nerror - Client: BigMap panel shows sectors with active combat
- Combat event payloads: participants gain
fighters/destroyed/corp_id; garrison gainsowner_character_id/owner_corp_id ship.destroyed: addsowner_character_id/corp_id; inference flipsNEVER → OWNEDso voice narrates the loss for the owner- Toll garrisons are now per-payer: paying a toll is a peace contract scoped to that payer ↔ that garrison. Other hostiles must still pay (no free rides), paid payers cannot attack the garrison or re-pay it, and the garrison re-targets to unpaid hostiles on escalation rounds. Combat ends
toll_satisfiedonly when every hostile has paid and nobody is attacking - Combat reference prompt rewritten around the four POVs and the new payload fields
- Voice combat announcements widened to cover observed combats, not just direct participation
combat.round_resolvedreports post-round fighter counts correctly (was reading the participant snapshot before losses applied)- A successful fleer is now moved out of the encounter every round, not only when combat happens to end the same round — previously a fleer who left mid-fight had their ship row stay in the combat sector until the rest of combat resolved
- Combat XML envelope attrs are escaped before being sent to the LLM — a ship name with quotes or angle brackets can no longer corrupt the event frame
- Multilingual voice support: select a voice/language in settings and the entire pipeline adapts — STT recognition, TTS synthesis, and LLM responses switch to the chosen language
- Supported languages: English, Spanish, French, Hindi, Portuguese, Turkish
- Runtime language switching: change language mid-session without reconnecting
- Quest voice changer gracefully skips voice swap for non-English languages, keeping the active voice
- Batch tool call inference: when the LLM issues multiple tool calls in one response, only trigger a single inference after the last tool completes instead of one per tool
- Corp ship task completions now include ship_name in the event XML so the voice agent can distinguish which ship finished
- Corporations: two-word invite passphrases (e.g.
nebula-drift) with collision retry - Corporations: two-step confirmation flow for joining and for kicking members, with client modals
- Corporations: details modal accessible from the top bar — roster, fleet size, and (founder-only) invite passphrase with copy/regenerate
- Corporations: join notifications for the joiner and existing members, with the voice agent learning about new members immediately
- Corporations: auto-leave when joining a different corp (silent if not last member; confirm-pending if last; refused if the corp still owns ships)
- Voice agent:
join_corporationandkick_corporation_membertools (previously TaskAgent-only) - Dev-only
VITE_BOT_URLto point the client at a local bot while using prod Supabase
- Corporations: details modal restyled (elbow card, data table for members)
- Corporations: invite regenerate uses reactive spinner (clears when new code arrives, no timeout)
- Corporations: corp-scoped event polling now works reliably from first connection
- Corporations: only the founder can kick, view the invite code, or regenerate it
- Corporations: invite codes accept spaces and underscores as dashes (so speech input like "nebula cortex" works)
- Corporations: tasks are actor-private — corpmates no longer see each other's task events
- Corporations: credit transfers to a corp ship now refresh every corpmate's UI in real-time
- Combat: unified friendly-fire checks across ship, garrison, and character targets
- Friendly-fire: corp-ship garrisons no longer attack corpmates (pseudo-character corp resolution gap)
- Corp-ship events no longer leak into the voice agent as if they were the player's own
- Task slot limits now correct in LLM prompts (was claiming unlimited)
- Bot startup crash from an empty
__init__.pystub in the Docker image
- Combat tools (
combat_initiate,combat_action) for TaskAgent with async completions - Disconnect safety warnings in voice agent, game overview, and disconnect modal
- Aria labels and roles for mic toggle, text input, and conversation panel
- Disabled event pruning cron by default; extended retention from 72 hours to 14 days
- Clarified combat targeting rules (any ship in sector is valid)
- Added ship destruction playbook to event_logs prompt
- Event pruning cron job to prune stale non-important events (72h retention)
- Name validator with profanity and charset checks
- Disposable email domain blocking on signup
- Maintenance mode killswitch for login edge function
- Version plumbing: bot logs version on startup, sends version to client via RTVI on join
/releaseskill for semver version bumps with CHANGELOG management