Skip to content

feat(host): keep sessions running in a background host you can attach to - #652

Draft
jiro-by-nori[bot] wants to merge 3 commits into
feat/multi-session-d-remotefrom
feat/multi-session-d2-host
Draft

jiro-by-nori[bot] wants to merge 3 commits into
feat/multi-session-d-remotefrom
feat/multi-session-d2-host

Conversation

@jiro-by-nori

@jiro-by-nori jiro-by-nori Bot commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

🤖 Generated with Nori

nori host now owns sessions in a background Unix process. Terminals can detach without stopping agents, and an empty host stays alive until nori host stop. nori host ls prints the flat ID / AGENT / STATUS / AGE / TITLE table, with TTY status colors, terminal-width title truncation, and full plain titles when piped.

Feature-gated nori attach opens the session list; nori attach <id> opens a conversation. Every host connection requires a private bearer token, including loopback; same-machine clients discover it automatically. The first terminal controls, later terminals watch, and Alt+T transfers control without disconnecting either terminal. Watcher prompts and permission answers are refused. Attachments reconnect after socket failure or a silent connection outage and resume from their applied sequence cursor without retrying ambiguous mutations. Reconnect validates the host instance before replay. Local attachments refresh discovered credentials once on an authentication rejection; a replaced host or failed refresh leaves a persistent explanation and asks the user to run nori attach again. Session-list selection keeps the short ID visible.

Stacked D2 on feat/multi-session-d-remote / #651. Draft; never merge. Existing single-session startup and the legacy --remote surface retain their behavior. Host attachment uses the existing multi_session flag.

Design and boundaries

  • Owner-only host directory/files, singleton OS lock, loopback listener, no unauthenticated /acp route on the host.
  • Controller ownership is checked at admission; slow agent work cannot block other clients from listing, taking control, or stopping the host. Already-admitted work may finish after handoff.
  • Left on an empty composer, the configured session-list hotkey (Alt+S by default), and /sessions open the list. Ctrl+R resumes a failed row under the same Nori ID. Ctrl+D detaches.
  • Replay is bounded and scoped to the current host process. Expiry is displayed rather than silently skipping history. Host restart starts an empty manager; cross-restart restoration and TLS termination are outside this workstream.

Validation

  • Regression tests were written first for host-instance identity, bounded local credential refresh, persistent restart notices, selected-row IDs, and Unicode-aware TTY truncation.
  • Formatting and scoped Clippy (--all-features --tests, -D warnings) pass.
  • Local suites run with --no-fail-fast: transport 114 passed, harness 432 passed / 1 ignored, TUI 1,472 passed / 4 ignored, CLI 69 passed, and all 8 D2 PTY tests passed. Full PTY: 193 passed / 9 ignored. Total: 2,280 passed, 0 failed, 14 ignored; the known cloud auth-error test passed on its first attempt.
  • Linux CI and cargo-deny passed on 9b65150833f4f4ed9232e6df381aea5d4d9fc587. Linux CI: 2,454 passed, 0 failed, 14 ignored, with formatting, build, and Clippy passing.

Original terminal proof and host/controller/watcher diagram.
Verifier-fix terminal proof: restart notice, truncated host table, and visible current-row ID.

Originating chat: https://demo.norisessions.com/chat/01a1df89-8a29-7764-9c1b-06afdcc05775

Share Nori with your team: https://www.npmjs.com/package/nori-skillsets

jiro-by-nori Bot pushed a commit that referenced this pull request Nov 15, 2026
@jiro-by-nori

jiro-by-nori Bot commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor Author

D2 visual verification for afe16c8c69befd6f6083a0708712880011358a80. These are actual terminal captures of the built CLI using the local mock ACP agent; no live provider credentials were used. The host kept running when both attached terminals detached and was stopped explicitly after capture.

Current PR head: b10c6121eb30a9cd2be7a45e589e6473f31a86a8. Its follow-up changes only listener-test process isolation and the corresponding documentation; the product behavior shown in these captures remains unchanged. The full TUI suite passes (1,463 passed, 2 ignored), and 1,000 concurrent listener-module repetitions pass (13,000 tests).

flowchart LR
    C["Terminal A · controller"] -->|"authenticated commands"| H["Background host"]
    W["Terminal B · watcher"] -->|"authenticated subscribe / Alt+T takeover"| H
    H -->|"sequenced replay + live events"| C
    H -->|"sequenced replay + live events"| W
    H --> M["SessionManager"]
    M --> A["ACP agent processes"]
    H --- O["One controller at admission; other terminals watch"]
Loading

nori host: detached process ready

nori host: detached process ready

Captured terminal text

nori host ls: flat table with TTY status color

nori host ls: flat table with TTY status color

Captured terminal text

Plain nori attach: session list

Plain nori attach: session list

Captured terminal text

Terminal A controls

Terminal A controls

Captured terminal text

Terminal B watches the same conversation, read-only

Terminal B watches the same conversation, read-only

Captured terminal text

After Alt+T in B: A remains attached and is told control moved

After Alt+T in B: A remains attached and is told control moved

Captured terminal text

Verification: 7 real-WebSocket auth/control tests, 6 CLI lifecycle/token tests, and all 6 D2 PTY scenarios passed, including permission refusal, detach/reattach, a dropped TCP link, and a silent blackhole with missed output replayed exactly once. TUI: 1,463 passed, 2 ignored. Workspace Clippy and nightly formatting passed. The known cloud auth-error flake passed its isolated retry after one failure in the broader run.

Capture notes: 120×40 terminals (host ls: 160×40), rendered from ANSI by Ghostty Web 0.4.0. The capture's final newline delimiter is removed for rendering so it does not scroll the first row off-screen; raw ANSI and plain text are retained beside each PNG. Proof media lives on nori/pr-proofs, outside the feature diff.

Final verification on b10c6121eb30a9cd2be7a45e589e6473f31a86a8: Linux CI and cargo-deny passed. Linux: 2,439 tests passed, 0 failed, 14 ignored; formatting, build, and Clippy passed. PR remains a draft against D1; nothing merged.

jiro-by-nori Bot pushed a commit that referenced this pull request Nov 15, 2026
@jiro-by-nori

jiro-by-nori Bot commented Nov 15, 2026

Copy link
Copy Markdown
Contributor Author

Independent verification of b10c6121eb30a9cd2be7a45e589e6473f31a86a8, stacked on D1. No product edits, PR-branch pushes, or merges.

Result: 0 blockers, 2 should-fix issues, 1 nit.

  1. Should-fix — host restart leaves existing attachments retrying permanently with a stale token. Start nori host --port 17420, attach and create a session, kill the host PID with kill -9, then start nori host --port 17420 again. A fresh attachment correctly opens an empty host. The original attachment remains RECONNECTING · sessions continue on the host after more than 35 seconds, without explaining that its credentials/replay are no longer valid. The old token gets HTTP 401. tui/src/attach/connection.rs:68–77 retries with the immutable initial endpoint/token and discards every connection error; cli/src/host/mod.rs:230 rotates the token. Cross-restart replay is explicitly out of scope; the defect is the endless, misleading recovery state. Surface a terminal auth/restart failure with a detach/reattach action; do not claim that the old sessions remain running.
  2. Should-fix — long titles break the flat terminal table. At 120×40, create a session with prompt/title D2 long title aligned columns should preserve the entire title without putting continuation text into the ID column, then run nori host ls on the TTY. TITLE starts at column 83 and is printed without a display-width limit; its continuation starts at column 1 beneath ID. cli/src/host/mod.rs:308 prints the whole title. Truncate or wrap within TITLE on a TTY while preserving the full plain piped value. TTY status colors and plain piped output otherwise pass.
  3. Nit — the current row's ID is hidden by its badge. Open a conversation, then Alt+S. The current row shows (curre… instead of its eight-character ID. The first column is fixed at eight characters (tui/src/attach/state.rs:87), and .current(...) makes the picker reserve the first cell for the ten-character (current) badge (tui-components/src/picker/render.rs:440–454). Keep the ID visible and place the marker elsewhere, such as TITLE.

