Deadline Tracking Routing Engines

Failover Routing When a Registered Agent Resigns

This guide is part of the Registered Agent Assignment Logic area within the Deadline Tracking & Routing Engines framework: it handles the one event that turns a steady-state assignment problem into a compliance emergency — a registered agent that resigns, is revoked, or otherwise goes dark, opening a statutory cure window during which the entity has no valid recipient for service of process. The parent area decides which agent owns an entity in steady state; this page decides what happens in the seconds and hours after that agent disappears, and how to restore a valid designation before the clock runs out.

Scope of This Page

This page covers automated failover: detecting the loss of a registered agent, selecting a ranked backup that holds statutory authority in the affected jurisdiction, and driving the change-of-agent filing to completion inside a hard internal SLA that sits safely ahead of the statutory deadline. It deliberately excludes the surrounding machinery documented elsewhere. It does not re-derive the steady-state assignment tree — that lives in the parent area. It does not compute the dollar-and-severity penalty exposure of a lapsed agency; that is the job of Penalty Avoidance & Grace-Period Mapping, whose severity output feeds the SLA this engine enforces. And it does not own the human workflow that catches an escalation — an exhausted failover hands off to Routing Compliance Tasks to Regional Legal-Ops Teams. Here we assume a resignation signal has already arrived and focus only on getting a valid agent back on record.

The Constraint That Makes a Missing Agent an Emergency

A vacant registered agency is not a paperwork gap — it is a live path to administrative dissolution, and the cure windows are short and jurisdiction-specific. When a Delaware agent resigns without naming a successor, the resignation becomes effective 30 days after the certificate is filed under 8 Del. C. § 136(b), and an entity that has not appointed a replacement by then ceases to be in good standing. Texas is stricter in spirit: Tex. Bus. Orgs. Code § 5.201 requires an entity to continuously maintain a registered agent, a resignation takes effect on the 31st day after filing under § 5.203, and a sustained vacancy is grounds for involuntary termination under § 11.251. California treats a resignation filed under Cal. Corp. Code § 1503 as effective essentially on filing, forcing an immediate replacement on the next Statement of Information (§ 1502) before the suspension machinery of § 2205 engages. New York revokes the agent designation 30 days after a resignation certificate is filed under N.Y. BCL § 305©, with service defaulting to the Department of State in the interim. Because the penalty for missing these windows is loss of good standing rather than a flat fee, the failover engine treats agent availability as a live signal and clamps every internal deadline to a margin inside the statutory window, never at it.

Prerequisites

  • Python 3.10+ — for match on failover states, X | Y unions, and zoneinfo timezone handling.
  • A resignation signal source — an agent-portal webhook, a scheduled good-standing sweep, or a commercial-agent bulk-resignation notice — normalized into a single typed event before it reaches this engine.
  • A backup-agent registry exposing, per candidate, the jurisdictions it is authorized in, remaining caseload capacity, a historical reliability score, and an activation latency (how fast it can accept a new designation).
  • A jurisdiction matrix supplying the statutory cure window per state, version-controlled and reviewed quarterly for amendment.
  • An append-only audit sink and a legal-ops escalation queue for failovers that cannot be satisfied within SLA.

Implementation: A Deadline-Bounded Failover Engine

The engine models failover as a forward-only state machine — ACTIVE → RESIGNATION_DETECTED → BACKUP_SELECTED → CHANGE_FILED → RESTORED — with a single off-ramp to ESCALATED whenever no ranked candidate can complete the change-of-agent filing before the internal SLA. Candidate ranking is deterministic: filter to agents with statutory authority and free capacity in the affected jurisdiction, then order by reliability and activation speed so the fastest trustworthy agent wins. The SLA guard is the compliance-critical gate: it projects the total time to activate and file against the remaining cure margin and refuses to commit a candidate that would finish late, escalating to humans instead of gambling on a tight window.

Registered-agent failover state machine bounded by the statutory cure clock Five forward-only states — ACTIVE, RESIGNATION-DETECTED, BACKUP-SELECTED, CHANGE-FILED, RESTORED — run left to right above a statutory clock track. An SLA guard between BACKUP-SELECTED and CHANGE-FILED escalates to a legal-ops queue when the projected activation-plus-filing time would breach the internal SLA, which is set with margin inside the statutory good-standing loss deadline. Fail over to a ranked backup, then file the change — always inside the statutory clock ACTIVE agent on record steady state RESIGNATION- DETECTED cure window opens BACKUP- SELECTED ranked candidate CHANGE- FILED submitted to registry RESTORED good standing preserved detect rank + SLA guard file ack SLA guard: projected activate + file ≤ remaining margin no candidate fits SLA ESCALATED → legal-ops queue STATUTORY CLOCK T0 · resignation detected internal SLA (change filed) safety margin statutory good-standing loss
from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field, replace
from datetime import datetime, timedelta, timezone
from enum import Enum

