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,208 @@
1
+ """
2
+ Concurrent Session Detection and Formatting.
3
+
4
+ Provides utilities to detect other active sessions and format them
5
+ for injection into the orchestrator's context at session start.
6
+ """
7
+
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import Any
10
+
11
+ from htmlgraph.db.schema import HtmlGraphDB
12
+
13
+
14
+ def get_concurrent_sessions(
15
+ db: HtmlGraphDB,
16
+ current_session_id: str,
17
+ minutes: int = 30,
18
+ ) -> list[dict[str, Any]]:
19
+ """
20
+ Get other sessions that are currently active.
21
+
22
+ Args:
23
+ db: Database connection
24
+ current_session_id: Current session to exclude
25
+ minutes: Look back window for activity
26
+
27
+ Returns:
28
+ List of concurrent session dicts with id, agent_id, last_user_query, etc.
29
+ """
30
+ if not db.connection:
31
+ db.connect()
32
+
33
+ try:
34
+ cursor = db.connection.cursor() # type: ignore[union-attr]
35
+ # Use datetime format that matches database (without timezone)
36
+ cutoff = (datetime.now(timezone.utc) - timedelta(minutes=minutes)).strftime(
37
+ "%Y-%m-%d %H:%M:%S"
38
+ )
39
+
40
+ cursor.execute(
41
+ """
42
+ SELECT
43
+ session_id as id,
44
+ agent_assigned as agent_id,
45
+ created_at,
46
+ status,
47
+ (SELECT input_summary FROM agent_events
48
+ WHERE session_id = sessions.session_id
49
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query,
50
+ (SELECT timestamp FROM agent_events
51
+ WHERE session_id = sessions.session_id
52
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query_at
53
+ FROM sessions
54
+ WHERE status = 'active'
55
+ AND session_id != ?
56
+ AND created_at > ?
57
+ ORDER BY created_at DESC
58
+ """,
59
+ (current_session_id, cutoff),
60
+ )
61
+
62
+ rows = cursor.fetchall()
63
+ return [dict(row) for row in rows]
64
+ except Exception: # pragma: no cover
65
+ # Gracefully handle database errors
66
+ return []
67
+
68
+
69
+ def format_concurrent_sessions_markdown(sessions: list[dict[str, Any]]) -> str:
70
+ """
71
+ Format concurrent sessions as markdown for context injection.
72
+
73
+ Args:
74
+ sessions: List of session dicts from get_concurrent_sessions
75
+
76
+ Returns:
77
+ Markdown formatted string for system prompt injection
78
+ """
79
+ if not sessions:
80
+ return ""
81
+
82
+ lines = ["## Concurrent Sessions (Active Now)", ""]
83
+
84
+ for session in sessions:
85
+ session_id = session.get("id", "unknown")
86
+ session_id = session_id[:12] if len(session_id) > 12 else session_id
87
+ agent = session.get("agent_id", "unknown")
88
+ query = session.get("last_user_query", "No recent query")
89
+ last_active = session.get("last_user_query_at")
90
+
91
+ # Calculate time ago
92
+ time_ago = "unknown"
93
+ if last_active:
94
+ try:
95
+ last_dt = datetime.fromisoformat(
96
+ last_active.replace("Z", "+00:00")
97
+ if isinstance(last_active, str)
98
+ else last_active
99
+ )
100
+ delta = datetime.now(timezone.utc) - last_dt
101
+ if delta.total_seconds() < 60:
102
+ time_ago = "just now"
103
+ elif delta.total_seconds() < 3600:
104
+ time_ago = f"{int(delta.total_seconds() // 60)} min ago"
105
+ else:
106
+ time_ago = f"{int(delta.total_seconds() // 3600)} hours ago"
107
+ except (ValueError, TypeError, AttributeError):
108
+ time_ago = "unknown"
109
+
110
+ # Truncate query for display
111
+ query_display = (
112
+ query[:50] + "..." if query and len(query) > 50 else (query or "Unknown")
113
+ )
114
+
115
+ lines.append(f'- **{session_id}** ({agent}): "{query_display}" - {time_ago}')
116
+
117
+ lines.append("")
118
+ lines.append("*Coordinate with concurrent sessions to avoid duplicate work.*")
119
+ lines.append("")
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ def get_recent_completed_sessions(
125
+ db: HtmlGraphDB,
126
+ hours: int = 24,
127
+ limit: int = 5,
128
+ ) -> list[dict[str, Any]]:
129
+ """
130
+ Get recently completed sessions for handoff context.
131
+
132
+ Args:
133
+ db: Database connection
134
+ hours: Look back window
135
+ limit: Maximum sessions to return
136
+
137
+ Returns:
138
+ List of recently completed session dicts
139
+ """
140
+ if not db.connection:
141
+ db.connect()
142
+
143
+ try:
144
+ cursor = db.connection.cursor() # type: ignore[union-attr]
145
+ # Use datetime format that matches database (without timezone)
146
+ cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).strftime(
147
+ "%Y-%m-%d %H:%M:%S"
148
+ )
149
+ cursor.execute(
150
+ """
151
+ SELECT session_id as id, agent_assigned as agent_id, created_at as started_at,
152
+ completed_at, total_events,
153
+ (SELECT input_summary FROM agent_events
154
+ WHERE session_id = sessions.session_id
155
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query
156
+ FROM sessions
157
+ WHERE status = 'completed'
158
+ AND completed_at > ?
159
+ ORDER BY completed_at DESC
160
+ LIMIT ?
161
+ """,
162
+ (cutoff, limit),
163
+ )
164
+ rows = cursor.fetchall()
165
+ return [dict(row) for row in rows]
166
+ except Exception: # pragma: no cover
167
+ # Gracefully handle database errors
168
+ return []
169
+
170
+
171
+ def format_recent_work_markdown(sessions: list[dict[str, Any]]) -> str:
172
+ """
173
+ Format recently completed sessions as markdown.
174
+
175
+ Args:
176
+ sessions: List of completed session dicts
177
+
178
+ Returns:
179
+ Markdown formatted string
180
+ """
181
+ if not sessions:
182
+ return ""
183
+
184
+ lines = ["## Recent Work (Last 24 Hours)", ""]
185
+
186
+ for session in sessions:
187
+ session_id = session.get("id", "unknown")
188
+ session_id = session_id[:12] if len(session_id) > 12 else session_id
189
+ query = session.get("last_user_query", "No query recorded")
190
+ total_events = session.get("total_events") or 0
191
+
192
+ query_display = (
193
+ query[:60] + "..." if query and len(query) > 60 else (query or "Unknown")
194
+ )
195
+
196
+ lines.append(f"- `{session_id}`: {query_display} ({total_events} events)")
197
+
198
+ lines.append("")
199
+
200
+ return "\n".join(lines)
201
+
202
+
203
+ __all__ = [
204
+ "get_concurrent_sessions",
205
+ "format_concurrent_sessions_markdown",
206
+ "get_recent_completed_sessions",
207
+ "format_recent_work_markdown",
208
+ ]
@@ -0,0 +1,350 @@
1
+ """
2
+ Hook Execution Context Manager.
3
+
4
+ Manages hook execution context including lazy-loading of expensive resources
5
+ (database, session manager) to minimize initialization overhead.
6
+
7
+ This module provides a centralized context object that hooks can use to:
8
+ - Access the graph directory and project directory
9
+ - Retrieve session information
10
+ - Access the database for event recording
11
+ - Perform unified logging
12
+
13
+ Key Design Principles:
14
+ - Lazy-loading: Expensive resources (DB, SessionManager) are only loaded on first access
15
+ - Resource cleanup: Context properly closes resources when done
16
+ - Type safety: Full type hints for all public methods and properties
17
+ - Error handling: Graceful degradation if resources fail to initialize
18
+ """
19
+
20
+ import logging
21
+ import os
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @dataclass
30
+ class HookContext:
31
+ """
32
+ Hook execution context with lazy-loaded resources.
33
+
34
+ Attributes:
35
+ project_dir: Absolute path to project root directory
36
+ graph_dir: Path to .htmlgraph directory for tracking data
37
+ session_id: Unique session identifier for this execution
38
+ agent_id: Agent/tool that's executing (e.g., 'claude-code', 'codex')
39
+ hook_input: Raw hook input data from Claude Code
40
+ model_name: Specific Claude model name (e.g., 'claude-haiku', 'claude-opus', 'claude-sonnet')
41
+ _session_manager: Cached SessionManager instance (lazy-loaded)
42
+ _database: Cached HtmlGraphDB instance (lazy-loaded)
43
+ """
44
+
45
+ project_dir: str
46
+ graph_dir: Path
47
+ session_id: str
48
+ agent_id: str
49
+ hook_input: dict[str, Any]
50
+ model_name: str | None = field(default=None, repr=False)
51
+ _session_manager: Any | None = field(default=None, repr=False)
52
+ _database: Any | None = field(default=None, repr=False)
53
+
54
+ @classmethod
55
+ def from_input(cls, hook_input: dict[str, Any]) -> "HookContext":
56
+ """
57
+ Create HookContext from raw hook input.
58
+
59
+ Performs automatic environment resolution:
60
+ - Extracts session_id from hook_input
61
+ - Detects agent_id from environment or hook_input
62
+ - Detects model_name (e.g., claude-haiku, claude-opus, claude-sonnet)
63
+ - Resolves project directory via bootstrap
64
+ - Initializes graph directory
65
+
66
+ Args:
67
+ hook_input: Raw hook input dict from Claude Code hook system
68
+
69
+ Returns:
70
+ Initialized HookContext instance
71
+
72
+ Raises:
73
+ ImportError: If bootstrap module cannot be imported
74
+ OSError: If graph directory cannot be created
75
+
76
+ Example:
77
+ ```python
78
+ hook_input = {
79
+ 'session_id': 'sess-abc123',
80
+ 'type': 'pretooluse',
81
+ 'tool_name': 'Edit',
82
+ ...
83
+ }
84
+ context = HookContext.from_input(hook_input)
85
+ logger.info(f"Session: {context.session_id}, Agent: {context.agent_id}, Model: {context.model_name}")
86
+ ```
87
+ """
88
+ # Import bootstrap locally to avoid circular imports
89
+ from htmlgraph.hooks.bootstrap import (
90
+ get_graph_dir,
91
+ resolve_project_dir,
92
+ )
93
+
94
+ # Resolve project directory first
95
+ project_dir = resolve_project_dir()
96
+ graph_dir = get_graph_dir(project_dir)
97
+
98
+ # Extract session ID with multiple fallbacks
99
+ # Priority order:
100
+ # 1. hook_input["session_id"] (if Claude Code passes it)
101
+ # 2. hook_input["sessionId"] (camelCase variant)
102
+ # 3. HTMLGRAPH_SESSION_ID environment variable
103
+ # 4. CLAUDE_SESSION_ID environment variable
104
+ # 5. Most recent active session from database (NEW)
105
+ # 6. "unknown" as last resort
106
+ #
107
+ # NOTE: We intentionally do NOT use SessionManager.get_active_session()
108
+ # as a fallback because the "active session" is stored in a global file
109
+ # (.htmlgraph/session.json) that's shared across all Claude windows.
110
+ # Using it would cause cross-window event contamination where tool calls
111
+ # from Window B get linked to UserQuery events from Window A.
112
+ #
113
+ # However, we DO query the database by status='active' and created_at,
114
+ # which is different because it retrieves the most recent session that
115
+ # was explicitly marked as active (e.g., by SessionStart hook), without
116
+ # relying on a shared global agent state file.
117
+ session_id = (
118
+ hook_input.get("session_id")
119
+ or hook_input.get("sessionId")
120
+ or os.environ.get("HTMLGRAPH_SESSION_ID")
121
+ or os.environ.get("CLAUDE_SESSION_ID")
122
+ )
123
+
124
+ # Fallback: Query database for session with most recent UserQuery event
125
+ # This solves the issue where PostToolUse hooks don't receive session_id
126
+ # in hook_input. UserPromptSubmit hooks DO receive it and create UserQuery
127
+ # events with the correct session_id, so we use that as the source of truth.
128
+ if not session_id:
129
+ db_path = graph_dir / "htmlgraph.db"
130
+ if db_path.exists():
131
+ try:
132
+ import sqlite3
133
+
134
+ conn = sqlite3.connect(str(db_path), timeout=1.0)
135
+ cursor = conn.cursor()
136
+ cursor.execute("""
137
+ SELECT session_id FROM agent_events
138
+ WHERE tool_name = 'UserQuery'
139
+ ORDER BY timestamp DESC
140
+ LIMIT 1
141
+ """)
142
+ row = cursor.fetchone()
143
+ conn.close()
144
+ if row:
145
+ session_id = row[0]
146
+ logger.info(f"Resolved session_id from database: {session_id}")
147
+ except Exception as e:
148
+ logger.warning(f"Failed to query active session from database: {e}")
149
+
150
+ # Final fallback to "unknown" if database query fails
151
+ if not session_id:
152
+ session_id = "unknown"
153
+ logger.warning(
154
+ "Could not resolve session_id from hook_input, environment, or database. "
155
+ "Events will not be linked to parent UserQuery. "
156
+ "For multi-window support, set HTMLGRAPH_SESSION_ID env var."
157
+ )
158
+
159
+ # Detect agent ID (priority order)
160
+ # 1. Explicit agent_id in hook input
161
+ # 2. HTMLGRAPH_AGENT_ID environment variable
162
+ # 3. CLAUDE_AGENT_NICKNAME environment variable (Claude Code)
163
+ # 4. Default to 'unknown'
164
+ agent_id = (
165
+ hook_input.get("agent_id")
166
+ or os.environ.get("HTMLGRAPH_AGENT_ID")
167
+ or os.environ.get("CLAUDE_AGENT_NICKNAME", "unknown")
168
+ )
169
+
170
+ # Detect model name (priority order)
171
+ # 1. Explicit model_name in hook input
172
+ # 2. CLAUDE_MODEL environment variable
173
+ # 3. HTMLGRAPH_MODEL environment variable
174
+ # 4. Status line cache (from ~/.cache/claude-code/status-{session_id}.json)
175
+ # 5. None (not available)
176
+ model_name = (
177
+ hook_input.get("model_name")
178
+ or hook_input.get("model")
179
+ or os.environ.get("CLAUDE_MODEL")
180
+ or os.environ.get("HTMLGRAPH_MODEL")
181
+ )
182
+
183
+ # Fallback: Try status line cache if model not detected yet
184
+ if not model_name and session_id and session_id != "unknown":
185
+ from htmlgraph.hooks.event_tracker import get_model_from_status_cache
186
+
187
+ model_name = get_model_from_status_cache(session_id)
188
+
189
+ logger.info(
190
+ f"Initializing hook context: session={session_id}, "
191
+ f"agent={agent_id}, model={model_name}, project={project_dir}"
192
+ )
193
+
194
+ return cls(
195
+ project_dir=project_dir,
196
+ graph_dir=graph_dir,
197
+ session_id=session_id,
198
+ agent_id=agent_id,
199
+ hook_input=hook_input,
200
+ model_name=model_name,
201
+ )
202
+
203
+ @property
204
+ def session_manager(self) -> Any:
205
+ """
206
+ Lazy-load and cache SessionManager instance.
207
+
208
+ Importing SessionManager is expensive (thousands of file system operations
209
+ for graph initialization), so we defer until first access.
210
+
211
+ Returns:
212
+ SessionManager instance for session tracking and activity attribution
213
+
214
+ Raises:
215
+ ImportError: If SessionManager cannot be imported
216
+ Exception: If SessionManager initialization fails
217
+
218
+ Note:
219
+ SessionManager is cached after first access. Multiple accesses
220
+ return the same instance.
221
+ """
222
+ if self._session_manager is not None:
223
+ return self._session_manager
224
+
225
+ try:
226
+ from htmlgraph.session_manager import SessionManager
227
+
228
+ logger.debug(f"Loading SessionManager for {self.graph_dir}")
229
+ self._session_manager = SessionManager(graph_dir=self.graph_dir)
230
+ logger.info("SessionManager loaded successfully")
231
+ return self._session_manager
232
+ except ImportError as e:
233
+ logger.error(f"Failed to import SessionManager: {e}")
234
+ raise
235
+ except Exception as e:
236
+ logger.error(f"Failed to initialize SessionManager: {e}")
237
+ raise
238
+
239
+ @property
240
+ def database(self) -> Any:
241
+ """
242
+ Lazy-load and cache HtmlGraphDB instance.
243
+
244
+ Database access is needed for event recording, but we defer initialization
245
+ until first access to minimize startup overhead.
246
+
247
+ Returns:
248
+ HtmlGraphDB instance for recording events and features
249
+
250
+ Raises:
251
+ ImportError: If HtmlGraphDB cannot be imported
252
+ Exception: If database connection fails
253
+
254
+ Note:
255
+ Database connection is cached after first access. Multiple accesses
256
+ return the same instance.
257
+ """
258
+ if self._database is not None:
259
+ return self._database
260
+
261
+ try:
262
+ from htmlgraph.db.schema import HtmlGraphDB
263
+
264
+ db_path = self.graph_dir / "htmlgraph.db"
265
+ logger.debug(f"Loading HtmlGraphDB at {db_path}")
266
+ self._database = HtmlGraphDB(str(db_path))
267
+ logger.info("HtmlGraphDB loaded successfully")
268
+ return self._database
269
+ except ImportError as e:
270
+ logger.error(f"Failed to import HtmlGraphDB: {e}")
271
+ raise
272
+ except Exception as e:
273
+ logger.error(f"Failed to initialize HtmlGraphDB: {e}")
274
+ raise
275
+
276
+ def close(self) -> None:
277
+ """
278
+ Clean up and close all resources gracefully.
279
+
280
+ Closes database connections and session manager resources.
281
+ Safe to call multiple times (idempotent).
282
+
283
+ This should be called in a finally block to ensure cleanup:
284
+
285
+ Example:
286
+ ```python
287
+ context = HookContext.from_input(hook_input)
288
+ try:
289
+ # Use context
290
+ context.session_manager.track_activity(...)
291
+ finally:
292
+ context.close() # Always cleanup
293
+ ```
294
+ """
295
+ # Close database if loaded
296
+ if self._database is not None:
297
+ try:
298
+ logger.debug("Closing database connection")
299
+ self._database.close()
300
+ self._database = None
301
+ logger.info("Database closed successfully")
302
+ except Exception as e:
303
+ logger.warning(f"Error closing database: {e}")
304
+
305
+ # Close session manager if loaded
306
+ if self._session_manager is not None:
307
+ try:
308
+ logger.debug("Closing session manager")
309
+ # SessionManager doesn't currently have a close method,
310
+ # but we keep this for future resource management
311
+ self._session_manager = None
312
+ logger.info("Session manager cleaned up")
313
+ except Exception as e:
314
+ logger.warning(f"Error closing session manager: {e}")
315
+
316
+ def log(self, level: str, message: str) -> None:
317
+ """
318
+ Unified logging for hooks.
319
+
320
+ Provides consistent logging across all hook modules with context
321
+ information (session_id, agent_id, project_dir).
322
+
323
+ Args:
324
+ level: Log level as string ('debug', 'info', 'warning', 'error', 'critical')
325
+ message: Message to log
326
+
327
+ Example:
328
+ ```python
329
+ context.log('info', 'Processing user query')
330
+ context.log('error', f'Failed to track activity: {error}')
331
+ ```
332
+ """
333
+ log_func = getattr(logger, level.lower(), logger.info)
334
+
335
+ # Prefix message with context for better debugging
336
+ context_msg = f"[{self.session_id[:8]}][{self.agent_id}] {message}"
337
+ log_func(context_msg)
338
+
339
+ def __enter__(self) -> "HookContext":
340
+ """Context manager entry."""
341
+ return self
342
+
343
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
344
+ """Context manager exit with resource cleanup."""
345
+ self.close()
346
+
347
+
348
+ __all__ = [
349
+ "HookContext",
350
+ ]