Skip to content

feat(dashboard): isolate heavy turns in compute host - #63096

Closed
Kyzcreig wants to merge 1 commit into
NousResearch:mainfrom
ANG-Ventures:feat/dashboard-turn-isolation-upstream
Closed

feat(dashboard): isolate heavy turns in compute host#63096
Kyzcreig wants to merge 1 commit into
NousResearch:mainfrom
ANG-Ventures:feat/dashboard-turn-isolation-upstream

Conversation

@Kyzcreig

@Kyzcreig Kyzcreig commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Related work

This is independent of #63082. That PR offloads blocking dashboard SessionDB handlers; this PR isolates GIL-heavy agent turns in a compute-host process. They touch different runtime boundaries and can be reviewed or merged in either order.

Problem

A dashboard/desktop chat turn runs the synchronous agent loop in a serving-process thread. Pure-Python work can hold the GIL long enough to stall unrelated WebSocket and REST requests in that same process. Thread-pool dispatch therefore does not provide a hard responsiveness boundary.

What changed

This ports the full fork mechanism hunk-by-hunk onto current upstream rather than cherry-picking the heavily diverged gateway file:

  • adds a persistent line-JSON compute-host child that owns live AIAgent turn state
  • adds a serving-process supervisor with bounded respawn and startup orphan reconciliation
  • streams RPC/event deltas from the child back through the existing session transport
  • routes interrupt, reload-MCP, compression, and host-owned slash mutations across the control protocol
  • mirrors read-only agent/session metadata back into the serving process without constructing a second agent
  • exits orphaned children when the PPID changes or becomes init
  • fails open to the historical inline turn path if compute-host dispatch fails synchronously
  • keeps the feature dark by default behind dashboard.turn_isolation: false
  • adds tui_gateway/synthetic_turn.py, a GIL-heavy test seam that is inert unless HERMES_ISO_CERTIFY_SYNTH_TURN=1
  • adds scripts/iso-certify.py, which launches an isolated scratch dashboard, drives concurrent synthetic turns, probes WebSocket session.list plus REST /api/status, validates the load, and emits a bounded PASS/FAIL/INCONCLUSIVE verdict

Safety invariants

  • The child sets HERMES_COMPUTE_HOST_CHILD=1, preventing recursive isolation.
  • The parent process remains authoritative for transport and request routing; the child owns live agent/history mutation only while isolation is active.
  • A failed pipe send does not emit a duplicate terminal turn error before the inline fallback runs.
  • The config gate defaults off at both the merged config schema and the raw runtime read site.
  • The certify harness uses a temporary HERMES_HOME, loopback-only spare port, and synthetic model seam; it does not touch the live dashboard or make model calls.

Verification

Latest upstream base: fc232f8ce648645b4df96d8be3fba2dc7cfd12d5.

scripts/run_tests.sh tests/tui_gateway tests/test_tui_gateway_server.py -q
684 passed, 0 failed

ruff check <all changed Python files>
All checks passed!

Full upstream-tree AC-4 run (6 concurrent lanes, 360s, 12s synthetic turns, 500ms probe cadence):

  • verdict: PASS
  • load: valid; 35 completed turns across all 6 lanes
  • median/min turn duration: 12.12s / 12.00s
  • worst-lane minimum deltas: 222
  • REST: 720 probes, p99 7.079ms, max 56.21ms
  • WebSocket: 720 probes, p99 1.688ms, max 13.73ms
  • combined serving p99: 7.079ms
  • probes over 1s / serving stalls: 0 / 0
  • lane errors / event-loop-stall log lines: 0 / 0

An immediate second 360s run measured 5.929ms serving p99 and 0 stalls, but correctly returned INCONCLUSIVE rather than PASS because one lane reported a zero minimum turn/delta sample. That result is not counted as certification; it demonstrates the harness's load-validity gate fails closed instead of producing a false green.

Short local port-validation A/B on the same checkout (2 concurrent lanes, 15s window, 3s GIL-heavy turns; this is not the full AC-4 duration):

mode load serving p99 stalls >1s verdict
isolation OFF valid, 7 turns 5656.9 ms 3 FAIL
isolation ON valid, 6 turns 24.85 ms 0 PASS

