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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {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
|