0053 — Runtime Services (Batch 6)¶
Status¶
Accepted
Context¶
Nine runtime-service decisions in the WR series sat unresolved (WR4, WR5, WR6, WR7, WR8, WR9, WR10, WR12, WR14 — WR13 already closed by ADR-0051 SK30 in Batch 4). Each one identifies a periodic service or batch job that the design implies but hasn't documented. Without explicit specs, the launch implementation has no spec for "when does this run, what does it do, what does it write."
The pattern is consistent: most are scheduled-tick services running on cadences from 60 seconds to daily. A few are runtime-event-triggered (eager seed at row creation, on-demand spawn). All are small, focused services that compose with existing surfaces — none requires substantial new architecture.
This ADR locks the specs so each service has an explicit cadence, scan target, idempotency rule, and failure mode. Bundled together for working coherence; each pick is independently shippable.
Decision¶
WR4 — Hostile NPC spawn schedule¶
Three hostile-faction surfaces, each with a different mechanism:
- Pirates — fully governed by ADR-0048 (ecosystem dynamics: weekly growth tick, virus-spread, evolution, Cleansed-region accolades). No separate spawn schedule. Ratified.
- Cabal HQ guards — pre-seeded at galaxy generation. Phase 12.6 (per ADR-0047 and ADR-0050) is extended to cover Cabal NPCs at the
special_interestCabal HQ cluster (parallel to the police pre-seeding pattern in Phase 12.5 and the pirate pre-seeding in 12.6's main body). The Cabal HQ cluster'sOutlawBaserow anchors a small named-NPC roster (1 Cabal Lieutenant + 4 enforcers per region with a Cabal HQ). - AM enforcement patrols — on-demand via NPC scheduler Loop B. Trigger event:
unlicensed_mining_extraction(the existing−10 AM rep per unlicensed extractionrep-action perFEATURES/economy/mining.md). On detection, the scheduler dispatches aFACTION_PATROLsquad from the nearest AM-aligned barracks; squad pursues the offending player per the standard engagement-routing flow.
WR5 — Bang import filter¶
The bang import pipeline accepts only worldgen-content tables. Runtime-content tables are rejected.
Worldgen-importable (the pipeline accepts these):
Region(top-level metadata)SectorClusterZoneStation(the structure; not its market state)PlanetWarpTunnelsector_warpsSpecialFormation
Runtime-rejected (presence triggers ERR_BANG_IMPORT_INCLUDES_RUNTIME_CONTENT):
MessageBeaconNPCCharacter,NPCDeathLog,NPCRosterMarketTransaction/enhanced_market_transactions,MarketPrice,PriceHistoryCombatLog,CombatStatsBountyClaim,BountyHunterMembershipARIAObservationLog,ARIAPersonalMemory,ARIAMarketIntelligence,ARIAExplorationMap,ARIAQuantumCachePirateKillLog,PirateHoldingOutlawBase(lodging is runtime-only; the worldgen-time pre-seeding in Phase 12.6 creates these via the gameserver, not via bang import)Player,User,PlayerCentralBankAccount,Reputation,TeamMembershipRaidProgress(📐 — when this surfaces post-launch)- All audit-trail tables (combat-side, economy-side, NPC-side)
The bang import endpoint validates both presence (every worldgen table is complete and consistent per the gameserver Phase 13 invariants — already covered by ADR-0050 SK20) and absence (no runtime-table rows in the payload). Rejection lists every offending table.
WR6 — Quantum nebula depletion replenishment tick¶
Cadence: every 60 seconds (UTC).
Scan target: Sector rows where Sector.depletion_state != HEALTHY and Sector.depletion_replenish_at <= now().
Action: transition state to the next-healthier value (DEPLETED → RECOVERING → HEALTHY) and clear depletion_replenish_at. If the new state is still not HEALTHY, set the next replenishment timer per the per-color schedule (Crimson 14 days; all others 5 days, per FEATURES/galaxy/quantum-resources.md).
Idempotency: repeated calls with no eligible rows are no-ops. Multiple service instances are safe (uses SELECT ... FOR UPDATE SKIP LOCKED).
Realtime emission: nebula_replenished event for the sector's subscribers when state transitions to HEALTHY.
Failure mode: a missed tick (service down) extends the replenishment by however long the service was down. On restart, the next tick processes any backlog. Acceptable — replenishment is slow-cycle and a few minutes of skew per outage is invisible to players.
Documented in a new SYSTEMS/quantum-depletion-service.md.
WR7 — Special Formation EMERGENT detector pass¶
Cadence: every 6 hours.
Scan target: the warp graph at the region level. The detector runs the same topology rules as the worldgen stamping templates (Bubble articulation-point detection, Tunnel chain detection, Dead-End single-degree detection, etc.) against the live sector_warps graph and identifies any topology that matches a formation template but has no SpecialFormation row associated.
Action: create new SpecialFormation rows with origin = EMERGENT and is_discovered = false. Per-player discovery still applies via ADR-0045.
Idempotency: a second pass on an already-detected emergent formation finds the existing row and skips (matches by topology hash, not just sector_id, to handle drift).
Cadence rationale: 6 hours is intentionally slow. Player-driven warp graph changes (new gates anchored, gates destroyed in cascade) are infrequent; 4 detector passes per day is plenty. A more frequent cadence would burn cycles re-scanning unchanged graphs.
Failure mode: a missed pass means newly-emergent formations surface 6 hours later. Acceptable.
Documented as a section in SYSTEMS/special-formations-generation.md (no separate file needed).
WR8 — Genesis Team 48-hour formation tick¶
Cadence: every 15 minutes.
Scan target: GenesisTeam rows where formation_window_active = True.
Action: for each active team:
- Verify all members satisfy the in-sector predicate (per AU2-1: same current_sector_id, OR docked in a Carrier hangar in-sector, OR in an escape pod in-sector, OR within 15-min reconnect grace).
- If all members satisfy: increment formation timer; if timer has reached 48 hours, fire genesis_team_formation_complete and unlock the team's deployable Genesis effect.
- If any member has been outside the predicate for > 15 min: cancel the formation; fire genesis_team_formation_cancelled with reason; reset timer to zero.
Idempotency: each tick computes the current state from the team's member positions; repeated calls produce the same result for the same state.
Realtime emission: genesis_team_formation_progress (every 15-min tick during a successful formation), genesis_team_formation_complete, genesis_team_formation_cancelled per the rules above.
Documented as a section in SYSTEMS/genesis-deploy.md.
WR9 — Subscription lifecycle promotion job¶
This ratifies the cron job structure already implicit in ADR-0050 Batch 3.
One daily cron at UTC midnight: services/gameserver/src/scheduling/cron_jobs.py:advance_region_lifecycle_states.
Three transitions in one job:
def advance_region_lifecycle_states():
"""Daily cron — advances regions through the lifecycle state machine."""
# Suspended → grace
db.query(Region).filter(
Region.status == 'suspended',
Region.suspended_at + timedelta(days=7) <= now(),
).update({'status': 'grace'})
# Grace → terminated
db.query(Region).filter(
Region.status == 'grace',
Region.suspended_at + timedelta(days=30) <= now(),
).update({
'status': 'terminated',
'terminated_at': now(),
'scheduled_hard_delete_at': now() + timedelta(days=7),
})
# Note: Setting status = 'terminated' triggers no immediate cleanup.
# The cleanup orchestrator runs on a separate trigger (see below).
# Terminated → cleanup → hard_delete
for region in db.query(Region).filter(
Region.status == 'terminated',
Region.scheduled_hard_delete_at <= now(),
).all():
cleanup_orchestrator(region) # per region-lifecycle.md
The cleanup orchestrator itself is the substantive work; the WR9 cron is just the timer that triggers state transitions. Failure modes per the failure-modes table in SYSTEMS/region-lifecycle.md.
WR10 — Stolen-report retract grace expiry tick¶
Cadence: every 60 seconds.
Scan target: Ship rows where stolen_status = True AND stolen_reported_at + 24h <= now() AND retract_grace_processed = false.
Action: set retract_grace_processed = true on each row. After the flag is set, retract attempts no longer get the 75% refund — they fall through to the standard "no refund after 24 hours" rule per the existing SYSTEMS/ship-registry.md spec.
Idempotency: the flag is set-once and never cleared on the same report. A subsequent retract + re-file produces a new stolen_reported_at and a fresh grace window with retract_grace_processed = false again.
Schema: Ship.retract_grace_processed BOOLEAN DEFAULT false. Set by the ticker; reset on retract+refile.
WR12 — Anchor-repair runtime service¶
Cadence: daily at UTC midnight (alongside the WR9 cron — same scheduler tick).
Scan target: each Region with status = 'active'.
Action: verify the four Phase-11 anchors per SYSTEMS/galaxy-generator-design.md:
- Capital sector has a TERRA welcome planet
- Class-1 station exists at
capital_sector_number + 1(or fallback within starter cluster) - SpaceDock #1 exists at the starter-cluster anchor (
capital_sector_number + 9or fallback) - SpaceDock #2 exists at the frontier anchor (
total_sectors − 5or fallback)
For each missing anchor:
1. Re-inject via the canonical Phase 11 placement logic (uses the same code paths as worldgen).
2. If injection succeeds, emit region_anchor_repaired event with the anchor type and the placement sector.
3. If injection fails (target sectors all occupied by player infrastructure, or other validation rule blocks it), emit region_anchor_repair_failed event + admin alert (email/Slack). Manual ops intervention required.
Idempotency: a region with all four anchors intact is a no-op pass.
Why daily: anchor destruction is rare (Capital welcome planet, Class-1 station, and SpaceDocks are heavily defended; players generally can't destroy them). Daily is enough for the rare detection-and-repair case.
Documented in a new SYSTEMS/anchor-repair-service.md.
WR14 — New-station market bootstrap¶
Eager seed at station-creation commit.
When a Station row is created (via worldgen Phase 10, Phase 11 anchor injection, or runtime construction like region-funded TradeDocks), the same transaction also creates MarketPrice rows for every commodity the station's class trades per the trade-pattern table in FEATURES/economy/trading.md:
def create_station_with_market_bootstrap(station_data):
with transaction():
station = Station.create(**station_data)
for commodity in CLASS_TRADE_PATTERN[station.station_class]:
base_price = Resource.get(commodity).base_price
base_quantity = CLASS_BASELINE_QUANTITY[station.station_class][commodity]
MarketPrice.create(
station_id=station.id,
commodity_type=commodity,
buy_price=base_price * BASELINE_BUY_SPREAD,
sell_price=base_price * BASELINE_SELL_SPREAD,
quantity=base_quantity,
supply_level=baseline_supply_level(commodity),
demand_level=baseline_demand_level(commodity),
last_updated=now(),
)
No lazy initialization — every Station row is born with its market book populated. This eliminates a class of "first trader gets undefined behavior" bugs and means market queries on freshly-constructed stations always return valid data.
Failure mode: if the MarketPrice insert fails for any reason, the entire transaction rolls back (no partial Station with no market book). Atomic.
Documented as a section in SYSTEMS/market-pricing.md.
Consequences¶
Positive:
- Every implied-but-undocumented runtime service now has an explicit cadence, scan target, idempotency rule, and failure mode. Launch implementers don't guess.
- WR9 (subscription lifecycle cron) ratifies a structure that ADR-0050 already implied; closing it eliminates a "is this implemented yet?" question.
- WR14 (eager market bootstrap) prevents a class of "first trader at fresh station" bugs; the MarketPrice rows are part of the Station's atomic creation transaction.
- Anchor-repair (WR12) closes a long-tail correctness concern — destroyed Phase-11 anchors no longer leave a region permanently broken; the daily scan auto-repairs.
- The bang import filter (WR5) makes the gameserver-canonical contract from ADR-0050 SK20 concrete: explicit allow-list and deny-list of importable tables.
Neutral:
- One new column on
Ship:retract_grace_processed BOOLEAN DEFAULT false(WR10). - One new column on
Sector:depletion_stateenum +depletion_replenish_at DateTime(WR6, if not already present in the existing quantum-resources schema). - Two new SYSTEMS docs (
quantum-depletion-service.md,anchor-repair-service.md); five existing docs gain new sections. - Five new scheduled jobs in the launch cron schedule. Total cron load is light — most run at low frequency or scan small row counts.
Negative:
- The 6-hour cadence on the EMERGENT formation detector means newly-formed emergent formations take up to 6 hours to surface in any player's view. Acceptable: emergent formations are intentionally slow-evolving.
- The daily anchor-repair scan means a destroyed anchor isn't auto-repaired immediately — up to 24 hours of degraded region experience for the affected players. Acceptable: anchor destruction is rare and the alternative (real-time anchor monitoring) is overengineered.
Alternatives considered¶
Real-time event-driven services instead of scheduled ticks (rejected for most). Replace scheduled scans with event-driven triggers (e.g., listen for sector-state-change events to detect formation emergence). Rejected because event-driven introduces complexity (event ordering, missed events on service restart, dead-letter handling) and the design intent for these services is "low-frequency, tolerant of delay." Scheduled scans are simpler and adequate.
Single mega-service handling all scheduled work (rejected). Combine WR6, WR7, WR8, WR9, WR10, WR12 into one "scheduled work" service. Rejected because each service has different scan targets and cadences; combining them would couple unrelated work into one failure surface. Separate services with focused scopes are easier to reason about and operate.
Lazy initialization of station markets (rejected for WR14). Initialize MarketPrice rows on first read instead of at Station creation. Rejected because lazy init introduces a "first reader does extra work" surface and risks race conditions when two readers hit a freshly-created station simultaneously. Eager seed at creation is atomic, simple, and predictable.
Per-player anchor-repair triggers (rejected for WR12). Run anchor checks when a player enters a region. Rejected because the cost is unbounded (a region with many active players runs many redundant checks) and the design intent is "low-rate batch repair" not "real-time enforcement."
Bang import allow-list rather than deny-list (kept; the design here is already an allow-list). The decision uses an explicit allow-list of importable tables. Tables not on the list are runtime-only by definition.
Related docs¶
SYSTEMS/quantum-depletion-service.md— new file (WR6).SYSTEMS/anchor-repair-service.md— new file (WR12).SYSTEMS/special-formations-generation.md— extended with EMERGENT detector pass (WR7).SYSTEMS/genesis-deploy.md— extended with formation tick (WR8).SYSTEMS/ship-registry.md— extended with retract-grace ticker (WR10).SYSTEMS/market-pricing.md— extended with eager bootstrap (WR14).SYSTEMS/galaxy-generator-design.md— Phase 12.6 extended to cover Cabal NPCs (WR4).SYSTEMS/bang-import-pipeline.md— extended with the runtime-content rejection filter (WR5).SYSTEMS/region-lifecycle.md— cron structure ratified (WR9).SYSTEMS/npc-scheduler.md— AM enforcement on-demand spawn flow (WR4).ADR/0048-pirate-ecosystem-dynamics.md— pirate spawn governed here (WR4).ADR/0050-batch3-provisioning-lifecycle-hardening.md— region-lifecycle parent decision (WR9, WR12).ADR/0051-batch4-scale-targets.md— closes WR13 (market-price tick rate-limiting).