Commit Graph

852 Commits

Author SHA1 Message Date
Teknium 8b01df963d feat: expose session-scoped subagent roster and live tail RPCs
Project live children and background units for shared TUI/Desktop consumers; read a bounded tail from the existing runtime transcript. Reuse existing queued steering semantics rather than claiming delivery. Projection adapted from PR #70899 with exact live session and transport ownership.

Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
2026-09-08 03:06:30 -07:00
Teknium b3014688f9 fix: isolate shared session membership and prune departed viewers 2026-09-07 22:25:12 -07:00
Ryan Tucker 68ed3ffd10 fix(tui_gateway): check steer authority by transport membership under fan-out
subagent.steer resolves authority by comparing the request's context-bound transport with the session's transport slot. Once a session mirrors to more than one client that slot holds a FanoutTransport, so the comparison fails for every client, the peer that commissioned the subagent included, and every steer is rejected. Fan-out without this check ships that regression, and no existing test catches it because the suite only exercises single-client sessions.

Authority now asks whether the request's transport is attached to the session, directly or through the fan-out. The single-client case is unchanged: a bare slot still compares by identity.

This widens authority. Any client attached to a mirrored session can steer that session's subagents, not only the peer that commissioned them. Narrowing it back to the commissioning peer requires recording that peer per subagent, which this change does not do. tests/tui_gateway/test_multi_client_fanout.py pins the commissioning peer's authority inside a fan-out, pins the widened case, and keeps a single-client control in which an unattached client is still refused.

The four browser.controller.* handlers gate on the session transport slot exactly as subagent.steer did, through one shared gate in the _controller_method decorator, and the conversion missed them. Once a session mirrors to more than one client the slot holds a FanoutTransport, which is identical to no peer's WSTransport, so browser.controller.register, .result, .heartbeat and .detach all answer "session is not owned by this transport" for every client, the peer that registered the controller included. Browser control is therefore unusable on any mirrored session. No existing test catches it because the browser-control suite only exercises single-client sessions.

All four now ask the same question steer asks: is the request's transport attached to this session, directly or through the fan-out. The single-client case is unchanged, because a bare slot still compares by identity.

Only registration widens. On .result, .heartbeat and .detach the broker's is_owner check sits below the session gate and compares controller.owner is owner against the transport recorded at attach time (gateway/browser_control_broker.py), so a mirrored peer that did not register the controller is still refused there, now with "controller is not owned by this transport" instead of the session message. browser.controller.register has no such check, so any client attached to a mirrored session may register a controller for it; the broker's principal lane keeps that inside one authenticated identity, and a second identity in the same lane hard-replaces the first. tests/tui_gateway/test_multi_client_fanout.py pins the registering peer's access inside a fan-out, pins the widened and broker-refused cases, and keeps a single-client control in which an unattached client is still refused on all three scope-gated handlers.
2026-09-07 22:25:12 -07:00
Ryan Tucker de25545dce fix(tui_gateway): fan session events out instead of rebinding the transport slot
A session held exactly one transport, and prompt.submit, session.resume, session.activate, and the queued-prompt drain all rebound that slot. A second client therefore took the stream away from the first: the earlier client stopped receiving the turn it was already rendering, and either client disconnecting parked the whole session on the drop sentinel.

FanoutTransport goes in the same slot and satisfies the same Transport protocol, so write_json and every other reader of the slot are unchanged. It delivers each frame to a snapshot of its peers, concurrently when more than one peer is attached and the caller is not on an event loop, and prunes any peer that returns False or raises. A dead client is dropped; a slow one costs the emitter at most one write timeout per frame rather than one per peer. Request/response RPCs are unaffected: they still answer on the request's context-bound transport, so a client only ever sees replies to its own calls.

The rebind sites become attach sites through _attach_session_transport, whose ladder keeps the single-client shape identical. The same object already in the slot is a no-op; an empty, stdio, or parked slot is taken outright; only the arrival of a second live client wraps both. The queued-prompt drain is included because it pinned the drained turn to the queuer and silenced everyone else. A non-peer newcomer such as stdio or the drop sentinel never displaces a live client, so an activate dispatched without a bound websocket cannot silence the socket that owns the session.

Disconnect detaches first. A session that retains another client keeps streaming and is neither parked nor reaped, and only the clientless ones follow the existing close_on_disconnect and park-sentinel path, so a single-client disconnect, the orphan reaper, and its grace window behave as before. _ws_session_is_orphaned is unchanged: it still asks whether the drop sentinel is in the slot, and a fan-out is never the sentinel, so a session that still has a peer is never reported as orphaned.

Attach performs no entitlement check: any authenticated peer may mirror any session.

The fan-out architecture follows the approach in #40822 by @OmarB97.

What the slot's later history forces. _close_sessions_for_transport drops the #83716 rebind-to-the-most-recent-surviving-viewer, which fan-out membership subsumes — a pop-out window is a peer, so a session that still shows in one is never returned as clientless — and keeps the #77129 revalidation before parking, now expressed as a liveness check under _session_transport_lock so it is race-free against attach and detach. _transport_is_live_peer defers its last answer to _transport_is_dead: a socket that already latched _closed is a departed client, and admitting it would keep a session out of both the park and the reap. _transport_is_dead also learns the fan-out: a FanoutTransport with no live peer is dead, so a session whose peers were all pruned by failed writes cannot outlive the TTL and LRU reapers. The upstream test that pinned the #83716 rebind, test_close_transport_rebinds_session_to_remaining_viewer, is re-expressed in fan-out terms: both windows attached, the pop-out closes, the session stays with the main window unparked and still receiving frames.
2026-09-07 22:25:12 -07:00
Teknium 04767e7aaa fix(display): distinguish estimated context from provider usage 2026-09-07 08:13:01 -07:00
Teknium 026e3e84ea fix: reject unavailable desktop profile and session targets 2026-09-07 06:26:22 -07:00
Teknium d87d7657c0 test(desktop): require successful isolated hook turns and exact calls 2026-09-07 04:55:48 -07:00
Teknium 3094cde513 fix(desktop): honor configured shell hooks for each chat profile
Wire the existing consent-aware, profile-keyed hook registrar at agent
construction, where the correct session home is already bound. This covers
serve and TUI agent construction without a startup-only registration or a
new helper that swallows registration failures.

Live isolated serve/WebSocket probes reproduce the missing registration on
base for both write_file and terminal, then verify each consented profile
blocks its configured tool while the unapproved profile still runs normally.
Repeated alpha construction does not duplicate hook callbacks.

Slim implementation of the agent-build placement proposed in #57020;
thanks also to the profile-scoped analysis in #102691.

Co-authored-by: grimmjoww578 <willies578@gmail.com>
2026-09-07 04:55:48 -07:00
Teknium ebf2473325 refactor: share CLI goal commands across interactive surfaces
Keep parsing, contracts, gates and persisted goal mutations in one dispatcher. Adapters retain authorization, rendering and scheduling; TUI drafting resolves the target session profile off the RPC reader. Document ACP as unsupported rather than implying a goal loop exists.
2026-09-07 00:51:41 -07:00
Teknium b0f87d7a7a fix(tui): publish session.control.update on every control mutation path; drop the Desktop-only heartbeat driver
- /goal and /loop are command.dispatch built-ins (never reach the slash worker), so the
  worker-branch publish never fired for the most common typed commands and the structured
  card stayed stale until the next turn. The publish helper now lives beside _snapshot_control
  in methods_session_control.py and is called from both slash paths plus the end of the turn
  tail — after the goal judge / loop tick evaluation, which mutate state AFTER message.complete.
- session.control skips the duplicate emit for dispatcher-backed actions (still exactly one
  update per mutation).
- Removed tui_gateway/desktop_heartbeat_driver.py, the entry/ws hooks and the hermes_cli lock
  rework: #104224 drives /heartbeat from the existing per-session poller for every TUI client.
- test_session_control.py: dropped presentation-shaped cases, added the publication invariants
  (both slash paths, unknown dispatch publishes nothing, real _run_prompt_submit turn publishes
  post-judge). 3/3 new tests fail on the contributor backend.
2026-09-06 09:21:45 -07:00
Jerry Gooch 45b7dabfb9 fix(desktop): run and surface Heartbeats reliably
(cherry picked from commit 77af78f1d8b7cb736d5d86e4fc7722449528a269)
2026-09-06 09:21:45 -07:00
Jerry Gooch 8cb2bcc8c1 feat(desktop): expose structured session controls
(cherry picked from commit 0dae0d9e7a36b57bc0489fbf0d6089de4f025d16)
2026-09-06 09:21:45 -07:00
Adolanium 9186e3ebc5 feat(desktop): session import view for foreign coding-agent transcripts
Browse the backend host's foreign CLI session logs, preview a bounded read-only
transcript, and continue a copy in Hermes under the selected profile. Reuses the
hermes_cli.foreign_sessions parsers and the portability validator/writer;
imports are transactional and deduplicated on the recorded origin.
2026-09-06 09:09:41 -07:00
kshitijk4poor 01ae7a5668 refactor(tui-gateway): _rebind_live_transport lives with the reattach contract in session_lifecycle
It is lifecycle logic (detached sentinel, reap cancel) and its siblings
_reattach_refusal / _cancel_ws_orphan_reap are already there; server.py is the
facade and gets no new definitions. Also trims the _claim_or_reuse_live comment
to point at where the reap cancel now happens.
2026-09-06 14:14:29 +05:30
kshitijk4poor c6cb7111a2 perf(tui-gateway): session.activate builds its payload outside _session_resume_lock
The salvaged fix correctly put the activate guard + transport rebind under the
process-wide resume lock, but it dragged _live_session_payload in with them. The
Desktop passes omit_messages=true (cheap), the Ink TUI does not: every TUI session
switch then read the full persisted history from the profile DB while holding the
lock that serializes every resume, disconnect and reap Timer.

