Skip to content

fix(nikcli): release the herdr pane from the process that actually exits - #233

Merged
nikomatt69 merged 3 commits into
live-mainfrom
fix/herdr-release-pane-on-exit
Aug 18, 2026
Merged

fix(nikcli): release the herdr pane from the process that actually exits#233
nikomatt69 merged 3 commits into
live-mainfrom
fix/herdr-release-pane-on-exit

Conversation

@SandroHub013

@SandroHub013 SandroHub013 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Issue for this PR

Closes #232

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Herdr drops an agent by itself only for the agents it detects by process. nikcli is reported, not detected, so its row stays until someone calls pane.release_agent. Nothing did — dispose() called HerdrBridge.stop(), which only resets local runtime state.

Fixing dispose() alone is not enough, and that's the interesting half. On Windows shutdownWorker() takes the non-terminating branch: it unrefs the worker and fires the shutdown RPC without awaiting it. Instance.disposeAll() never finishes before the TUI process exits, so no plugin dispose runs at all. macOS and Linux await it, which is why the row only sticks on Windows.

So the release happens in the TUI process, right after shutdownWorker() returns — the process that is actually about to exit on every platform. No change to the worker teardown contract, and quit still doesn't wait on a hanging worker.

releasePaneSync() shells out to the herdr CLI instead of using the socket, because the shutdown path has no event loop left to await a write on. A hard kill (taskkill /F) still leaves the row, same as every other herdr integration.

How did you verify your code works?

  • bun test packages/nikcli/test/plugin/herdr/release-on-exit.test.ts packages/nikcli/test/tui/thread.test.ts: 9 pass. The argv assertion covers the contract herdr keys agent authority on; thread.test.ts still proves quit doesn't block on a hanging worker.
  • tsc --noEmit clean on packages/util and packages/nikcli.
  • Against a live herdr server: a process that reports the pane and then runs the same release goes from "agent":"nikcli" to no agent at all. Before, the row survived the process and only went away when the pane's shell exited.

Reviewers on Windows: report-protocol.test.ts fails on this branch for an unrelated reason — #230 / #231. Neither file touched here needs a socket.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Herdr clears an agent on its own only for the agents it detects by
process. nikcli is reported, not detected, so its row stayed in the agent
panel until the pane's shell itself exited — dispose() called
HerdrBridge.stop(), which only resets local runtime state.

releasePaneSync() shells out to the herdr CLI because process.on("exit")
cannot await a socket write. No signal handlers: nikcli reads ctrl+c as a
key in raw mode and owns its quit path, so taking SIGINT here would change
behaviour unrelated to herdr. A hard kill still leaves the row, same as
every other integration.

Fixes #232
@github-actions github-actions Bot added size/M PR touches 74 lines needs:compliance labels Aug 18, 2026
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions

Copy link
Copy Markdown

lgtm

The plugin's dispose() is not reachable on Windows. shutdownWorker() takes
the non-terminating branch there: it unrefs the worker and fires the
shutdown RPC without awaiting, so Instance.disposeAll() — and with it the
herdr plugin's dispose — is cut off when the TUI process exits. The agent
row survived until the pane's own shell exited.

Release from the TUI process instead, right after shutdownWorker returns.
That is the process that is about to exit on every platform, so this needs
no change to the worker teardown contract and does not make quit wait on a
hanging worker.

Drops installExitRelease(): a `process.on("exit")` listener registered in
the worker is exactly what worker.ts warns against, and it never fired on
the path that matters.

Fixes #232
@SandroHub013 SandroHub013 changed the title fix(nikcli): hand the herdr pane back when nikcli exits fix(nikcli): release the herdr pane from the process that actually exits Aug 18, 2026
@github-actions

Copy link
Copy Markdown

Thanks for updating your PR! It now meets our contributing guidelines. 👍

stop() resets runtime.released, so calling it after releasePaneSync()
could let a late report reclaim the pane. Flip the dispose order and
pass windowsHide on the synchronous CLI spawn.

Co-authored-by: Cursor <cursoragent@cursor.com>

@nikomatt69 nikomatt69 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review (local verification, 2026-08-18)

The Windows analysis is right: shutdownWorker() on win32 unrefs and does not await, so plugin dispose() never runs and HerdrBridge.stop() never handed the pane back. Releasing from the TUI process with a synchronous CLI spawn is the process that actually exits.

I pushed one follow-up on this branch (dc302edf9b) because the original dispose order had a real bug:

  1. releasePaneSync() set runtime.released = true
  2. HerdrBridge.stop() then set runtime.released = false

stop() is documented to reset local runtime state, including released. A late reportAgent after dispose could reclaim the pane. Dispose now does stop() then releasePaneSync(), and the CLI spawnSync gets windowsHide: true (repo convention; otherwise Windows can flash a console on quit).

Verified

  • Overlay of this branch onto live-main + bun test test/plugin/herdr/release-on-exit.test.ts test/tui/thread.test.ts10 pass, 0 fail (argv contract, no-pane no-op, stop-then-release, and existing thread shutdown tests including “quit does not block on a hanging Windows worker”)
  • CI typecheck passed on the original commits
  • Merges clean onto current live-main (no conflicts)

CI that is red is not this PR

  • test (windows): Bun remap / exit 255, same as #225/#227
  • smoke (windows): SystemPrompt.Service timed out at 5s — unrelated to herdr; dispose log line ran and did not hang (no HERDR_PANE_ID on the runner)
  • Linux test still queued

Related, not this PR: #231 (Windows named-pipe dial) was closed without merge. Socket releasePane() still cannot work on Windows until that lands. This CLI path is the correct Windows workaround; please reopen #231 separately so reporting (not just release) works there.

releasePaneSync() no-ops if herdr is not on PATH / HERDR_BIN_PATH. That matches resolveHerdrBin(); worth knowing if a user still sees a sticky row.

Merge: yes, after #225 and #227.

@nikomatt69
nikomatt69 merged commit 8f92f22 into live-main Aug 18, 2026
13 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli size/M PR touches 74 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

herdr agent panel keeps a stale nikcli row after nikcli quits

2 participants