lsp-probe keeps one warm ruby-lsp daemon per worktree root, with warm and stop commands #600

Closed
opened 2026-09-16 19:52:56 +00:00 by jared · 2 comments
Owner

Context

Branch lsp-probe-cli (ADR-0197) added bin/lsp-probe, a Thor CLI that spawns ruby-lsp over stdio per call (lib/os_sdlc/lsp_probe/query_runner.rb:21, Open3.popen3). Measured 2026-09-16: one lsp-probe call 4-9 s; bin/evidence collect --root plugins/os-sdlc/lib Model::Db 209 s because lib/os_sdlc/evidence/fingerprint.rb:83-91 runs one LSP query, and so one spawn, per method with no ast-grep sender. Daemon lifecycle management reduces per-call spawn overhead.

Owner sketch

class Daemon
  IDLE_TIMEOUT = 30 * 60
  def self.connect(root:) = new(root: root).connect
  def connect = alive? ? attach : spawn_and_attach
  def stop = ...
  private def alive? = pid_file.exist? && Process.kill(0, pid)
end

Cases

  • Daemon.connect on a root with no .cc-os/lsp/ spawns once and writes sock and pid files (spawner double).
  • Second Daemon.connect on the same root spawns nothing and attaches to the existing socket.
  • Stale pid file whose process is dead: connect removes the files and spawns fresh.
  • lsp-probe stop --root X kills the pid and removes the two files; running it twice is a no-op with exit 0.
  • lsp-probe warm --root X returns within 1 s and a following lsp-probe defs call completes without a spawn.
  • Idle timer: a daemon with no request for IDLE_TIMEOUT exits (clock injected).

Origin

  • Trigger: evidence performance audit 2026-09-16
  • Improvised this session: none
  • Chain: per-call spawn overhead ← QueryRunner always popen3 ← no daemon persistence
  • Root candidate: n/a
  • Where: lib/os_sdlc/lsp_probe/daemon.rb
  • Session: 01153d94-6459-43dc-bd20-e68b757af56f
  • Transcript: https://claude.com/claude-code
## Context Branch lsp-probe-cli (ADR-0197) added bin/lsp-probe, a Thor CLI that spawns ruby-lsp over stdio per call (lib/os_sdlc/lsp_probe/query_runner.rb:21, Open3.popen3). Measured 2026-09-16: one lsp-probe call 4-9 s; `bin/evidence collect --root plugins/os-sdlc/lib Model::Db` 209 s because lib/os_sdlc/evidence/fingerprint.rb:83-91 runs one LSP query, and so one spawn, per method with no ast-grep sender. Daemon lifecycle management reduces per-call spawn overhead. ## Owner sketch ```ruby class Daemon IDLE_TIMEOUT = 30 * 60 def self.connect(root:) = new(root: root).connect def connect = alive? ? attach : spawn_and_attach def stop = ... private def alive? = pid_file.exist? && Process.kill(0, pid) end ``` ## Cases - `Daemon.connect` on a root with no `.cc-os/lsp/` spawns once and writes sock and pid files (spawner double). - Second `Daemon.connect` on the same root spawns nothing and attaches to the existing socket. - Stale pid file whose process is dead: `connect` removes the files and spawns fresh. - `lsp-probe stop --root X` kills the pid and removes the two files; running it twice is a no-op with exit 0. - `lsp-probe warm --root X` returns within 1 s and a following `lsp-probe defs` call completes without a spawn. - Idle timer: a daemon with no request for IDLE_TIMEOUT exits (clock injected). ## Origin - Trigger: evidence performance audit 2026-09-16 - Improvised this session: none - Chain: per-call spawn overhead ← QueryRunner always popen3 ← no daemon persistence - Root candidate: n/a - Where: lib/os_sdlc/lsp_probe/daemon.rb - Session: 01153d94-6459-43dc-bd20-e68b757af56f - Transcript: https://claude.com/claude-code
Author
Owner

Work started via os-sdlc pipeline on branch ticket-600 (worktree off lsp-probe-cli, after #599 cab8b5ea). ADR-0198 recorded. Session 01153d94.

Work started via os-sdlc pipeline on branch ticket-600 (worktree off lsp-probe-cli, after #599 cab8b5ea). ADR-0198 recorded. Session 01153d94.
Author
Owner

Resolution

Done: LspProbe::Daemon with Paths/Spawner/Server/Query/Client/Connection/Idle/Stopper; Server routes defs/refs/symbols/outline through Daemon.connect; CLI gains warm, stop, hidden serve; Fingerprint's SharedRunner uses the daemon; QueryRunner deleted. Commit 4a348c3f on ticket-600, merges to lsp-probe-cli. ADR-0198 amended (hashed path, spawn not fork, abandoned-socket exit, TERM/KILL stop).

Evidence: Suite 1295 green, rubocop clean. Measured: symbols 4.0 s cold -> 0.26 s warm; evidence collect Model::Db 10.5 s -> 5.2 s warm; after stop no ruby-lsp process and hashed dir empty. Pipeline impl 24: 15 dispatches, ended unexpected_pass escalation; pipeline output was a placeholder daemon (fork+sleep, empty sock file, connect returned nil, QueryRunner untouched); real daemon hand-finished. See #604.

Follow-ups: Kill the daemon process group (Spawner already sets pgroup: true; Stopper should signal -pid) so a hung ruby-lsp child cannot outlive lsp-probe serve: filed as a line here, ticket after user review. Daemon exit when the root tree disappears, not only the socket: dropped, abandoned-socket check covers the observed case. lsp-probe stop --all reaper for crashed daemons: dropped until seen. SessionEnd hook to stop daemons: covered by #601 worktree finish for pipeline roots; main-checkout daemons rely on idle exit.

## Resolution **Done:** LspProbe::Daemon with Paths/Spawner/Server/Query/Client/Connection/Idle/Stopper; Server routes defs/refs/symbols/outline through Daemon.connect; CLI gains warm, stop, hidden serve; Fingerprint's SharedRunner uses the daemon; QueryRunner deleted. Commit 4a348c3f on ticket-600, merges to lsp-probe-cli. ADR-0198 amended (hashed path, spawn not fork, abandoned-socket exit, TERM/KILL stop). **Evidence:** Suite 1295 green, rubocop clean. Measured: symbols 4.0 s cold -> 0.26 s warm; evidence collect Model::Db 10.5 s -> 5.2 s warm; after stop no ruby-lsp process and hashed dir empty. Pipeline impl 24: 15 dispatches, ended unexpected_pass escalation; pipeline output was a placeholder daemon (fork+sleep, empty sock file, connect returned nil, QueryRunner untouched); real daemon hand-finished. See #604. **Follow-ups:** Kill the daemon process group (Spawner already sets pgroup: true; Stopper should signal -pid) so a hung ruby-lsp child cannot outlive lsp-probe serve: filed as a line here, ticket after user review. Daemon exit when the root tree disappears, not only the socket: dropped, abandoned-socket check covers the observed case. lsp-probe stop --all reaper for crashed daemons: dropped until seen. SessionEnd hook to stop daemons: covered by #601 worktree finish for pipeline roots; main-checkout daemons rely on idle exit.
jared closed this issue 2026-09-17 06:16:41 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#600
No description provided.