htmlgraph 0.9.3__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 (331) 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 +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,707 @@
1
+ """
2
+ Cross-Session Graph Queries - Session Relationship Index.
3
+
4
+ Provides indexed graph queries over the SQLite store for efficient
5
+ cross-session analytics. Replaces expensive linear scans of event logs
6
+ and git commands with O(log n) indexed lookups and recursive CTEs.
7
+
8
+ Key capabilities:
9
+ - sessions_for_feature: Find all sessions that touched a feature via index
10
+ - features_for_session: Find all features a session worked on
11
+ - delegation_chain: Follow parent_session_id links recursively
12
+ - handoff_path: Find path between sessions via handoffs
13
+ - feature_timeline: Chronological timeline of all work on a feature
14
+ - related_sessions: Find sessions related through shared features or delegation
15
+
16
+ Design:
17
+ - Uses SQLite indexes for O(log n) lookups instead of O(n) scans
18
+ - Recursive CTEs for delegation chain traversal
19
+ - Zero external dependencies (SQLite only)
20
+ - Works with existing HtmlGraphDB schema
21
+
22
+ Example:
23
+ from htmlgraph.db.schema import HtmlGraphDB
24
+ from htmlgraph.analytics.session_graph import SessionGraph
25
+
26
+ db = HtmlGraphDB(db_path="path/to/htmlgraph.db")
27
+ graph = SessionGraph(db)
28
+
29
+ # Find all sessions that worked on a feature
30
+ sessions = graph.sessions_for_feature("feat-abc123")
31
+
32
+ # Follow delegation chain
33
+ chain = graph.delegation_chain("session-xyz")
34
+
35
+ # Build feature timeline
36
+ timeline = graph.feature_timeline("feat-abc123")
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import logging
42
+ import sqlite3
43
+ from collections import deque
44
+ from dataclasses import dataclass, field
45
+ from datetime import datetime
46
+ from typing import TYPE_CHECKING
47
+
48
+ if TYPE_CHECKING:
49
+ from htmlgraph.db.schema import HtmlGraphDB
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+
54
+ @dataclass
55
+ class SessionNode:
56
+ """A node in the session graph representing a single session."""
57
+
58
+ session_id: str
59
+ agent: str
60
+ status: str
61
+ created_at: datetime
62
+ features_worked_on: list[str] = field(default_factory=list)
63
+ parent_session_id: str | None = None
64
+ depth: int = 0
65
+
66
+
67
+ @dataclass
68
+ class FeatureEvent:
69
+ """A single event related to a feature across any session."""
70
+
71
+ session_id: str
72
+ agent: str
73
+ timestamp: datetime
74
+ event_type: str
75
+ tool_name: str | None = None
76
+ summary: str | None = None
77
+
78
+
79
+ class SessionGraph:
80
+ """
81
+ Property-graph view over SQLite tables for cross-session queries.
82
+
83
+ Provides indexed lookups and recursive traversals over the session,
84
+ event, and handoff tables in the HtmlGraph SQLite database.
85
+ """
86
+
87
+ def __init__(self, db: HtmlGraphDB) -> None:
88
+ """
89
+ Initialize SessionGraph with database reference.
90
+
91
+ Args:
92
+ db: HtmlGraphDB instance with active connection
93
+ """
94
+ self.db = db
95
+
96
+ def ensure_indexes(self) -> None:
97
+ """
98
+ Create optimized indexes for cross-session graph queries.
99
+
100
+ These indexes supplement the existing schema indexes with
101
+ composite indexes specifically designed for graph traversal
102
+ patterns. Safe to call multiple times (idempotent).
103
+ """
104
+ if not self.db.connection:
105
+ self.db.connect()
106
+
107
+ cursor = self.db.connection.cursor() # type: ignore[union-attr]
108
+
109
+ indexes = [
110
+ # Feature -> Session mapping (sessions_for_feature)
111
+ "CREATE INDEX IF NOT EXISTS idx_events_feature_session "
112
+ "ON agent_events(feature_id, session_id)",
113
+ # Session -> Feature mapping (features_for_session)
114
+ "CREATE INDEX IF NOT EXISTS idx_events_session_feature "
115
+ "ON agent_events(session_id, feature_id)",
116
+ # Delegation chain traversal
117
+ "CREATE INDEX IF NOT EXISTS idx_sessions_parent "
118
+ "ON sessions(parent_session_id)",
119
+ # Continuation chain traversal
120
+ "CREATE INDEX IF NOT EXISTS idx_sessions_continued "
121
+ "ON sessions(continued_from)",
122
+ # Handoff from-session lookups
123
+ "CREATE INDEX IF NOT EXISTS idx_handoff_from "
124
+ "ON handoff_tracking(from_session_id)",
125
+ # Handoff to-session lookups
126
+ "CREATE INDEX IF NOT EXISTS idx_handoff_to "
127
+ "ON handoff_tracking(to_session_id)",
128
+ ]
129
+
130
+ for index_sql in indexes:
131
+ try:
132
+ cursor.execute(index_sql)
133
+ except sqlite3.OperationalError as e:
134
+ logger.warning(f"Index creation warning: {e}")
135
+
136
+ self.db.connection.commit() # type: ignore[union-attr]
137
+
138
+ def sessions_for_feature(self, feature_id: str) -> list[SessionNode]:
139
+ """
140
+ Find all sessions that touched a feature - O(log n) via index.
141
+
142
+ Uses the idx_events_feature_session index for fast lookup
143
+ instead of scanning all events linearly.
144
+
145
+ Args:
146
+ feature_id: Feature ID to query
147
+
148
+ Returns:
149
+ List of SessionNode objects for sessions that worked on this feature
150
+ """
151
+ if not self.db.connection:
152
+ self.db.connect()
153
+
154
+ try:
155
+ cursor = self.db.connection.cursor() # type: ignore[union-attr]
156
+ cursor.execute(
157
+ """
158
+ SELECT DISTINCT
159
+ s.session_id,
160
+ s.agent_assigned,
161
+ s.status,
162
+ s.created_at,
163
+ s.parent_session_id,
164
+ s.features_worked_on
165
+ FROM agent_events ae
166
+ JOIN sessions s ON ae.session_id = s.session_id
167
+ WHERE ae.feature_id = ?
168
+ ORDER BY s.created_at ASC
169
+ """,
170
+ (feature_id,),
171
+ )
172
+
173
+ nodes = []
174
+ for row in cursor.fetchall():
175
+ row_dict = dict(row)
176
+ features = self._parse_features_list(row_dict.get("features_worked_on"))
177
+ if feature_id not in features:
178
+ features.append(feature_id)
179
+
180
+ nodes.append(
181
+ SessionNode(
182
+ session_id=row_dict["session_id"],
183
+ agent=row_dict["agent_assigned"],
184
+ status=row_dict["status"],
185
+ created_at=self._parse_datetime(row_dict["created_at"]),
186
+ features_worked_on=features,
187
+ parent_session_id=row_dict.get("parent_session_id"),
188
+ depth=0,
189
+ )
190
+ )
191
+
192
+ return nodes
193
+
194
+ except sqlite3.Error as e:
195
+ logger.error(f"Error querying sessions for feature: {e}")
196
+ return []
197
+
198
+ def features_for_session(self, session_id: str) -> list[str]:
199
+ """
200
+ Find all features a session worked on.
201
+
202
+ Uses the idx_events_session_feature index for fast lookup.
203
+
204
+ Args:
205
+ session_id: Session ID to query
206
+
207
+ Returns:
208
+ Sorted list of feature IDs
209
+ """
210
+ if not self.db.connection:
211
+ self.db.connect()
212
+
213
+ try:
214
+ cursor = self.db.connection.cursor() # type: ignore[union-attr]
215
+ cursor.execute(
216
+ """
217
+ SELECT DISTINCT feature_id
218
+ FROM agent_events
219
+ WHERE session_id = ?
220
+ AND feature_id IS NOT NULL
221
+ ORDER BY feature_id
222
+ """,
223
+ (session_id,),
224
+ )
225
+
226
+ return [row["feature_id"] for row in cursor.fetchall()]
227
+
228
+ except sqlite3.Error as e:
229
+ logger.error(f"Error querying features for session: {e}")
230
+ return []
231
+
232
+ def delegation_chain(
233
+ self, session_id: str, max_depth: int = 10
234
+ ) -> list[SessionNode]:
235
+ """
236
+ Follow parent_session_id links to build delegation chain.
237
+
238
+ Uses a recursive CTE to efficiently traverse the delegation
239
+ tree upward from the given session to its root ancestor.
240
+
241
+ Args:
242
+ session_id: Starting session ID
243
+ max_depth: Maximum depth to traverse (default 10)
244
+
245
+ Returns:
246
+ List of SessionNode objects from the starting session
247
+ up to the root ancestor, ordered by depth (0 = starting session)
248
+ """
249
+ if not self.db.connection:
250
+ self.db.connect()
251
+
252
+ try:
253
+ cursor = self.db.connection.cursor() # type: ignore[union-attr]
254
+ cursor.execute(
255
+ """
256
+ WITH RECURSIVE chain AS (
257
+ SELECT session_id, parent_session_id, agent_assigned,
258
+ status, created_at, features_worked_on, 0 as depth
259
+ FROM sessions
260
+ WHERE session_id = ?
261
+
262
+ UNION ALL
263
+
264
+ SELECT s.session_id, s.parent_session_id, s.agent_assigned,
265
+ s.status, s.created_at, s.features_worked_on, c.depth + 1
266
+ FROM sessions s
267
+ JOIN chain c ON s.session_id = c.parent_session_id
268
+ WHERE c.depth < ?
269
+ )
270
+ SELECT * FROM chain
271
+ ORDER BY depth ASC
272
+ """,
273
+ (session_id, max_depth),
274
+ )
275
+
276
+ nodes = []
277
+ for row in cursor.fetchall():
278
+ row_dict = dict(row)
279
+ nodes.append(
280
+ SessionNode(
281
+ session_id=row_dict["session_id"],
282
+ agent=row_dict["agent_assigned"],
283
+ status=row_dict["status"],
284
+ created_at=self._parse_datetime(row_dict["created_at"]),
285
+ features_worked_on=self._parse_features_list(
286
+ row_dict.get("features_worked_on")
287
+ ),
288
+ parent_session_id=row_dict.get("parent_session_id"),
289
+ depth=row_dict["depth"],
290
+ )
291
+ )
292
+
293
+ return nodes
294
+
295
+ except sqlite3.Error as e:
296
+ logger.error(f"Error querying delegation chain: {e}")
297
+ return []
298
+
299
+ def handoff_path(
300
+ self, from_session: str, to_session: str, max_depth: int = 10
301
+ ) -> list[SessionNode] | None:
302
+ """
303
+ Find the path from one session to another via handoffs.
304
+
305
+ Performs a BFS over the handoff_tracking table to find the
306
+ shortest path between two sessions. Follows both from_session_id
307
+ and to_session_id links bidirectionally.
308
+
309
+ Args:
310
+ from_session: Starting session ID
311
+ to_session: Target session ID
312
+ max_depth: Maximum search depth (default 10)
313
+
314
+ Returns:
315
+ List of SessionNode objects forming the path, or None if no path exists
316
+ """
317
+ if from_session == to_session:
318
+ node = self._get_session_node(from_session, depth=0)
319
+ return [node] if node else None
320
+
321
+ if not self.db.connection:
322
+ self.db.connect()
323
+
324
+ try:
325
+ # BFS to find path through handoffs
326
+ visited: set[str] = set()
327
+ # Queue of (session_id, path_so_far)
328
+ queue: deque[tuple[str, list[str]]] = deque()
329
+ queue.append((from_session, [from_session]))
330
+ visited.add(from_session)
331
+
332
+ cursor = self.db.connection.cursor() # type: ignore[union-attr]
333
+
334
+ while queue:
335
+ current_id, path = queue.popleft()
336
+
337
+ if len(path) > max_depth + 1:
338
+ continue
339
+
340
+ # Find sessions reachable via handoffs from current
341
+ cursor.execute(
342
+ """
343
+ SELECT to_session_id FROM handoff_tracking
344
+ WHERE from_session_id = ? AND to_session_id IS NOT NULL
345
+ UNION
346
+ SELECT from_session_id FROM handoff_tracking
347
+ WHERE to_session_id = ?
348
+ """,
349
+ (current_id, current_id),
350
+ )
351
+
352
+ for row in cursor.fetchall():
353
+ neighbor_id = row[0]
354
+ if neighbor_id in visited:
355
+ continue
356
+
357
+ new_path = path + [neighbor_id]
358
+
359
+ if neighbor_id == to_session:
360
+ # Found the target - build SessionNode list
361
+ return self._build_path_nodes(new_path)
362
+
363
+ visited.add(neighbor_id)
364
+ queue.append((neighbor_id, new_path))
365
+
366
+ return None
367
+
368
+ except sqlite3.Error as e:
369
+ logger.error(f"Error finding handoff path: {e}")
370
+ return None
371
+
372
+ def feature_timeline(self, feature_id: str) -> list[FeatureEvent]:
373
+ """
374
+ Build chronological timeline of all work on a feature across sessions.
375
+
376
+ Queries agent_events for all events associated with the given
377
+ feature, ordered chronologically.
378
+
379
+ Args:
380
+ feature_id: Feature ID to query
381
+
382
+ Returns:
383
+ List of FeatureEvent objects in chronological order
384
+ """
385
+ if not self.db.connection:
386
+ self.db.connect()
387
+
388
+ try:
389
+ cursor = self.db.connection.cursor() # type: ignore[union-attr]
390
+ cursor.execute(
391
+ """
392
+ SELECT
393
+ ae.session_id,
394
+ s.agent_assigned,
395
+ ae.timestamp,
396
+ ae.event_type,
397
+ ae.tool_name,
398
+ ae.input_summary
399
+ FROM agent_events ae
400
+ JOIN sessions s ON ae.session_id = s.session_id
401
+ WHERE ae.feature_id = ?
402
+ ORDER BY ae.timestamp ASC
403
+ """,
404
+ (feature_id,),
405
+ )
406
+
407
+ events = []
408
+ for row in cursor.fetchall():
409
+ row_dict = dict(row)
410
+ events.append(
411
+ FeatureEvent(
412
+ session_id=row_dict["session_id"],
413
+ agent=row_dict["agent_assigned"],
414
+ timestamp=self._parse_datetime(row_dict["timestamp"]),
415
+ event_type=row_dict["event_type"],
416
+ tool_name=row_dict.get("tool_name"),
417
+ summary=row_dict.get("input_summary"),
418
+ )
419
+ )
420
+
421
+ return events
422
+
423
+ except sqlite3.Error as e:
424
+ logger.error(f"Error querying feature timeline: {e}")
425
+ return []
426
+
427
+ def related_sessions(
428
+ self, session_id: str, max_depth: int = 3
429
+ ) -> list[SessionNode]:
430
+ """
431
+ Find sessions related through shared features or delegation.
432
+
433
+ Performs a BFS starting from the given session, expanding via:
434
+ 1. Shared features (sessions that worked on the same features)
435
+ 2. Delegation links (parent_session_id chains)
436
+ 3. Continuation links (continued_from chains)
437
+
438
+ Args:
439
+ session_id: Starting session ID
440
+ max_depth: Maximum traversal depth (default 3)
441
+
442
+ Returns:
443
+ List of related SessionNode objects (excluding the starting session),
444
+ ordered by depth then created_at
445
+ """
446
+ if not self.db.connection:
447
+ self.db.connect()
448
+
449
+ try:
450
+ visited: set[str] = {session_id}
451
+ result: list[SessionNode] = []
452
+ current_layer: set[str] = {session_id}
453
+
454
+ cursor = self.db.connection.cursor() # type: ignore[union-attr]
455
+
456
+ for depth in range(1, max_depth + 1):
457
+ next_layer: set[str] = set()
458
+
459
+ for current_id in current_layer:
460
+ # 1. Sessions sharing features
461
+ neighbors = self._find_feature_neighbors(cursor, current_id)
462
+ next_layer.update(neighbors - visited)
463
+
464
+ # 2. Delegation links (parent and children)
465
+ neighbors = self._find_delegation_neighbors(cursor, current_id)
466
+ next_layer.update(neighbors - visited)
467
+
468
+ # 3. Continuation links
469
+ neighbors = self._find_continuation_neighbors(cursor, current_id)
470
+ next_layer.update(neighbors - visited)
471
+
472
+ # Build SessionNodes for the new layer
473
+ for neighbor_id in next_layer:
474
+ node = self._get_session_node(neighbor_id, depth=depth)
475
+ if node:
476
+ result.append(node)
477
+
478
+ visited.update(next_layer)
479
+ current_layer = next_layer
480
+
481
+ if not next_layer:
482
+ break
483
+
484
+ # Sort by depth then created_at
485
+ result.sort(key=lambda n: (n.depth, n.created_at))
486
+ return result
487
+
488
+ except sqlite3.Error as e:
489
+ logger.error(f"Error querying related sessions: {e}")
490
+ return []
491
+
492
+ # === Private helper methods ===
493
+
494
+ def _get_session_node(self, session_id: str, depth: int = 0) -> SessionNode | None:
495
+ """
496
+ Load a single session as a SessionNode.
497
+
498
+ Args:
499
+ session_id: Session ID to load
500
+ depth: Depth value to assign
501
+
502
+ Returns:
503
+ SessionNode or None if not found
504
+ """
505
+ if not self.db.connection:
506
+ self.db.connect()
507
+
508
+ try:
509
+ cursor = self.db.connection.cursor() # type: ignore[union-attr]
510
+ cursor.execute(
511
+ """
512
+ SELECT session_id, agent_assigned, status, created_at,
513
+ parent_session_id, features_worked_on
514
+ FROM sessions
515
+ WHERE session_id = ?
516
+ """,
517
+ (session_id,),
518
+ )
519
+
520
+ row = cursor.fetchone()
521
+ if not row:
522
+ return None
523
+
524
+ row_dict = dict(row)
525
+ features = self._parse_features_list(row_dict.get("features_worked_on"))
526
+
527
+ # Also get features from events
528
+ event_features = self.features_for_session(session_id)
529
+ for f in event_features:
530
+ if f not in features:
531
+ features.append(f)
532
+
533
+ return SessionNode(
534
+ session_id=row_dict["session_id"],
535
+ agent=row_dict["agent_assigned"],
536
+ status=row_dict["status"],
537
+ created_at=self._parse_datetime(row_dict["created_at"]),
538
+ features_worked_on=features,
539
+ parent_session_id=row_dict.get("parent_session_id"),
540
+ depth=depth,
541
+ )
542
+
543
+ except sqlite3.Error as e:
544
+ logger.error(f"Error loading session node: {e}")
545
+ return None
546
+
547
+ def _build_path_nodes(self, session_ids: list[str]) -> list[SessionNode]:
548
+ """
549
+ Build a list of SessionNodes from a list of session IDs.
550
+
551
+ Args:
552
+ session_ids: Ordered list of session IDs forming a path
553
+
554
+ Returns:
555
+ List of SessionNode objects with depth set to position in path
556
+ """
557
+ nodes = []
558
+ for i, sid in enumerate(session_ids):
559
+ node = self._get_session_node(sid, depth=i)
560
+ if node:
561
+ nodes.append(node)
562
+ return nodes
563
+
564
+ def _find_feature_neighbors(
565
+ self, cursor: sqlite3.Cursor, session_id: str
566
+ ) -> set[str]:
567
+ """
568
+ Find sessions that share features with the given session.
569
+
570
+ Args:
571
+ cursor: SQLite cursor
572
+ session_id: Session to find neighbors for
573
+
574
+ Returns:
575
+ Set of neighbor session IDs
576
+ """
577
+ cursor.execute(
578
+ """
579
+ SELECT DISTINCT ae2.session_id
580
+ FROM agent_events ae1
581
+ JOIN agent_events ae2 ON ae1.feature_id = ae2.feature_id
582
+ WHERE ae1.session_id = ?
583
+ AND ae2.session_id != ?
584
+ AND ae1.feature_id IS NOT NULL
585
+ """,
586
+ (session_id, session_id),
587
+ )
588
+ return {row[0] for row in cursor.fetchall()}
589
+
590
+ def _find_delegation_neighbors(
591
+ self, cursor: sqlite3.Cursor, session_id: str
592
+ ) -> set[str]:
593
+ """
594
+ Find sessions linked via delegation (parent/child).
595
+
596
+ Args:
597
+ cursor: SQLite cursor
598
+ session_id: Session to find neighbors for
599
+
600
+ Returns:
601
+ Set of neighbor session IDs
602
+ """
603
+ neighbors: set[str] = set()
604
+
605
+ # Parent session
606
+ cursor.execute(
607
+ "SELECT parent_session_id FROM sessions WHERE session_id = ?",
608
+ (session_id,),
609
+ )
610
+ row = cursor.fetchone()
611
+ if row and row[0]:
612
+ neighbors.add(row[0])
613
+
614
+ # Child sessions
615
+ cursor.execute(
616
+ "SELECT session_id FROM sessions WHERE parent_session_id = ?",
617
+ (session_id,),
618
+ )
619
+ for row in cursor.fetchall():
620
+ neighbors.add(row[0])
621
+
622
+ return neighbors
623
+
624
+ def _find_continuation_neighbors(
625
+ self, cursor: sqlite3.Cursor, session_id: str
626
+ ) -> set[str]:
627
+ """
628
+ Find sessions linked via continuation (continued_from).
629
+
630
+ Args:
631
+ cursor: SQLite cursor
632
+ session_id: Session to find neighbors for
633
+
634
+ Returns:
635
+ Set of neighbor session IDs
636
+ """
637
+ neighbors: set[str] = set()
638
+
639
+ # Session this one continued from
640
+ cursor.execute(
641
+ "SELECT continued_from FROM sessions WHERE session_id = ?",
642
+ (session_id,),
643
+ )
644
+ row = cursor.fetchone()
645
+ if row and row[0]:
646
+ neighbors.add(row[0])
647
+
648
+ # Sessions that continued from this one
649
+ cursor.execute(
650
+ "SELECT session_id FROM sessions WHERE continued_from = ?",
651
+ (session_id,),
652
+ )
653
+ for row in cursor.fetchall():
654
+ neighbors.add(row[0])
655
+
656
+ return neighbors
657
+
658
+ @staticmethod
659
+ def _parse_features_list(value: str | list[str] | None) -> list[str]:
660
+ """
661
+ Parse features_worked_on field which may be JSON string or list.
662
+
663
+ Args:
664
+ value: Raw value from database (JSON string, list, or None)
665
+
666
+ Returns:
667
+ List of feature ID strings
668
+ """
669
+ if value is None:
670
+ return []
671
+
672
+ if isinstance(value, list):
673
+ return [str(item) for item in value]
674
+
675
+ if isinstance(value, str):
676
+ import json
677
+
678
+ try:
679
+ parsed = json.loads(value)
680
+ if isinstance(parsed, list):
681
+ return [str(item) for item in parsed]
682
+ except (json.JSONDecodeError, TypeError):
683
+ pass
684
+
685
+ return []
686
+
687
+ @staticmethod
688
+ def _parse_datetime(value: str | datetime | None) -> datetime:
689
+ """
690
+ Parse datetime from various formats.
691
+
692
+ Args:
693
+ value: Datetime string, datetime object, or None
694
+
695
+ Returns:
696
+ Parsed datetime (defaults to datetime.min if unparseable)
697
+ """
698
+ if isinstance(value, datetime):
699
+ return value
700
+
701
+ if isinstance(value, str):
702
+ try:
703
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
704
+ except (ValueError, AttributeError):
705
+ pass
706
+
707
+ return datetime.min