fix(mobile): wire plugin catalog and session-scoped subagent controls

This commit is contained in:
yilsem committed 2026-10-07 22:37:49 +03:00
1 parent 61d765e2df
commit 1acb5dd90f
27 files changed
+1573 -17

No files matched your search

+41 -2
View File
@@ -3572,14 +3572,53 @@ def _(rid, params: dict) -> dict:
return _ok(rid, {"paused": set_spawn_paused(paused)})
@method("subagent.list")
def _(rid, params: dict) -> dict:
from tui_gateway.mobile_subagents import owned_records, roster
_, err = _sess_nowait(params, rid)
if err:
return err
session_id = str(params.get("session_id") or "").strip()
transport, session = _current_session_steer_authority(session_id)
if transport is None or session is None:
return _err(rid, 4001, "session not found or not owned by this transport")
return _ok(rid, roster(owned_records(session_id, transport, session, include_lineage=True)))
@method("subagent.tail")
def _(rid, params: dict) -> dict:
from tui_gateway.mobile_subagents import owned_records, transcript_tail
_, err = _sess_nowait(params, rid)
if err:
return err
subagent_id = str(params.get("subagent_id") or "").strip()
if not subagent_id:
return _err(rid, 4000, "subagent_id required")
session_id = str(params.get("session_id") or "").strip()
transport, session = _current_session_steer_authority(session_id)
if transport is None or session is None:
return _err(rid, 4001, "session not found or not owned by this transport")
with _session_home_scope(session):
result = transcript_tail(owned_records(session_id, transport, session), subagent_id)
return _ok(rid, result)
@method("subagent.interrupt")
def _(rid, params: dict) -> dict:
from tools.delegate_tool import interrupt_subagent
from tui_gateway.mobile_subagents import owned_records
from agent.interrupt_compat import request_hard_interrupt
subagent_id = str(params.get("subagent_id") or "").strip()
if not subagent_id:
return _err(rid, 4000, "subagent_id required")
ok = interrupt_subagent(subagent_id)
session_id = str(params.get("session_id") or "").strip()
transport, session = _current_session_steer_authority(session_id)
if transport is None or session is None:
return _err(rid, 4001, "session not found or not owned by this transport")
record = next((r for r in owned_records(session_id, transport, session)
if r.get("subagent_id") == subagent_id), None)
child = record.get("agent") if record else None
ok = bool(request_hard_interrupt(child, "Interrupted via mobile")) if child is not None else False
return _ok(rid, {"found": ok, "subagent_id": subagent_id})
+55
View File
@@ -0,0 +1,55 @@
"""Read-only mobile subagent views, scoped to the invoking live session."""
import time
from pathlib import Path
def owned_records(session_id, transport, session, *, include_lineage=False):
from tools.delegate_tool import _active_subagents, _active_subagents_lock, _owns_subagent_record
if transport is None or session is None:
return []
with _active_subagents_lock:
records = [dict(row) for row in _active_subagents.values()]
return [row for row in records
if (row.get("owner_session_id") == session_id
and row.get("owner_transport") is transport
and row.get("owner_session_record") is session)
or (include_lineage and _owns_subagent_record(row, session.get("agent")))]
def roster(records):
fields = ("subagent_id", "parent_id", "depth", "goal", "delegation_id", "model",
"started_at", "status", "tool_count", "last_tool", "accepting_steer")
rows = []
for record in records:
row = {key: record[key] for key in fields if key in record}
started = record.get("started_at")
if isinstance(started, (int, float)):
row["elapsed_seconds"] = max(0, time.time() - started)
rows.append(row)
return {"subagents": rows, "delegations": []}
def transcript_tail(records, subagent_id):
result = {"subagent_id": subagent_id, "available": False, "text": "", "bytes_read": 0, "truncated": False}
record = next((r for r in records if r.get("subagent_id") == subagent_id), None)
if record is None:
return result
path = getattr(record.get("agent"), "_live_transcript_path", None)
if not isinstance(path, (str, Path)):
return result
try:
# Only the registry's own file is read; no client-supplied path is accepted.
from tools.delegation_live_log import live_transcript_root
path = Path(path)
root = live_transcript_root().resolve()
if path.is_symlink() or not path.resolve().is_relative_to(root):
return result
with path.open("rb") as stream:
size = stream.seek(0, 2)
stream.seek(max(0, size - 16384))
raw = stream.read(16384)
result.update(available=True, text=raw.decode("utf-8", errors="replace"),
bytes_read=len(raw), truncated=size > len(raw))
except OSError:
pass # A just-completed child's temporary transcript may already be gone.
return result