The lifecycle/auth/controller/replay checks below pass. Child-row alignment is not covered end to end: D2 exposes no supported child-creation operation; HostService::_nori/session/new always uses parent: None. This is a coverage limit, not a claim that child rendering passed.

Test totals

Real mock ACP subprocesses, real host processes and sockets, and tmux-driven attached terminals. All requested suites ran with --no-fail-fast, MOCK_ACP_AGENT_BIN set, and test parallelism limited to 2. Counts include each package’s integration and documentation tests.

Suite Passed Failed Ignored
nori-acp-host 112 0 0
nori-harness 432 0 1
nori-tui 1464 0 4
nori-cli 66 0 0
tui-pty-e2e 191 0 9
Total 2265 0 14

Independent wire/lifecycle probe: 38 assertions passed, 0 failed (separate from Cargo totals). The known flaky test_cloud_mode_surfaces_auth_error_from_child_stderr passed on this run. The tmux checks additionally reproduced the three findings above.

Full test logs and reproduction scripts

Completed suite totals and verification summary

Completed suite totals and verification summary

Terminal text

Finding evidence

Should-fix: old attachment remains reconnecting after same-port restart

Should-fix: old attachment remains reconnecting after same-port restart

Terminal text

Fresh attachment sees the new empty host

Fresh attachment sees the new empty host

Terminal text

Should-fix: long TITLE wraps beneath ID at 120 columns

Should-fix: long TITLE wraps beneath ID at 120 columns

Terminal text

Nit: current badge consumes the current row ID

Nit: current badge consumes the current row ID

Terminal text

Wire proof

Tokens are deliberately omitted. These excerpts came from real connections to the running host:

GET /sessions, missing bearer -> HTTP 401
GET /sessions, wrong bearer   -> HTTP 401
GET /acp, valid bearer        -> HTTP 404
GET /acp, missing bearer      -> HTTP 404
GET /sessions, old token after restart -> HTTP 401
{"jsonrpc":"2.0","id":3,"error":{"code":-32016,"message":"watcher is read-only; take control before changing a session"}}

All three clients received contiguous prefixes 1..27. Reconnecting at cursor 27 replayed only [28,29,30,31,32,33,34]. No prompt mutations were retried. Raw redacted wire log.

Scenario captures

Every image is a 120×40 terminal rendered from actual tmux ANSI using Ghostty Web 0.4.0. DejaVu Sans Mono, Noto Sans Symbols and Noto Sans Symbols 2 were installed first. Raw ANSI is retained; only the final row delimiter was removed for rendering, preventing the header from scrolling off-screen. Summary panels show actual probe results, not simulated application screens.

Lifecycle and auth: start, duplicate, last close, stop, stale kill-9 recovery, occupied port and permissions

Host starts detached; duplicate start retains PID

Host starts detached; duplicate start retains PID

Terminal text

Independent lifecycle and auth assertions

Independent lifecycle and auth assertions

Terminal text

Last session is closed; attached host stays alive

Last session is closed; attached host stays alive

Terminal text

Host remains listable after last close and stops explicitly

Host remains listable after last close and stops explicitly

Terminal text

Attach, creation, detach and reattach history

Plain attach opens the empty session list

Plain attach opens the empty session list

Terminal text

Plain attach opens the nonempty session list

Plain attach opens the nonempty session list

Terminal text

Session created from attach; first client controls

Session created from attach; first client controls

Terminal text

Named reattach restores full short transcript once

Named reattach restores full short transcript once

Terminal text

Controller/watcher: three clients, refused mutations and takeover

Watcher prompt is refused with a clear read-only error

Watcher prompt is refused with a clear read-only error

Terminal text

Former controller is notified and continues watching

Former controller is notified and continues watching

Terminal text

Watcher cannot resolve pending permission

Watcher cannot resolve pending permission

Terminal text

Third client takes control and resolves the pending permission

Third client takes control and resolves the pending permission

Terminal text

Takeover during a streaming turn; new controller cancels

Takeover during a streaming turn; new controller cancels

Terminal text

