Skip to content

feat(tui): detach running turns into background - #68375

Open
JasperKallfelz wants to merge 10 commits into
NousResearch:mainfrom
JasperKallfelz:feat/detach-running-turn-upstream
Open

feat(tui): detach running turns into background#68375
JasperKallfelz wants to merge 10 commits into
NousResearch:mainfrom
JasperKallfelz:feat/detach-running-turn-upstream

Conversation

@JasperKallfelz

@JasperKallfelz JasperKallfelz commented Jul 21, 2026

Copy link
Copy Markdown

Summary

  • make bare /background and /detach in the Ink TUI detach the exact in-flight foreground turn and activate a fresh live session
  • keep the source AIAgent, task, history, tool state, prompt cache, and durable transcript authoritative—no cancellation, replay, or duplicated side effects
  • retain and deliver completion/error/interrupt state to the fresh owner exactly once, including reconnect and active-session-cap failure handling
  • route clarify, approval, sudo, secret, and terminal input requests from hidden local or compute-host turns through an ownership-checked FIFO
  • make replacement builds, disconnect/reconnect, reaping, and profile-scoped active-session leases race-safe

Prompted /background <prompt>, /bg <prompt>, and /btw <prompt> keep their existing isolated-agent behavior. Bare /bg and /btw keep their previous usage behavior.

UX

While a normal TUI turn is running:

/background

or:

/detach

opens a fresh live chat immediately while the exact original turn continues in its source session. Its terminal result is reported in the fresh chat, and the source transcript remains resumable.

If max_concurrent_sessions has no free slot, detach fails cleanly and leaves the original foreground turn unchanged.

Implementation notes

  • Detach changes presentation/completion ownership only; it never moves or reconstructs agent execution state.
  • Detached source dispatchers remain teardown-protected through their final notification drain, independently of result consumption.
  • Compute-host terminal settlement raises a request-owned barrier at the final quiescent snapshot, retains it through successful turn.end or replacement turn.error serialization, defers pollers/idle-gated controls, and makes an already accepted successor wait through the terminal handoff instead of being dropped.
  • Compute-host agent creation, _init_session, slash-worker setup, and minimal fallback construction share the remote profile's HERMES_HOME and SessionDB from the start.
  • Interactive requests are queued per presentation session and event kind, deduplicated by request ID, and responses are routed back to the real source—including through the compute-host control pipe.
  • Replacement-session profile/cwd/routing metadata is installed before lazy construction, and late builders cannot attach or leak into removed sessions.
  • Active-session leases retain their claim-time registry paths so cross-profile release/transfer cannot affect the ambient profile.
  • Remote-profile branches keep DB, model/config context, agent, slash worker, session metadata, and lease ownership in the source profile from initialization onward.
  • Prompt response and Ctrl+C dismissal paths are request-ID guarded; late acknowledgements cannot erase newer same-kind prompts or overwrite a different prompt kind's waiting status.
  • Compute-host turn dispatch, busy-input interrupts, and session.interrupt are serialized through a per-session dispatch lock and bound to per-host wire request IDs, so a delayed interrupt or a late/duplicated terminal frame can never stop or settle a successor turn.
  • Compute-host turn admissions are reserved at wire order; real overlapping turns are rejected synchronously and idle-gated controls cannot overtake an already admitted turn.
  • In-flight provider interrupts are counted (_interrupt_call_count), so an explicit blocking Stop and a busy-input interrupt both defer the next prompt until the provider call returns, then drain the queued prompt exactly once.
  • Every fresh-work claim that flips running=True for a completion notification — the live poller, the poller's shutdown drain, and the post-turn drain — atomically clears stale _turn_cancel_requested/turn_settled state, so a Stop from a finished turn cannot cancel the notification turn after message.start was emitted (which lost the event and left message.start without a terminal message.complete).

Test plan

  • scripts/run_tests.sh tests/test_tui_gateway_server.py tests/tui_gateway/test_compute_host_phase1.py -- -q — 466 passed, 0 failed
  • broad TUI-gateway suite scripts/run_tests.sh tests/tui_gateway/ tests/test_tui_gateway_server.py — 848 passed, 0 failed (37 files)
  • deterministic race regression tests incl. test_post_turn_drain_clears_stale_stop_before_notification_turn (verified red without the fix, green with it)
  • 20x race loop over both focused files — 20/20 runs, 466 passed each
  • full Ink Vitest suite (env -u NODE_ENV) — 1,237 passed, 4 skipped; Ink typecheck
  • Desktop typecheck (env -u NODE_ENV) — clean; Desktop Vitest failures are pre-existing on the base commit (identical 106 failures with and without this diff, unrelated store/theme suites)
  • Python py_compile
  • git diff --check
  • independent adversarial read-only review (Claude Opus) — verdict READY, no Blocker/High findings

@JasperKallfelz
JasperKallfelz marked this pull request as draft July 21, 2026 04:11
@alt-glitch alt-glitch added type/feature New feature or request comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/desktop Electron desktop app (apps/desktop/*) area/sessions Session lifecycle, resume, persistence, history P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 21, 2026
@JasperKallfelz
JasperKallfelz marked this pull request as ready for review July 21, 2026 05:45
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the detailed detachment design and race coverage. The feature remains needed: current Ink still renders bare /background as usage text (ui-tui/src/app/slash/commands/session.ts:80-88) and has no /detach command.

Problems

  • Blocking: the replacement-session path eagerly acquires an active-session lease in the tui_gateway/server.py creation hunk. Current main deliberately claims this lease only when prompt.submit starts a real turn (tui_gateway/server.py:521-545, 11163-11164). The behavioral test at tests/test_tui_gateway_server.py:43-83 verifies that idle session.create calls do not consume the shared cap. Restoring eager acquisition would reintroduce the idle desktop/reconnect session starvation fixed by e35c2f6049633d3b6d897d63e637fc63b1b8b8c6.

Suggested changes

  • Preserve first-turn lease acquisition for normal create/resume. Model detach capacity as a dedicated admission/reservation transition, with behavior coverage for an idle replacement not holding a slot.

This is an automated hermes-sweeper review.

@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:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 30, 2026
Keep the exact source turn running while Ink activates a fresh live session. Retain terminal results, proxy interactive requests across local and compute-host execution, and harden lifecycle cleanup and profile-scoped session leases.
Wait for compute-host successor turns before terminal settlement, guard prompt overlays against late response acknowledgements, and claim active-session leases in the owning profile registry.
Guard Ctrl-C prompt dismissal against stale overlays, preserve cross-kind prompt status across late acknowledgements, and keep remote-profile branch DB, agent, session, and lease ownership aligned.
Raise a request-owned barrier at the final quiescent snapshot so notification pollers and overlapping parent turns cannot claim the child session until turn.end is on the wire.
Keep the remote HERMES_HOME active through session initialization and fallback construction, pass profile_home before slash-worker setup, and use the profile SessionDB consistently.
Wait through the request-owned terminal barrier instead of dropping an already accepted successor, block idle-gated controls from invalidating the terminal snapshot, and retain the barrier through replacement error serialization.
Serialize compute-host turn dispatch, busy interrupts, and session.interrupt
through a per-session dispatch lock bound to per-host wire request IDs, so a
delayed or duplicated interrupt/terminal frame can never stop or settle a
successor turn. Reserve compute-host turn admissions at wire order, gate
idle-only controls against in-flight admissions, and serialize terminal
settlement with successor handoff.

Count in-flight provider interrupts (_interrupt_call_count) so an explicit
blocking Stop and a busy-input interrupt both defer the next prompt until the
provider call returns, then drain the queued prompt exactly once.

Clear stale turn cancellation state (_turn_cancel_requested, turn_settled)
atomically at every fresh-work claim: the live notification poller, the
poller's shutdown drain, and the post-turn notification drain. Previously the
post-turn drain could claim a completion event with running=True while a Stop
flag from the finished turn was still set, so the nested prompt dispatch
dropped the event as cancelled-before-start after message.start was emitted -
losing the notification and leaving message.start without a terminal
message.complete.

Add deterministic regression tests for each interleaving, including
Stop-during-turn followed by a post-turn drain claim.
@JasperKallfelz
JasperKallfelz force-pushed the feat/detach-running-turn-upstream branch from 20d083c to fc80421 Compare August 2, 2026 21:02
@JasperKallfelz

JasperKallfelz commented Aug 2, 2026

Copy link
Copy Markdown
Author

@saphira Thanks — good catch. I rebased this onto current main and changed detach admission to reserve capacity atomically before any replacement/task mutation. Idle create/resume/replacement sessions now stay lease-free until actual work begins, with profile-local, serialized admission and rollback-safe cleanup across prompt, compute-host, notification, and detach paths. I also added race and failure-path coverage (including concurrent last-slot detach, registry corruption, release retries, compute-host handoff/close, and notification/Kanban rollback). The focused gateway/lease suites are green locally (691 passed, 1 skipped). I synced the branch again with the latest main; the PR is now conflict-free.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/desktop Electron desktop app (apps/desktop/*) comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit 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-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.

3 participants