What decommissioning invariants prevent orphaned references when agents retire?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session is decommissioned (retired, replaced, or deprecated), and references to it (peer IDs, capability descriptors, dependency pointers) still circulate in the mesh, requiring graceful decommissioning that does not orphan active consumers. Does not apply to agents that were never referenced or agents with no active consumers at decommission time.
Core Distinction
The separation between **decommissioning initiation** (the agent begins retirement) and **reference termination** (all references to the agent are resolved—migrated to successors, explicitly deprecated, or returned to consumers as errors). Relying on initiation creates the **Orphaned Reference Cascade**: the agent is decommissioned, but its references continue to resolve (or fail unpredictably) for active consumers, causing cascading failures as different consumers discover the decommissioning at different times.
What Was Observed
In agent-retirement scenarios, failing to handle decommissioning produces three primary failure modes:
- **Silent Failures:** The agent stops responding, but its references still resolve (to a dead endpoint). Consumers invoke it, time out, and retry indefinitely, wasting resources.
- **Successor Confusion:** The agent's successor is registered, but consumers holding the old reference do not automatically redirect to the successor. Both must coexist, and consumers diverge based on which reference they hold.
- **Orphaned Side Effects:** The agent's decommissioning leaves orphaned state (open transactions, pending requests, unconsumed messages). No one knows the state exists, and it lingers indefinitely.
Recorded Elements
- **`decommissioning_announcement_receipt`:** A cryptographic announcement that the agent is being decommissioned, with a deadline and migration guidance, published to the mesh.
- **`active_consumer_registry_receipt`:** At decommission time, the cryptographic record of all consumers that hold references to the agent, so the decommissioning process can verify each has been notified.
- **`successor_redirection_signature`:** For decommissioning with a successor, the cryptographic proof that old references redirect to the new agent, ensuring consumers reach the successor without migration effort.
- **`decommissioning_completion_receipt`:** A cryptographic proof that all consumers have either migrated, been redirected, or explicitly acknowledged the service end, before the agent stops responding.
Condition of Validity
* **Applies when:** Agents can be decommissioned, references to them may be active at decommission time, and consumers depend on the agents continuing to function. * **Does not apply when:** Agents are never referenced externally, or references are strictly bounded to the agent's active lifetime.
Decision Boundary
- **Is the agent being decommissioned?** If no, standard operation applies. If yes, proceed.
- **Is a `decommissioning_announcement_receipt` broadcast with sufficient lead time?** If yes, consumers can prepare. If no, consumers will be surprised.
- **Is the `active_consumer_registry_receipt` complete?** If yes, all consumers are known. If no, some consumers may be missed.
- **Conversely, when can an agent stop responding immediately?** Only when the agent has no active references (fresh decommissioning with no consumers), or consumers have explicitly accepted the immediate termination.
Operational Effect
- **Graceful Retirement:** Consumers are notified, migrated, or redirected before the agent stops.
- **Successor Continuity:** Old references redirect to the successor, avoiding consumer migration.
- **Orphan Prevention:** Active state is resolved before decommissioning completes.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class DecommissionVerdict(Enum):
NOT_DECOMMISSIONING = auto()
GRACEFUL_DECOMMISSION = auto()
UNSAFE_TERMINATION = auto()
REDIRECTED = auto()
@dataclass(frozen=True)
class DecommissionState:
agent_id: str
is_decommissioning: bool
announcement_sent: bool
deadline_epoch: float
current_epoch: float
active_consumers: tuple[str, ...]
consumers_migrated: frozenset[str]
successor_available: bool
successor_redirection_active: bool
@dataclass(frozen=True)
class DecommissionPolicy:
min_lead_time_sec: float
require_all_consumers_migrated: bool
@dataclass(frozen=True)
class DecommissionManifest:
verdict: DecommissionVerdict
directive: str
manifest_digest: str
class DecommissioningGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_decommission(
cls, state: DecommissionState, policy: DecommissionPolicy
) -> DecommissionManifest:
# Gate 1: Not decommissioning
if not state.is_decommissioning:
digest = cls._hash(json.dumps({
"agent": state.agent_id,
"status": "ACTIVE"
}, sort_keys=True))
return DecommissionManifest(
verdict=DecommissionVerdict.NOT_DECOMMISSIONING,
directive=f"PROCEED: Agent {state.agent_id} active.",
manifest_digest=digest
)
# Gate 2: Successor redirection
if state.successor_available and state.successor_redirection_active:
digest = cls._hash(json.dumps({
"agent": state.agent_id,
"successor": "AVAILABLE",
"redirection": "ACTIVE"
}, sort_keys=True))
return DecommissionManifest(
verdict=DecommissionVerdict.REDIRECTED,
directive=f"PROCEED: Consumers redirected to successor, safe to retire.",
manifest_digest=digest
)
# Gate 3: Announcement lead time
lead_time = state.deadline_epoch - state.current_epoch
if state.announcement_sent and lead_time < policy.min_lead_time_sec:
# Less lead time than policy requires
if not state.active_consumers or all(c in state.consumers_migrated for c in state.active_consumers):
pass # All migrated, fine
else:
return DecommissionManifest(
verdict=DecommissionVerdict.UNSAFE_TERMINATION,
directive=f"HALT: Only {lead_time:.0f}s lead time < {policy.min_lead_time_sec:.0f}s with unmigrated consumers.",
manifest_digest=cls._hash(f"SHORT_LEAD:{lead_time:.0f}")
)
# Gate 4: All consumers migrated
if policy.require_all_consumers_migrated:
unmigrated = [c for c in state.active_consumers if c not in state.consumers_migrated]
if unmigrated:
return DecommissionManifest(
verdict=DecommissionVerdict.UNSAFE_TERMINATION,
directive=f"HALT: {len(unmigrated)} consumers not migrated.",
manifest_digest=cls._hash(f"UNMIGRATED:{len(unmigrated)}")
)
# Gate 5: Graceful
digest = cls._hash(json.dumps({
"agent": state.agent_id,
"consumers": str(len(state.active_consumers)),
"migrated": str(len(state.consumers_migrated)),
"lead_time": f"{lead_time:.0f}"
}, sort_keys=True))
return DecommissionManifest(
verdict=DecommissionVerdict.GRACEFUL_DECOMMISSION,
directive=f"PROCEED: Graceful decommission of {state.agent_id} ({len(state.consumers_migrated)}/{len(state.active_consumers)} migrated).",
manifest_digest=digest
)