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,587 @@
1
+ """
2
+ Session File Registry - Core file-based session tracking system.
3
+
4
+ Manages the session registry for parallel Claude instance support with:
5
+ - Per-instance registration files (atomic write, no locks needed)
6
+ - Index file for fast lookups
7
+ - Archive support for completed sessions
8
+ - Heartbeat tracking for liveness detection
9
+
10
+ Architecture:
11
+ .htmlgraph/sessions/
12
+ ├── registry/
13
+ │ ├── active/
14
+ │ │ ├── {instance_id}.json # One file per Claude instance
15
+ │ │ └── ...
16
+ │ ├── .index.json # Fast lookup index
17
+ │ └── archive/ # Archived session registrations
18
+ │ └── {instance_id}.json
19
+ ├── {session_id}.html # Session data files
20
+ └── _archive/
21
+ └── {year}/{month}/
22
+ ├── {session_id}.html
23
+ └── ...
24
+
25
+ Data Formats:
26
+
27
+ Instance Registration File (.htmlgraph/sessions/registry/active/{instance_id}.json):
28
+ {
29
+ "instance_id": "inst-12345-hostname-1234567890",
30
+ "session_id": "sess-abc123",
31
+ "created": "2026-01-08T12:34:56Z",
32
+ "repo": {
33
+ "path": "/Users/shakes/DevProjects/htmlgraph",
34
+ "remote": "https://github.com/user/htmlgraph.git",
35
+ "branch": "main",
36
+ "commit": "d78e458"
37
+ },
38
+ "instance": {
39
+ "pid": 12345,
40
+ "hostname": "hostname",
41
+ "start_time": "2026-01-08T12:34:56Z"
42
+ },
43
+ "status": "active",
44
+ "last_activity": "2026-01-08T12:35:10Z"
45
+ }
46
+
47
+ Index File (.htmlgraph/sessions/registry/.index.json):
48
+ {
49
+ "version": "1.0",
50
+ "updated_at": "2026-01-08T12:35:10Z",
51
+ "active_sessions": {
52
+ "sess-abc123": {
53
+ "instance_id": "inst-12345-hostname-1234567890",
54
+ "created": "2026-01-08T12:34:56Z",
55
+ "last_activity": "2026-01-08T12:35:10Z"
56
+ }
57
+ }
58
+ }
59
+ """
60
+
61
+ import json
62
+ import logging
63
+ import os
64
+ import socket
65
+ import time
66
+ from datetime import datetime, timezone
67
+ from pathlib import Path
68
+ from typing import Any, cast
69
+
70
+ logger = logging.getLogger(__name__)
71
+
72
+
73
+ class SessionRegistry:
74
+ """
75
+ Manages session file registry for parallel instance support.
76
+
77
+ Provides atomic file operations, instance tracking, and index management
78
+ without requiring locks or external dependencies.
79
+
80
+ Attributes:
81
+ registry_dir: Path to the registry directory (.htmlgraph/sessions/registry)
82
+ """
83
+
84
+ # Default registry location relative to working directory
85
+ DEFAULT_REGISTRY_SUBPATH = ".htmlgraph/sessions/registry"
86
+
87
+ # Index file name
88
+ INDEX_FILE = ".index.json"
89
+
90
+ # Subdirectories
91
+ ACTIVE_DIR = "active"
92
+ ARCHIVE_DIR = "archive"
93
+
94
+ def __init__(self, registry_dir: Path | None = None):
95
+ """
96
+ Initialize registry with custom or default directory.
97
+
98
+ Args:
99
+ registry_dir: Optional custom registry directory path.
100
+ Defaults to .htmlgraph/sessions/registry in current directory.
101
+
102
+ Raises:
103
+ OSError: If directory creation fails due to permission issues.
104
+ """
105
+ if registry_dir is None:
106
+ registry_dir = Path.cwd() / self.DEFAULT_REGISTRY_SUBPATH
107
+ else:
108
+ registry_dir = Path(registry_dir)
109
+
110
+ self.registry_dir = registry_dir
111
+ self.active_dir = self.registry_dir / self.ACTIVE_DIR
112
+ self.archive_dir = self.registry_dir / self.ARCHIVE_DIR
113
+ self.index_file = self.registry_dir / self.INDEX_FILE
114
+
115
+ # Create directory structure if missing
116
+ self._ensure_directories()
117
+
118
+ def _ensure_directories(self) -> None:
119
+ """
120
+ Create registry directory structure if it doesn't exist.
121
+
122
+ Creates:
123
+ - registry_dir
124
+ - registry_dir/active
125
+ - registry_dir/archive
126
+
127
+ Raises:
128
+ OSError: If directory creation fails.
129
+ """
130
+ try:
131
+ self.registry_dir.mkdir(parents=True, exist_ok=True)
132
+ self.active_dir.mkdir(parents=True, exist_ok=True)
133
+ self.archive_dir.mkdir(parents=True, exist_ok=True)
134
+ except OSError as e:
135
+ logger.error(f"Failed to create registry directories: {e}")
136
+ raise
137
+
138
+ def get_instance_id(self) -> str:
139
+ """
140
+ Get unique instance ID for this process.
141
+
142
+ Generates a stable instance ID based on:
143
+ - Process ID (PID)
144
+ - Hostname
145
+ - Start timestamp (seconds since epoch)
146
+
147
+ Format: inst-{pid}-{hostname}-{timestamp}
148
+
149
+ The ID is stable for the lifetime of this process (same PID always
150
+ generates the same ID). Different processes always get different IDs.
151
+
152
+ Returns:
153
+ Unique instance identifier string.
154
+
155
+ Example:
156
+ >>> registry = SessionRegistry()
157
+ >>> instance_id = registry.get_instance_id()
158
+ >>> instance_id
159
+ 'inst-12345-hostname-1234567890'
160
+ """
161
+ pid = os.getpid()
162
+ hostname = socket.gethostname()
163
+ # Use integer seconds for stability - always same for same process
164
+ start_time = int(time.time())
165
+
166
+ return f"inst-{pid}-{hostname}-{start_time}"
167
+
168
+ def register_session(
169
+ self,
170
+ session_id: str,
171
+ repo_info: dict[str, Any],
172
+ instance_info: dict[str, Any],
173
+ ) -> Path:
174
+ """
175
+ Register new session, return registry file path.
176
+
177
+ Creates a registration file in .htmlgraph/sessions/registry/active/
178
+ and updates the index file atomically.
179
+
180
+ Args:
181
+ session_id: Unique session identifier (e.g., "sess-abc123")
182
+ repo_info: Repository information dict with keys:
183
+ - path: str (repository path)
184
+ - remote: str (remote URL)
185
+ - branch: str (current branch)
186
+ - commit: str (current commit hash)
187
+ instance_info: Instance information dict with keys:
188
+ - pid: int (process ID)
189
+ - hostname: str (machine hostname)
190
+ - start_time: str (ISO 8601 timestamp)
191
+
192
+ Returns:
193
+ Path to the created registration file.
194
+
195
+ Raises:
196
+ OSError: If file write fails.
197
+ ValueError: If session_id is empty or invalid.
198
+
199
+ Example:
200
+ >>> registry = SessionRegistry()
201
+ >>> repo_info = {
202
+ ... "path": "/path/to/repo",
203
+ ... "remote": "https://github.com/user/repo.git",
204
+ ... "branch": "main",
205
+ ... "commit": "abc123"
206
+ ... }
207
+ >>> instance_info = {
208
+ ... "pid": 12345,
209
+ ... "hostname": "myhost",
210
+ ... "start_time": "2026-01-08T12:34:56Z"
211
+ ... }
212
+ >>> path = registry.register_session("sess-abc123", repo_info, instance_info)
213
+ >>> path.exists()
214
+ True
215
+ """
216
+ if not session_id or not isinstance(session_id, str):
217
+ raise ValueError(f"Invalid session_id: {session_id}")
218
+
219
+ instance_id = self.get_instance_id()
220
+ now = self._get_utc_timestamp()
221
+
222
+ registration = {
223
+ "instance_id": instance_id,
224
+ "session_id": session_id,
225
+ "created": now,
226
+ "repo": repo_info,
227
+ "instance": instance_info,
228
+ "status": "active",
229
+ "last_activity": now,
230
+ }
231
+
232
+ # Write registration file
233
+ reg_file = self.active_dir / f"{instance_id}.json"
234
+ self._write_atomic(reg_file, registration)
235
+
236
+ # Update index
237
+ self._update_index(session_id, instance_id, now)
238
+
239
+ logger.info(
240
+ f"Registered session {session_id} with instance {instance_id} at {reg_file}"
241
+ )
242
+
243
+ return reg_file
244
+
245
+ def get_current_sessions(self) -> list[dict[str, Any]]:
246
+ """
247
+ Get all active session registrations.
248
+
249
+ Reads all JSON files in the active/ directory and returns their contents.
250
+ Handles and logs parsing errors gracefully.
251
+
252
+ Returns:
253
+ List of session registration dicts, each containing:
254
+ - instance_id: str
255
+ - session_id: str
256
+ - created: str (ISO 8601)
257
+ - repo: dict
258
+ - instance: dict
259
+ - status: str
260
+ - last_activity: str (ISO 8601)
261
+
262
+ Example:
263
+ >>> registry = SessionRegistry()
264
+ >>> sessions = registry.get_current_sessions()
265
+ >>> len(sessions)
266
+ 2
267
+ >>> sessions[0]["session_id"]
268
+ 'sess-abc123'
269
+ """
270
+ sessions: list[dict[str, Any]] = []
271
+
272
+ if not self.active_dir.exists():
273
+ return sessions
274
+
275
+ try:
276
+ for reg_file in self.active_dir.glob("*.json"):
277
+ try:
278
+ session = self._read_json(reg_file)
279
+ if session:
280
+ sessions.append(session)
281
+ except (json.JSONDecodeError, OSError) as e:
282
+ logger.warning(f"Failed to read registration {reg_file}: {e}")
283
+ continue
284
+ except OSError as e:
285
+ logger.warning(f"Failed to list active registrations: {e}")
286
+
287
+ return sessions
288
+
289
+ def read_session(self, instance_id: str) -> dict[str, Any] | None:
290
+ """
291
+ Read specific session registration.
292
+
293
+ Args:
294
+ instance_id: Instance identifier to read.
295
+
296
+ Returns:
297
+ Session registration dict if found, None otherwise.
298
+
299
+ Example:
300
+ >>> registry = SessionRegistry()
301
+ >>> session = registry.read_session("inst-12345-hostname-1234567890")
302
+ >>> session is not None
303
+ True
304
+ >>> session["session_id"]
305
+ 'sess-abc123'
306
+ """
307
+ reg_file = self.active_dir / f"{instance_id}.json"
308
+
309
+ if not reg_file.exists():
310
+ logger.debug(f"Registration file not found: {reg_file}")
311
+ return None
312
+
313
+ try:
314
+ return self._read_json(reg_file)
315
+ except (json.JSONDecodeError, OSError) as e:
316
+ logger.error(f"Failed to read registration {reg_file}: {e}")
317
+ return None
318
+
319
+ def update_activity(self, instance_id: str) -> bool:
320
+ """
321
+ Update last_activity timestamp for heartbeat.
322
+
323
+ Updates the last_activity field in the registration file to current time.
324
+ Used to indicate that the session is still active (liveness heartbeat).
325
+
326
+ Args:
327
+ instance_id: Instance identifier to update.
328
+
329
+ Returns:
330
+ True if update succeeded, False otherwise.
331
+
332
+ Example:
333
+ >>> registry = SessionRegistry()
334
+ >>> success = registry.update_activity("inst-12345-hostname-1234567890")
335
+ >>> success
336
+ True
337
+ """
338
+ session = self.read_session(instance_id)
339
+ if not session:
340
+ logger.warning(f"Cannot update activity: session {instance_id} not found")
341
+ return False
342
+
343
+ try:
344
+ session["last_activity"] = self._get_utc_timestamp()
345
+ reg_file = self.active_dir / f"{instance_id}.json"
346
+ self._write_atomic(reg_file, session)
347
+
348
+ # Update index
349
+ self._update_index(
350
+ session["session_id"], instance_id, session["last_activity"]
351
+ )
352
+
353
+ logger.debug(f"Updated activity for instance {instance_id}")
354
+ return True
355
+ except OSError as e:
356
+ logger.error(f"Failed to update activity for {instance_id}: {e}")
357
+ return False
358
+
359
+ def archive_session(self, instance_id: str) -> bool:
360
+ """
361
+ Move session from active to archive.
362
+
363
+ Reads the active registration, writes it to archive directory,
364
+ and removes the active registration.
365
+
366
+ Args:
367
+ instance_id: Instance identifier to archive.
368
+
369
+ Returns:
370
+ True if archival succeeded, False otherwise.
371
+
372
+ Example:
373
+ >>> registry = SessionRegistry()
374
+ >>> success = registry.archive_session("inst-12345-hostname-1234567890")
375
+ >>> success
376
+ True
377
+ >>> # File now in archive/
378
+ >>> (registry.archive_dir / f"{instance_id}.json").exists()
379
+ True
380
+ """
381
+ active_file = self.active_dir / f"{instance_id}.json"
382
+
383
+ if not active_file.exists():
384
+ logger.warning(f"Cannot archive: registration {instance_id} not found")
385
+ return False
386
+
387
+ try:
388
+ session = self._read_json(active_file)
389
+ if not session:
390
+ return False
391
+
392
+ # Write to archive
393
+ archive_file = self.archive_dir / f"{instance_id}.json"
394
+ self._write_atomic(archive_file, session)
395
+
396
+ # Remove from active
397
+ active_file.unlink()
398
+
399
+ # Update index
400
+ self._remove_from_index(session["session_id"])
401
+
402
+ logger.info(
403
+ f"Archived session {session['session_id']} (instance {instance_id})"
404
+ )
405
+ return True
406
+ except OSError as e:
407
+ logger.error(f"Failed to archive session {instance_id}: {e}")
408
+ return False
409
+
410
+ def get_session_file_path(self, instance_id: str) -> Path:
411
+ """
412
+ Get file path for session registration.
413
+
414
+ Returns the path where the registration file should be stored.
415
+ Does not verify if the file exists.
416
+
417
+ Args:
418
+ instance_id: Instance identifier.
419
+
420
+ Returns:
421
+ Path to the registration file.
422
+
423
+ Example:
424
+ >>> registry = SessionRegistry()
425
+ >>> path = registry.get_session_file_path("inst-12345-hostname-1234567890")
426
+ >>> str(path)
427
+ '.htmlgraph/sessions/registry/active/inst-12345-hostname-1234567890.json'
428
+ """
429
+ return self.active_dir / f"{instance_id}.json"
430
+
431
+ # Private helper methods
432
+
433
+ @staticmethod
434
+ def _get_utc_timestamp() -> str:
435
+ """
436
+ Get current UTC timestamp in ISO 8601 format.
437
+
438
+ Returns:
439
+ Timestamp string (e.g., "2026-01-08T12:34:56.123456Z")
440
+ """
441
+ now = datetime.now(timezone.utc)
442
+ return now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
443
+
444
+ @staticmethod
445
+ def _write_atomic(path: Path, data: dict[str, Any]) -> None:
446
+ """
447
+ Atomic file write using temp file + rename pattern.
448
+
449
+ Ensures:
450
+ - No partial writes visible to readers
451
+ - No corruption from concurrent writes
452
+ - Crash-safe (either old or new content, never mixed)
453
+
454
+ Args:
455
+ path: File path to write to.
456
+ data: Data dict to write as JSON.
457
+
458
+ Raises:
459
+ OSError: If write or rename fails.
460
+ """
461
+ pid = os.getpid()
462
+ temp_path = Path(f"{path}.{pid}.tmp")
463
+
464
+ try:
465
+ # Write to temp file
466
+ with open(temp_path, "w") as f:
467
+ json.dump(data, f, indent=2)
468
+ f.flush()
469
+ os.fsync(f.fileno()) # Ensure written to disk
470
+
471
+ # Atomic rename
472
+ temp_path.replace(path)
473
+ except OSError as e:
474
+ # Clean up temp file on failure
475
+ try:
476
+ temp_path.unlink(missing_ok=True)
477
+ except OSError:
478
+ pass
479
+ raise e
480
+
481
+ @staticmethod
482
+ def _read_json(path: Path) -> dict[str, Any] | None:
483
+ """
484
+ Read JSON file with retry for transient failures.
485
+
486
+ Handles file-not-found gracefully (returns None).
487
+ Retries on JSON decode errors in case file is mid-write.
488
+
489
+ Args:
490
+ path: Path to JSON file.
491
+
492
+ Returns:
493
+ Parsed dict if successful, None if file not found or unrecoverable.
494
+
495
+ Raises:
496
+ OSError: For non-transient I/O errors.
497
+ """
498
+ max_retries = 3
499
+
500
+ for attempt in range(max_retries):
501
+ try:
502
+ with open(path) as f:
503
+ data = json.load(f)
504
+ return cast(dict[str, Any], data)
505
+ except FileNotFoundError:
506
+ return None
507
+ except json.JSONDecodeError:
508
+ # File might be mid-write, retry with backoff
509
+ if attempt < max_retries - 1:
510
+ time.sleep(0.1 * (attempt + 1))
511
+ else:
512
+ logger.error(
513
+ f"Failed to parse JSON after {max_retries} retries: {path}"
514
+ )
515
+ return None
516
+ except OSError as e:
517
+ # Non-transient error
518
+ raise e
519
+
520
+ return None
521
+
522
+ def _update_index(self, session_id: str, instance_id: str, timestamp: str) -> None:
523
+ """
524
+ Update index file with session information.
525
+
526
+ Atomically updates the index file to include/update the session entry.
527
+
528
+ Args:
529
+ session_id: Session identifier.
530
+ instance_id: Instance identifier.
531
+ timestamp: ISO 8601 timestamp of last activity.
532
+
533
+ Raises:
534
+ OSError: If index update fails.
535
+ """
536
+ index_data = self._read_json(self.index_file) or {
537
+ "version": "1.0",
538
+ "updated_at": self._get_utc_timestamp(),
539
+ "active_sessions": {},
540
+ }
541
+
542
+ # Update session entry
543
+ if "active_sessions" not in index_data:
544
+ index_data["active_sessions"] = {}
545
+
546
+ index_data["active_sessions"][session_id] = {
547
+ "instance_id": instance_id,
548
+ "created": self._get_utc_timestamp(),
549
+ "last_activity": timestamp,
550
+ }
551
+
552
+ # Update timestamp
553
+ index_data["updated_at"] = self._get_utc_timestamp()
554
+
555
+ # Write atomically
556
+ try:
557
+ self._write_atomic(self.index_file, index_data)
558
+ except OSError as e:
559
+ logger.error(f"Failed to update index file: {e}")
560
+ raise
561
+
562
+ def _remove_from_index(self, session_id: str) -> None:
563
+ """
564
+ Remove session entry from index file.
565
+
566
+ Args:
567
+ session_id: Session identifier to remove.
568
+
569
+ Raises:
570
+ OSError: If index update fails.
571
+ """
572
+ index_data = self._read_json(self.index_file)
573
+ if not index_data:
574
+ return
575
+
576
+ if (
577
+ "active_sessions" in index_data
578
+ and session_id in index_data["active_sessions"]
579
+ ):
580
+ del index_data["active_sessions"][session_id]
581
+ index_data["updated_at"] = self._get_utc_timestamp()
582
+
583
+ try:
584
+ self._write_atomic(self.index_file, index_data)
585
+ except OSError as e:
586
+ logger.error(f"Failed to update index file: {e}")
587
+ raise