Skip to content

Unify sleep/wake recovery with resumable shell and exact terminal restoration #21

Description

@myobie

Production report

A macOS laptop currently exhibits one resilience gap across three layers:

  1. after a long sleep, Fabric may require a manual fabric restart before peer traffic works again;
  2. fabric shell <peer> drops on a transient network/path loss;
  3. if the shell client is terminated on an exceptional/signal path, the caller terminal can remain in raw mode (ordinary keys then emit control behavior).

Treat these as one end-to-end session-lifecycle direction: the endpoint must recover, an interactive session must survive the recovery window, and the local terminal must always be restored when the client finally exits.

Current code mapping

  • macOS startup issue macOS daemon startup can hang indefinitely in CoreWLAN before control socket bind #20 bounded the optional CoreWLAN probe before endpoint bind, but post-start wake recovery still awaits Endpoint::network_change() without a deadline. The network-change health path also accepts local Endpoint::online() as recovery before proving a trusted peer is reachable.
  • Generic Fabric dials already implement offset/ACK replay, bounded reconnect backoff, a detached server-session TTL, and endpoint-generation retry. Built-in shell explicitly bypasses that layer and uses a raw one-shot fabric/shell/0 stream, so one iroh stream loss tears down the PTY.
  • RawModeGuard relies on ordinary Rust Drop plus crossterm::disable_raw_mode(). A process-directed signal can bypass Drop, and generic cooked-mode restoration does not preserve the exact pre-shell termios state.

Required behavior

Sleep/wake and daemon path

  • A wake/network transition never leaves the recovery loop stuck indefinitely.
  • Recovery proves usable peer reachability, not only local endpoint relay-online state.
  • Repeated failures escalate through bounded network-change/reconnect attempts to the existing rate-limited endpoint recycle path, without a manual daemon restart.
  • Existing service identity, peer trust, sync state, and endpoint identity remain intact.

Shell continuity

  • A shell PTY is bound to a logical resumable session, not to one transient iroh attach.
  • Transient path loss keeps the local shell command alive, visibly reports reconnecting/resumed state on stderr, buffers/replays unacknowledged bytes exactly once, and preserves the same remote PTY/process.
  • Reconnect uses bounded exponential backoff and a finite server detached-session lifetime. If the session expired or cannot resume, the client exits clearly rather than silently creating a second shell.
  • Trust and default-deny allow_shell behavior remain unchanged.

Caller terminal safety

  • Capture the exact pre-shell terminal state before enabling raw mode.
  • Restore it on normal remote exit, protocol/transport error, panic unwind, and handled termination/hangup/quit signals; suspend/continue semantics must not strand raw mode.
  • Signal handling restores the terminal and then preserves expected process exit/signal behavior.

Acceptance evidence

  • Deterministic two-node real-iroh shell test: run a uniquely identified remote PTY, force a transport detach/endpoint recycle, send input during the outage, resume, and prove the same PTY/process plus exactly-once ordered output.
  • Expired-session test: detach beyond the configured TTL and prove a visible terminal-safe failure with no replacement PTY.
  • Unix pseudoterminal subprocess tests: compare termios before/after normal exit, connection error, SIGTERM/SIGHUP/SIGQUIT, and suspend/continue where supported.
  • Bounded recovery tests with injected hung/failed network-change and peer probes, plus a documented macOS sleep/wake acceptance recipe (pmset sleepnow or lid sleep, wake on a changed/renewed path, no manual restart).
  • README architecture/runbook and CHANGELOG describe the unified semantics and limits.

Non-goals

  • Do not persist a shell across a Fabric daemon process restart.
  • Do not add tmux-like durable history or silently launch a replacement shell after resume expiry.
  • Do not weaken the peer allow-list or daemon-global shell opt-in.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions