refactor(tui_gateway): table-driven groups passthrough handlers, pid/authority helpers, docstring compaction (group E)

This commit is contained in:
Teknium
2026-09-03 00:17:58 -07:00
parent 3d80006765
commit ae94886814
5 changed files with 158 additions and 222 deletions
+30 -41
View File
@@ -1,8 +1,5 @@
"""Persistent dashboard compute-host process.
The long-lived child that owns live AIAgent objects when ``dashboard.turn_isolation``
is enabled; frames are line-JSON over stdin/stdout.
"""
"""Persistent dashboard compute-host child: owns live AIAgent objects when
``dashboard.turn_isolation`` is enabled; frames are line-JSON over stdin/stdout."""
from __future__ import annotations
@@ -101,17 +98,15 @@ class ComputeHost:
def shutdown(self, *, reason: str = "shutdown", wait: float = 10.0) -> None:
"""Drain in-flight turns, then finalize every session.
Order matters: ``_finalize_session`` is a one-shot latch, so finalizing before
the drain would spend the flush's single chance mid-turn, fire
``on_session_end(interrupted=True)`` against a running session and release the
active-session lease under a live turn. ``_FLUSH_RESERVE_SECS`` (never more than
half of ``wait``) is withheld from the drain so the flush still runs when turns
outlast the window. Sessions whose turn is *still running* at the deadline are
excluded from the flush (``_executor.shutdown`` does not join them): finalizing
one mid-turn would leave it un-finalizable with its lease released, whereas
leaving it unfinalized keeps it recoverable. ``server._shutdown_sessions``
(atexit) may still re-finalize skipped sessions on the SIGTERM / stdin_closed
paths; the orphan path (``os._exit``) bypasses atexit.
Order matters: ``_finalize_session`` is a one-shot latch, so finalizing before the
drain would spend it mid-turn, fire ``on_session_end(interrupted=True)`` on a running
session and release its lease. ``_FLUSH_RESERVE_SECS`` (at most half of ``wait``) is
withheld from the drain so the flush still runs when turns outlast the window.
Sessions still running at the deadline are skipped (``_executor.shutdown`` does not
join them): finalizing mid-turn would leave them un-finalizable with the lease
released; unfinalized keeps them recoverable. ``server._shutdown_sessions`` (atexit)
may re-finalize skipped sessions on SIGTERM / stdin_closed; ``os._exit`` (orphan)
bypasses atexit.
"""
self._closed.set()
budget = max(0.0, wait)
@@ -130,7 +125,7 @@ class ComputeHost:
with self._turn_futures_lock:
live_sids = {sid for f, sid in self._turn_futures.items() if sid and not f.done()}
self.flush_all_sessions(reason=reason, skip_sids=live_sids)
self._executor.shutdown(wait=False, cancel_futures=True)
self.close()
def flush_all_sessions(
self, *, reason: str = "shutdown", skip_sids: Collection[str] | None = None) -> None:
@@ -160,12 +155,10 @@ class ComputeHost:
self.emit({"type": "shutdown.ack", "request_id": frame.get("request_id")})
# Explicit supervisor/test shutdown is a clean child-process close;
# SIGTERM and orphan paths are the durability flush paths.
self._closed.set()
self._executor.shutdown(wait=False, cancel_futures=True)
self.close()
def _track_turn_future(self, future: concurrent.futures.Future, sid: str) -> None:
"""Register an in-flight turn against its session; the done callback must pop
under the lock or the mapping grows for the host's life."""
"""Track an in-flight turn; the done callback pops it or the map grows forever."""
with self._turn_futures_lock:
self._turn_futures[future] = sid
future.add_done_callback(self._untrack_turn_future)
@@ -252,16 +245,13 @@ class ComputeHost:
if run_thread is not None and hasattr(run_thread, "join"):
run_thread.join()
with session["history_lock"]:
history_version = int(session.get("history_version", 0))
message_count = len(session.get("history") or [])
meta = _history_meta(session)
interrupted = bool(session.get("_turn_cancel_requested"))
session_key = str(session.get("session_key") or "")
session_info = server._session_info(session.get("agent"), session)
self._bump_progress()
self._reply(
"turn.end", sid, request_id, history_version=history_version,
session_key=session_key, message_count=message_count, interrupted=interrupted,
ended_ns=now_ns(), session_info=session_info, session_info_emitted=True)
"turn.end", sid, request_id, **meta, interrupted=interrupted, ended_ns=now_ns(),
session_info=session_info, session_info_emitted=True)
except Exception as exc:
with contextlib.suppress(Exception):
from tui_gateway import server
@@ -392,13 +382,6 @@ class ComputeHost:
_error(str(response["error"].get("message") or failure))
return None
return response
def _history_meta() -> dict[str, Any]:
"""Ack metadata read under ``history_lock`` (caller holds it)."""
return {
"session_key": str(session.get("session_key") or ""),
"history_version": int(session.get("history_version", 0)),
"message_count": len(session.get("history") or [])}
try:
from tui_gateway import server
route = MUTATOR_ROUTE_TABLE.get(route_name)
@@ -429,7 +412,7 @@ class ComputeHost:
if response is None:
return
with session["history_lock"]:
meta = _history_meta()
meta = _history_meta(session)
_ack(
result=response.get("result") or {}, **meta,
session_info=server._session_info(session.get("agent"), session))
@@ -438,11 +421,10 @@ class ComputeHost:
output = server._mirror_slash_side_effects(sid, session, command) if command else ""
with session["history_lock"]:
messages = server._history_to_messages(list(session.get("history") or []))
meta = _history_meta()
meta = _history_meta(session)
_ack(
output=output, session_key=meta["session_key"],
history_version=meta["history_version"], message_count=meta["message_count"],
messages=messages, session_info=server._session_info(session.get("agent"), session))
output=output, **meta, messages=messages,
session_info=server._session_info(session.get("agent"), session))
except Exception as exc:
if route_name in {"session.compress", "slash.compress"}:
# The compress mirror defers the context-engine boundary notification until
@@ -482,6 +464,14 @@ class ComputeHost:
os._exit(0)
def _history_meta(session: dict) -> dict[str, Any]:
"""Transcript identity for turn.end / control.ack frames; caller holds history_lock."""
return {
"session_key": str(session.get("session_key") or ""),
"history_version": int(session.get("history_version", 0)),
"message_count": len(session.get("history") or [])}
def _rss_mb(pid: int) -> float:
try:
out = subprocess.check_output(
@@ -547,8 +537,7 @@ def run_host(stdin: Any = None, stdout: Any = None) -> None:
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Dashboard compute-host process")
parser.parse_args(argv)
argparse.ArgumentParser(description="Dashboard compute-host process").parse_args(argv)
run_host()
return 0
+3 -6
View File
@@ -1,9 +1,6 @@
"""Compute-host (turn isolation) bridge: relay prompts/controls to the child process
and mirror its metadata/clarify/compress acks back into the session.
Bodies are rebound onto server.py's globals at install time (see
method_ctx.bind_module), so they reference server.py globals bare.
"""
"""Compute-host (turn isolation) bridge: relay prompts/controls to the child process and
mirror its metadata/clarify/compress acks back into the session. Bodies are rebound onto
server.py's globals at install time (method_ctx.bind_module), so they use them bare."""
from __future__ import annotations
+27 -46
View File
@@ -1,9 +1,6 @@
"""Supervisor for the dashboard compute-host child process.
When ``dashboard.turn_isolation`` is enabled, agent turns move behind one persistent
``python -m tui_gateway.compute_host`` child so compute-heavy agent threads do not
contend with the serving process' event loop for the same GIL.
"""
"""Supervisor for the dashboard compute-host child: with ``dashboard.turn_isolation``
agent turns run in one persistent ``python -m tui_gateway.compute_host`` child so heavy
agent threads do not contend with the serving process' event loop for the GIL."""
from __future__ import annotations
@@ -52,10 +49,9 @@ _CONTROL_REPLY_TYPES = frozenset({
def append_log_record(path: str | Path, record: str) -> None:
"""Append one log record using O_APPEND and exactly one os.write call."""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
Path(path).parent.mkdir(parents=True, exist_ok=True)
text = record if record.endswith("\n") else f"{record}\n"
fd = os.open(str(p), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
try:
os.write(fd, text.encode("utf-8", errors="replace"))
finally:
@@ -87,13 +83,21 @@ def _pid_alive(pid: int) -> bool:
return False
try:
os.kill(pid, 0)
return True
except Exception as exc:
return isinstance(exc, PermissionError)
return True
def _signal_pid(pid: int, sig: int, label: str) -> bool:
"""Send ``sig``; False when the pid is gone or the signal failed (logged)."""
try:
os.kill(pid, sig)
except ProcessLookupError:
return False
except PermissionError:
return True
except Exception:
logger.debug("failed to %s compute host pid=%s", label, pid, exc_info=True)
return False
return True
def _pid_command(pid: int) -> str:
@@ -155,10 +159,6 @@ class HostSupervisor:
proc = self._proc
return int(proc.pid or 0) if proc is not None else 0
@property
def hello(self) -> dict[str, Any]:
return dict(self._hello)
def is_running(self) -> bool:
proc = self._proc
return proc is not None and proc.poll() is None and not self._stopped_respawning
@@ -266,10 +266,9 @@ class HostSupervisor:
) -> dict:
"""Send a control frame; with ``wait`` block up to ``timeout`` for its ack.
``on_late_ack`` (only with ``wait``) keeps the request adoptable after the
waiter gives up: the host's eventual ``control.ack``/``control.error``/``error``
for this ``request_id`` fires the handler once instead of being dropped.
Bounded by ``_LATE_CONTROL_TTL_SECS`` / ``_LATE_CONTROL_MAX``.
``on_late_ack`` (only with ``wait``) keeps the request adoptable after the waiter
gives up: the host's eventual ``control.ack``/``control.error``/``error`` fires it
once instead of being dropped (bounded by ``_LATE_CONTROL_TTL_SECS``/``_MAX``).
"""
if route_name not in MUTATOR_ROUTE_TABLE:
raise ValueError(f"unclassified host mutator route: {route_name}")
@@ -459,18 +458,14 @@ class HostSupervisor:
with self._lock:
pending = self._pending_turns
self._pending_turns = {}
failure = {"reason": reason, "message": message}
for request_id, (sid, cb) in pending.items():
self.rpc_sink({
"jsonrpc": "2.0",
"method": "event",
"params": {
"type": "error", "session_id": sid,
"payload": {"message": message, "reason": reason}}})
"jsonrpc": "2.0", "method": "event",
"params": {"type": "error", "session_id": sid, "payload": dict(failure)}})
if cb is not None:
try:
cb({
"type": "turn.error", "sid": sid, "request_id": request_id,
"reason": reason, "message": message})
cb({"type": "turn.error", "sid": sid, "request_id": request_id, **failure})
except Exception:
logger.exception("compute host error callback failed")
# A crashed host never emits the late acks timed-out control waiters still
@@ -480,9 +475,7 @@ class HostSupervisor:
self._late_control_handlers = {}
for request_id, (_registered_at, handler) in late.items():
try:
handler({
"type": "control.error", "request_id": request_id, "reason": reason,
"message": message})
handler({"type": "control.error", "request_id": request_id, **failure})
except Exception:
logger.exception("compute host late control error handler failed")
@@ -512,34 +505,22 @@ class HostSupervisor:
_pid_matches_compute_host = staticmethod(is_compute_host_identity)
def _terminate_pid(self, pid: int, *, timeout: float = _SHUTDOWN_TIMEOUT_SECS) -> None:
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
return
except Exception:
logger.debug("failed to SIGTERM compute host pid=%s", pid, exc_info=True)
if not _signal_pid(pid, signal.SIGTERM, "SIGTERM"):
return
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if not _pid_alive(pid):
return
time.sleep(0.05)
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
return
except Exception:
logger.debug("failed to SIGKILL compute host pid=%s", pid, exc_info=True)
_signal_pid(pid, signal.SIGKILL, "SIGKILL")
def _terminate_process(self, proc: subprocess.Popen[str]) -> None:
if proc.poll() is not None:
return
try:
with contextlib.suppress(Exception):
proc.terminate()
proc.wait(timeout=_SHUTDOWN_TIMEOUT_SECS)
return
except Exception:
pass
with contextlib.suppress(Exception):
proc.kill()
with contextlib.suppress(Exception):
+31 -47
View File
@@ -35,6 +35,10 @@ _TERMINAL_STATUSES = ("deferred", "settled", "failed", "cancelled")
_LIVE_STATUSES = ("queued", "running", "stopping")
_STOPPABLE_STATUSES = ("queued", "running", "indeterminate", "deferred", "stopping")
_RETRYABLE_STATUSES = ("indeterminate", "deferred")
# (message, error_code) for a refreshed peer catalog whose digests drift from the dispatch.
_POLICY_DRIFT = (
"peer room execution policy needs reauthorization", "room_execution_policy_changed")
_CATALOG_DRIFT = ("peer room capabilities need reauthorization", "room_capability_catalog_changed")
def _hosted_room_turn_timeout_seconds() -> float:
@@ -85,10 +89,9 @@ class HostedRoomService:
self._load_stored_links()
except Exception as exc:
self._link_load_error = str(exc)
supplied_routes = dict(peer_routes or {})
supplied_clients = dict(peer_clients or {})
self.peer_routes.update(supplied_routes)
for key, route in supplied_routes.items():
for key, route in dict(peer_routes or {}).items():
self.peer_routes[key] = route
client = supplied_clients.get(key)
if client is None:
client = supplied_clients.get(route.target_install_id)
@@ -149,12 +152,13 @@ class HostedRoomService:
def _room(self, room_id: str) -> dict[str, Any]:
return hosted_rooms.room_state(self.db_path, room_id=room_id)
def _owned_room(self, room_id: str) -> dict[str, Any]:
room = self._room(room_id)
if str(room["authority_gateway_id"]) != hosted_rooms.local_authority_gateway_id():
def _owned_authority(self, room_id: str) -> tuple[str, int]:
"""(gateway_id, epoch) of a room this gateway owns; conflict error otherwise."""
gateway_id, epoch = _authority(self._room(room_id))
if gateway_id != hosted_rooms.local_authority_gateway_id():
raise hosted_rooms.AuthorityConflictError(
"This Group Chat is managed by another gateway.")
return room
return gateway_id, epoch
@contextlib.contextmanager
def _turn_lock(self, profile: str) -> Iterator[None]:
@@ -215,11 +219,9 @@ class HostedRoomService:
self.runtime.wakeup()
def revoke_room_routes(self, room_id: str) -> int:
"""Revoke and forget every scoped peer route for one room.
Remote revocation is the boundary: an unreachable target leaves the room
intact for retry rather than reporting a false disband with a live grant.
"""
"""Revoke and forget every scoped peer route for one room. Remote revocation is the
boundary: an unreachable target leaves the room intact for retry rather than
reporting a false disband with a live grant."""
with self._policy_lock:
routes = [(key, route) for key, route in self.peer_routes.items() if key[0] == room_id]
for key, route in routes:
@@ -335,11 +337,8 @@ class HostedRoomService:
route = self.peer_routes.get(key)
if route is None:
raise RuntimeError("peer room route is unavailable")
stored = next(
(
link for link in hosted_room_links.load_room_links(self.db_path)
if (link.room_id, link.member_id) == key),
None)
links = hosted_room_links.load_room_links(self.db_path)
stored = next((l for l in links if (l.room_id, l.member_id) == key), None)
if stored is None:
raise RuntimeError("peer room route cannot be renewed before persistence")
digests = {}
@@ -390,10 +389,6 @@ class HostedRoomService:
raise RuntimeError("hosted room replay cursor did not advance")
cursor = next_cursor
def _append_plan(self, room_id: str, plan: discussion.PublicationPlan) -> None:
for event in plan.events:
hosted_rooms.append_event(self.db_path, **event.append_kwargs(room_id))
def _policy_snapshot(self, room: Mapping[str, Any]) -> PolicySnapshot:
return self.policy_checkpoint.snapshot(
room_id=str(room["room_id"]), latest_seq=int(room["latest_seq"]))
@@ -417,7 +412,8 @@ class HostedRoomService:
room, task_events, plan, status=status, result=task.get("result"),
execution_generation=execution_generation if status == "deferred" else None,
local_profiles=local_profiles)
self._append_plan(room_id, publication)
for event in publication.events:
hosted_rooms.append_event(self.db_path, **event.append_kwargs(room_id))
changed = True
return changed
@@ -442,7 +438,7 @@ class HostedRoomService:
def prepare_room(self, binding: HostedRoomBinding) -> None:
with self._policy_lock:
room = self._room(binding.room_id)
snapshot = self._policy_snapshot(room)
snapshot = self._policy_snapshot(room) # sync() side effect feeds the publish below
if self._publish_terminal_tasks(room):
room = self._room(binding.room_id)
snapshot = self._policy_snapshot(room)
@@ -494,7 +490,7 @@ class HostedRoomService:
def send(self, *, room_id: str, event_id: str, payload: Any) -> dict[str, Any]:
normalized = discussion.validate_user_payload(payload)
gateway_id, epoch = _authority(self._owned_room(room_id))
gateway_id, epoch = self._owned_authority(room_id)
event = hosted_rooms.append_event(
self.db_path, room_id=room_id, event_id=event_id, kind="message.user",
actor={"kind": "user", "id": "desktop"}, payload=normalized,
@@ -508,11 +504,10 @@ class HostedRoomService:
def stop_room(
self, room_id: str, *, cancel_id: str, require_acknowledged: bool = False) -> int:
gateway_id, epoch = _authority(self._owned_room(room_id))
gateway_id, epoch = self._owned_authority(room_id)
hosted_rooms.request_room_stop(
self.db_path, room_id=room_id, cancel_id=cancel_id, expected_gateway_id=gateway_id,
expected_epoch=epoch)
cancelled = 0
pending = 0
with self._policy_lock:
tasks = {
@@ -523,9 +518,9 @@ class HostedRoomService:
str(task.get("cancel_id") or "") if task.get("status") == "stopping" else "")
result = self.runtime.cancel(
task["identity"], cancel_id=task_cancel_id or cancel_id)
cancelled += 1
if result["status"] == "stopping":
pending += 1
cancelled = len(tasks)
if require_acknowledged and pending:
raise RuntimeError("room work is still stopping; retry deletion after Stop completes")
self.runtime.wakeup()
@@ -533,11 +528,8 @@ class HostedRoomService:
def retry_room_task(self, room_id: str, *, task_id: str) -> dict[str, Any]:
"""Retry one uncertain or deferred task only after explicit user action."""
task = next(
(
candidate for candidate in self._list_tasks(room_id, _RETRYABLE_STATUSES)
if candidate["identity"].task_id == task_id),
None)
candidates = self._list_tasks(room_id, _RETRYABLE_STATUSES)
task = next((c for c in candidates if c["identity"].task_id == task_id), None)
if task is None:
raise driver.InvalidTaskTransitionError("no retryable room task matches task_id")
return self.runtime.retry_indeterminate(task["identity"])
@@ -601,8 +593,7 @@ class HostedRoomService:
if action_room_id == room_id)
return {
"running": runtime["running"],
"working": bool(
counts.get("running") or counts.get("queued") or counts.get("stopping")),
"working": any(counts.get(s) for s in _LIVE_STATUSES),
"blocked": room_id in runtime["blocked_rooms"]
or bool(counts.get("indeterminate") or counts.get("stopping")),
"counts": dict(counts),
@@ -622,13 +613,10 @@ class _RouteStatusPeerClient:
self._on_refreshed = on_refreshed
def _refresh_grant(self, kwargs: dict) -> dict:
"""Rotate an expiring grant before dispatch; return the kwargs to send.
Refresh failures only escalate to reauthorization when the peer says so or the
grant is already past its hard expiry; otherwise the original grant is tried
as-is. A refreshed catalog whose digests drift from the dispatch is a policy
change and is refused before any dispatch.
"""
"""Rotate an expiring grant before dispatch; return the kwargs to send. Refresh
failures escalate to reauthorization only when the peer says so or the grant is
past its hard expiry; otherwise the original grant is tried as-is. A refreshed
catalog whose digests drift from the dispatch is a policy change: refused."""
grant = kwargs["grant"]
if not room_grant_needs_dispatch_refresh(grant):
return kwargs
@@ -654,13 +642,9 @@ class _RouteStatusPeerClient:
refreshed_catalog = GatewayRoomCatalog.from_mapping(refreshed.get("catalog"))
drift = None
if refreshed_catalog.execution_policy.policy_digest != checked.execution_policy_digest:
drift = (
"peer room execution policy needs reauthorization",
"room_execution_policy_changed")
drift = _POLICY_DRIFT
elif refreshed_catalog.catalog_digest != checked.capability_digest:
drift = (
"peer room capabilities need reauthorization", "room_capability_catalog_changed"
)
drift = _CATALOG_DRIFT
if drift is not None:
self._on_reauthorization()
raise PeerRunsHTTPError(
+67 -82
View File
@@ -1,17 +1,14 @@
"""Hosted-room JSON-RPC contract.
These methods expose durable room identity, replay, and the process-owned
"""Hosted-room JSON-RPC contract: durable room identity, replay, and the process-owned
same-gateway Discussion driver. ``groups.capabilities`` keeps that boundary
machine-readable so older clients stay on the renderer-owned room path.
Handlers are rebound onto server.py's globals at install (see method_ctx.py), so
bodies see only server globals plus the names methods_bot_relay.register publishes;
module-private helpers reach them through keyword defaults. ``_room_method`` wraps
each handler with the shared service-lookup / error-code envelope.
"""
Handlers are rebound onto server.py's globals at install (method_ctx.py), so bodies see
only server globals plus what methods_bot_relay.register publishes; module-private
helpers reach them through keyword defaults. ``_room_method`` is the shared envelope."""
from .method_ctx import HandlerRegistry
import importlib
import os
import threading
@@ -181,11 +178,10 @@ def _room_method(
service_message: str = _DRIVER_UNAVAILABLE, db: bool = False):
"""Register ``fn`` under ``name`` with the shared hosted-room error envelope.
``service_code`` set: the live service is required and passed as a third argument;
when absent the handler fails with that code. ``db``: the default room db path is
passed as the next argument. ``room_code`` maps ``HostedRoomError`` (or only
``ReplicaError`` when ``replica_only``) to a 4xxx client error, attaching
``{"reason"}`` data when ``with_reason``; any other exception maps to ``code``.
``service_code``: the live service is required (else fail with that code) and passed
as a third argument; ``db``: the default room db path follows. ``room_code`` maps
``HostedRoomError`` (only ``ReplicaError`` when ``replica_only``) to a 4xxx client
error with ``{"reason"}`` data when ``with_reason``; anything else maps to ``code``.
"""
def dec(fn):
@@ -376,11 +372,8 @@ def _(rid, params: dict, db_path) -> dict:
"groups.create", code=5111, room_code=4110, service_code=4123,
service_message=_WORKER_UNAVAILABLE)
def _(rid, params: dict, service) -> dict:
"""Create a hosted room idempotently.
Required params: ``room_id``, ``name``, and ``members``. Authority is
derived from this gateway's stable install identity, never from the client.
"""
"""Create a hosted room idempotently; authority comes from this gateway's stable
install identity, never from the client."""
room = service.create_room(
room_id=params.get("room_id"), name=params.get("name"), members=params.get("members"))
return _ok(rid, {"room": room})
@@ -404,12 +397,8 @@ def _(rid, params: dict, db_path) -> dict:
"groups.send", code=5112, room_code=4111, service_code=4123, service_message=_WORKER_UNAVAILABLE
)
def _(rid, params: dict, service) -> dict:
"""Append one typed event to a hosted room idempotently.
Required params: ``room_id``, ``event_id``, and object ``payload``. Only
inert ``message.user`` events are accepted through this client-facing
method; the actor is server-owned rather than trusted from params.
"""
"""Append one typed event idempotently. Only inert ``message.user`` events are
accepted from clients; the actor is server-owned rather than trusted from params."""
from gateway.hosted_rooms import user_event_id
client_event_id = params.get("event_id")
event = service.send(
@@ -420,16 +409,6 @@ def _(rid, params: dict, service) -> dict:
})
@_room_method("groups.rename", code=5117, room_code=4117, db=True)
def _(rid, params: dict, db_path) -> dict:
"""Rename one hosted room atomically with its replay event."""
from gateway.hosted_rooms import rename_room
renamed = rename_room(
db_path, room_id=params.get("room_id"), event_id=params.get("event_id"),
name=params.get("name"))
return _ok(rid, {"room": renamed})
@_room_method(
"groups.disband", code=5114, room_code=4113, service_code=4123,
service_message=_WORKER_UNAVAILABLE)
@@ -500,70 +479,76 @@ def _(rid, params: dict, service) -> dict:
return _ok(rid, {"retried": True, "task": receipt})
@_room_method("groups.log", code=5113, room_code=4112, db=True)
def _(rid, params: dict, db_path) -> dict:
"""Return a monotonic room-log delta after ``since_seq``."""
from gateway.hosted_rooms import read_events
delta = read_events(
db_path, room_id=params.get("room_id"), since_seq=params.get("since_seq", 0),
limit=params.get("limit", 100), include_disbanded=params.get("include_disbanded") is True)
return _ok(rid, delta)
def _passthrough(
name: str, module: str, fn_name: str, doc: str, *, code: int, room_code: int,
params: tuple, replica_only: bool = False, wrap: str | None = None,
) -> None:
"""Register a method whose result is ``module.fn(db_path, **params)`` verbatim (or under
key ``wrap``). ``params`` items are ``key`` (-> ``params.get(key)``) or
``(key, extractor(params))``."""
@_room_method(
name, code=code, room_code=room_code, replica_only=replica_only,
with_reason=not replica_only, db=True)
def handler(rid, params_in: dict, db_path) -> dict:
fn = getattr(importlib.import_module(module), fn_name)
kwargs = {}
for spec in params:
if isinstance(spec, str):
kwargs[spec] = params_in.get(spec)
else:
kwargs[spec[0]] = spec[1](params_in)
result = fn(db_path, **kwargs)
return _ok(rid, {wrap: result} if wrap else result)
handler.__doc__ = doc
@_room_method(
"groups.replicate", code=5116, room_code=4116, replica_only=True, with_reason=False, db=True)
def _(rid, params: dict, db_path) -> dict:
"""Persist one authority-stamped replay page into the local replica store.
``page`` is the verbatim ``groups.log`` result read from the room's
authority gateway; ingest is idempotent and refuses sequence gaps and
authority-epoch regressions.
"""
from gateway.hosted_room_replicas import ingest_page
result = ingest_page(
db_path, room_id=params.get("room_id"), room_name=params.get("room_name"),
members=params.get("members"), page=params.get("page"))
return _ok(rid, result)
def _include_disbanded(params: dict) -> bool:
return params.get("include_disbanded") is True
@_room_method(
"groups.replica_state", code=5117, room_code=4117, replica_only=True, with_reason=False, db=True
)
def _(rid, params: dict, db_path) -> dict:
"""Report the local replica's coverage and authority lineage."""
from gateway.hosted_room_replicas import replica_state
return _ok(rid, replica_state(db_path, room_id=params.get("room_id")))
_passthrough(
"groups.rename", "gateway.hosted_rooms", "rename_room",
"""Rename one hosted room atomically with its replay event.""",
code=5117, room_code=4117, params=("room_id", "event_id", "name"), wrap="room")
_passthrough(
"groups.log", "gateway.hosted_rooms", "read_events",
"""Return a monotonic room-log delta after ``since_seq``.""",
code=5113, room_code=4112,
params=(
"room_id", ("since_seq", lambda p: p.get("since_seq", 0)),
("limit", lambda p: p.get("limit", 100)), ("include_disbanded", _include_disbanded)))
_passthrough(
"groups.replicate", "gateway.hosted_room_replicas", "ingest_page",
"""Persist one authority-stamped replay page (a verbatim ``groups.log`` result) into
the local replica store; idempotent, refuses sequence gaps and epoch regressions.""",
code=5116, room_code=4116, params=("room_id", "room_name", "members", "page"),
replica_only=True)
_passthrough(
"groups.replica_state", "gateway.hosted_room_replicas", "replica_state",
"""Report the local replica's coverage and authority lineage.""",
code=5117, room_code=4117, params=("room_id",), replica_only=True)
@_room_method("groups.promote", code=5118, room_code=4118, with_reason=False, db=True)
def _(rid, params: dict, db_path) -> dict:
"""Continue a replicated room on THIS gateway at ``epoch + 1``.
Requires ``confirm: true`` — the caller asserts the previous authority can
no longer commit (explicit user action; a lease/quorum driver later).
"""
"""Continue a replicated room on THIS gateway at ``epoch + 1``. Requires ``confirm:
true`` — the caller asserts the previous authority can no longer commit."""
from gateway.hosted_room_replicas import promote_replica
if params.get("confirm") is not True:
return _err(
rid, 4118,
"promotion requires confirm=true acknowledging the previous "
"authority can no longer commit")
result = promote_replica(
db_path, room_id=params.get("room_id"), reason=params.get("reason", "authority-unreachable")
)
return _ok(rid, result)
reason = params.get("reason", "authority-unreachable")
return _ok(rid, promote_replica(db_path, room_id=params.get("room_id"), reason=reason))
@_room_method(
"groups.demote", code=5119, room_code=4119, replica_only=True, with_reason=False, db=True)
def _(rid, params: dict, db_path) -> dict:
"""Fence this gateway's stale room authority against a proven newer epoch."""
from gateway.hosted_room_replicas import demote_room
result = demote_room(
db_path, room_id=params.get("room_id"),
observed_gateway_id=params.get("observed_gateway_id"),
observed_epoch=params.get("observed_epoch"))
return _ok(rid, result)
_passthrough(
"groups.demote", "gateway.hosted_room_replicas", "demote_room",
"""Fence this gateway's stale room authority against a proven newer epoch.""",
code=5119, room_code=4119, params=("room_id", "observed_gateway_id", "observed_epoch"),
replica_only=True)
def register(server) -> None: