Describe the bug
When a Claude Code agent requests permission (e.g., git pull), the request is displayed both in the Claude Code terminal and in the GitLens Graph UI. If the user waits ~15 minutes before responding via the GitLens Graph UI (Allow button), the following happens:
- GitLens resolves the permission internally and updates the session state to "Working"
- Claude Code never receives the response — the terminal remains stuck at the permission prompt
- The user sees a desync: GitLens says the agent is working, but Claude Code is still waiting for input
The only recovery is to respond directly in the Claude Code terminal or restart the session.
Steps to reproduce
- Enable
gitlens.graph.experimentalFeatures.enabled: true
- Start a Claude Code agent session on a branch in the workspace
- Trigger an action that requires permission (e.g., a
git pull Bash command)
- Observe the permission request appears in both:
- Claude Code terminal: "This command requires approval — Do you want to proceed?"
- GitLens Graph: agent pill changes to "Needs input" with Allow/Deny buttons
- Wait approximately 15 minutes without responding
- Click "Allow" in the GitLens Graph UI
- Observe: GitLens transitions to "Working", but Claude Code terminal remains stuck
Expected behavior
Either:
- The permission response should be successfully delivered to Claude Code regardless of wait time, OR
- GitLens should detect the stale/dead connection and show an error state instead of transitioning to "Working"
Root cause analysis
The issue stems from three gaps in the IPC response delivery chain:
1. No error handling on HTTP response (packages/ipc/src/ipcServer.ts:88-169)
The onRequest() handler calls res.end(JSON.stringify(result)) without:
res.on('error') handler — silent failure if socket is broken
res.on('close') listener — no detection of client disconnect
req.on('close') or req.on('aborted') listener — no detection of request abort
The response write is fire-and-forget with no delivery confirmation.
2. Optimistic state update (packages/plus/agents/src/providers/claudeCodeProvider.ts:600-629)
resolvePermission() updates session state to "Working" before (and without) confirming the HTTP response was delivered:
Line 611: pending.resolve({ ... }) // resolves the IPC handler Promise
Line 622: _pendingPermissions.delete() // cleans up local state
Line 627: updateSessionStatus() // transitions to "Working" in UI
The resolve() call only completes the handler's Promise return — it does not verify the TCP payload reached the client.
3. No connection health monitoring
- No TCP keep-alive configured on the IPC server
- No heartbeat mechanism between hook process and server
- After ~15 minutes of idle, the OS-level TCP timeout silently drops the connection
- Neither side detects the dead socket until a write is attempted (and even then, the error is unhandled)
Suggested fix directions
-
Detect dead connections proactively: Add req.on('close') / req.on('aborted') listeners in the IPC server. When a client disconnects while a blocking handler is pending, reject the pending permission and revert session state.
-
Verify delivery before state transition: Wrap res.end() with error handling. Only update session state to "Working" after confirming the response was flushed to the socket (e.g., via the res.end() callback or res.writableFinished).
-
Add connection keep-alive: Configure TCP keep-alive on the IPC server socket to detect dead connections earlier than the OS default (~15 min).
-
Add a timeout on the pending permission Promise: If no response is delivered within N minutes, auto-reject the permission and update the UI to reflect the timeout. This prevents the session from being held indefinitely in a stale state.
Environment
- GitLens pre-release 2026.5.140551
- Windows 11
- Claude Code (CLI) via terminal
gitlens.graph.experimentalFeatures.enabled: true
Describe the bug
When a Claude Code agent requests permission (e.g.,
git pull), the request is displayed both in the Claude Code terminal and in the GitLens Graph UI. If the user waits ~15 minutes before responding via the GitLens Graph UI (Allow button), the following happens:The only recovery is to respond directly in the Claude Code terminal or restart the session.
Steps to reproduce
gitlens.graph.experimentalFeatures.enabled: truegit pullBash command)Expected behavior
Either:
Root cause analysis
The issue stems from three gaps in the IPC response delivery chain:
1. No error handling on HTTP response (
packages/ipc/src/ipcServer.ts:88-169)The
onRequest()handler callsres.end(JSON.stringify(result))without:res.on('error')handler — silent failure if socket is brokenres.on('close')listener — no detection of client disconnectreq.on('close')orreq.on('aborted')listener — no detection of request abortThe response write is fire-and-forget with no delivery confirmation.
2. Optimistic state update (
packages/plus/agents/src/providers/claudeCodeProvider.ts:600-629)resolvePermission()updates session state to "Working" before (and without) confirming the HTTP response was delivered:The
resolve()call only completes the handler's Promise return — it does not verify the TCP payload reached the client.3. No connection health monitoring
Suggested fix directions
Detect dead connections proactively: Add
req.on('close')/req.on('aborted')listeners in the IPC server. When a client disconnects while a blocking handler is pending, reject the pending permission and revert session state.Verify delivery before state transition: Wrap
res.end()with error handling. Only update session state to "Working" after confirming the response was flushed to the socket (e.g., via theres.end()callback orres.writableFinished).Add connection keep-alive: Configure TCP keep-alive on the IPC server socket to detect dead connections earlier than the OS default (~15 min).
Add a timeout on the pending permission Promise: If no response is delivered within N minutes, auto-reject the permission and update the UI to reflect the timeout. This prevents the session from being held indefinitely in a stale state.
Environment
gitlens.graph.experimentalFeatures.enabled: true