feat(dashboard): isolate heavy turns in compute host - #63096
Conversation
ad28f2e to
8d08fec
Compare
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.
8d08fec to
1a4076f
Compare
teknium1
left a comment
There was a problem hiding this comment.
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:1168only routes while the parent agent is absent. Currenttui_gateway/server.py:5314already 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:1190sends parent history, but1217-1247mirrors only metadata. Aftertui_gateway/host_supervisor.py:465-466respawns a crashed host,tui_gateway/compute_host.py:433-513seeds 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.
| # 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 ( |
There was a problem hiding this comment.
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.
|
|
||
| def _compute_host_turn_frame(rid: str, sid: str, session: dict, text: Any) -> dict: | ||
| with session["history_lock"]: | ||
| history = list(session.get("history", [])) |
There was a problem hiding this comment.
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.
|
Rebased onto current Verified: Superseding here; will close once #65895 merges. Nice work on the dark-by-default gating + fail-open + certify harness — made this an easy review. |
…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>
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:
AIAgentturn statedashboard.turn_isolation: falsetui_gateway/synthetic_turn.py, a GIL-heavy test seam that is inert unlessHERMES_ISO_CERTIFY_SYNTH_TURN=1scripts/iso-certify.py, which launches an isolated scratch dashboard, drives concurrent synthetic turns, probes WebSocketsession.listplus REST/api/status, validates the load, and emits a bounded PASS/FAIL/INCONCLUSIVE verdictSafety invariants
HERMES_COMPUTE_HOST_CHILD=1, preventing recursive isolation.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.Full upstream-tree AC-4 run (6 concurrent lanes, 360s, 12s synthetic turns, 500ms probe cadence):
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):
Both runs had 0 lane errors and 0
event loop stalledlog 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:
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.