github/copilot-sdk · error · RuntimeError

Failed to detach session

Error message

Failed to detach session {self.session_id}: {detail}

What it means

CopilotSession.disconnect() sends a 'session.detach' JSON-RPC request to the runtime. If the runtime responds without success:true, the SDK raises this RuntimeError with the runtime-provided error detail. It means the backend refused or failed to detach the session, so the session's in-memory handlers are NOT cleaned up and the session object remains usable.

Solutions

  1. Inspect the 'detail' in the message to see the runtime's underlying error (e.g. session not found vs connection failure).
  2. If the runtime already discarded the session, the session is effectively dead — discard the object or call client.delete_session/resume_session as appropriate; guard disconnect with try/except if cleanup-on-exit should be best-effort.
  3. Verify the runtime/CLI process is alive and the transport (stdio/TCP) is healthy before disconnecting; reconnect and retry the disconnect if the transport failed.
  4. Check the session_id was not already detached or deleted elsewhere in your code (idempotency is only local via _destroyed, not server-side).

Example fix

// before
await session.disconnect()
// after
try:
    await session.disconnect()
except RuntimeError as exc:
    logger.warning('best-effort disconnect failed: %s', exc)
Defensive patterns

Strategy: try-catch

Validate before calling

if session._destroyed:
    pass  # already disconnected locally; skip the RPC
else:
    await session.disconnect()

Try / catch

try:
    await session.disconnect()
except RuntimeError as exc:
    if 'Failed to detach session' in str(exc):
        logger.warning('detach failed, session may already be gone: %s', exc)
    else:
        raise

Prevention

When it happens

Trigger: Calling await session.disconnect() (directly or via async-with exit, or via resume_session/_initialize_session teardown paths) when the runtime returns {success: false, error: ...} for session.detach — e.g. the session already terminated server-side, the runtime process crashed or was restarted, or the session ID is no longer known to the backend.

Common situations: App shutdown racing a runtime crash; double-disconnect across processes where the server already dropped the session; killing/restarting the Copilot CLI process while sessions are open; network/pipe failure between client and runtime causing a non-success response.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/70afe686f9f66ee9. Report an issue: GitHub.

Appendix: source

Thrown at python/copilot/session.py:3099

        This method is idempotent—calling it multiple times is safe and will
        not raise an error if the session is already disconnected.

        Raises:
            Exception: If the connection fails (on first disconnect call).

        Example:
            >>> # Clean up when done — session can still be resumed later
            >>> await session.disconnect()
        """
        async with self._disconnect_lock:
            with self._event_handlers_lock:
                if self._destroyed:
                    return

            response = await self._client.request("session.detach", {"sessionId": self.session_id})
            if not response.get("success"):
                detail = response.get("error") or "unknown error"
                raise RuntimeError(f"Failed to detach session {self.session_id}: {detail}")

            self._cancel_pending_external_tools()
            self._run_disconnect_callback()
            with self._event_handlers_lock:
                self._destroyed = True
                self._event_handlers.clear()
            with self._tool_handlers_lock:
                self._tool_handlers.clear()
            with self._permission_handler_lock:
                self._permission_handler = None
            with self._command_handlers_lock:
                self._command_handlers.clear()
            with self._elicitation_handler_lock:
                self._elicitation_handler = None
            with self._exit_plan_mode_handler_lock:
                self._exit_plan_mode_handler = None
            with self._auto_mode_switch_handler_lock:
                self._auto_mode_switch_handler = None

View on GitHub (pinned to cd8cf15dc3)