Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

persistent-nvim

A nvim instance that survives terminal close, can't crash your system, and any terminal can attach to. Built on systemd + abduco.

Why

Two problems this solves.

Editor state is tied to the terminal. Close the alacritty/foot/kitty window and you lose your buffers, splits, undo history, scroll position. Every reopen pays the LSP-startup cost again.

Heavy LSPs can OOM the whole system. A Kotlin Multiplatform project with kotlin-language-server can spawn 10+ gradle daemons during classpath resolution, each grabbing 1–4 GiB. Combined with browsers, electron apps, and assorted Wayland gunk, you cross 23 GiB physical and the kernel OOM-killer picks something — often the wrong thing. We hit this twice in a row, both times the kernel killed Claude Code instead of the actual gradle daemons (because gradle had a lower oom_score_adj).

The fix has two parts:

  1. Decouple nvim from the terminal. abduco hosts nvim; any terminal can abduco attach to see it.
  2. Cap nvim's memory at the cgroup level. systemd enforces a hard 2 GiB limit. If nvim crosses it, systemd stops the whole service cleanly with a logged failure — no silent kernel SIGKILL, no half-dead state, no system-wide thrashing.

Architecture

┌──────────────────────────────────────────────────────┐
│  systemd user service   (lifecycle + memory cap)     │
│  ┌────────────────────────────────────────────────┐  │
│  │  abduco session "editor"                       │  │
│  │  (persistent PTY for the TUI)                  │  │
│  │  ┌──────────────────────────────────────────┐  │  │
│  │  │  nvim --listen $XDG_RUNTIME_DIR/nvim.sock │  │  │
│  │  └──────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘
       ▲                                    ▲
       │ abduco attach                        │ neovim-remote (nvr)
       │                                    │
   nvim-attach                          nvim-show <path>
   (user, in any terminal)              (Claude / scripts)

Three commands cover everything:

Command Purpose
nvim-attach User reattaches a terminal to the running nvim. Detach with Ctrl-\ (abduco's default) — nvim survives.
nvim-show <abs-path> Open file(s) in the running nvim. For Claude / scripts. Fails loudly if service isn't up; never auto-spawns a ghost.
systemctl --user {start,stop,restart,status} nvim.service Lifecycle. Logs via journalctl --user -u nvim.service.

Install

Dependencies: nvim, abduco, systemd (user units enabled), neovim-remote (for nvr).

# Get nvr if missing
uv tool install neovim-remote      # or: pipx install neovim-remote

# Clone and install
git clone https://github.com/<you>/persistent-nvim ~/Codes/persistent-nvim
cd ~/Codes/persistent-nvim
./install.sh

install.sh symlinks nvim.service into ~/.config/systemd/user/, the three scripts into ~/.local/bin/, then enables and starts the service. Symlinks (not copies), so git pull propagates changes immediately.

After install: open any terminal, run nvim-attach, you're in.

Memory cap design

The unit file enforces four overlapping guarantees. All four matter; removing any one weakens the rest.

MemoryAccounting=yes
MemoryMax=2G                  # cgroup ceiling, kernel-enforced
MemorySwapMax=0               # no swap allowed
OOMPolicy=stop                # cgroup-OOM → systemd stops service cleanly
OOMScoreAdjust=-500           # system-wide OOM picks something else first
Setting What it prevents
MemoryMax=2G nvim ballooning to 5–10 GiB during a runaway plugin / lua script.
MemorySwapMax=0 Swap thrashing. Without this, nvim can spill to swap and drag load average to 20+ before the cap fires — the system feels frozen for minutes.
OOMPolicy=stop Kernel SIGKILL leaving partial state. With stop, systemd treats cgroup-OOM as a unit failure: clean stop, status=failed, full journal entry, ExecStopPost runs.
OOMScoreAdjust=-500 nvim getting picked when SOMETHING ELSE (chrome explosion, etc.) causes system-wide OOM. We're not contributing to the OOM and shouldn't be sacrificed for it.

Restart=on-failure brings the service back automatically two seconds after any failure (including memory-cap stop). nvim-show works again immediately.

When the service stops for any reason, ExecStopPost= runs nvim-postmortem, which dumps:

  • SERVICE_RESULT (success / oom-kill / timeout / signal)
  • systemctl status snapshot
  • Last 200 lines of journal
  • cgroup memory.peak, memory.events, memory.swap.peak (if cgroup still readable)
  • System-wide free -h

Reports land in ~/.local/state/nvim-postmortems/ with a last.txt symlink to the most recent.

Alternatives considered

tmux instead of abduco. tried first, doesn't work: on systemd-aware distros, tmux uses dbus to ask systemd-logind to put each new session in its own scope. The spawned nvim ends up in a sibling cgroup (tmux-spawn-<uuid>.scope), not in nvim.service. MemoryMax= set on the service has zero effect on processes that escaped it. abduco doesn't do scope migration — children inherit the parent cgroup, period. (If you must use tmux, you need ExecStartPost= hooks to manually echo $pid > cgroup.procs for every spawned nvim child, which is fragile.)

Headless nvim + nvim --remote-ui clients. Cleaner separation in theory: backend nvim runs headless, frontend nvim attaches over the socket as a UI client. In practice, LazyVim's plugin set has too many startup-time UI assumptions (snacks.dashboard, render-markdown, theme priority loading). Some break silently in headless mode. abduco gives nvim a real (pseudo-)TTY so all plugins behave normally.

Niri-only persistence. Niri keeps the alacritty window alive across compositor sessions, so why not just let alacritty hold the nvim? Because (a) closing alacritty kills nvim regardless, (b) Niri can't auto-restart alacritty if alacritty itself crashes, (c) no memory cap.

A daemon written from scratch. Pointless. systemd already provides everything needed: lifecycle, restart, cgroup limits, logging, state queries. abduco already provides PTY persistence. Both are battle-tested in places this code never will be.

License

MIT.

About

Persistent nvim in abduco+systemd with a hard 2GiB cgroup memory cap. Survives terminal close. Won't OOM-cascade your system.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages