heph.nvim: plug-and-play managed daemon (autostart, self-heal, client/server guardrail)

The plugin now manages its own hephd by default (autostart = true): if nothing
is serving the socket it spawns a local daemon against the default XDG paths,
kills only what it spawned on VimLeavePre, and self-heals — rpc.call retries
once through a respawn hook when the connection drops (the prior owner releases
the DB lock on exit, so a respawn can claim it).

- daemon.ensure() connects to an already-running daemon (any mode) or spawns one
  we own; stop_spawned()/is_managed() track lifecycle.
- A server/client daemon you started is always respected (spawn only when nothing
  serves the socket). autostart = false → connect-only, warns/errors if down,
  and clears the self-heal hook so it fails loudly.
- config: autostart defaults true; new `db` option; $HEPH_SOCKET / $HEPH_DB
  fallbacks isolate a dev Neovim onto a separate daemon + DB.

e2e: managed_daemon_spec covers autostart spawn, self-heal-after-kill, and
connect-only error. 10 specs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-06-02 09:32:32 -07:00
commit e3db2ac550
11 changed files with 277 additions and 18 deletions

View file

@ -182,9 +182,34 @@ function M.session()
return M._default
end
--- Blocking call on the default session.
--- Register a hook that (re)ensures the daemon — called once to self-heal a
--- dropped connection before a single retry. `nil` disables self-heal (used when
--- autostart is off, so a connect-only setup fails loudly instead of respawning).
function M.set_respawn(fn)
M._respawn = fn
end
local function is_connection_error(msg)
msg = tostring(msg)
return msg:find("connect", 1, true) ~= nil
or msg:find("connection", 1, true) ~= nil
or msg:find("timeout", 1, true) ~= nil
end
--- Blocking call on the default session. If the call fails because the
--- connection is dead and a respawn hook is set, ensure the daemon and retry
--- once (the prior owner releases the DB lock on exit, so a respawn can claim it).
function M.call(method, params, opts)
return M.session():call(method, params, opts)
local ok, result = pcall(M.session().call, M.session(), method, params, opts)
if ok then
return result
end
if M._respawn and is_connection_error(result) then
pcall(M._respawn)
M.session():close() -- drop the dead connection so the retry reconnects
return M.session():call(method, params, opts)
end
error(result)
end
--- An isolated session for a socket — used by tests for independent assertions.