htmlgraph 0.26.25__py3-none-any.whl → 0.27.1__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 (175) hide show
  1. htmlgraph/__init__.py +23 -1
  2. htmlgraph/__init__.pyi +123 -0
  3. htmlgraph/agent_registry.py +2 -1
  4. htmlgraph/analytics/cli.py +3 -3
  5. htmlgraph/analytics/cost_analyzer.py +5 -1
  6. htmlgraph/analytics/cost_monitor.py +664 -0
  7. htmlgraph/analytics/cross_session.py +13 -9
  8. htmlgraph/analytics/dependency.py +10 -6
  9. htmlgraph/analytics/strategic/__init__.py +80 -0
  10. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  11. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  12. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  13. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  14. htmlgraph/analytics/work_type.py +15 -11
  15. htmlgraph/analytics_index.py +2 -1
  16. htmlgraph/api/cost_alerts_websocket.py +416 -0
  17. htmlgraph/api/main.py +167 -62
  18. htmlgraph/api/websocket.py +538 -0
  19. htmlgraph/attribute_index.py +2 -1
  20. htmlgraph/builders/base.py +2 -1
  21. htmlgraph/builders/bug.py +2 -1
  22. htmlgraph/builders/chore.py +2 -1
  23. htmlgraph/builders/epic.py +2 -1
  24. htmlgraph/builders/feature.py +2 -1
  25. htmlgraph/builders/insight.py +2 -1
  26. htmlgraph/builders/metric.py +2 -1
  27. htmlgraph/builders/pattern.py +2 -1
  28. htmlgraph/builders/phase.py +2 -1
  29. htmlgraph/builders/spike.py +2 -1
  30. htmlgraph/builders/track.py +2 -1
  31. htmlgraph/cli/analytics.py +2 -1
  32. htmlgraph/cli/base.py +2 -1
  33. htmlgraph/cli/core.py +2 -1
  34. htmlgraph/cli/main.py +2 -1
  35. htmlgraph/cli/models.py +2 -1
  36. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  37. htmlgraph/cli/work/__init__.py +2 -1
  38. htmlgraph/cli/work/browse.py +2 -1
  39. htmlgraph/cli/work/features.py +2 -1
  40. htmlgraph/cli/work/orchestration.py +2 -1
  41. htmlgraph/cli/work/report.py +2 -1
  42. htmlgraph/cli/work/sessions.py +2 -1
  43. htmlgraph/cli/work/snapshot.py +2 -1
  44. htmlgraph/cli/work/tracks.py +2 -1
  45. htmlgraph/collections/base.py +10 -5
  46. htmlgraph/collections/bug.py +2 -1
  47. htmlgraph/collections/chore.py +2 -1
  48. htmlgraph/collections/epic.py +2 -1
  49. htmlgraph/collections/feature.py +2 -1
  50. htmlgraph/collections/insight.py +2 -1
  51. htmlgraph/collections/metric.py +2 -1
  52. htmlgraph/collections/pattern.py +2 -1
  53. htmlgraph/collections/phase.py +2 -1
  54. htmlgraph/collections/session.py +12 -7
  55. htmlgraph/collections/spike.py +6 -1
  56. htmlgraph/collections/task_delegation.py +7 -2
  57. htmlgraph/collections/todo.py +2 -1
  58. htmlgraph/collections/traces.py +15 -10
  59. htmlgraph/config/cost_models.json +56 -0
  60. htmlgraph/context_analytics.py +2 -1
  61. htmlgraph/db/schema.py +67 -6
  62. htmlgraph/dependency_models.py +2 -1
  63. htmlgraph/edge_index.py +2 -1
  64. htmlgraph/event_log.py +83 -64
  65. htmlgraph/event_migration.py +2 -1
  66. htmlgraph/file_watcher.py +12 -8
  67. htmlgraph/find_api.py +2 -1
  68. htmlgraph/git_events.py +6 -2
  69. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  70. htmlgraph/hooks/drift_handler.py +3 -3
  71. htmlgraph/hooks/event_tracker.py +40 -61
  72. htmlgraph/hooks/installer.py +5 -1
  73. htmlgraph/hooks/orchestrator.py +4 -0
  74. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  75. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  76. htmlgraph/hooks/posttooluse.py +4 -0
  77. htmlgraph/hooks/prompt_analyzer.py +5 -5
  78. htmlgraph/hooks/session_handler.py +2 -1
  79. htmlgraph/hooks/session_summary.py +6 -2
  80. htmlgraph/hooks/validator.py +8 -4
  81. htmlgraph/ids.py +2 -1
  82. htmlgraph/learning.py +2 -1
  83. htmlgraph/mcp_server.py +2 -1
  84. htmlgraph/operations/analytics.py +2 -1
  85. htmlgraph/operations/bootstrap.py +2 -1
  86. htmlgraph/operations/events.py +2 -1
  87. htmlgraph/operations/fastapi_server.py +2 -1
  88. htmlgraph/operations/hooks.py +2 -1
  89. htmlgraph/operations/initialization.py +2 -1
  90. htmlgraph/operations/server.py +2 -1
  91. htmlgraph/orchestration/claude_launcher.py +23 -20
  92. htmlgraph/orchestration/command_builder.py +2 -1
  93. htmlgraph/orchestration/headless_spawner.py +6 -2
  94. htmlgraph/orchestration/model_selection.py +7 -3
  95. htmlgraph/orchestration/plugin_manager.py +24 -19
  96. htmlgraph/orchestration/spawners/claude.py +5 -2
  97. htmlgraph/orchestration/spawners/codex.py +12 -19
  98. htmlgraph/orchestration/spawners/copilot.py +13 -18
  99. htmlgraph/orchestration/spawners/gemini.py +12 -19
  100. htmlgraph/orchestration/subprocess_runner.py +6 -3
  101. htmlgraph/orchestration/task_coordination.py +16 -8
  102. htmlgraph/orchestrator.py +2 -1
  103. htmlgraph/parallel.py +2 -1
  104. htmlgraph/query_builder.py +2 -1
  105. htmlgraph/reflection.py +2 -1
  106. htmlgraph/refs.py +2 -1
  107. htmlgraph/repo_hash.py +2 -1
  108. htmlgraph/repositories/__init__.py +292 -0
  109. htmlgraph/repositories/analytics_repository.py +455 -0
  110. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  111. htmlgraph/repositories/feature_repository.py +581 -0
  112. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  113. htmlgraph/repositories/feature_repository_memory.py +607 -0
  114. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  115. htmlgraph/repositories/filter_service.py +620 -0
  116. htmlgraph/repositories/filter_service_standard.py +445 -0
  117. htmlgraph/repositories/shared_cache.py +621 -0
  118. htmlgraph/repositories/shared_cache_memory.py +395 -0
  119. htmlgraph/repositories/track_repository.py +552 -0
  120. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  121. htmlgraph/repositories/track_repository_memory.py +508 -0
  122. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  123. htmlgraph/sdk/__init__.py +398 -0
  124. htmlgraph/sdk/__init__.pyi +14 -0
  125. htmlgraph/sdk/analytics/__init__.py +19 -0
  126. htmlgraph/sdk/analytics/engine.py +155 -0
  127. htmlgraph/sdk/analytics/helpers.py +178 -0
  128. htmlgraph/sdk/analytics/registry.py +109 -0
  129. htmlgraph/sdk/base.py +484 -0
  130. htmlgraph/sdk/constants.py +216 -0
  131. htmlgraph/sdk/core.pyi +308 -0
  132. htmlgraph/sdk/discovery.py +120 -0
  133. htmlgraph/sdk/help/__init__.py +12 -0
  134. htmlgraph/sdk/help/mixin.py +699 -0
  135. htmlgraph/sdk/mixins/__init__.py +15 -0
  136. htmlgraph/sdk/mixins/attribution.py +113 -0
  137. htmlgraph/sdk/mixins/mixin.py +410 -0
  138. htmlgraph/sdk/operations/__init__.py +12 -0
  139. htmlgraph/sdk/operations/mixin.py +427 -0
  140. htmlgraph/sdk/orchestration/__init__.py +17 -0
  141. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  142. htmlgraph/sdk/orchestration/spawner.py +204 -0
  143. htmlgraph/sdk/planning/__init__.py +19 -0
  144. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  145. htmlgraph/sdk/planning/mixin.py +211 -0
  146. htmlgraph/sdk/planning/parallel.py +186 -0
  147. htmlgraph/sdk/planning/queue.py +210 -0
  148. htmlgraph/sdk/planning/recommendations.py +87 -0
  149. htmlgraph/sdk/planning/smart_planning.py +319 -0
  150. htmlgraph/sdk/session/__init__.py +19 -0
  151. htmlgraph/sdk/session/continuity.py +57 -0
  152. htmlgraph/sdk/session/handoff.py +110 -0
  153. htmlgraph/sdk/session/info.py +309 -0
  154. htmlgraph/sdk/session/manager.py +103 -0
  155. htmlgraph/sdk/strategic/__init__.py +26 -0
  156. htmlgraph/sdk/strategic/mixin.py +563 -0
  157. htmlgraph/server.py +21 -17
  158. htmlgraph/session_warning.py +2 -1
  159. htmlgraph/sessions/handoff.py +4 -3
  160. htmlgraph/system_prompts.py +2 -1
  161. htmlgraph/track_builder.py +2 -1
  162. htmlgraph/transcript.py +2 -1
  163. htmlgraph/watch.py +2 -1
  164. htmlgraph/work_type_utils.py +2 -1
  165. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/METADATA +1 -1
  166. htmlgraph-0.27.1.dist-info/RECORD +332 -0
  167. htmlgraph/sdk.py +0 -3500
  168. htmlgraph-0.26.25.dist-info/RECORD +0 -274
  169. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/dashboard.html +0 -0
  170. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/styles.css +0 -0
  171. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  172. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  173. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  174. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/WHEEL +0 -0
  175. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/entry_points.txt +0 -0
