feat(orchestrator): S5.3 agent states, Status menu and 3D faces

Every Agents view marks each agent as working, alive, sleeping, suspending, unreachable or dead with an animated symbol; a Status menu hides states and switches the 3D faces, kept through a reload. The hub's container sees neither ~/.claude/sessions nor host processes, so the host timer kvb-orchestrator-sessions.timer runs client/sessions.py every 15 seconds while the page polls: it reads the session records without following symlinks, only regular files up to 64 KiB, keeps name, status and statusUpdatedAt within type and length bounds, checks the pid's process start, and writes agent-sessions.json atomically. The hub ignores a snapshot that is missing, malformed, older than five minutes or dated in the future. make orchestrator-install enables the timer and orchestrator-check reports it.

🎯 Fix: 90 %
💥 Break: 5 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-11 07:44:02 +00:00
1 parent 57ab29eb0d
commit fdf8722fc1
24 files changed
+798 -13

No files matched your search

+19
View File
@@ -169,6 +169,25 @@ def night_mode(council: Path) -> str:
return mode if mode in MODES else "day"
def night_parts(council: Path) -> dict[str, str]:
"""Args:
council: the council directory with night.json.
Returns:
Per session the part that differs from night_mode: drain or suspend
for one set alone, day for the active and the resumed sessions while
a mode holds.
"""
data = read_json(council / "night.json")
if not isinstance(data, dict):
return {}
resumed = data.get("resumed", data.get("awake"))
working = [data.get("active"), *(resumed if isinstance(resumed, list) else [])]
parts = {sid: "day" for sid in working if isinstance(sid, str)} if night_mode(council) != "day" else {}
alone = data.get("parts")
return {**parts, **({sid: mode for sid, mode in alone.items() if mode in MODES} if isinstance(alone, dict) else {})}
def silence_of(council: Path, now: float) -> dict | None:
data = read_json(council / "radio-silence.json")
if not isinstance(data, dict):
+3
View File
@@ -25,6 +25,7 @@ from .mcp import Mcp
from .messaging import Messaging
from .names import Names
from .offices import Offices
from .presence import SNAPSHOT
from .redirects import Redirects
from .requesting import Requesting
from .reviewing import Reviewing
@@ -85,6 +86,7 @@ class Hub(Mcp, Reviewing, Asking, Messaging, Choosing, Approving, Denying, Reque
names: Names
watch: Watch
tree: Tree
agent_sessions: Path
model: Model
timers: Path
settings: Settings
@@ -132,6 +134,7 @@ def open_hub(council: Path, pages: Path, public_port: int, state: Path, host_sta
redirects=redirects,
watch=Watch(),
tree=Tree(file=places.tree_file, heartbeat=state / TREE_HEARTBEAT, lock=threading.Lock(), cached=None, stamp=None),
agent_sessions=state / SNAPSHOT,
model=model,
timers=places.timers,
council=council,
+87
View File
@@ -0,0 +1,87 @@
from __future__ import annotations
from pathlib import Path
from .council import HUMAN, MODES, agent
from .records import epoch, read_json
STATES = ("sleeping", "suspending", "alive", "working", "dead", "unreachable")
STALE = 15 * 60
UNANSWERED = 10 * 60
SNAPSHOT = "agent-sessions.json"
SNAPSHOT_STALE = 5 * 60
def read_snapshot(path: Path, now: float) -> dict[str, dict]:
"""Args:
path: the snapshot client/sessions.py writes on the host.
now: the moment to judge at.
Returns:
Its session records by session id; empty when it is missing,
malformed, written more than SNAPSHOT_STALE ago or dated in the
future.
"""
data = read_json(path)
if not isinstance(data, dict) or not isinstance(data.get("sessions"), dict):
return {}
written = data.get("written")
if isinstance(written, bool) or not isinstance(written, int | float) or not 0 <= now - written <= SNAPSHOT_STALE:
return {}
return {sid: record for sid, record in data["sessions"].items() if isinstance(record, dict)}
def unanswered(news: list[dict], now: float) -> list[tuple[str, float]]:
"""Returns:
(callee name, call time) of every call older than UNANSWERED that its
callee never receipted.
"""
receipts = {(agent(row.get("from")), round(epoch(row.get("call")) or 0, 3)) for row in news if row.get("class") == "receipt"}
late = []
for row in news:
ts = epoch(row.get("ts"))
if row.get("class") != "call" or ts is None or now - ts < UNANSWERED or not isinstance(row.get("to"), list):
continue
late += [(name, ts) for name in map(agent, row["to"]) if name and (name, round(ts, 3)) not in receipts]
return late
def presence(sessions: dict[str, dict], parts: dict[str, str], mode: str, claims: list[dict], news: list[dict], now: float) -> dict[str, str]:
"""Args:
sessions: the session records of read_snapshot; running says
whether a record's process still runs.
parts: night_parts, a session's own part over the mode.
mode: night_mode, drain, suspend or day.
claims: the council claims; a session without a record shows alive
or dead by its freshest beat.
news: the radio rows, for calls without a receipt.
now: the moment to judge at.
Returns:
One of STATES per session id (decision 59).
"""
late = unanswered(news, now)
beats: dict[str, list[float]] = {}
names: dict[str, set[str]] = {}
for claim in claims:
if claim["session"] != "-" and claim["agent"] != HUMAN:
beats.setdefault(claim["session"], []).append(claim["beat"] or 0)
names.setdefault(claim["session"], set()).add(claim["agent"])
states = {sid: "alive" if now - max(beat) < STALE else "dead" for sid, beat in beats.items() if sid not in sessions}
for sid, record in sessions.items():
if record.get("running") is not True:
states[sid] = "dead"
continue
busy = record.get("status") == "busy"
stamp = record.get("statusUpdatedAt")
updated = (epoch(stamp / 1000) if isinstance(stamp, int | float) and not isinstance(stamp, bool) else None) or 0
called = names.get(sid, set()) | {agent(record.get("name"))}
if (busy and now - updated > STALE) or any(name in called and ts > updated for name, ts in late):
states[sid] = "unreachable"
continue
part = parts.get(sid, mode)
if part in MODES:
states[sid] = "suspending" if busy else "sleeping"
else:
states[sid] = "working" if busy else "alive"
return states
+4
View File
@@ -8,6 +8,7 @@ from .council import (
NEWS,
grants,
night_mode,
night_parts,
radio_rows,
read_claims,
sessions,
@@ -22,6 +23,7 @@ from .jobs import JOB_OPEN
from .messaging import message_row
from .orbits import TRACK_STEPS, track
from .phrasing import phrase
from .presence import presence, read_snapshot
from .records import epoch, iso_epoch, read_jsonl
from .register import (
AGENT_ORDER,
@@ -138,6 +140,8 @@ class View:
"claims": claims,
"grants": grant_rows,
"night": night_mode(self.council),
"night_parts": night_parts(self.council),
"presence": presence(read_snapshot(self.agent_sessions, now), night_parts(self.council), night_mode(self.council), claims, news, now),
"silence": silence_of(self.council, now),
"graph": drawn,
}