Skip to content

feat(acp): recover from an unresponsive agent without restarting Neovim#267

Open
schmoelder wants to merge 6 commits into
carlos-algms:mainfrom
schmoelder:fix/agent-reconnect-on-hang
Open

feat(acp): recover from an unresponsive agent without restarting Neovim#267
schmoelder wants to merge 6 commits into
carlos-algms:mainfrom
schmoelder:fix/agent-reconnect-on-hang

Conversation

@schmoelder

Copy link
Copy Markdown
Contributor

After the machine resumes from suspend (or the agent child otherwise hangs), a prompt gets no reply and the connection can't recover; the only fix was to quit Neovim and restore, since quitting is what actually kills the child. This adds in-place recovery, split out of #266 per your request.

  • Control-RPC inactivity watchdog: a stalled control request (initialize/session/load/session/list/set_mode/set_model) is rejected with a timeout instead of hanging forever. session/prompt is deliberately excluded, since it can legitimately run silently for minutes during a tool call.
  • Agentic.reconnect(): kills the hung child (which drains the stuck request and clears the spinner), respawns and re-initializes, then reloads the current session so the conversation continues, with no Neovim restart needed.

Follow-up to #266.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds a libuv-based inactivity watchdog to ACPClient for non-session/prompt requests, with timeout handling, activity tracking, and cleanup on response or transport failure. It also adds reconnect support in ACPClient, SessionManager, and Agentic, along with tests covering watchdog behavior, reconnect flows, and transport restart-in-place behavior.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Agentic
  participant SessionRegistry
  participant SessionManager
  participant ACPClient
  participant Transport

  User->>Agentic: reconnect()
  Agentic->>SessionRegistry: get_session_for_tab_page()
  SessionRegistry-->>Agentic: session
  Agentic->>SessionManager: session:reconnect()
  SessionManager->>ACPClient: agent:reconnect()
  ACPClient->>Transport: stop()
  ACPClient->>Transport: _connect()
  ACPClient-->>SessionManager: agent:when_ready
  alt loadSession supported
    SessionManager->>SessionManager: load_acp_session(session_id)
  else
    SessionManager->>SessionManager: new_session()
  end
Loading
sequenceDiagram
  participant Caller
  participant ACPClient
  participant Timer
  participant Transport

  Caller->>ACPClient: _send_request()
  ACPClient->>ACPClient: record watched request
  ACPClient->>Timer: arm watchdog
  ACPClient->>Transport: send(request)
  Transport-->>ACPClient: inbound message
  ACPClient->>ACPClient: update last activity
  Timer->>ACPClient: _check_watchdog()
  alt idle past timeout
    ACPClient->>ACPClient: reject watched callbacks
  else response received
    ACPClient->>ACPClient: clear watched request
  end
Loading

Possibly related PRs

  • carlos-algms/agentic.nvim#178: Changes _drain_pending_callbacks in acp_client.lua for connection-drop handling, which is extended here with watchdog cleanup and error-code handling.

Poem

🐇 I listened for silence, then ticked like a drum,
I hopped through reconnects till fresh signals come.
When prompts keep on whispering, I let them stay near,
But stalled little requests get a timeout, my dear.
Through session reloads and transport hops bright,
This bunny keeps agents awake through the night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: in-place recovery from an unresponsive agent without restarting Neovim.
Description check ✅ Passed The description directly matches the changes, covering the inactivity watchdog and the new reconnect flow.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lua/agentic/session_manager.lua`:
- Around line 1406-1412: In the callback passed to self.agent:when_ready(), the
code captures session_id and uses it without re-validating if it's still the
current session. Before calling self:load_acp_session(session_id), add a check
to ensure the captured session_id matches the current session state. If the
session has changed since the callback was registered, skip the load_acp_session
call and proceed with new_session instead to prevent overwriting newer session
state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2581e8f-346b-4e18-ab57-ab14fb4508f5

📥 Commits

Reviewing files that changed from the base of the PR and between a19fee6 and 7d47f36.

📒 Files selected for processing (6)
  • lua/agentic/acp/acp_client.lua
  • lua/agentic/acp/acp_client.test.lua
  • lua/agentic/acp/acp_transport.test.lua
  • lua/agentic/init.lua
  • lua/agentic/session_manager.lua
  • lua/agentic/session_manager.test.lua

Comment thread lua/agentic/session_manager.lua
@schmoelder

Copy link
Copy Markdown
Contributor Author

full transparency, I vibecoded all of this (using agentic.nvim! ;-))

@schmoelder
schmoelder force-pushed the fix/agent-reconnect-on-hang branch from 7a5ae01 to b087b6f Compare June 26, 2026 07:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lua/agentic/acp/acp_client_types.lua`:
- Around line 329-330: `ACPProviderConfig` currently exposes both `timeout` and
`request_timeout`, but only `request_timeout` is used by the watchdog in
`acp_client.lua` via the active request handling path. Update the config
definition and related consumers to remove the unused `timeout` field or wire it
into real logic if it is meant to control a separate deadline; if keeping the
watchdog setting, rename `request_timeout` to a clearer symbol such as
`watchdog_timeout` or `request_silence_timeout` and update all references
consistently. Make sure the behavior matches the documented field names so users
are not configuring a no-op.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2a66f9b1-ee41-41f1-a316-1036046c43fc