htmlgraph/api/main.py CHANGED
@@ -20,7 +20,7 @@ import logging
20
20
  import random
21
21
  import sqlite3
22
22
  import time
23
- from datetime import datetime
23
+ from datetime import datetime, timedelta
24
24
  from pathlib import Path
25
25
  from typing import Any
26
26
 
@@ -204,7 +204,37 @@ def get_app(db_path: str) -> FastAPI:
204
204
  return "0"
205
205
  return f"{value:,}"
206
206
 
207
+ def format_duration(seconds: float | int | None) -> str:
208
+ """Format duration in seconds to human-readable string."""
209
+ if seconds is None:
210
+ return "0.00s"
211
+ return f"{float(seconds):.2f}s"
212
+
213
+ def format_bytes(bytes_size: int | float | None) -> str:
214
+ """Format bytes to MB with 2 decimal places."""
215
+ if bytes_size is None:
216
+ return "0.00MB"
217
+ return f"{int(bytes_size) / (1024 * 1024):.2f}MB"
218
+
219
+ def truncate_text(text: str | None, length: int = 50) -> str:
220
+ """Truncate text to specified length with ellipsis."""
221
+ if text is None:
222
+ return ""
223
+ return text[:length] + "..." if len(text) > length else text
224
+
225
+ def format_timestamp(ts: Any) -> str:
226
+ """Format timestamp to readable string."""
227
+ if ts is None:
228
+ return ""
229
+ if hasattr(ts, "strftime"):
230
+ return str(ts.strftime("%Y-%m-%d %H:%M:%S"))
231
+ return str(ts)
232
+
207
233
  templates.env.filters["format_number"] = format_number
