Skip to content

0063 — NPC scheduler and lifecycle hardening (Group H)

Status

Accepted.

Context

Eight audit findings on the NPC scheduler and lifecycle layer. One critical (coverage-gap grief), seven standard (correctness, default values, edge cases, social-graph future work). The cluster is mostly clarification work plus one structural change (zero-gap handoff after a Marshal KIA).

Decision

N-F1 — Zero-gap promotion after Marshal KIA

The "15-minute coverage gap" between a Marshal KIA and the next NPC stepping in was the time until backup-fill from the recruit pool. The closure: the secondary on-duty NPC promotes to primary role immediately on KIA detection. The recruit-pool fill happens for the now-vacant backup slot — a fresh recruit takes the slot immediately and serves at reduced stats throughout the 7-day recruit stage per ../SYSTEMS/npc-lifecycle.md. The (formerly-backup, now-promoted) primary covers normally during this period — there is always a designated authority on duty.

def handle_npc_kia(npc, sector):
    role = npc.duty_role  # 'primary_marshal' | 'backup_marshal' | 'enforcer' | ...
    if role == 'primary_marshal':
        backup = sector.npc_roster.find_by_role('backup_marshal', status='on_duty')
        if backup:
            backup.duty_role = 'primary_marshal'
            backup.promotion_at = utcnow()
            outbox.queue('npc.role_promoted', { ... })
            sector.npc_roster.queue_recruit('backup_marshal')   # 7d recruit stage (engagement-eligible at reduced stats)
        else:
            # No backup on duty — fall through to emergency spawn (rare; only on
            # double-KIA or sectors with reduced rosters by design)
            sector.npc_roster.queue_emergency_recruit('primary_marshal')

The dual-KIA edge case (both primary and backup down simultaneously) falls through to an emergency recruit; this is rare enough that the temporary gap is acceptable. The on-station rotation pattern is the load-bearing defence.

N-D2 — Respawn cooldown vs recruit training period

The audit confused two timers and one activity-block. They are distinct:

  • Respawn cooldown — same NPC identity returns after KIA. 15 minutes. During this window NPCCharacter.status = respawning and the slot is vacant. After cooldown the NPC re-enters its existing slot at full stats (its career and reputation persist).
  • Recruit lifecycle stage — a new NPC entering the roster (succession after KIA-without-respawn, slot expansion, post-genocide refill). 7 real-time days at lifecycle_stage = recruit per the existing npc-lifecycle.md (not changed by this ADR). During the recruit stage the NPC is on the duty roster and is engagement-eligible — with reduced combat effectiveness, scanner range, and a "Trainee" flavor cue. The faction commits to having an authority on duty even if it's a learner.
  • train activity block — within a single day's schedule, the NPC may have a train activity block (at a training facility honing skills). During that block, engagement_eligible = false for that NPC. This is unrelated to the lifecycle stage; an active veteran NPC may still have train blocks in their schedule.

The respawn-cooldown timer and the recruit-stage timer are independent. A respawning NPC re-occupies its existing slot at the 15-minute mark; a fresh recruit takes a vacant slot immediately and serves throughout the 7-day recruit stage at reduced effectiveness, then transitions to active.

NPCCharacter.status enum gains a respawning value (per ../DATA_MODELS/npcs.md update) for the 15-minute cooldown window. The lifecycle-stage column already exists.

N-D3 — KIA reroute supersedes scheduled shift handoff

When a scheduled shift handoff is pending and a KIA fires, the KIA-driven reroute takes precedence. The pending handoff is cancelled and rebuilt from the post-KIA roster state. This avoids the "teleport redundancy" artifact where two NPCs would swap places at the same moment a third NPC's death triggered a different reroute.

Rule: any KIA event invalidates pending shift-handoff orders for the affected sector. The shift scheduler reads the post-KIA roster on its next tick.

N-F4 — NPC barracks "advisory cap" semantics

Barracks have two cap thresholds:

  • Advisory cap — soft limit defined per barracks. Recruits added past advisory cap enter delayed_train status — they exist in the roster but their training timer is paused until an active slot frees up. Reflects "we're at capacity, please wait."
  • Hard cap = 2× advisory cap. Attempts to add a recruit past hard cap reject with ERR_BARRACKS_FULL. The faction's recruit-generation logic stops queueing additional recruits for this barracks until the queue drains.

Existing barracks rows get advisory_cap populated from the per-class default (Class-3 garrison: 8 advisory / 16 hard; Class-5 mega-barracks: 24 advisory / 48 hard). Operators can tune advisory_cap per-barracks; hard_cap is computed.

N-I1 — Engagement-routing distance cap

Default 5 sectors warp-tunnel distance for NPC engagement routing. NPCs won't pursue threats past this radius from their assigned duty sector. Implementation reads the warp-tunnel graph; no Quantum-Jump pursuit (consistent with the Roving Fleet Camp reachability rule from ADR-0060 G-V3).