📥 Commits

Reviewing files that changed from the base of the PR and between 7a5ae01 and b087b6f.

📒 Files selected for processing (7)
  • lua/agentic/acp/acp_client.lua
  • lua/agentic/acp/acp_client.test.lua
  • lua/agentic/acp/acp_client_types.lua
  • lua/agentic/acp/acp_transport.test.lua
  • lua/agentic/init.lua
  • lua/agentic/session_manager.lua
  • lua/agentic/session_manager.test.lua

Comment thread lua/agentic/acp/acp_client_types.lua Outdated
After a suspend or hang the agent child keeps its stdio pipes open and never exits, so the process-exit path never drains pending callbacks and a control request hangs forever. A connection-level timer, armed only while a control request is in flight and reset by any inbound message, rejects those requests with TIMEOUT_ERROR after sustained silence. session/prompt is deliberately excluded: it can run silently for minutes during a tool call, so silence is not a death signal there and a long generation must never be cancelled; the prompt path is guarded separately by a liveness probe. Explicit transport send failures are rejected immediately for every method.
When the agent child hangs (e.g. the machine resumes from suspend with a dead connection) the only recovery was to quit Neovim and restore, since quitting is what actually killed the child. Agentic.reconnect() does that in place: stop the child, which drains the stuck request callbacks and resets the generating UI, respawn and re-initialize, then reload the current session so the conversation continues. The agent process is shared per provider, so this also resets other tabs on that provider; they re-establish on next use.
Re-initialization after a reconnect is async; capturing session_id up front could reload a session that was replaced during the window, clobbering newer state. Read it inside the when_ready callback so the current session is reloaded instead.
@schmoelder
schmoelder force-pushed the fix/agent-reconnect-on-hang branch from b087b6f to 77bb52e Compare June 26, 2026 07:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lua/agentic/acp/acp_client.lua`:
- Around line 280-286: The watchdog flow in `_check_watchdog()` and
`_on_request_stall()` can reject fresh watched requests after activity has
already resumed. Re-check the timeout condition right at the start of
`_on_request_stall()` using `_last_activity` and the same `timeout` before
touching `_watched_pending`, and return early if the request is no longer stale.
Keep the existing disarm/schedule behavior, but ensure `_on_request_stall()`
only rejects requests when the inactivity window is still valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 72ebcb2d-9051-4453-a74b-c6f8963272f3

📥 Commits

Reviewing files that changed from the base of the PR and between b087b6f and 77bb52e.

📒 Files selected for processing (7)
  • lua/agentic/acp/acp_client.lua
  • lua/agentic/acp/acp_client.test.lua
  • lua/agentic/acp/acp_client_types.lua
  • lua/agentic/acp/acp_transport.test.lua
  • lua/agentic/init.lua
  • lua/agentic/session_manager.lua
  • lua/agentic/session_manager.test.lua

Comment thread lua/agentic/acp/acp_client.lua
Between _check_watchdog disarming and _on_request_stall running, a response can arrive and update _last_activity. Without a re-check, the stall handler rejects a request that is no longer stale.
uv.now() counts ms since the event loop started, not wall time. On a fresh CI runner it can be only a few seconds, so _last_activity = 0 no longer reliably falls outside the 60s timeout window. Pin the stale timestamp as uv.now() - 70000 instead.
@schmoelder

Copy link
Copy Markdown
Contributor Author

The only typecheck fail seems to be fixed upstream (a37f19f). I rebased, hopefully this now passes.

@carlos-algms

Copy link
Copy Markdown
Owner

Hey @schmoelder, I'm doing a deeper review of this PR, and it seems way more complex than initially assessed.

Which OS, and how often do you face this unresponsive issue?
Which provider as well? Claude, codex, gemini, other?

This PR will introduce some hard-to-debug edge cases, such as:

  1. Multi-tab will break, a reconnect on Tab A breaks the agent on Tab B
  2. The readiness closure captures the tab’s SessionManager, but destroy() neither invalidates it nor unregisters the listener
  3. Reconnect does not block Agentic.new_session() or /new
  4. Reconnect is not idempotent. Two calls enqueue two readiness callbacks.
  5. Successful reconnect never clears _connection_error
  6. Only outbound events are monitored by the new code, and session/prompt is not monitored, so to recover, users would have to trigger a configuration change.

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