feat: Tailscale graceful degradation — chain never fails

RouteCost.lean:
- latencyClass 4 = 'offline' (Tailscale down/unreachable)
- networkLatencyCost returns qOne for offline (maximum cost)
- Computation continues with local-only fallback

scale_space_solver.py:
- detect_tailscale(): returns available=False if not installed/running
- get_latency_class(): returns 4 (offline) when Tailscale unavailable
- latency_to_voltage/sigma(): map any class to FPGA parameters
- Chain never raises — offline is just another latency class

Verified:
- Tailscale up: 4 peers detected, latency classes assigned
- Tailscale down: returns class 4 (offline), computation continues
- Unknown IP: returns class 4 (offline), no crash
This commit is contained in:
Brandon Schneider 2026-05-28 19:19:14 -05:00
parent eeec672dc7
commit 5698d6e54b
2 changed files with 103 additions and 0 deletions

View file

@ -210,12 +210,18 @@ def fammMemoryBonus (a b : RouteNode) : Nat :=
- near (1): same cluster, 1-10ms → qEighth
- far (2): cross-network, 10-100ms → qQuarter
- derp (3): DERP relay, >100ms → qHalf
- offline (4): Tailscale down/unreachable → qOne (maximum cost)
When Tailscale doesn't exist or is down, the chain must not fail.
Offline nodes get latencyClass=4, which routes computation to local-only.
The cost is still computable — offline is just another latency class.
The latency class determines the FPGA voltage mode:
- local → σ₀ (1.2V, exact)
- near → σ₁ (1.0V, normal)
- far → σ₂ (0.8V, approximate)
- derp → σ₃ (0.6V, coarse)
- offline → σ₃ (0.6V, coarse, local-only fallback)
This is the "coursing agent" — latency shapes the computation. -/
def networkLatencyCost (a b : RouteNode) : Nat :=
@ -227,6 +233,7 @@ def networkLatencyCost (a b : RouteNode) : Nat :=
| 1 => qEighth -- near: 1-10ms
| 2 => qQuarter -- far: 10-100ms
| 3 => qHalf -- derp: >100ms (DERP relay)
| 4 => qOne -- offline: Tailscale down, maximum cost
| _ => qOne -- unknown: maximum cost
def weighted (weight component : Nat) : Nat :=

View file

@ -15,9 +15,105 @@ smoothing. At each scale σ, nodes whose pairwise cost is below σ·max_cost
are clustered together. The reduced problem is solved, then expanded back.
"""
import json
import math
import subprocess
from typing import Optional
# ── Tailscale Detection (graceful degradation) ──────────────────────────
_LATENCY_CLASSES = {
0: {'name': 'local', 'ms_max': 1, 'voltage': 1200, 'sigma': 0.0},
1: {'name': 'near', 'ms_max': 10, 'voltage': 1000, 'sigma': 0.25},
2: {'name': 'far', 'ms_max': 100, 'voltage': 800, 'sigma': 0.50},
3: {'name': 'derp', 'ms_max': 1000, 'voltage': 600, 'sigma': 1.0},
4: {'name': 'offline', 'ms_max': None, 'voltage': 600, 'sigma': 1.0},
}
def detect_tailscale() -> dict:
"""Detect Tailscale status. Returns dict with 'available', 'peers', 'latency_map'.
If Tailscale is not installed or not running, returns available=False
with empty peers and latency_map. The chain never fails.
"""
result = {
'available': False,
'peers': {},
'latency_map': {},
'derp_region': None,
}
# Check if tailscale binary exists
try:
proc = subprocess.run(
['tailscale', 'status', '--json'],
capture_output=True, text=True, timeout=5
)
if proc.returncode != 0:
return result # tailscale not running
except (FileNotFoundError, subprocess.TimeoutExpired):
return result # tailscale not installed
try:
status = json.loads(proc.stdout)
result['available'] = True
result['derp_region'] = status.get('CurrentTailnet', {}).get('Name')
for peer_id, peer in status.get('Peer', {}).items():
hostname = peer.get('HostName', peer_id)
tailscale_ip = peer.get('TailscaleIPs', [None])[0]
relay = peer.get('Relay', '')
latency = peer.get('CurAddr', '')
# Classify latency
if not peer.get('Online', False):
latency_class = 4 # offline
elif relay: # DERP relay
latency_class = 3 # derp
elif tailscale_ip:
latency_class = 1 # near (same tailnet)
else:
latency_class = 2 # far
result['peers'][hostname] = {
'ip': tailscale_ip,
'latency_class': latency_class,
'relay': relay,
'online': peer.get('Online', False),
}
if tailscale_ip:
result['latency_map'][tailscale_ip] = latency_class
except (json.JSONDecodeError, KeyError, TypeError):
pass # malformed status, return what we have
return result
def get_latency_class(node_ip: str, ts_status: Optional[dict] = None) -> int:
"""Get latency class for a node. Returns 4 (offline) if Tailscale unavailable.
The chain never fails offline is just another latency class.
"""
if ts_status is None:
ts_status = detect_tailscale()
if not ts_status['available']:
return 4 # offline — Tailscale not running
return ts_status['latency_map'].get(node_ip, 4) # default to offline
def latency_to_voltage(latency_class: int) -> int:
"""Map latency class to FPGA voltage in millivolts."""
return _LATENCY_CLASSES.get(latency_class, _LATENCY_CLASSES[4])['voltage']
def latency_to_sigma(latency_class: int) -> float:
"""Map latency class to scale space sigma."""
return _LATENCY_CLASSES.get(latency_class, _LATENCY_CLASSES[4])['sigma']
try:
import numpy as np
HAS_NUMPY = True