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
- Inspect the 'detail' in the message to see the runtime's underlying error (e.g. session not found vs connection failure).
- 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.
- 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.
- 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
- Keep one owner of session lifecycle; avoid concurrent disconnect/delete of the same session ID.
- Treat disconnect during shutdown as best-effort and log rather than crash.
- Monitor runtime/CLI process health so you notice crashes before teardown.
- Suppress duplicate disconnects by relying on the SDK's idempotent _destroyed check rather than your own flags.
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
- Request handler must return a JSON-serializable value, got
- -32603
- unknown session
- -32603
- Invalid entry '*': there is no bare wildcard. Use one or…
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 = NoneView on GitHub (pinned to cd8cf15dc3)