codeframe-ai 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,354 @@
1
+ """WebSocket router for per-session streaming agent chat.
2
+
3
+ Endpoint:
4
+ WS /ws/sessions/{session_id}/chat?token=<JWT>
5
+
6
+ Client → Server message types:
7
+ {"type": "message", "content": "..."}
8
+ {"type": "interrupt"}
9
+ {"type": "ping"}
10
+
11
+ Server → Client message types:
12
+ {"type": "text_delta", "content": "..."}
13
+ {"type": "tool_use_start", "tool_name": "...", "tool_input": {...}}
14
+ {"type": "tool_result", "tool_name": "...", "content": "..."}
15
+ {"type": "thinking", "content": "..."}
16
+ {"type": "cost_update", "cost_usd": 0.003, "input_tokens": 100, "output_tokens": 50}
17
+ {"type": "done"}
18
+ {"type": "error", "message": "..."}
19
+ {"type": "pong"}
20
+
21
+ Architecture note: This router intentionally handles session orchestration
22
+ (activation, interrupt coordination, cost aggregation) rather than delegating
23
+ to a core service. A future refactoring (TODO: extract to
24
+ core.session_chat_service) would make this a thin transport adapter. See
25
+ GitHub issue #502 for context.
26
+
27
+ Workspace scoping: This endpoint is intentionally exempt from workspace_path
28
+ query param validation. Auth is already scoped to a user via JWT and the
29
+ session_id identifies the resource — workspace scoping on a per-session WS
30
+ would be redundant. Revisit if multi-tenant workspace isolation is required.
31
+ """
32
+
33
+ import asyncio
34
+ import json
35
+ import logging
36
+ from pathlib import Path
37
+ from typing import Optional
38
+
39
+ import jwt as pyjwt
40
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
41
+ from sqlalchemy import select
42
+
43
+ from codeframe.auth.manager import SECRET, JWT_ALGORITHM, JWT_AUDIENCE, get_async_session_maker
44
+ from codeframe.auth.models import User
45
+ from codeframe.core.adapters.streaming_chat import StreamingChatAdapter
46
+ from codeframe.ui.shared import session_chat_manager
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ router = APIRouter(tags=["websocket"])
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Helpers
55
+ # ---------------------------------------------------------------------------
56
+
57
+
58
+ async def _authenticate_websocket(websocket: WebSocket) -> Optional[int]:
59
+ """Validate JWT from query param. Returns user_id or closes with 1008."""
60
+ token = websocket.query_params.get("token")
61
+ if not token:
62
+ await websocket.close(code=1008, reason="Authentication required: missing token")
63
+ return None
64
+
65
+ try:
66
+ payload = pyjwt.decode(token, SECRET, algorithms=[JWT_ALGORITHM], audience=JWT_AUDIENCE)
67
+ user_id_str = payload.get("sub")
68
+ if not user_id_str:
69
+ await websocket.close(code=1008, reason="Invalid token: missing subject")
70
+ return None
71
+ user_id = int(user_id_str)
72
+ except pyjwt.ExpiredSignatureError:
73
+ await websocket.close(code=1008, reason="Token expired")
74
+ return None
75
+ except (pyjwt.InvalidTokenError, ValueError) as exc:
76
+ logger.debug("WebSocket JWT decode error: %s", exc)
77
+ await websocket.close(code=1008, reason="Invalid authentication token")
78
+ return None
79
+
80
+ try:
81
+ async_session_maker = get_async_session_maker()
82
+ async with async_session_maker() as session:
83
+ result = await session.execute(select(User).where(User.id == user_id))
84
+ user = result.scalar_one_or_none()
85
+ if user is None:
86
+ await websocket.close(code=1008, reason="User not found")
87
+ return None
88
+ if not user.is_active:
89
+ await websocket.close(code=1008, reason="User is inactive")
90
+ return None
91
+ except Exception as exc:
92
+ logger.error("WebSocket user lookup error: %s", exc)
93
+ await websocket.close(code=1008, reason="Authentication failed")
94
+ return None
95
+
96
+ return user_id
97
+
98
+
99
+ async def _run_streaming_adapter(
100
+ session_id: str,
101
+ user_message: str,
102
+ token_queue: asyncio.Queue,
103
+ interrupt_event: asyncio.Event,
104
+ db_repo,
105
+ workspace_path: Path,
106
+ ) -> None:
107
+ """Drive the StreamingChatAdapter and forward ChatEvents into the token queue.
108
+
109
+ Args:
110
+ session_id: ID of the interactive session.
111
+ user_message: The user's message content.
112
+ token_queue: Queue consumed by the relay task for WebSocket forwarding.
113
+ interrupt_event: Signals the adapter to stop mid-stream.
114
+ db_repo: ``InteractiveSessionRepository`` for history load/persist.
115
+ workspace_path: Absolute path to the workspace for file-system tools.
116
+ """
117
+ try:
118
+ from codeframe.adapters.llm.anthropic import AnthropicProvider
119
+ provider = AnthropicProvider()
120
+ adapter = StreamingChatAdapter(
121
+ session_id=session_id,
122
+ db_repo=db_repo,
123
+ workspace_path=workspace_path,
124
+ provider=provider,
125
+ )
126
+ async for event in adapter.send_message(
127
+ content=user_message,
128
+ history=[],
129
+ interrupt_event=interrupt_event,
130
+ ):
131
+ await token_queue.put(event.to_dict())
132
+ except Exception as exc:
133
+ logger.error("_run_streaming_adapter error: %s", exc, exc_info=True)
134
+ await token_queue.put({"type": "error", "message": str(exc)})
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Endpoint
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ @router.websocket("/ws/sessions/{session_id}/chat")
143
+ async def session_chat_ws(session_id: str, websocket: WebSocket) -> None:
144
+ """Bidirectional WebSocket for streaming agent chat on an interactive session."""
145
+ # --- Auth ---
146
+ user_id = await _authenticate_websocket(websocket)
147
+ if user_id is None:
148
+ return
149
+
150
+ # --- Session validation ---
151
+ db = getattr(websocket.app.state, "db", None)
152
+ if db is None:
153
+ await websocket.close(code=1011, reason="Database unavailable")
154
+ return
155
+
156
+ session = await asyncio.to_thread(db.interactive_sessions.get, session_id)
157
+ if session is None or session.get("state") == "ended":
158
+ await websocket.close(code=4008, reason="Session not found or ended")
159
+ return
160
+
161
+ # --- Accept connection; everything after this point must run inside the
162
+ # try/finally so unregister() and close() always execute even if
163
+ # update_state, register, or get_token_queue raises. ---
164
+ await websocket.accept()
165
+
166
+ relay: Optional[asyncio.Task] = None
167
+ adapter_task: list[Optional[asyncio.Task]] = [None]
168
+
169
+ try:
170
+ await asyncio.to_thread(db.interactive_sessions.update_state, session_id, "active")
171
+ await session_chat_manager.register(session_id, websocket)
172
+
173
+ token_queue = await session_chat_manager.get_token_queue(session_id)
174
+
175
+ # ---- Relay task: token_queue → WebSocket ----------------------------
176
+ async def _relay() -> None:
177
+ """Forward adapter events to the client; persist cost on 'done'."""
178
+ turn_cost = {"cost_usd": 0.0, "input_tokens": 0, "output_tokens": 0}
179
+ try:
180
+ while True:
181
+ event = await token_queue.get()
182
+ event_type = event.get("type")
183
+
184
+ if event_type == "cost_update":
185
+ turn_cost["cost_usd"] += event.get("cost_usd", 0.0)
186
+ turn_cost["input_tokens"] += event.get("input_tokens", 0)
187
+ turn_cost["output_tokens"] += event.get("output_tokens", 0)
188
+ try:
189
+ await websocket.send_json(event)
190
+ except Exception as exc:
191
+ logger.warning(
192
+ "session_id=%s send_json(cost_update) failed: %s", session_id, exc
193
+ )
194
+ return
195
+
196
+ elif event_type == "done":
197
+ # Persist cost BEFORE sending "done" so clients that
198
+ # immediately fetch session stats observe accurate totals.
199
+ if (
200
+ turn_cost["cost_usd"]
201
+ or turn_cost["input_tokens"]
202
+ or turn_cost["output_tokens"]
203
+ ):
204
+ try:
205
+ await asyncio.to_thread(
206
+ db.interactive_sessions.update_cost,
207
+ session_id,
208
+ turn_cost["cost_usd"],
209
+ turn_cost["input_tokens"],
210
+ turn_cost["output_tokens"],
211
+ )
212
+ except Exception as exc:
213
+ logger.error(
214
+ "session_id=%s update_cost failed: %s turn_cost=%s",
215
+ session_id,
216
+ exc,
217
+ turn_cost,
218
+ )
219
+ turn_cost = {"cost_usd": 0.0, "input_tokens": 0, "output_tokens": 0}
220
+ try:
221
+ await websocket.send_json(event)
222
+ except Exception as exc:
223
+ logger.warning(
224
+ "session_id=%s send_json(done) failed: %s", session_id, exc
225
+ )
226
+ return
227
+
228
+ else:
229
+ try:
230
+ await websocket.send_json(event)
231
+ except Exception as exc:
232
+ logger.warning(
233
+ "session_id=%s send_json(%s) failed: %s",
234
+ session_id,
235
+ event_type,
236
+ exc,
237
+ )
238
+ return
239
+ finally:
240
+ # Flush any cost accumulated during a cancelled/aborted turn
241
+ if (
242
+ turn_cost["cost_usd"]
243
+ or turn_cost["input_tokens"]
244
+ or turn_cost["output_tokens"]
245
+ ):
246
+ try:
247
+ await asyncio.to_thread(
248
+ db.interactive_sessions.update_cost,
249
+ session_id,
250
+ turn_cost["cost_usd"],
251
+ turn_cost["input_tokens"],
252
+ turn_cost["output_tokens"],
253
+ )
254
+ except Exception as exc:
255
+ logger.error(
256
+ "session_id=%s relay finally update_cost failed: %s", session_id, exc
257
+ )
258
+
259
+ # ---- Receive task: WebSocket → dispatch -----------------------------
260
+ async def _receive() -> None:
261
+ """Read client messages and dispatch to ping/interrupt/message handlers."""
262
+ while True:
263
+ try:
264
+ raw = await websocket.receive_text()
265
+ except WebSocketDisconnect:
266
+ raise
267
+
268
+ try:
269
+ msg = json.loads(raw)
270
+ except json.JSONDecodeError:
271
+ try:
272
+ await websocket.send_json({"type": "error", "message": "Invalid JSON"})
273
+ except Exception:
274
+ pass
275
+ continue
276
+
277
+ msg_type = msg.get("type")
278
+
279
+ if msg_type == "ping":
280
+ await websocket.send_json({"type": "pong"})
281
+
282
+ elif msg_type == "interrupt":
283
+ await session_chat_manager.signal_interrupt(session_id)
284
+
285
+ elif msg_type == "message":
286
+ content = msg.get("content", "")
287
+
288
+ # Cancel any in-flight adapter
289
+ if adapter_task[0] and not adapter_task[0].done():
290
+ adapter_task[0].cancel()
291
+ try:
292
+ await adapter_task[0]
293
+ except (asyncio.CancelledError, Exception) as exc:
294
+ logger.debug(
295
+ "session_id=%s adapter cancelled: %s", session_id, exc
296
+ )
297
+
298
+ # Reset interrupt and drain stale queue items
299
+ await session_chat_manager.reset_interrupt(session_id)
300
+ while not token_queue.empty():
301
+ try:
302
+ token_queue.get_nowait()
303
+ except asyncio.QueueEmpty:
304
+ break
305
+
306
+ interrupt_event = await session_chat_manager.get_interrupt_event(session_id)
307
+ raw_workspace = session.get("workspace_path")
308
+ if not raw_workspace:
309
+ logger.warning(
310
+ "session_id=%s has no workspace_path set; "
311
+ "file tools will scope to server CWD",
312
+ session_id,
313
+ )
314
+ workspace_path = Path(raw_workspace or ".")
315
+ adapter_task[0] = asyncio.create_task(
316
+ _run_streaming_adapter(
317
+ session_id,
318
+ content,
319
+ token_queue,
320
+ interrupt_event,
321
+ db.interactive_sessions,
322
+ workspace_path,
323
+ )
324
+ )
325
+
326
+ relay = asyncio.create_task(_relay())
327
+ await _receive()
328
+
329
+ except WebSocketDisconnect:
330
+ logger.debug("Session chat WebSocket disconnected: session_id=%s", session_id)
331
+ except Exception as exc:
332
+ logger.error("Session chat WebSocket error: %s", exc, exc_info=True)
333
+ try:
334
+ await websocket.send_json({"type": "error", "message": str(exc)})
335
+ except Exception:
336
+ pass
337
+ finally:
338
+ if relay is not None:
339
+ relay.cancel()
340
+ try:
341
+ await relay
342
+ except (asyncio.CancelledError, Exception):
343
+ pass
344
+ if adapter_task[0] and not adapter_task[0].done():
345
+ adapter_task[0].cancel()
346
+ try:
347
+ await adapter_task[0]
348
+ except (asyncio.CancelledError, Exception) as exc:
349
+ logger.debug("session_id=%s adapter cleanup: %s", session_id, exc)
350
+ await session_chat_manager.unregister(session_id, websocket)
351
+ try:
352
+ await websocket.close()
353
+ except Exception:
354
+ pass