234
+ templates.env.filters["format_duration"] = format_duration
235
+ templates.env.filters["format_bytes"] = format_bytes
236
+ templates.env.filters["truncate"] = truncate_text
237
+ templates.env.filters["format_timestamp"] = format_timestamp
208
238
 
209
239
  # Setup static files
210
240
  static_dir = Path(__file__).parent / "static"
@@ -287,8 +317,8 @@ def get_app(db_path: str) -> FastAPI:
287
317
 
288
318
  # Execute query with timing
289
319
  exec_start = time.time()
290
- cursor = await db.execute(query)
291
- rows = await cursor.fetchall()
320
+ async with db.execute(query) as cursor:
321
+ rows = await cursor.fetchall()
292
322
  exec_time_ms = (time.time() - exec_start) * 1000
293
323
 
294
324
  agents = []
@@ -434,8 +464,8 @@ def get_app(db_path: str) -> FastAPI:
434
464
 
435
465
  # Execute query with timing
436
466
  exec_start = time.time()
437
- cursor = await db.execute(query, params)
438
- rows = await cursor.fetchall()
467
+ async with db.execute(query, params) as cursor:
468
+ rows = await cursor.fetchall()
439
469
  exec_time_ms = (time.time() - exec_start) * 1000
440
470
 
441
471
  # Build result models