Three-client wire assertions and exact replay cursors

Three-client wire assertions and exact replay cursors

Terminal text

Dropped TCP and silent loss

Abrupt TCP close: reconnecting after 0.3 seconds

Abrupt TCP close: reconnecting after 0.3 seconds

Terminal text

TCP recovery: missed prompt and response replay once

TCP recovery: missed prompt and response replay once

Terminal text

Sockets stay open without traffic: detected after 9.9 seconds

Sockets stay open without traffic: detected after 9.9 seconds

Terminal text

Silent-loss recovery: missed prompt and response replay once

Silent-loss recovery: missed prompt and response replay once

Terminal text

Recovered client takes control and submits another prompt

Recovered client takes control and submits another prompt

Terminal text

D1 Ctrl+R and single-session regression

Failed row selected for Ctrl+R

Failed row selected for Ctrl+R

Terminal text

Ctrl+R preserves the ID, second-row position and history

Ctrl+R preserves the ID, second-row position and history

Terminal text

Existing watcher receives the post-restart turn under the same ID

Existing watcher receives the post-restart turn under the same ID

Terminal text

multi_session=false: ordinary single-session prompt and answer

multi_session=false: ordinary single-session prompt and answer

Terminal text

Build and all testing used the exact head above. Product tree remained unchanged; proof files live only on nori/pr-proofs. PR remains draft, stacked on D1; nothing merged.

jiro-by-nori Bot pushed a commit that referenced this pull request Nov 15, 2026
@jiro-by-nori

jiro-by-nori Bot commented Nov 15, 2026

Copy link
Copy Markdown
Contributor Author

D2 verifier fixes — terminal proof

Captured from 9b65150833f4f4ed9232e6df381aea5d4d9fc587, fixing the verifier findings. All captures use the real CLI/TUI at 120×40 with an isolated local host and the repository's mock ACP agent.

Same-port restart

Started a host, attached, created a session, and retained an unsent draft. Stopped the host, then started a new empty host on the same port. The old attachment refreshed the local token once, detected the new host instance, and stopped reconnecting before replay. The persistent message says “Host restarted; sessions were lost. Re-run nori attach.” History and the draft remain visible; Ctrl+D detaches.

Attached TUI stopped after host restart

Terminal text · ANSI capture

Long title in the host table

nori host ls keeps the row within the terminal width and ends the title with an ellipsis. Piped output retains the complete title. Regression coverage includes wide characters, combining marks, emoji sequences, and expanded metadata columns.

Host listing with a truncated title

Terminal text · ANSI capture

Current session ID

The selected row keeps short ID 64de8892 visible; (current) appears in TITLE.

Attached session list with current row ID visible

Terminal text · ANSI capture

Verification

  • Test-first regressions reproduced the original failures before implementation; regression coverage also checks rejected refreshed credentials, unreadable local tokens, remote auth failure, unchanged tokens with a changed host identity, and restart messages beside a tall draft.
  • Nightly formatting, scoped Clippy with --all-features --tests -- -D warnings, and the CLI build passed.
  • All requested local suites ran with --no-fail-fast: ACP host 114, harness 432, TUI 1,472, CLI 69, full PTY 193 passed. Total: 2,280 passed, 0 failed, 14 ignored, including all 8 D2 PTY tests. The known cloud auth-error test passed on the first attempt.
  • One TUI invocation was interrupted by SIGTERM without a test failure; its complete rerun passed.

Linux CI and cargo-deny passed on 9b65150833f4f4ed9232e6df381aea5d4d9fc587. Linux CI reports 2,454 passed, 0 failed, 14 ignored, with formatting, build, and Clippy also passing.

The PR remains draft, based on feat/multi-session-d-remote. Nothing merged. Cross-restart session restoration remains outside D2; attachments now report that boundary explicitly.

@CSRessel
CSRessel added this pull request to stack #650 November 15, 2026 18:34
@jiro-by-nori jiro-by-nori Bot changed the title D2: background session host and authenticated terminal attachments feat(host): keep sessions running in a background host you can attach to Nov 15, 2026

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant