Skip to content

Comments

fix(provider): terminate job before closing terminal to prevent orphaned processes#168

Merged
nickjvandyke merged 15 commits intonickjvandyke:mainfrom
pedropombeiro:fix/terminate-job-on-stop
Feb 20, 2026
Merged

fix(provider): terminate job before closing terminal to prevent orphaned processes#168
nickjvandyke merged 15 commits intonickjvandyke:mainfrom
pedropombeiro:fix/terminate-job-on-stop

Conversation

@pedropombeiro
Copy link
Contributor

@pedropombeiro pedropombeiro commented Feb 9, 2026

Summary

When Neovim exits, the opencode --port process stays orphaned in memory instead of being terminated.

Root Cause

During VimLeavePre, snacks.terminal has already cleaned up its internal state, which invalidates the Neovim job. This means:

  • jobstop(job_id) returns true but doesn't actually terminate the process
  • jobpid(job_id) returns nil - Neovim no longer knows which PID the job maps to

Solution

Capture the PID (not just job ID) when the terminal opens, and use shell kill to terminate it directly during VimLeavePre. This bypasses Neovim's job tracking entirely.

Changes

  • snacks provider: Track _pid at toggle/start time using jobpid(terminal_job_id)
  • snacks provider: Use vim.fn.system('kill -TERM ' .. pid) in stop() for reliable termination
  • plugin/provider.lua: Change autocmd from VimLeave to VimLeavePre for earlier cleanup

Testing

  1. Start opencode via :Opencode
  2. Exit Neovim (:qa)
  3. Verify no orphaned processes: ps aux | grep "opencode --port" should return nothing

@nickjvandyke
Copy link
Owner

Thanks for taking a crack at this!

Is it possible to stop the job and all its children too? The child process (opencode) seems to stick around for me. Not sure if this is specific to fish.

image

@pedropombeiro pedropombeiro force-pushed the fix/terminate-job-on-stop branch from 9a5837e to d41da26 Compare February 9, 2026 23:02
@pedropombeiro pedropombeiro reopened this Feb 9, 2026
@pedropombeiro pedropombeiro force-pushed the fix/terminate-job-on-stop branch from d41da26 to f56624a Compare February 9, 2026 23:34
@pedropombeiro
Copy link
Contributor Author

Thanks for taking a crack at this!

Is it possible to stop the job and all its children too? The child process (opencode) seems to stick around for me. Not sure if this is specific to fish.

Can you try again with the latest version?

@pedropombeiro pedropombeiro force-pushed the fix/terminate-job-on-stop branch from f56624a to 733c590 Compare February 9, 2026 23:40
Track the terminal's PID at toggle/start time and use shell 'kill'
to terminate it during VimLeavePre. This is more reliable than
jobstop() because snacks.terminal cleanup can invalidate the job
before our stop() is called.
@pedropombeiro pedropombeiro force-pushed the fix/terminate-job-on-stop branch from 733c590 to ca9251c Compare February 9, 2026 23:41
@nickjvandyke
Copy link
Owner

I think the root issue is in opencode. Opened anomalyco/opencode#13001. Will see what happens there first 🙂

Copy link
Owner

@nickjvandyke nickjvandyke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this clever solution! Just a couple questions 😁

pedropombeiro and others added 5 commits February 19, 2026 22:56
… Windows support

Move PID capture logic into an on_buf callback in the constructor so it
fires automatically for both toggle() and open(), eliminating duplicated
code. Wrap the user's existing on_buf callback to preserve custom behavior
(e.g., keymap application).

Guard the shell 'kill -TERM' call with a Unix platform check and fall back
to vim.uv.kill() on non-Unix systems for cross-platform compatibility.
…l provider

Extract process kill and PID capture logic into opencode.provider.util,
shared by both snacks and terminal providers. This eliminates duplicated
code and ensures consistent behavior.

Key fix: use os.execute() instead of vim.fn.system() for process group
kill during VimLeavePre, and skip jobstop when PID kill succeeds to
prevent SIGHUP from causing the process to respawn.

Apply the same PID-based termination to the terminal provider, which
suffered from the same orphaned process issue as the snacks provider.
Add diagnostic disable/enable annotations around private field access
in the on_buf closure, which is part of the constructor but lua-ls
treats as an external context.
…haned processes

Add PID-based process group kill to the tmux provider, matching the
approach used by the snacks and terminal providers. Capture the pane PID
via tmux display-message and kill the process group before tmux kill-pane.

This is a workaround for anomalyco/opencode#13001
(opencode does not handle SIGHUP gracefully). Document the upstream issue
in provider.util so the workaround is easy to identify and remove later.
Copy link
Owner

@nickjvandyke nickjvandyke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great, thank you 😀 just a couple more questions, then I think we are set! Thanks for all the digging you did - turns out there are a lot of quirks here 😅

…ture

Remove job_id parameter from util.kill() and the jobstop fallback, since
jobstop sends SIGHUP which causes the process to respawn — making it
counterproductive. Only PID-based process group kill is effective.

Add comments explaining why PID must be captured eagerly at startup:
terminal_job_id is no longer available by the time VimLeavePre/stop() runs.
TermOpen fires exactly when the terminal job starts, rather than relying
on a constant delay that may be too early or too late.
@pedropombeiro pedropombeiro force-pushed the fix/terminate-job-on-stop branch from 1a1fdf3 to b3a2d68 Compare February 20, 2026 16:27
@pedropombeiro
Copy link
Contributor Author

Thanks @nickjvandyke! I've addressed your comments 🏓

@nickjvandyke
Copy link
Owner

nickjvandyke commented Feb 20, 2026

Thank you, looks great!

I may just check out the branch and move code around a bit according to the long-term provider API I have in mind if that's alright. Specifically Provider:get_pid() (which is really neat that we get that in this PR for a few of them!) Then we're good to go 😀

@pedropombeiro
Copy link
Contributor Author

Sure, feel free to make any changes. Looking forward to having this solved 👍

@nickjvandyke
Copy link
Owner

Interestingly turns out we do need to vim.fn.jobstop in Terminal:stop when Neovim isn't exiting. I guess it attempts to restart the process or something if it's killed while the job is still running? Not necessary for snacks.terminal - I guess it does that internally in its win:close() or such.

Thank you for the quality contribution! 😁

@nickjvandyke nickjvandyke merged commit 125c7dc into nickjvandyke:main Feb 20, 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.

2 participants