# Structured JSON logging — every failover transition is an auditable event.
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("registered_agent.failover")


class FailoverState(str, Enum):
    ACTIVE = "active"                          # agent on record, nothing to do
    RESIGNATION_DETECTED = "resignation_detected"
    BACKUP_SELECTED = "backup_selected"
    CHANGE_FILED = "change_filed"
    RESTORED = "restored"                      # terminal success
    ESCALATED = "escalated"                    # terminal — no candidate fits the SLA


@dataclass(frozen=True)
class ResignationEvent:
    entity_id: str
    jurisdiction: str
    resigning_agent_id: str
    detected_at_utc: datetime
    # Statutory instant the entity loses good standing if no agent is on record.
    statutory_deadline_utc: datetime
    # Fraction of the cure window we refuse to spend — the internal SLA lives here.
    safety_margin: float = 0.35


@dataclass(frozen=True)
class BackupCandidate:
    agent_id: str
    authorized_jurisdictions: frozenset[str]
    capacity_remaining: int          # open caseload slots; 0 means unavailable
    reliability_score: float         # historical accept/file success in [0, 1]
    activation_hours: float          # lead time before the agent can accept
    filing_hours: float              # time to prepare + submit the change-of-agent


@dataclass(frozen=True)
class FailoverResult:
    entity_id: str
    jurisdiction: str
    state: FailoverState
    selected_agent_id: str | None = None
    sla_deadline_utc: datetime | None = None
    projected_completion_utc: datetime | None = None
    reason: str = ""


class FailoverEngine:
    """Select a ranked backup agent and file a change of agent inside a hard SLA."""

    def __init__(self, candidates: list[BackupCandidate]) -> None:
        self._candidates = candidates

    def _sla_deadline(self, event: ResignationEvent) -> datetime:
        # The internal SLA sits *inside* the statutory window by safety_margin,
        # so a filed change never races the actual good-standing cliff.
        window = event.statutory_deadline_utc - event.detected_at_utc
        return event.statutory_deadline_utc - window * event.safety_margin

    def _eligible(self, event: ResignationEvent) -> list[BackupCandidate]:
        # Only agents with statutory authority in-jurisdiction and free capacity.
        return [
            c for c in self._candidates
            if event.jurisdiction in c.authorized_jurisdictions
            and c.capacity_remaining > 0
            and c.agent_id != event.resigning_agent_id
        ]

    @staticmethod
    def _rank_key(c: BackupCandidate) -> tuple[float, float]:
        # Prefer the most reliable agent; break ties by fastest total lead time.
        return (-c.reliability_score, c.activation_hours + c.filing_hours)

    def run(self, event: ResignationEvent) -> FailoverResult:
        sla = self._sla_deadline(event)
        logger.info(json.dumps({
            "event": "resignation_detected", "entity_id": event.entity_id,
            "jurisdiction": event.jurisdiction, "sla_deadline": sla.isoformat(),
            "statutory_deadline": event.statutory_deadline_utc.isoformat(),
        }))

        ranked = sorted(self._eligible(event), key=self._rank_key)
        for candidate in ranked:
            lead = timedelta(hours=candidate.activation_hours + candidate.filing_hours)
            projected = event.detected_at_utc + lead
            # COMPLIANCE GUARD: never commit an agent that would file past the SLA.
            if projected > sla:
                logger.info(json.dumps({
                    "event": "candidate_rejected_sla", "entity_id": event.entity_id,
                    "agent_id": candidate.agent_id,
                    "projected": projected.isoformat(), "sla_deadline": sla.isoformat(),
                }))
                continue
            logger.info(json.dumps({
                "event": "backup_selected", "entity_id": event.entity_id,
                "agent_id": candidate.agent_id,
                "reliability": candidate.reliability_score,
                "projected_completion": projected.isoformat(),
            }))
            # File the change-of-agent; on registry acknowledgement, good standing holds.
            logger.info(json.dumps({
                "event": "change_of_agent_filed", "entity_id": event.entity_id,
                "agent_id": candidate.agent_id, "jurisdiction": event.jurisdiction,
            }))
            return FailoverResult(
                entity_id=event.entity_id, jurisdiction=event.jurisdiction,
                state=FailoverState.RESTORED, selected_agent_id=candidate.agent_id,
                sla_deadline_utc=sla, projected_completion_utc=projected,
                reason="backup activated and change filed within SLA",
            )

        # No eligible candidate can finish in time — hand to humans, do not gamble.
        logger.warning(json.dumps({
            "event": "failover_escalated", "entity_id": event.entity_id,
            "jurisdiction": event.jurisdiction, "eligible": len(ranked),
        }))
        return FailoverResult(
            entity_id=event.entity_id, jurisdiction=event.jurisdiction,
            state=FailoverState.ESCALATED, sla_deadline_utc=sla,
            reason="no ranked candidate could file the change within the SLA",
        )

The engine is a leaf: it consumes one already-detected resignation, returns a typed FailoverResult, and makes no penalty judgement of its own. The safety_margin is what keeps the design honest — by projecting completion against an SLA set inside the statutory window rather than at it, a candidate that would file on the very last legal day is treated as a failure, not a success, and routed to a human while there is still slack in the real clock.

Configuration Reference

These parameters are dictated by each registry’s enforced resignation behaviour and by the reliability of the backup roster, never by branches in the failover loop.

Parameter Suggested value Legal / operational justification
safety_margin 0.35 Reserves 35% of the cure window so a filed change never races the statutory cliff; raise it for portals with slow acknowledgement.
statutory_deadline_utc per jurisdiction Read from the version-controlled matrix: DE/NY 30 days, TX 31 days (BOC § 5.203), CA effective on filing (Corp. Code § 1503).
activation_hours per candidate Lead time before a backup accepts the designation; the dominant term for tight windows.
filing_hours per candidate Time to prepare and submit the change-of-agent; portal-specific and business-hours bound in CA.
capacity_remaining per candidate A caseload-capped agent at zero is skipped, not queued, to avoid routing into a statutory caseload ceiling.
reliability_score [0, 1] Primary rank key; a marginally slower but far more reliable agent should win a near-cliff filing.

Failure Modes and Fallback Routing

Each fault maps onto the parent area’s error taxonomy — AgentUnavailableError, JurisdictionalMismatchError, and DeadlineBreachError — and the engine responds to each differently.

  1. Every eligible backup fails the SLA guard (deadline breach). The projected activation-plus-filing time overruns the internal SLA for all ranked candidates. The engine returns ESCALATED rather than committing the least-bad agent, and the obligation is diverted to Routing Compliance Tasks to Regional Legal-Ops Teams with the remaining margin attached so a human can expedite an emergency designation.
  2. A commercial-agent bulk resignation fans out to thousands of entities (agent unavailable, at scale). One resignation notice covers an entire book of business. Each affected entity must be materialized as its own ResignationEvent — never processed as one transaction — so a single slow filing cannot block the rest, and the backup roster’s capacity_remaining is decremented per commitment to avoid over-assigning one agent.
  3. The selected backup lacks authority in a jurisdiction it appeared to cover (jurisdictional mismatch). A stale roster lists an agent as authorized in a state where its license has lapsed. The eligibility filter keys on authorized_jurisdictions, and agent-authority webhooks must invalidate the cached roster before the next failover pass so a mismatched candidate is never selected.
  4. A resignation signal arrives after the window has effectively closed (deadline breach, terminal). A late-detected resignation leaves negative margin. The engine escalates immediately, and ingestion falls back to the last good-standing snapshot rather than recording a spurious restore; the severity of the now-open exposure is quantified downstream by Penalty Avoidance & Grace-Period Mapping.

Frequently Asked Questions

Why set an internal SLA inside the statutory window instead of filing right up to the deadline?

Because registry acknowledgement is not instantaneous and a filed change is not a confirmed change. Portals batch, queue, and occasionally reject submissions, so a change-of-agent filed on the last statutory day can still post after the entity has already dropped out of good standing. The safety_margin reserves a fixed slice of the cure window — 35% by default — so the engine treats a candidate that would finish on the real cliff as a failure and escalates while there is still slack to expedite. Penalties accrue on the statutory clock, not the filing clock.

Why rank on reliability before speed when the clock is tight?

A fast agent that fumbles the filing costs a full round trip you cannot afford near a cure boundary. The rank key sorts by reliability_score first and total lead time second, so a marginally slower agent with a strong accept-and-file history is preferred over a quick but flaky one — as long as the reliable agent still clears the SLA guard. Speed only decides ties among agents that are all trustworthy enough to commit.

Delaware, New York, and Texas all give roughly 30 days — why store the windows as data?

Because “roughly 30 days” hides the differences that decide a failover. Texas measures to the 31st day after filing under BOC § 5.203 and can involuntarily terminate for a sustained vacancy, while California under Corp. Code § 1503 makes a resignation effective essentially on filing, leaving almost no window at all. A version-controlled jurisdiction matrix lets the engine recompute the real deadline per state without a code change, and produces an auditable record of which statute version drove each SLA.