Skip to content

Live sessions: stale-session cleanup, crash recovery and hardening - #196

Merged
raiseCatError merged 3 commits into
devfrom
feature/131-live-session-hardening
Sep 28, 2026
Merged

raiseCatError merged 3 commits into
devfrom
feature/131-live-session-hardening

Conversation

@raiseCatError

Copy link
Copy Markdown
Owner

Closes #131

What it does

  • Safe updates: each protocol version gets its own service socket (nmshd.sock for v1, nmshd-v<N>.sock after that). An older service keeps its sessions running after an update. A newer frontend never archives, attaches to or ends sessions it cannot verify, and it says so at launch.
  • Crash and reboot recovery: at launch and when /resume opens, sessions that ended with no window attached are archived with a factual note. If the shell exited while detached, the note has its exit code and the detached output is kept. If the service stopped or the system restarted, the note says so. A spool is claimed by rename before archiving, so it is never archived twice. On failure the spool is kept.
  • Service death under a window: the frontend reports it, archives the transcript and exits. It never claims the session survived.
  • Limit: 16 live sessions per service (NMSH_MAX_SESSIONS). A new window over the limit falls back to in-process with a notice. Detached sessions are never ended to make room.
  • /resume shows how long each idle live session has been at its prompt.
  • Exiting service: a service that drops the connection while shutting down (closed / EPIPE / ECONNRESET) counts as having no sessions, so nmsh --list no longer errors in that window.
  • Docs: new "Live-session recovery, updates and limits" section with known limitations, plus a CHANGELOG entry.

Test stability work

Earlier failures in the full hardening file came from test synchronization, not product bugs. Each was reproduced first:

  • mouse after reattach: the test typed its next command while the fullscreen app still owned input. It now waits for the alt-screen exit and the composer.
  • another-protocol-version: the frontend was SIGKILLed before its journal had persisted. It now waits for the journal, and the assertion prints the archive state.
  • sandbox teardown: it now waits until no service accepts connections (a SIGKILLed service leaves a stale socket file) before removing the sandbox.
  • Ctrl+Z: the fixed 500 ms and 300 ms waits are replaced by polling the uniquely named sleep process state (+ foreground, not T).
  • scripts/pty-history-smoke.mjs is retired. It asserted retired UI text, ran against the real config and live service, and is covered by deterministic tests.

Verification

  • build and typecheck clean; git diff --check clean
  • full suite: 552 tests. Across the final full runs, the only failure was one miss by the unchanged, pre-existing suggestion-latency benchmark (tests/suggestionRanking.test.ts, a wall-clock p95 budget) under heavy parallel load
  • hardening + Ctrl+Z + service files run together (concurrent): 13 consecutive green runs across the fixes, 5 on the final code
  • new listLiveSessions test fails without the fix

Known limitations / follow-ups (not in this PR)

  • Processes inside a shell cannot survive the service being killed or a reboot. Only the transcript and captured output are kept.
  • A shell killed with SIGKILL leaves its small temporary nmsh-zdotdir-* behind. The pre-existing semantic helper also leaves nmsh-semantic-* directories. Both are pre-existing and tiny, and are left for a separate issue.

Work in progress for the hardening checkpoint; not yet fully verified.
Adds per-protocol-version service sockets, recovery that archives live
sessions that ended with no window attached (service crash, reboot, exit
while detached), a lost-connection note, a live-session limit, idle age
in /resume, and a real-process hardening test suite.
…y, docs

Treat a service that drops the connection while exiting (closed, EPIPE or
ECONNRESET) as having no live sessions, so listing sessions (nmsh --list)
no longer fails in that window.

Replace fixed test delays with condition-based synchronization: wait for
the fullscreen app to exit before typing after it, resend the mouse report
until the reattached app answers, wait for the sleep to own the terminal
before Ctrl+Z and to be running again before Ctrl+C, wait for the journal
before killing a frontend, and dispose sandboxes only once no service
accepts connections.

Retire scripts/pty-history-smoke.mjs: it asserted retired UI text, ran
against the real config and session service, and is covered by
deterministic tests. Document recovery, updates, limits and known
limitations.
Recovery could archive one detached session twice: a launch that lost the
spool claim still archived the linked journal from an empty spool, and
TranscriptStore.save does not serialize writers, so its journal-only
archive could overwrite the complete one. Kill Session read the spool
without claiming it and could race recovery the same way. A leftover spool
whose journal was already ended was replayed over the finished transcript.

Recovery and Kill Session now share finalizeLiveSession. It takes a
cross-process lock on the journal (a dead owner's lock is taken over),
claims the spool by rename, skips a journal that is already ended, and
puts the spool back on failure. A launch that lost the claim no longer
archives. Live journal checkpoints take the same lock, and claims
abandoned by a crashed launch are restored.

Reattaching to a fullscreen app suspended NMSh's own mouse and paste modes
but never re-sent the app's, so a real terminal stopped reporting the
mouse. The service now tracks the input modes the app set and the
reattaching frontend re-applies them.

Tests: deterministic concurrent-recovery, finalized-journal, crashed-claim,
spool-less and lock tests; a host-mode assertion on mouse reattach; the
fallback notice states the window cannot detach; job-control tests wait on
process state instead of fixed delays, with an escaped pgrep pattern and
sandbox cleanup on failure.
@raiseCatError
raiseCatError merged commit 4a3b358 into dev Sep 28, 2026
2 checks passed
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