Extract _rebind_live_transport from _live_session_payload; activate does guard +
rebind under the lock (the part that must be atomic with grace expiry) and builds
the payload after releasing it.
2026-09-06 14:14:29 +05:30
BearHuddleston 8513c5984c [verified] fix(gateway): fence orphan timers and reconnect interrupt claims
Re-scope #98106 onto the activity-based orphan policy from #100504. Keep timer ownership across callbacks and continuations, serialize reconnect paths against interrupt claims, and avoid recursive eager-resume locking. Leave cleanup polling armed when concurrent cold reuse is rejected.
2026-09-06 14:14:29 +05:30
Teknium 7a33369e81 simplify(compat): interrupt — drop _ThreadAwareEventProxy/_interrupt_event legacy alias, repoint 2 test files
No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt()
directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on
the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker.
2026-09-03 14:00:59 -07:00
Teknium c93ace77c2 simplify(compat): config/runtime_provider/plugins/commands/secrets_cli/kanban — drop 96 re-exports (incl. PEP 562 facades) + 3 aliases (get_pre_tool_call_directive/_block_message, get_telegram_handler_factories), repoint 56 callers + 50 test files 2026-09-03 14:00:17 -07:00
Teknium 14791b4d4e simplify(compat): approval — drop 43 facade re-exports + _command_detection_variants late-bind seam, repoint 30 callers + 46 test files
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/
human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined
names directly (module-attribute reads on tools.approval_context so patching the defining module
still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied,
_denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval.
approval_detection calls its own _command_detection_variants instead of late-binding through the facade.
2026-09-03 13:49:57 -07:00
Teknium 53db597201 simplify(compat): hermes_state — drop 81 re-exports + 3 registry aliases + 3 shims, repoint 45 callers + 60 test files
hermes_state.py: delete every '# noqa: F401 (re-exported...)' import block (hermes_state_common/errors/guard/
readpool/sessions/fts/dbfile/wal/repair/registry + agent.context_compressor _DB_PERSISTED_MARKER_KEY); keep
only the names hermes_state.py itself uses, without noqa.
hermes_state_registry.py: drop get_shared_session_db/release_shared_session_db/close_shared_session_dbs
aliases; every caller (gateway/, tools/, tui_gateway/, cron/, mcp_serve, run_agent, tests) now imports
acquire/release/close_all/release_or_close from hermes_state_registry.
hermes_state_titles.py: drop set_auto_title_if_empty shim (title_generator keeps its getattr fallback).
Re-remove shim-only names restored by 34abf954bd: latest_user_message_row_id (tests call
latest_message_row_id(key, role='user'); role-targeting assertions kept) and get_session_activity (tests
build the snapshot via agent.session_activity.build_activity_snapshot over db.get_session(sid)).
hermes_state_wal._log_once resolves its dedupe sets as module globals instead of via hermes_state;
hermes_state_repair helpers call module globals directly (tests patch hermes_state_repair.<name>).
Frozen updater surface untouched (update_cmd_maint imports only SessionDB from hermes_state).
2026-09-03 13:46:50 -07:00
Teknium fcbe4acbef simplify(compat): tools/mcp_tool — repoint 20 non-test callers to the defining mcp_tool_* siblings 2026-09-03 13:29:35 -07:00
Teknium eeb7671e69 simplify(compat): hermes_cli small facades — drop 7 re-exports/aliases (+relay_runtime alias module), repoint 12 callers/tests 2026-09-03 13:05:57 -07:00
Teknium d9141c425f review-fix(suppress-audit): run_shutdown/browser_tool_cdp/tui server — restore BASE exception semantics 2026-09-03 09:48:37 -07:00
Teknium e83816a4d1 review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
2026-09-03 09:44:26 -07:00
Teknium 198406297c refactor(tui): hug multi-line log/notice calls and trivial locals in server.py 2026-09-03 01:51:05 -07:00
Teknium de85dd5e39 refactor(tui): try/except-return -> contextlib.suppress fall-through in server.py; fix tour-bridge empty-record regression 2026-09-03 01:44:13 -07:00
Teknium 0ac25f6a11 refactor(tui): fold optional-key dict builds and guard ladders in server.py 2026-09-03 01:35:52 -07:00
Teknium fe42315882 refactor(tui): collapse defensive layers and redundant locals in server.py 2026-09-03 01:28:13 -07:00
Teknium c0362aedd9 refactor(tui): compact server.py docstrings/comments, keep every WHY 2026-09-03 01:21:09 -07:00
Teknium 19ff6082dc refactor(tui): inline single-caller server helpers, compact kwargs/dict literals 2026-09-03 01:15:20 -07:00
Teknium 042710a89d refactor(tui): server.py — keep _tour_request probing an existing empty session record 2026-09-03 00:40:35 -07:00
Teknium 11709ae585 refactor(tui): server.py — walrus/early-return collapses, session.update batching, final line joins 2026-09-03 00:26:25 -07:00
Teknium cec1f60eea refactor(tui): server.py — _live_session_agent_db helper, walrus/tuple-assign collapses, unify mcp_servers fallback 2026-09-03 00:11:27 -07:00
Teknium e4012388e5 refactor(tui): server.py — collapse small defensive ladders (approval choices, mcp names, config key write, drains) 2026-09-03 00:03:59 -07:00
Teknium 65d0be5ff4 refactor(tui): server.py — AST-neutral line joins, docstring/comment compaction 2026-09-02 23:57:46 -07:00
Teknium a87a3818af refactor(tui): server.py — _load_resume_transcript/_pet_cfg helpers, _CLI_EXEC_BLOCKED table, dead section banners removed, compact pet/spawn-tree/completion docstrings 2026-09-02 23:50:11 -07:00
Teknium fe08ed631b refactor(tui): server.py — _startup_system_prompt/_hydrate_session_cwd/_effective_approval_state/_turn_started_at helpers, _tui_notice, compact session-info/toolset/runtime docstrings 2026-09-02 23:41:53 -07:00
Teknium 4023fe4a9b refactor(tui): server.py — _parse_model_config/_is_routable_provider/_env_model_seed helpers, alias tables for service tier + tool progress, compact runtime-override docstrings 2026-09-02 23:34:28 -07:00
Teknium df373c0e35 refactor(tui): server.py — phase helpers for _start_agent_build, shared config path/expand helpers, _session_for_key, _EXPIRING_REQUESTS table, compact docstrings 2026-09-02 23:25:54 -07:00
Teknium 60fd0893c1 refactor(tui): server.py — one crash-record helper for both panic hooks, one WS-orphan setting resolver, packed _LONG_HANDLERS 2026-09-02 23:09:30 -07:00
Teknium 646aae4ba6 refactor(tui): move the projects RPC surface (workspaces, repo discovery, sidebar tree) out of server.py into methods_projects.py 2026-09-02 14:09:13 -07:00
Teknium a0c04543e0 refactor(tui): move billing/usage/subscription serializers out of server.py into billing_view.py 2026-09-02 14:09:13 -07:00
Teknium 99433742dc refactor(tui): extract the prompt turn into prompt_turn.py; compact methods_*/session_*/agent_callbacks; split _start_agent_build into scope/wiring helpers
- server.py 7663 -> 5319: _run_prompt_submit and its goal/loop/voice/scope
  phases move to tui_gateway/prompt_turn.py (bound via method_ctx.bind_module);
  _start_agent_build split into _bind/_release_build_profile_scopes,
  _deferred_build_agent_kwargs, _wire_session_agent, _start_session_services;
  _load_enabled_toolsets split (_enabled_mcp_server_names, _resolve_explicit_toolsets).
- methods_slash: _LIVE_SLASH_OUTPUT dispatch table; methods_tools: _SLASH_BUILTINS,
  _guarded; tool_progress: _PROGRESS_HANDLERS; methods_config_set:
  _REASONING_DISPLAY_WORDS; methods_voice: _VOICE_TOGGLE_ACTIONS.
- Unified: _denied_source (methods_session) replaces _WORKER_SOURCES in
  methods_profiles; _compress_live_with_feedback / _compute_host_slash shared by
  the slash mirror and /compress; _end_voice_chat shared by stop-phrase paths;
  _watcher_mtime_ns; _reaper_session_is_detached_idle; _notif_* helpers.
- Dead: _profile_dir_or_err, _resume_info, _slash_builtin_table (refs.py: 0 hits).
- acp_adapter/server.py: docstring compaction only (AST-identical).
- Every file semantically reviewed hunk-by-hunk for wire/log/lock/order parity.
2026-09-02 14:09:13 -07:00
Teknium 2eed968c87 refactor(tui): split session reaper/flush/heartbeat out of server.py; env knobs via _env.env_float/env_int; drop dead MCP shims 2026-09-02 14:09:13 -07:00
Teknium c6011955df refactor(tui): split compute-host bridge, session workdir/db-row, and session lifecycle out of server.py 2026-09-02 14:09:13 -07:00
Teknium 0a1057c709 refactor(tui): split live model-switch and compression logic out of server.py 2026-09-02 14:09:12 -07:00
Teknium 0095f908cd refactor(tui): split change watcher and tool-progress callbacks out of server.py; bind_module rebinds nested dispatch tables 2026-09-02 14:09:11 -07:00
Teknium c9312b6eee refactor(tui): split notification poller and prompt attachment staging out of server.py 2026-09-02 14:09:10 -07:00
Teknium 1134e5171d refactor(tui): split auto-continue, session history shaping and agent-callback wiring out of server.py 2026-09-02 14:09:08 -07:00
Teknium a0a41abf72 refactor(tui): move slash.exec mirror + completion helpers out of server.py 2026-09-02 14:09:06 -07:00