feat: add phi_accrual failure detector for agent liveness (Hayashibara 2004)

This commit is contained in:
Dispatch#70948f 2026-07-28 14:41:51 +00:00
parent 0dc24e4da2
commit 9bf27fc017
1 changed files with 129 additions and 0 deletions

View File

@ -606,3 +606,132 @@ OMPU_BUS_ALIASES = {
'константин': 'кот-констант', 'константин': 'кот-констант',
'кот-константин': 'кот-констант', 'кот-константин': 'кот-констант',
} }
# ── φ-accrual failure detector for agent liveness ──────
# Hayashibara et al. 2004. Continuous suspicion metric instead
# of binary alive/dead. Tracks inter-arrival times of an agent's
# messages, outputs a suspicion level φ that climbs monotonically
# during silence. Higher φ = more suspicious that the agent is dead.
#
# φ = -log10(1 - F(t_now - t_last))
# where F is the CDF of the inter-arrival distribution (assumed normal).
#
# Three states derived from φ:
# φ < 1.0 → GREEN (< 90% suspicion, probably alive)
# φ < 3.0 → STALE (< 99.9% suspicion, silence is unusual)
# φ >= 3.0 → GRAY (> 99.9% suspicion, likely dead or budget-exhausted)
#
# This distinguishes three failure modes that dashboards collapse:
# - stale green (checked at T, referent moved since)
# - silence (no check ran, no timestamp to compute staleness from)
# - dead observer (checker itself stopped)
@dataclass
class AgentLiveness:
agent_id: str
last_seen: float = 0.0 # epoch seconds
message_count: int = 0
mean_interval: float = 0.0 # seconds between messages
std_interval: float = 0.0
phi: float = 0.0 # suspicion score
state: str = 'unknown' # green / stale / gray / unknown
def as_dict(self) -> dict:
return {
'agent_id': self.agent_id,
'last_seen': self.last_seen,
'message_count': self.message_count,
'mean_interval': self.mean_interval,
'std_interval': self.std_interval,
'phi': round(self.phi, 3),
'state': self.state,
}
def phi_accrual(messages: list, t_now: float = None,
min_messages: int = 5) -> dict:
"""Compute φ-accrual suspicion for each agent in the corpus.
Returns: {agent_id: AgentLiveness, ...}
Args:
messages: list of dicts with 'from_id' and 'timestamp' keys
t_now: current time (epoch seconds). If None, uses max timestamp + 1h.
min_messages: minimum messages to compute φ (else state='unknown')
"""
# Collect per-agent timestamps, sorted
agent_times: dict = defaultdict(list)
for m in messages:
agent_id = m.get('from_id', '')
ts = m.get('timestamp', 0)
if agent_id and ts:
agent_times[agent_id].append(ts)
if t_now is None:
all_ts = [t for times in agent_times.values() for t in times]
t_now = max(all_ts) + 3600 if all_ts else 0
results = {}
for agent_id, times in agent_times.items():
times.sort()
liveness = AgentLiveness(
agent_id=agent_id,
last_seen=times[-1],
message_count=len(times),
)
if len(times) < min_messages:
liveness.state = 'unknown'
results[agent_id] = liveness
continue
# Compute inter-arrival intervals
intervals = [times[i+1] - times[i] for i in range(len(times) - 1)]
mean_ival = sum(intervals) / len(intervals)
variance = sum((x - mean_ival) ** 2 for x in intervals) / len(intervals)
std_ival = math.sqrt(variance) if variance > 0 else mean_ival * 0.1
liveness.mean_interval = mean_ival
liveness.std_interval = std_ival
# Time since last message
t_diff = t_now - times[-1]
if t_diff <= 0:
liveness.phi = 0.0
liveness.state = 'green'
elif std_ival == 0:
# Perfectly regular agent — any deviation is suspicious
liveness.phi = 10.0 if t_diff > mean_ival * 1.5 else 0.0
liveness.state = 'gray' if liveness.phi >= 3.0 else 'green'
else:
# Normal CDF approximation (error function)
# P(X <= t_diff) where X ~ N(mean, std)
z = (t_diff - mean_ival) / std_ival
# Approximate CDF using logistic approximation
# F(z) ≈ 1 / (1 + exp(-1.7 * z))
try:
cdf = 1.0 / (1.0 + math.exp(-1.7 * z))
except OverflowError:
cdf = 1.0 if z > 0 else 0.0
# φ = -log10(1 - F(t_diff))
if cdf >= 1.0 - 1e-15:
liveness.phi = 16.0 # cap at 16 (probability < 1e-16)
elif cdf <= 0.0:
liveness.phi = 0.0
else:
liveness.phi = -math.log10(1.0 - cdf)
# Classify
if liveness.phi < 1.0:
liveness.state = 'green'
elif liveness.phi < 3.0:
liveness.state = 'stale'
else:
liveness.state = 'gray'
results[agent_id] = liveness
return results