Both runs had 0 lane errors and 0 event loop stalled log lines. The isolated run completed 30 WebSocket and 30 REST probes with 0 over-threshold probes.

Prior fork measurements (not re-measured on upstream)

These are fork-side acceptance/deployment measurements from the original rollout, included for provenance and explicitly not represented as measurements from this upstream branch:

  • dark-code AC-4 p99: 13.87s
  • baseline: 3.33s
  • real fix pass 1: 0.41s
  • real fix pass 2: 0.44s
  • gate: <1s
  • live before arm: 55 stalls / 24h, worst 11.09s
  • live after arm: 0 stalls

Origin

This is the upstream extraction of the full mechanism required by fork PR #307, including the isolation core that fork PR #307 itself assumed had already landed there. The upstream port deliberately includes the core, delta/control protocol, PPID guard, fail-open fallback, synthetic seam, and reusable certify harness in one reviewable feature commit.

@alt-glitch alt-glitch added type/feature New feature or request comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state P3 Low — cosmetic, nice to have labels Jul 12, 2026
@Kyzcreig
Kyzcreig force-pushed the feat/dashboard-turn-isolation-upstream branch from ad28f2e to 8d08fec Compare July 12, 2026 09:37
Add the flag-gated compute-host supervisor, delta/control protocol, PPID orphan guard, inline fail-open path, synthetic GIL-heavy turn seam, and AC-4 certify harness.

Verified on current origin/main: 343 focused tests pass; Ruff and diff checks pass; 360s AC-4 run with six heavy lanes passes at 6.11ms serving p99 with zero stalls and valid load.
@Kyzcreig
Kyzcreig force-pushed the feat/dashboard-turn-isolation-upstream branch from 8d08fec to 1a4076f Compare July 12, 2026 09:44

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for carrying the process-boundary approach through transport, controls, and a dark config gate. The current-main premise is real: tui_gateway/ws.py:388 dispatches through a thread, and tui_gateway/server.py:8547 / 9103 run AIAgent.run_conversation() in the serving process.

Problems

  • tui_gateway/server.py:1168 only routes while the parent agent is absent. Current tui_gateway/server.py:5314 already schedules a deferred parent build at session creation, so normal first-prompt timing can bypass isolation; the opposite race can construct both parent and host agents.
  • tui_gateway/server.py:1190 sends parent history, but 1217-1247 mirrors only metadata. After tui_gateway/host_supervisor.py:465-466 respawns a crashed host, tui_gateway/compute_host.py:433-513 seeds it from stale parent history, losing host-only context for the next turn.

Suggested changes

  • Establish host ownership at session creation and suppress/cancel the parent build when isolation is enabled.
  • Recover transcript state from an authoritative persisted source or mirror it atomically; add crash-then-continuation E2E coverage.

Automated hermes-sweeper review.

