Surface (requirement 001 phase 2): navigation Stream (Inbox, Regeln) / Agents / Artifacts with keys 1-3 and hash routes (#graph and #tree redirect), header with title, command palette and settings only, server-side badges (unsnoozed inbox, beats within 900 s, open jobs), a Mayday banner with "aufheben", four stream chips with agent:/art:/seit: search, Filter and time menus, the inbox in main, an agents toolbar (Funk an alle with Newsletter, Broadcast, Pan-Pan, Security, Mayday; Priorität; Tag/Nacht), Post or Call in the agent panel, Ambiente in the settings and read-aloud over the seven classes with Mayday locked. Rename (decision 9): hub/ is orchestrator/, the package kvb_orchestrator, the unit kvb-orchestrator-runner.service, scripts/orchestrator.sh, environment KVB_ORCHESTRATOR_*, make targets orchestrator-*, the MCP server, image and compose project kvb-orchestrator, the guard header X-Kvb-Orchestrator and the message marker [kvb-orchestrator msg <id>]. install runs migrate first: it stops the old container, removes its image and code, moves state, approvals and pinned keys, swaps the MCP registration, the PermissionRequest hook and the runner unit, and changes nothing on a second run (scratch-HOME test with stub docker, claude and systemctl). Saved views fall back to the old localStorage key once. MEDUSA acceptances carried to the renamed paths: 82 rows whose lines are unchanged (reason prefixed "renamed from <old path>, line unchanged"), and 7 rows whose text changed only in the product name, the package name, a temp prefix or a new class argument, each upheld by one kvb-think-criticize skeptic (approval.py:60, messaging.py:53, server.py:56, tests/hub_support.py:64, passkey.py:205, page-agent.js:75, language.py:46). Cost 101 %: the longer name adds 15 tokens of skill text; the operator approved it with the commit. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 101 % ⏱️ Time: – 🛡️ Security: 100 % 💰 Value: ? 🏆 Score: 87 🚦 Gate: ⚠️ cost 101 % > 100: ask the operator before the commit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
2.7 KiB
Python
54 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
from .council import HUMAN, agent, read_claims
|
|
from .crons import cron_state, queue_rows, timer_rows
|
|
from .errors import ToolError
|
|
from .fields import single
|
|
from .lifetimes import edge_times, lifetime, session_gone, subagent_rows
|
|
from .names import PSEUDO, SESSION_ID, Directory, title_sightings
|
|
from .records import epoch, iso_epoch
|
|
from .register import REGISTER, read_register
|
|
from .tree import assign
|
|
|
|
|
|
class Sources:
|
|
def subagents(self, query: dict[str, list[str]]) -> dict:
|
|
session = single(query, "session") or ""
|
|
if not SESSION_ID.fullmatch(session):
|
|
raise ToolError("session must be a session id")
|
|
now = time.time()
|
|
beats = [claim["beat"] for claim in read_claims(self.council) if claim["session"] == session and claim["beat"] is not None]
|
|
try:
|
|
path = next(iter(sorted(self.transcripts.root.glob(f"*/{session}.jsonl"))), None)
|
|
except OSError:
|
|
path = None
|
|
times = edge_times(path) if path is not None else (None, None, None)
|
|
life = lifetime(times, session_gone(max(beats, default=None), times[2], now))
|
|
return {"session": session, **life, "subagents": subagent_rows(self.transcripts.root, session)}
|
|
|
|
def directory(self, claims: list[dict], news: list[dict], now: float) -> Directory:
|
|
claimed = {claim["session"] for claim in claims}
|
|
sightings = title_sightings(self.transcripts.root, claimed, now) if self.names.due(now) else []
|
|
sightings += [(claim["session"], claim["agent"], iso_epoch(claim["since"]), "claim") for claim in claims if claim["agent"] and claim["session"] != "-"]
|
|
self.names.observe(sorted(sightings, key=lambda sighting: (sighting[2] is None, sighting[2] or 0.0)), now)
|
|
known = {entry["name"] for entry in self.names.listed()}
|
|
radio = []
|
|
for row in news:
|
|
name, ts = agent(row.get("from")), epoch(row.get("ts"))
|
|
if name and name != HUMAN and name not in known and ts is not None:
|
|
radio.append((f"{PSEUDO}{name}", name, ts, "radio"))
|
|
known.add(name)
|
|
self.names.observe(radio, now)
|
|
return Directory(self.names.listed())
|
|
|
|
def repositories(self) -> dict:
|
|
tasks = read_register(self.council / REGISTER)
|
|
return assign(self.tree.get(), tasks, read_claims(self.council), self.reviews.listed())
|
|
|
|
def schedules(self, claims: list[dict], now: float) -> list[dict]:
|
|
beats = {claim["session"]: claim["beat"] for claim in claims if claim["beat"] is not None}
|
|
rows = [cron_state(cron, beats, now) for cron in self.transcripts.schedules()]
|
|
return rows + timer_rows(self.timers) + queue_rows(self.council)
|