The cap is per-NPC-class tunable (e.g., Sector Marshals 5 hops; Faction Patrol Captains 8 hops; Pirate Lords 3 hops — tighter, they don't roam far from their stronghold). Defaults live in NPC_CLASS_ROUTING_DISTANCE per ../SYSTEMS/npc-scheduler.md.

N-I2 — Social affinity / rivals graph

📐 Design-only future work. A future iteration will add an NPCRelationship table for affinity / rivalry / mentor relationships between NPCs in the same faction or sector. Effects could include morale modifiers when NPCs work alongside friends or rivals, narrative beats when a rival NPC is killed, and ARIA-narrated faction politics.

Not Launch-blocking. The placeholder is documented in ../SYSTEMS/npc-lifecycle.md so future work has a known landing surface.

N-I3 — Patrol-squad coherence scan

A periodic service (per ADR-0053) verifies squad members are within 3 sectors of squad leader every 5 minutes. Out-of-coherence members receive a reroute order to rejoin. The scan also catches squads with KIA leaders and re-elects a new leader from the surviving members (highest-rank or longest-serving tiebreaker).

# patrol_squad_coherence_sweep — 5-minute cadence
for squad in active_patrol_squads():
    if squad.leader_status == 'kia':
        new_leader = elect_new_leader(squad)
        squad.leader_id = new_leader.id
    for member in squad.members:
        distance = warp_tunnel_distance(member.sector, squad.leader.sector)
        if distance > 3:
            issue_reroute(member, target_sector=squad.leader.sector)

N-V4 — Genocide rapid-recovery (no region-wide penalty)

Per the user pick, coordinated Marshal wipes don't trigger a region-wide state change. Instead, the detection signal triggers a rapid-recovery flood:

  • Trigger: ≥3 Marshal-tier NPC kills within a 30-minute window in the same region.
  • Response: faction recruit-generation rate doubles for 1 hour in the affected region. Recruits spawned during this window reach active lifecycle stage at half the normal duration (3.5 days instead of 7).
  • No region-wide debuff: patrol response, faction-rep, and policing all continue at normal rates. The region recovers naturally; players don't suffer ambient penalty for the wipe.

The lighter touch reflects a design preference for rapid, faction-led recovery rather than a region-state debuff that punishes the whole population for a coordinated team's actions. ARIA narrates: "Heavy losses among regional authorities — faction reinforcements arriving in waves." No mandatory tribute, no rep penalty on attackers, no Lawless Region state.

The detection itself logs a npc.coordinated_genocide_detected realtime event and surfaces in the operator dashboard, which gives ops an early signal of coordinated griefing patterns even though no automatic gameplay penalty fires.

Consequences

  • N-F1 introduces an npc.role_promoted realtime event and the NPCCharacter.duty_role field becomes mutable mid-shift. Existing scheduler logic that assumes static duty assignment over a shift must read live duty_role, not a snapshot from shift start.
  • N-D2 schema additions are forward-only; existing NPC rows get engagement_eligible_at = created_at (i.e., immediately eligible) at migration, since their training period is presumed past.
  • N-D3 removes a class of teleport-redundancy bugs but requires the shift scheduler to re-read roster state on every tick rather than caching across handoff cycles.
  • N-F4 advisory/hard cap distinction is operator-visible — admin UI surfaces both numbers per barracks. The training-pause semantics for delayed_train recruits requires a tiny scheduler change but the schema is backwards-compatible.
  • N-I1 the 5-sector default may need tuning per-faction at Launch; the table-driven approach makes this a config change, not code.
  • N-I3 patrol-squad coherence sweep is a small periodic job. Squad leader re-election logic is deterministic (rank then longest-serving) — no ambiguity in tiebreakers.
  • N-V4 is a deliberately lighter touch than a full Lawless Region state. If griefing patterns emerge that the rapid-recovery flood doesn't deter, a follow-up ADR can promote the response to a region-state change without changing the detection signal.

Alternatives considered

  • Emergency NPC spawn at reduced capability (N-F1). Rejected per user pick — weaker authority is still exploitable; zero-gap promote is structurally cleaner.
  • Lawless Region state with mandatory tribute + 24-72h duration (N-V4). Considered (was the recommended pick); rejected per user pick. Region-wide debuff punishes the whole population for a coordinated team's actions; rapid-recovery flood targets the actual gap (NPC supply) without ambient cost.
  • Per-sector advisory cap with no hard cap (N-F4). Rejected — unbounded queue creates training-time-pressure attacks where the queue depth itself becomes a faction debility.
  • Live social-affinity graph at Launch (N-I2). Rejected — out of scope for Launch; the placeholder documents the future direction.