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.
- htmlgraph/__init__.py +23 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/cli.py +3 -3
- htmlgraph/analytics/cost_analyzer.py +5 -1
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cross_session.py +13 -9
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +15 -11
- htmlgraph/analytics_index.py +2 -1
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +167 -62
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/builders/base.py +2 -1
- htmlgraph/builders/bug.py +2 -1
- htmlgraph/builders/chore.py +2 -1
- htmlgraph/builders/epic.py +2 -1
- htmlgraph/builders/feature.py +2 -1
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +2 -1
- htmlgraph/builders/spike.py +2 -1
- htmlgraph/builders/track.py +2 -1
- htmlgraph/cli/analytics.py +2 -1
- htmlgraph/cli/base.py +2 -1
- htmlgraph/cli/core.py +2 -1
- htmlgraph/cli/main.py +2 -1
- htmlgraph/cli/models.py +2 -1
- htmlgraph/cli/templates/cost_dashboard.py +2 -1
- htmlgraph/cli/work/__init__.py +2 -1
- htmlgraph/cli/work/browse.py +2 -1
- htmlgraph/cli/work/features.py +2 -1
- htmlgraph/cli/work/orchestration.py +2 -1
- htmlgraph/cli/work/report.py +2 -1
- htmlgraph/cli/work/sessions.py +2 -1
- htmlgraph/cli/work/snapshot.py +2 -1
- htmlgraph/cli/work/tracks.py +2 -1
- htmlgraph/collections/base.py +10 -5
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +12 -7
- htmlgraph/collections/spike.py +6 -1
- htmlgraph/collections/task_delegation.py +7 -2
- htmlgraph/collections/todo.py +2 -1
- htmlgraph/collections/traces.py +15 -10
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/db/schema.py +67 -6
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/edge_index.py +2 -1
- htmlgraph/event_log.py +83 -64
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +6 -2
- htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
- htmlgraph/hooks/drift_handler.py +3 -3
- htmlgraph/hooks/event_tracker.py +40 -61
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +4 -0
- htmlgraph/hooks/orchestrator_reflector.py +4 -0
- htmlgraph/hooks/post_tool_use_failure.py +7 -3
- htmlgraph/hooks/posttooluse.py +4 -0
- htmlgraph/hooks/prompt_analyzer.py +5 -5
- htmlgraph/hooks/session_handler.py +2 -1
- htmlgraph/hooks/session_summary.py +6 -2
- htmlgraph/hooks/validator.py +8 -4
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +2 -1
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/operations/analytics.py +2 -1
- htmlgraph/operations/bootstrap.py +2 -1
- htmlgraph/operations/events.py +2 -1
- htmlgraph/operations/fastapi_server.py +2 -1
- htmlgraph/operations/hooks.py +2 -1
- htmlgraph/operations/initialization.py +2 -1
- htmlgraph/operations/server.py +2 -1
- htmlgraph/orchestration/claude_launcher.py +23 -20
- htmlgraph/orchestration/command_builder.py +2 -1
- htmlgraph/orchestration/headless_spawner.py +6 -2
- htmlgraph/orchestration/model_selection.py +7 -3
- htmlgraph/orchestration/plugin_manager.py +24 -19
- htmlgraph/orchestration/spawners/claude.py +5 -2
- htmlgraph/orchestration/spawners/codex.py +12 -19
- htmlgraph/orchestration/spawners/copilot.py +13 -18
- htmlgraph/orchestration/spawners/gemini.py +12 -19
- htmlgraph/orchestration/subprocess_runner.py +6 -3
- htmlgraph/orchestration/task_coordination.py +16 -8
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/parallel.py +2 -1
- htmlgraph/query_builder.py +2 -1
- htmlgraph/reflection.py +2 -1
- htmlgraph/refs.py +2 -1
- htmlgraph/repo_hash.py +2 -1
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +21 -17
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/handoff.py +4 -3
- htmlgraph/system_prompts.py +2 -1
- htmlgraph/track_builder.py +2 -1
- htmlgraph/transcript.py +2 -1
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/METADATA +1 -1
- htmlgraph-0.27.1.dist-info/RECORD +332 -0
- htmlgraph/sdk.py +0 -3500
- htmlgraph-0.26.25.dist-info/RECORD +0 -274
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
-
|
|
810
|
-
|
|
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
|
-
|
|
855
|
-
|
|
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
|
-
|
|
906
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
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
|
|
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
|
-
|
|
1355
|
+
created_at,
|
|
1326
1356
|
status,
|
|
1327
1357
|
start_commit,
|
|
1328
|
-
|
|
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
|
|
1368
|
+
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
1339
1369
|
params.extend([limit, offset])
|
|
1340
1370
|
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
|
|
1352
|
-
|
|
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],
|
|
1364
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
1420
|
-
|
|
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
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
-
|
|
2211
|
-
#
|
|
2212
|
-
|
|
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
|
|
2350
|
+
for event_row in rows_list:
|
|
2247
2351
|
# Parse context JSON if present
|
|
2248
2352
|
context_data = {}
|
|
2249
|
-
if
|
|
2353
|
+
if event_row[12]: # context column
|
|
2250
2354
|
try:
|
|
2251
|
-
context_data = json.loads(
|
|
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":
|
|
2258
|
-
"agent_id":
|
|
2259
|
-
"event_type":
|
|
2260
|
-
"timestamp":
|
|
2261
|
-
"tool_name":
|
|
2262
|
-
"input_summary":
|
|
2263
|
-
"output_summary":
|
|
2264
|
-
"session_id":
|
|
2265
|
-
"status":
|
|
2266
|
-
"model":
|
|
2267
|
-
"parent_event_id":
|
|
2268
|
-
"execution_duration_seconds":
|
|
2269
|
-
"cost_tokens":
|
|
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
|
|
2387
|
-
|
|
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
|
|