@@ -485,15 +515,15 @@ def get_app(db_path: str) -> FastAPI:
485
515
  (SELECT COUNT(DISTINCT agent_id) FROM agent_events) as total_agents,
486
516
  (SELECT COUNT(*) FROM sessions) as total_sessions
487
517
  """
488
- cursor = await db.execute(stats_query)
489
- row = await cursor.fetchone()
518
+ async with db.execute(stats_query) as cursor:
519
+ row = await cursor.fetchone()
490
520
 
491
521
  # Query distinct agent IDs for the agent set
492
522
  agents_query = (
493
523
  "SELECT DISTINCT agent_id FROM agent_events WHERE agent_id IS NOT NULL"
494
524
  )
495
- agents_cursor = await db.execute(agents_query)
496
- agents_rows = await agents_cursor.fetchall()
525
+ async with db.execute(agents_query) as agents_cursor:
526
+ agents_rows = await agents_cursor.fetchall()
497
527
  agents = [row[0] for row in agents_rows]
498
528
 
499
529
  if row is None:
@@ -616,8 +646,8 @@ def get_app(db_path: str) -> FastAPI:
616
646
  parent_query += " ORDER BY timestamp DESC LIMIT ?"
617
647
  parent_params.append(limit)
618
648
 
619
- cursor = await db.execute(parent_query, parent_params)
620
- parent_rows = await cursor.fetchall()
649
+ async with db.execute(parent_query, parent_params) as cursor:
650
+ parent_rows = await cursor.fetchall()
621
651
 
622
652
  traces: list[dict[str, Any]] = []
623
653
 
@@ -655,8 +685,8 @@ def get_app(db_path: str) -> FastAPI:
655
685
  WHERE parent_event_id = ?
656
686
  ORDER BY timestamp ASC
657
687
  """
658
- child_cursor = await db.execute(child_query, (parent_event_id,))
659
- child_rows = await child_cursor.fetchall()
688
+ async with db.execute(child_query, (parent_event_id,)) as child_cursor:
689
+ child_rows = await child_cursor.fetchall()
660
690
 
661
691
  child_events = []
662
692
  for child_row in child_rows:
@@ -806,8 +836,8 @@ def get_app(db_path: str) -> FastAPI:
806
836
  params.append(limit)
807
837
 
808
838
  exec_start = time.time()
809
- cursor = await db.execute(query, params)
810
- rows = await cursor.fetchall()
839
+ async with db.execute(query, params) as cursor:
840
+ rows = await cursor.fetchall()
811
841
 
812
842
  for row in rows:
813
843
  events.append(
@@ -851,8 +881,8 @@ def get_app(db_path: str) -> FastAPI:
851
881
  spike_query += " ORDER BY created_at DESC LIMIT ?"
852
882
  spike_params.append(limit)
853
883
 
854
- spike_cursor = await db.execute(spike_query, spike_params)
855
- spike_rows = await spike_cursor.fetchall()
884
+ async with db.execute(spike_query, spike_params) as spike_cursor:
885
+ spike_rows = await spike_cursor.fetchall()
856
886
 
857
887
  for row in spike_rows:
858
888
  events.append(
@@ -902,8 +932,8 @@ def get_app(db_path: str) -> FastAPI:
902
932
  collab_query += " ORDER BY timestamp DESC LIMIT ?"
903
933
  collab_params.append(limit)
904
934
 
905
- collab_cursor = await db.execute(collab_query, collab_params)
906
- collab_rows = await collab_cursor.fetchall()
935
+ async with db.execute(collab_query, collab_params) as collab_cursor:
936
+ collab_rows = await collab_cursor.fetchall()
907
937
 
908
938
  for row in collab_rows:
909
939
  events.append(
@@ -1016,8 +1046,8 @@ def get_app(db_path: str) -> FastAPI:
1016
1046
  LIMIT ?
1017
1047
  """
1018
1048
 
1019
- cursor = await db.execute(user_query_query, [limit])
1020
- user_query_rows = await cursor.fetchall()
1049
+ async with db.execute(user_query_query, [limit]) as cursor:
1050
+ user_query_rows = await cursor.fetchall()
1021
1051
 
1022
1052
  conversation_turns: list[dict[str, Any]] = []
1023
1053
 
@@ -1061,8 +1091,8 @@ def get_app(db_path: str) -> FastAPI:
1061
1091
  if depth >= max_depth:
1062
1092
  return [], 0.0, 0, 0
1063
1093
 
1064
- cursor = await db.execute(children_query, [parent_id])
1065
- rows = await cursor.fetchall()
1094
+ async with db.execute(children_query, [parent_id]) as cursor:
1095
+ rows = await cursor.fetchall()
1066
1096
 
1067
1097
  children_list: list[dict[str, Any]] = []
1068
1098
  total_dur = 0.0
@@ -1316,16 +1346,16 @@ def get_app(db_path: str) -> FastAPI:
1316
1346
  exec_start = time.time()
1317
1347
 
1318
1348
  # Build query with optional status filter
1319
- # Note: Database uses agent_assigned but started_at/ended_at (partial migration)
1349
+ # Note: Database uses agent_assigned, created_at, and completed_at
1320
1350
  query = """
1321
1351
  SELECT
1322
1352
  session_id,
1323
1353
  agent_assigned,
1324
1354
  continued_from,
1325
- started_at,
1355
+ created_at,
1326
1356
  status,
1327
1357
  start_commit,
1328
- ended_at
1358
+ completed_at
1329
1359
  FROM sessions
1330
1360
  WHERE 1=1
1331
1361
  """
@@ -1335,11 +1365,11 @@ def get_app(db_path: str) -> FastAPI:
1335
1365
  query += " AND status = ?"
1336
1366
  params.append(status)
1337
1367
 
1338
- query += " ORDER BY started_at DESC LIMIT ? OFFSET ?"
1368
+ query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
1339
1369
  params.extend([limit, offset])
1340
1370
 
1341
- cursor = await db.execute(query, params)
1342
- rows = await cursor.fetchall()
1371
+ async with db.execute(query, params) as cursor:
1372
+ rows = await cursor.fetchall()
1343
1373
 
1344
1374
  # Get total count for pagination
1345
1375
  count_query = "SELECT COUNT(*) FROM sessions WHERE 1=1"
@@ -1348,8 +1378,8 @@ def get_app(db_path: str) -> FastAPI:
1348
1378
  count_query += " AND status = ?"
1349
1379
  count_params.append(status)
1350
1380
 
1351
- count_cursor = await db.execute(count_query, count_params)
1352
- count_row = await count_cursor.fetchone()
1381
+ async with db.execute(count_query, count_params) as count_cursor:
1382
+ count_row = await count_cursor.fetchone()
1353
1383
  total = int(count_row[0]) if count_row else 0
1354
1384
 
1355
1385
  # Build session objects
@@ -1360,11 +1390,11 @@ def get_app(db_path: str) -> FastAPI:
1360
1390
  {
1361
1391
  "session_id": row[0],
1362
1392
  "agent": row[1], # agent_assigned -> agent for API compat
1363
- "continued_from": row[2], # parent_session_id
1364
- "started_at": row[3], # created_at -> started_at for API compat
1393
+ "continued_from": row[2],
1394
+ "created_at": row[3], # created_at timestamp
1365
1395
  "status": row[4] or "unknown",
1366
1396
  "start_commit": row[5],
1367
- "ended_at": row[6], # completed_at -> ended_at for API compat
1397
+ "completed_at": row[6], # completed_at timestamp
1368
1398
  }
1369
1399
  )
1370
1400
 
@@ -1416,8 +1446,8 @@ def get_app(db_path: str) -> FastAPI:
1416
1446
  LIMIT 50
1417
1447
  """
1418
1448
 
1419
- cursor = await db.execute(query)
1420
- rows = list(await cursor.fetchall())
1449
+ async with db.execute(query) as cursor:
1450
+ rows = list(await cursor.fetchall())
1421
1451
  logger.debug(f"orchestration_view: Query executed, got {len(rows)} rows")
1422
1452
 
1423
1453
  delegations = []
@@ -2193,23 +2223,97 @@ def get_app(db_path: str) -> FastAPI:
2193
2223
  # ========== WEBSOCKET FOR REAL-TIME UPDATES ==========
2194
2224
 
2195
2225
  @app.websocket("/ws/events")
2196
- async def websocket_events(websocket: WebSocket) -> None:
2226
+ async def websocket_events(websocket: WebSocket, since: str | None = None) -> None:
2197
2227
  """WebSocket endpoint for real-time event streaming.
2198
2228
 
2199
2229
  OPTIMIZATION: Uses timestamp-based filtering to minimize data transfers.
2200
2230
  The timestamp > ? filter with DESC index makes queries O(log n) instead of O(n).
2201
2231
 
2202
- IMPORTANT: Initializes last_timestamp to current time to only stream NEW events.
2203
- Historical events are already counted in /api/initial-stats, so streaming them
2204
- again would cause double-counting in the header stats.
2232
+ FIX 3: Now supports loading historical events via 'since' parameter.
2233
+ - If 'since' provided: Load events from that timestamp onwards
2234
+ - If 'since' not provided: Load events from last 1 hour (default)
2235
+ - After historical load: Continue streaming real-time events
2205
2236
 
2206
2237
  LIVE EVENTS: Also polls live_events table for real-time spawner activity
2207
2238
  streaming. These events are marked as broadcast after sending and cleaned up.
2239
+
2240
+ Args:
2241
+ since: Optional ISO timestamp to start streaming from (e.g., "2025-01-16T10:00:00")
2242
+ If not provided, defaults to 1 hour ago
2208
2243
  """
2209
2244
  await websocket.accept()
2210
- # Initialize to current time - only stream events created AFTER connection
2211
- # This prevents double-counting: initial-stats already includes historical events
2212
- last_timestamp: str = datetime.now().isoformat()
2245
+
2246
+ # FIX 3: Determine starting timestamp
2247
+ if since:
2248
+ try:
2249
+ # Validate timestamp format
2250
+ datetime.fromisoformat(since.replace("Z", "+00:00"))
2251
+ last_timestamp = since
2252
+ except (ValueError, AttributeError):
2253
+ # Invalid timestamp - default to 1 hour ago
2254
+ last_timestamp = (datetime.now() - timedelta(hours=1)).isoformat()
2255
+ else:
2256
+ # Default: Load events from last 1 hour
2257
+ last_timestamp = (datetime.now() - timedelta(hours=1)).isoformat()
2258
+
2259
+ # FIX 3: Load historical events first (before real-time streaming)
2260
+ db = await get_db()
2261
+ try:
2262
+ historical_query = """
2263
+ SELECT event_id, agent_id, event_type, timestamp, tool_name,
2264
+ input_summary, output_summary, session_id, status, model,
2265
+ parent_event_id, execution_duration_seconds, context,
2266
+ cost_tokens
2267
+ FROM agent_events
2268
+ WHERE timestamp >= ? AND timestamp < datetime('now')
2269
+ ORDER BY timestamp ASC
2270
+ LIMIT 1000
2271
+ """
2272
+ cursor = await db.execute(historical_query, [last_timestamp])
2273
+ historical_rows = await cursor.fetchall()
2274
+
2275
+ # Send historical events first
2276
+ if historical_rows:
2277
+ historical_rows_list = list(historical_rows)
2278
+ for row in historical_rows_list:
2279
+ row_list = list(row)
2280
+ # Parse context JSON if present
2281
+ context_data = {}
2282
+ if row_list[12]: # context column
2283
+ try:
2284
+ context_data = json.loads(row_list[12])
2285
+ except (json.JSONDecodeError, TypeError):
2286
+ pass
2287
+
2288
+ event_data = {
2289
+ "type": "event",
2290
+ "event_id": row_list[0],
2291
+ "agent_id": row_list[1] or "unknown",
2292
+ "event_type": row_list[2],
2293
+ "timestamp": row_list[3],
2294
+ "tool_name": row_list[4],
2295
+ "input_summary": row_list[5],
2296
+ "output_summary": row_list[6],
2297
+ "session_id": row_list[7],
2298
+ "status": row_list[8],
2299
+ "model": row_list[9],
2300
+ "parent_event_id": row_list[10],
2301
+ "execution_duration_seconds": row_list[11] or 0.0,
2302
+ "cost_tokens": row_list[13] or 0,
2303
+ "context": context_data,
2304
+ }
2305
+ await websocket.send_json(event_data)
2306
+
2307
+ # Update last_timestamp to last historical event
2308
+ last_timestamp = historical_rows_list[-1][3]
2309
+
2310
+ except Exception as e:
2311
+ logger.error(f"Error loading historical events: {e}")
2312
+ finally:
2313
+ await db.close()
2314
+
2315
+ # Update to current time for real-time streaming
2316
+ last_timestamp = datetime.now().isoformat()
2213
2317
  poll_interval = 0.5 # OPTIMIZATION: Adaptive polling (reduced from 1s)
2214
2318
  last_live_event_id = 0 # Track last broadcast live event ID
2215
2319
 
@@ -2238,35 +2342,35 @@ def get_app(db_path: str) -> FastAPI:
2238
2342
 
2239
2343
  if rows:
2240
2344
  has_activity = True
2241
- rows_list = [list(row) for row in rows]
2345
+ rows_list: list[list[Any]] = [list(row) for row in rows]
2242
2346
  # Update last timestamp (last row since ORDER BY ts ASC)
2243
2347
  last_timestamp = rows_list[-1][3]
2244
2348
 
2245
2349
  # Send events in order (no need to reverse with ASC)
2246
- for row in rows_list:
2350
+ for event_row in rows_list:
2247
2351
  # Parse context JSON if present
2248
2352
  context_data = {}
2249
- if row[12]: # context column
2353
+ if event_row[12]: # context column
2250
2354
  try:
2251
- context_data = json.loads(row[12])
2355
+ context_data = json.loads(event_row[12])
2252
2356
  except (json.JSONDecodeError, TypeError):
2253
2357
  pass
2254
2358
 
2255
2359
  event_data = {
2256
2360
  "type": "event",
2257
- "event_id": row[0],
2258
- "agent_id": row[1] or "unknown",
2259
- "event_type": row[2],
2260
- "timestamp": row[3],
2261
- "tool_name": row[4],
2262
- "input_summary": row[5],
2263
- "output_summary": row[6],
2264
- "session_id": row[7],
2265
- "status": row[8],
2266
- "model": row[9],
2267
- "parent_event_id": row[10],
2268
- "execution_duration_seconds": row[11] or 0.0,
2269
- "cost_tokens": row[13] or 0,
2361
+ "event_id": event_row[0],
2362
+ "agent_id": event_row[1] or "unknown",
2363
+ "event_type": event_row[2],
2364
+ "timestamp": event_row[3],
2365
+ "tool_name": event_row[4],
2366
+ "input_summary": event_row[5],
2367
+ "output_summary": event_row[6],
2368
+ "session_id": event_row[7],
2369
+ "status": event_row[8],
2370
+ "model": event_row[9],
2371
+ "parent_event_id": event_row[10],
2372
+ "execution_duration_seconds": event_row[11] or 0.0,
2373
+ "cost_tokens": event_row[13] or 0,
2270
2374
  "context": context_data,
2271
2375
  }
2272
2376
  await websocket.send_json(event_data)
@@ -2383,8 +2487,9 @@ def get_app(db_path: str) -> FastAPI:
2383
2487
  def create_app(db_path: str | None = None) -> FastAPI:
2384
2488
  """Create FastAPI app with default database path."""
2385
2489
  if db_path is None:
2386
- # Use default database location - htmlgraph.db is the unified database
2387
- db_path = str(Path.home() / ".htmlgraph" / "htmlgraph.db")
2490
+ # Use index.sqlite - this is where AnalyticsIndex writes events
2491
+ # Note: index.sqlite is the rebuildable analytics cache, not htmlgraph.db
2492
+ db_path = str(Path.home() / ".htmlgraph" / "index.sqlite")
2388
2493
 
2389
2494
  return get_app(db_path)
2390
2495