htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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 (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,416 @@
1
+ """
2
+ WebSocket Integration for Real-Time Cost Alerts - Phase 3.1
3
+
4
+ Streams cost monitoring alerts to clients with:
5
+ - <1s latency (critical requirement)
6
+ - Real-time budget warnings
7
+ - Cost trajectory predictions
8
+ - Per-model and per-tool cost breakdowns
9
+
10
+ Integrates with:
11
+ - WebSocketManager for connection handling
12
+ - CostMonitor for alert generation
13
+ - EventSubscriptionFilter for cost-specific filtering
14
+ """
15
+
16
+ import asyncio
17
+ import json
18
+ import logging
19
+ from datetime import datetime, timezone
20
+ from typing import Any
21
+
22
+ from htmlgraph.analytics.cost_monitor import CostMonitor
23
+ from htmlgraph.api.websocket import EventSubscriptionFilter, WebSocketManager
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class CostAlertFilter(EventSubscriptionFilter):
29
+ """Extended filter for cost alert subscriptions."""
30
+
31
+ def __init__(
32
+ self,
33
+ session_id: str | None = None,
34
+ alert_types: list[str] | None = None,
35
+ min_severity: str = "info",
36
+ cost_threshold_usd: float | None = None,
37
+ ):
38
+ """
39
+ Initialize cost alert filter.
40
+
41
+ Args:
42
+ session_id: Filter by session
43
+ alert_types: Filter by alert types (e.g., ["budget_warning", "breach"])
44
+ min_severity: Minimum severity level ("info", "warning", "critical")
45
+ cost_threshold_usd: Alert on costs >= threshold
46
+ """
47
+ # Initialize parent with cost-specific event types
48
+ super().__init__(
49
+ event_types=["cost_alert"],
50
+ session_id=session_id,
51
+ statuses=["alert"],
52
+ )
53
+ self.alert_types = alert_types or [
54
+ "budget_warning",
55
+ "trajectory_overage",
56
+ "model_overage",
57
+ "breach",
58
+ ]
59
+ self.min_severity = min_severity
60
+ self.cost_threshold_usd = cost_threshold_usd
61
+
62
+ def matches_cost_alert(self, alert: dict[str, Any]) -> bool:
63
+ """Check if alert matches all cost-specific filters."""
64
+ # Alert type filter
65
+ if alert.get("alert_type") not in self.alert_types:
66
+ return False
67
+
68
+ # Severity filter (info < warning < critical)
69
+ severity_levels = {"info": 0, "warning": 1, "critical": 2}
70
+ alert_severity = severity_levels.get(alert.get("severity", "info"), 0)
71
+ min_level = severity_levels.get(self.min_severity, 0)
72
+ if alert_severity < min_level:
73
+ return False
74
+
75
+ # Cost threshold filter
76
+ if self.cost_threshold_usd:
77
+ if alert.get("current_cost_usd", 0) < self.cost_threshold_usd:
78
+ return False
79
+
80
+ return True
81
+
82
+
83
+ class CostAlertStreamManager:
84
+ """
85
+ Manages real-time cost alert streaming via WebSocket.
86
+
87
+ Features:
88
+ - <1s latency for alert delivery
89
+ - Per-client cost alert filtering
90
+ - Alert aggregation and deduplication
91
+ - Trajectory prediction streaming
92
+ - Cost breakdown updates
93
+ """
94
+
95
+ def __init__(
96
+ self,
97
+ websocket_manager: WebSocketManager,
98
+ cost_monitor: CostMonitor,
99
+ poll_interval_ms: float = 100.0,
100
+ alert_batch_size: int = 10,
101
+ ):
102
+ """
103
+ Initialize cost alert stream manager.
104
+
105
+ Args:
106
+ websocket_manager: WebSocketManager for connection handling
107
+ cost_monitor: CostMonitor for cost data
108
+ poll_interval_ms: How often to check for new alerts
109
+ alert_batch_size: Max alerts per batch
110
+ """
111
+ self.websocket_manager = websocket_manager
112
+ self.cost_monitor = cost_monitor
113
+ self.poll_interval_ms = poll_interval_ms / 1000.0 # Convert to seconds
114
+ self.alert_batch_size = alert_batch_size
115
+
116
+ # Track last seen alert timestamp per session
117
+ self.last_alert_timestamp: dict[str, str] = {}
118
+
119
+ async def stream_cost_alerts(
120
+ self, session_id: str, client_id: str, cost_alert_filter: CostAlertFilter
121
+ ) -> None:
122
+ """
123
+ Stream cost alerts to a connected client.
124
+
125
+ Maintains <1s latency by:
126
+ - Quick database queries (indexed on timestamp)
127
+ - Efficient batching
128
+ - Minimal processing per alert
129
+
130
+ Args:
131
+ session_id: Session ID
132
+ client_id: Client ID
133
+ cost_alert_filter: Subscription filter for alerts
134
+ """
135
+ if (
136
+ session_id not in self.websocket_manager.connections
137
+ or client_id not in self.websocket_manager.connections[session_id]
138
+ ):
139
+ logger.warning(f"Client not found: {session_id}/{client_id}")
140
+ return
141
+
142
+ client = self.websocket_manager.connections[session_id][client_id]
143
+ last_alert_time = self.last_alert_timestamp.get(
144
+ session_id, "1970-01-01T00:00:00Z"
145
+ )
146
+ consecutive_empty_polls = 0
147
+ max_empty_polls = 20
148
+
149
+ try:
150
+ while True:
151
+ try:
152
+ # Fetch new alerts since last poll (rapid query)
153
+ alerts = await self._fetch_new_alerts(
154
+ session_id, last_alert_time, cost_alert_filter
155
+ )
156
+
157
+ if alerts:
158
+ consecutive_empty_polls = 0
159
+ last_alert_time = alerts[-1]["timestamp"]
160
+ self.last_alert_timestamp[session_id] = last_alert_time
161
+
162
+ # Batch alerts for sending
163
+ for i in range(0, len(alerts), self.alert_batch_size):
164
+ batch = alerts[i : i + self.alert_batch_size]
165
+
166
+ message = {
167
+ "type": "cost_alerts",
168
+ "session_id": session_id,
169
+ "timestamp": datetime.now(timezone.utc).isoformat(),
170
+ "alerts": batch,
171
+ "count": len(batch),
172
+ }
173
+
174
+ # Send with timestamp for latency tracking
175
+ message["sent_at"] = datetime.now(timezone.utc).isoformat()
176
+
177
+ try:
178
+ await client.websocket.send_json(message)
179
+ client.events_sent += 1
180
+ client.bytes_sent += len(json.dumps(message))
181
+ self.websocket_manager.metrics[
182
+ "total_events_broadcast"
183
+ ] += 1
184
+ self.websocket_manager.metrics["total_bytes_sent"] += (
185
+ len(json.dumps(message))
186
+ )
187
+ except Exception as e:
188
+ logger.error(f"Failed to send alert: {e}")
189
+ return
190
+ else:
191
+ consecutive_empty_polls += 1
192
+
193
+ # Adaptive polling: increase interval if no alerts
194
+ if consecutive_empty_polls > max_empty_polls:
195
+ poll_interval = min(self.poll_interval_ms * 2, 1.0)
196
+ else:
197
+ poll_interval = self.poll_interval_ms
198
+
199
+ await asyncio.sleep(poll_interval)
200
+
201
+ except Exception as e:
202
+ logger.error(f"Error in cost alert streaming: {e}")
203
+ await asyncio.sleep(self.poll_interval_ms)
204
+
205
+ except asyncio.CancelledError:
206
+ logger.info(f"Cost alert stream cancelled for {session_id}/{client_id}")
207
+ except Exception as e:
208
+ logger.error(f"Unexpected error in cost alert stream: {e}")
209
+
210
+ async def _fetch_new_alerts(
211
+ self,
212
+ session_id: str,
213
+ since_timestamp: str,
214
+ cost_alert_filter: CostAlertFilter,
215
+ ) -> list[dict[str, Any]]:
216
+ """
217
+ Fetch new cost alerts for a session.
218
+
219
+ Optimized for <1s latency with indexed queries.
220
+
221
+ Args:
222
+ session_id: Session ID
223
+ since_timestamp: Only fetch alerts after this timestamp
224
+ cost_alert_filter: Filter for alert types
225
+
226
+ Returns:
227
+ List of alert dictionaries
228
+ """
229
+ conn = self.cost_monitor.connect()
230
+ cursor = conn.cursor()
231
+
232
+ try:
233
+ # Query with composite index on (session_id, timestamp DESC)
234
+ cursor.execute(
235
+ """
236
+ SELECT event_id, session_id, alert_type, message,
237
+ current_cost_usd, budget_usd, severity, timestamp
238
+ FROM cost_events
239
+ WHERE session_id = ? AND alert_type IS NOT NULL
240
+ AND timestamp > ?
241
+ ORDER BY timestamp ASC
242
+ LIMIT ?
243
+ """,
244
+ (session_id, since_timestamp, self.alert_batch_size * 2),
245
+ )
246
+
247
+ alerts = []
248
+ for row in cursor.fetchall():
249
+ alert_dict = {
250
+ "alert_id": row["event_id"],
251
+ "alert_type": row["alert_type"],
252
+ "session_id": row["session_id"],
253
+ "message": row["message"],
254
+ "current_cost_usd": row["current_cost_usd"],
255
+ "budget_usd": row["budget_usd"],
256
+ "severity": row["severity"],
257
+ "timestamp": row["timestamp"],
258
+ }
259
+
260
+ # Apply custom cost alert filters
261
+ if cost_alert_filter.matches_cost_alert(alert_dict):
262
+ alerts.append(alert_dict)
263
+
264
+ return alerts
265
+
266
+ except Exception as e:
267
+ logger.error(f"Error fetching alerts: {e}")
268
+ return []
269
+
270
+ async def stream_cost_breakdown(
271
+ self, session_id: str, client_id: str, update_interval_seconds: float = 5.0
272
+ ) -> None:
273
+ """
274
+ Stream cost breakdown updates to a client.
275
+
276
+ Sends periodic updates of cost by model, tool, and agent.
277
+
278
+ Args:
279
+ session_id: Session ID
280
+ client_id: Client ID
281
+ update_interval_seconds: How often to send updates
282
+ """
283
+ if (
284
+ session_id not in self.websocket_manager.connections
285
+ or client_id not in self.websocket_manager.connections[session_id]
286
+ ):
287
+ return
288
+
289
+ client = self.websocket_manager.connections[session_id][client_id]
290
+
291
+ try:
292
+ while True:
293
+ try:
294
+ # Get current cost breakdown
295
+ breakdown = self.cost_monitor.get_cost_breakdown(session_id)
296
+
297
+ message = {
298
+ "type": "cost_breakdown",
299
+ "session_id": session_id,
300
+ "timestamp": datetime.now(timezone.utc).isoformat(),
301
+ "by_model": breakdown.by_model,
302
+ "by_tool": breakdown.by_tool,
303
+ "by_agent": breakdown.by_agent,
304
+ "by_subagent_type": breakdown.by_subagent_type,
305
+ "total_cost_usd": breakdown.total_cost_usd,
306
+ "total_tokens": breakdown.total_tokens,
307
+ }
308
+
309
+ await client.websocket.send_json(message)
310
+ client.events_sent += 1
311
+ self.websocket_manager.metrics["total_events_broadcast"] += 1
312
+
313
+ except Exception as e:
314
+ logger.error(f"Error sending cost breakdown: {e}")
315
+ return
316
+
317
+ await asyncio.sleep(update_interval_seconds)
318
+
319
+ except asyncio.CancelledError:
320
+ logger.info(f"Cost breakdown stream cancelled for {session_id}/{client_id}")
321
+
322
+ async def stream_cost_trajectory(
323
+ self,
324
+ session_id: str,
325
+ client_id: str,
326
+ update_interval_seconds: float = 10.0,
327
+ lookback_minutes: int = 5,
328
+ ) -> None:
329
+ """
330
+ Stream cost trajectory predictions to a client.
331
+
332
+ Periodically recalculates cost trajectory and sends predictions.
333
+
334
+ Args:
335
+ session_id: Session ID
336
+ client_id: Client ID
337
+ update_interval_seconds: How often to update predictions
338
+ lookback_minutes: How far back to analyze
339
+ """
340
+ if (
341
+ session_id not in self.websocket_manager.connections
342
+ or client_id not in self.websocket_manager.connections[session_id]
343
+ ):
344
+ return
345
+
346
+ client = self.websocket_manager.connections[session_id][client_id]
347
+
348
+ try:
349
+ while True:
350
+ try:
351
+ # Get trajectory prediction
352
+ prediction = self.cost_monitor.predict_cost_trajectory(
353
+ session_id, lookback_minutes=lookback_minutes
354
+ )
355
+
356
+ message = {
357
+ "type": "cost_trajectory",
358
+ "session_id": session_id,
359
+ "timestamp": datetime.now(timezone.utc).isoformat(),
360
+ "prediction": prediction,
361
+ }
362
+
363
+ if prediction.get("prediction_available"):
364
+ await client.websocket.send_json(message)
365
+ client.events_sent += 1
366
+ self.websocket_manager.metrics["total_events_broadcast"] += 1
367
+
368
+ except Exception as e:
369
+ logger.error(f"Error sending cost trajectory: {e}")
370
+ return
371
+
372
+ await asyncio.sleep(update_interval_seconds)
373
+
374
+ except asyncio.CancelledError:
375
+ logger.info(
376
+ f"Cost trajectory stream cancelled for {session_id}/{client_id}"
377
+ )
378
+
379
+
380
+ async def create_cost_alert_subscription(
381
+ session_id: str,
382
+ client_id: str,
383
+ websocket_manager: WebSocketManager,
384
+ cost_monitor: CostMonitor,
385
+ alert_types: list[str] | None = None,
386
+ min_severity: str = "warning",
387
+ ) -> CostAlertStreamManager:
388
+ """
389
+ Create a cost alert subscription for a WebSocket client.
390
+
391
+ Factory function to set up cost alert streaming.
392
+
393
+ Args:
394
+ session_id: Session ID
395
+ client_id: Client ID
396
+ websocket_manager: WebSocketManager instance
397
+ cost_monitor: CostMonitor instance
398
+ alert_types: Which alert types to subscribe to
399
+ min_severity: Minimum alert severity
400
+
401
+ Returns:
402
+ CostAlertStreamManager instance
403
+ """
404
+ manager = CostAlertStreamManager(websocket_manager, cost_monitor)
405
+ filter_obj = CostAlertFilter(
406
+ session_id=session_id,
407
+ alert_types=alert_types,
408
+ min_severity=min_severity,
409
+ )
410
+
411
+ # Start streaming tasks
412
+ asyncio.create_task(manager.stream_cost_alerts(session_id, client_id, filter_obj))
413
+ asyncio.create_task(manager.stream_cost_breakdown(session_id, client_id))
414
+ asyncio.create_task(manager.stream_cost_trajectory(session_id, client_id))
415
+
416
+ return manager