Comment thread tui_gateway/server.py
# Phase 1 routes lazy/dashboard sessions whose live AIAgent has not been
# built inside the serving process. Already-built in-process sessions keep
# the historical path unless a prior isolated turn marked host ownership.
return bool(session.get("_compute_host_active")) or (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gate is race-dependent: session creation still schedules the deferred parent agent build (_schedule_agent_build on current main). If that build finishes before the first prompt, the session never isolates; if the prompt wins, it can leave both a parent and host agent alive. Establish ownership at session creation and suppress or cancel the parent build for isolated sessions.

Comment thread tui_gateway/server.py

def _compute_host_turn_frame(rid: str, sid: str, session: dict, text: Any) -> dict:
with session["history_lock"]:
history = list(session.get("history", []))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent history is never updated by the completion mirror, which only carries metadata/version. After a host crash and respawn, the replacement host is seeded from this stale frame history, so its next turn can lose all prior host-owned context. Recover from SessionDB or mirror the authoritative transcript atomically, and cover crash → respawn → continuation.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@OutThisLife

Copy link
Copy Markdown
Collaborator

Rebased onto current main in #65895 (your commits preserved via Co-authored-by). 624 commits stale but remarkably clean — one conflict in server.py's info dict where main added "approval_mode" and this PR reads tools/skills from the compute-host mirror; kept both (compatible — with isolation dark, mirror.get("tools") is absent → {}, same as main). Gate + tests auto-merged.

Verified: tests/tui_gateway + test_tui_gateway_server.py = 718 pass / 0 fail on the rebased tree. Kept separate from #65893 (SessionDB offload) as you intended — different runtime boundary.

Superseding here; will close once #65895 merges. Nice work on the dark-by-default gating + fail-open + certify harness — made this an easy review.

@OutThisLife

Copy link
Copy Markdown
Collaborator

Landed via #65895 (merged) — commits preserved with Co-authored-by. Closing as superseded. Thanks @Kyzcreig.

Kyzcreig added a commit to ANG-Ventures/hermes-agent that referenced this pull request Jul 26, 2026
…y + extract deltas (#436)

* feat(parity): migrate fork manifest to lifecycle schema v2

Classify all existing fork features, retain legacy v1 loading as fork-permanent, and reject invalid lifecycle values or absorbed entries without upstream provenance.\n\nVerified: 33 tests in tests/scripts/test_hermes_parity.py pass; lint-manifest exits 0.

* docs(sync): absorption sweep — register the absorbed turn-isolation family + extract deltas

First D2 absorption sweep over docs/sync/fork-features.json (card t_d82545b8).
Walked every entry against current origin/main by git, not by memory.

Registry:
- ADD "compute-host turn isolation" -> lifecycle=absorbed,
  upstream_ref=NousResearch#65895, absorbed_date=2026-07-16.
  tui_gateway/{compute_host,host_supervisor,synthetic_turn}.py and
  scripts/iso-certify.py were fork-authored (2026-07-05/07-12) and upstream
  re-authored + merged them as NousResearch#65895 (commit 7d27a31) carrying
  Co-authored-by: Kyzcreig; our PR NousResearch#63096 was closed superseded the same day.
  The family was previously unregistered while still carrying live fork deltas.
- CORRECT the telegram entry's provenance. It cited PR NousResearch#52844 as an absorption
  of our work; NousResearch#52844 is authored by teknium1 against third-party issue NousResearch#46621
  (reporter otopba) — D3 parallel invention, not D2 absorption. lifecycle stays
  absorbed (upstream's impl is canonical) but the why no longer implies we wrote it.
- The six upstream-intended entries keep upstream_ref=null: verified by exact-path
  match that no open Kyzcreig PR touches any of their fork_ext paths, so they are
  genuine backlog rather than in-flight.

Deltas (docs/sync/absorbed-delta-candidates.md, candidates only — no PR opened):
- synthetic_turn.py: upstream samples `now` BEFORE the CPU burn + sleep, then gates
  the delta cadence and resets last_delta on that stale value. Corrupts first-delta
  latency AND total delta count from one cause. Still live on current origin/main.
- iso-certify.py: probe-sampling adequacy is folded into serving_ok, so a broken
  probe client emits FAIL ("isolation broke serving") instead of INCONCLUSIVE.
  Upstream already grants load_valid its own INCONCLUSIVE leg for this exact reason.
- compute_host.py `source=` kwarg: NO-PR-NEEDED. AST of the callee shows upstream
  _make_agent has no `source` param (only platform_override); the fork kwarg binds
  solely because the fork widened its own signature. Porting it would TypeError.
  Fork-side follow-up, not an upstream bug.

Method note: deltas were derived against merge-base a7a696b, not from a raw
origin/main..fork/main diff — the fork is 405 commits behind, so a raw diff
attributes upstream-newer code (e.g. host_supervisor.py encoding= lines) to us.

Detection note: no Kyzcreig PR is merged upstream; `gh pr list --state merged`
returns []. Upstream absorbs fork work under maintainer PRs with a Co-authored-by
trailer, so `git log origin/main --grep=Kyzcreig` is the reliable absorption probe.

Verified:
- tests/scripts/test_hermes_parity.py -> 33 passed
- canary tests for the new absorbed entry:
  tests/tui_gateway/{test_iso_certify_seam,test_compute_host,test_compute_host_phase1}.py
  -> 22 passed
- lint-manifest --vacuous-ok -> exit 0 (schema/paths/nodeids all clean)
- both delta patches `git apply --check` clean onto current origin/main

---------

Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants