claude-mpm 4.5.5__py3-none-any.whl → 4.5.8__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_OPS.md +10 -0
- claude_mpm/agents/PM_INSTRUCTIONS.md +28 -4
- claude_mpm/agents/templates/local_ops_agent.json +45 -9
- claude_mpm/services/async_session_logger.py +131 -93
- claude_mpm/services/claude_session_logger.py +70 -60
- claude_mpm/services/mcp_config_manager.py +186 -17
- claude_mpm/services/project/archive_manager.py +0 -1
- claude_mpm/services/project/documentation_manager.py +0 -1
- claude_mpm/services/project/project_organizer.py +1 -4
- claude_mpm/services/response_tracker.py +16 -5
- claude_mpm/services/session_manager.py +174 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +0 -1
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +7 -3
- claude_mpm/services/unified/config_strategies/context_strategy.py +1 -3
- {claude_mpm-4.5.5.dist-info → claude_mpm-4.5.8.dist-info}/METADATA +1 -1
- {claude_mpm-4.5.5.dist-info → claude_mpm-4.5.8.dist-info}/RECORD +21 -20
- {claude_mpm-4.5.5.dist-info → claude_mpm-4.5.8.dist-info}/WHEEL +0 -0
- {claude_mpm-4.5.5.dist-info → claude_mpm-4.5.8.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.5.5.dist-info → claude_mpm-4.5.8.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.5.5.dist-info → claude_mpm-4.5.8.dist-info}/top_level.txt +0 -0
@@ -41,6 +41,14 @@ class MCPConfigManager:
|
|
41
41
|
"kuzu-memory",
|
42
42
|
}
|
43
43
|
|
44
|
+
# Known missing dependencies for MCP services that pipx doesn't handle automatically
|
45
|
+
# Maps service names to list of missing dependencies that need injection
|
46
|
+
SERVICE_MISSING_DEPENDENCIES = {
|
47
|
+
"mcp-ticketer": ["gql"], # mcp-ticketer v0.1.8+ needs gql but doesn't declare it
|
48
|
+
# Add more services here as needed, e.g.:
|
49
|
+
# "another-service": ["dep1", "dep2"],
|
50
|
+
}
|
51
|
+
|
44
52
|
# Static known-good MCP service configurations
|
45
53
|
# These are the correct, tested configurations that work reliably
|
46
54
|
# Note: Commands will be resolved to full paths dynamically in get_static_service_config()
|
@@ -943,6 +951,11 @@ class MCPConfigManager:
|
|
943
951
|
)
|
944
952
|
|
945
953
|
if result.returncode == 0:
|
954
|
+
# Inject any missing dependencies if needed
|
955
|
+
if service_name in self.SERVICE_MISSING_DEPENDENCIES:
|
956
|
+
self.logger.debug(f"Injecting missing dependencies for newly installed {service_name}...")
|
957
|
+
self._inject_missing_dependencies(service_name)
|
958
|
+
|
946
959
|
# Verify installation worked
|
947
960
|
if self._verify_service_installed(service_name, "pipx"):
|
948
961
|
return True, "pipx"
|
@@ -1092,18 +1105,39 @@ class MCPConfigManager:
|
|
1092
1105
|
failed_services.append(f"{service_name} (reinstall failed)")
|
1093
1106
|
|
1094
1107
|
elif issue_type == "missing_dependency":
|
1095
|
-
# Fix missing dependencies
|
1108
|
+
# Fix missing dependencies - try injection first, then reinstall if needed
|
1096
1109
|
self.logger.info(
|
1097
|
-
f" {service_name} has missing dependencies -
|
1110
|
+
f" {service_name} has missing dependencies - attempting fix..."
|
1098
1111
|
)
|
1112
|
+
|
1113
|
+
# First try to inject dependencies without reinstalling
|
1114
|
+
injection_success = self._inject_missing_dependencies(service_name)
|
1115
|
+
|
1116
|
+
if injection_success:
|
1117
|
+
# Verify the fix worked
|
1118
|
+
issue_after_injection = self._detect_service_issue(service_name)
|
1119
|
+
if issue_after_injection is None:
|
1120
|
+
fixed_services.append(f"{service_name} (dependencies injected)")
|
1121
|
+
self.logger.info(f" ✅ Fixed {service_name} with dependency injection")
|
1122
|
+
continue # Move to next service
|
1123
|
+
|
1124
|
+
# If injection alone didn't work, try full reinstall
|
1125
|
+
self.logger.info(" Dependency injection insufficient, trying full reinstall...")
|
1099
1126
|
success = self._auto_reinstall_mcp_service(service_name)
|
1100
1127
|
if success:
|
1101
|
-
fixed_services.append(f"{service_name} (auto-reinstalled)")
|
1128
|
+
fixed_services.append(f"{service_name} (auto-reinstalled with dependencies)")
|
1102
1129
|
else:
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1130
|
+
# Provide specific manual fix for known services
|
1131
|
+
if service_name == "mcp-ticketer":
|
1132
|
+
self.logger.warning(
|
1133
|
+
f" Auto-fix failed for {service_name}. Manual fix: "
|
1134
|
+
f"pipx uninstall {service_name} && pipx install {service_name} && pipx inject {service_name} gql"
|
1135
|
+
)
|
1136
|
+
else:
|
1137
|
+
self.logger.warning(
|
1138
|
+
f" Auto-reinstall failed for {service_name}. Manual fix: "
|
1139
|
+
f"pipx uninstall {service_name} && pipx install {service_name}"
|
1140
|
+
)
|
1107
1141
|
failed_services.append(f"{service_name} (auto-reinstall failed)")
|
1108
1142
|
|
1109
1143
|
elif issue_type == "path_issue":
|
@@ -1129,7 +1163,11 @@ class MCPConfigManager:
|
|
1129
1163
|
message += "\n\n💡 Manual fix instructions:"
|
1130
1164
|
for failed in failed_services:
|
1131
1165
|
service = failed.split(" ")[0]
|
1132
|
-
|
1166
|
+
if service in self.SERVICE_MISSING_DEPENDENCIES:
|
1167
|
+
deps = " ".join([f"&& pipx inject {service} {dep}" for dep in self.SERVICE_MISSING_DEPENDENCIES[service]])
|
1168
|
+
message += f"\n • {service}: pipx uninstall {service} && pipx install {service} {deps}"
|
1169
|
+
else:
|
1170
|
+
message += f"\n • {service}: pipx uninstall {service} && pipx install {service}"
|
1133
1171
|
|
1134
1172
|
return success, message
|
1135
1173
|
|
@@ -1148,7 +1186,58 @@ class MCPConfigManager:
|
|
1148
1186
|
|
1149
1187
|
# Try to run the service with --help to detect issues
|
1150
1188
|
try:
|
1151
|
-
#
|
1189
|
+
# First check if service is installed in pipx venv
|
1190
|
+
pipx_venv_bin = self.pipx_base / service_name / "bin" / service_name
|
1191
|
+
if pipx_venv_bin.exists():
|
1192
|
+
# Test the installed version directly (has injected dependencies)
|
1193
|
+
# This avoids using pipx run which downloads a fresh cache copy without dependencies
|
1194
|
+
self.logger.debug(f"Testing {service_name} from installed pipx venv: {pipx_venv_bin}")
|
1195
|
+
result = subprocess.run(
|
1196
|
+
[str(pipx_venv_bin), "--help"],
|
1197
|
+
capture_output=True,
|
1198
|
+
text=True,
|
1199
|
+
timeout=10,
|
1200
|
+
check=False,
|
1201
|
+
)
|
1202
|
+
|
1203
|
+
# Check for specific error patterns in installed version
|
1204
|
+
stderr_lower = result.stderr.lower()
|
1205
|
+
stdout_lower = result.stdout.lower()
|
1206
|
+
combined_output = stderr_lower + stdout_lower
|
1207
|
+
|
1208
|
+
# Import errors in installed version (should be rare if dependencies injected)
|
1209
|
+
if (
|
1210
|
+
"modulenotfounderror" in combined_output
|
1211
|
+
or "importerror" in combined_output
|
1212
|
+
):
|
1213
|
+
# Check if it's specifically the gql dependency for mcp-ticketer
|
1214
|
+
if service_name == "mcp-ticketer" and "gql" in combined_output:
|
1215
|
+
return "missing_dependency"
|
1216
|
+
return "import_error"
|
1217
|
+
|
1218
|
+
# Path issues
|
1219
|
+
if "no such file or directory" in combined_output:
|
1220
|
+
return "path_issue"
|
1221
|
+
|
1222
|
+
# If help text appears, service is working
|
1223
|
+
if (
|
1224
|
+
"usage:" in combined_output
|
1225
|
+
or "help" in combined_output
|
1226
|
+
or result.returncode in [0, 1]
|
1227
|
+
):
|
1228
|
+
self.logger.debug(f"{service_name} is working correctly")
|
1229
|
+
return None # Service is working
|
1230
|
+
|
1231
|
+
# Unknown issue
|
1232
|
+
if result.returncode not in [0, 1]:
|
1233
|
+
self.logger.debug(f"{service_name} returned unexpected exit code: {result.returncode}")
|
1234
|
+
return "unknown_error"
|
1235
|
+
|
1236
|
+
return None # Default to working if no issues detected
|
1237
|
+
|
1238
|
+
# Service not installed in pipx venv - use pipx run for detection
|
1239
|
+
# Note: pipx run uses cache which may not have injected dependencies
|
1240
|
+
self.logger.debug(f"Testing {service_name} via pipx run (not installed in venv)")
|
1152
1241
|
result = subprocess.run(
|
1153
1242
|
["pipx", "run", service_name, "--help"],
|
1154
1243
|
capture_output=True,
|
@@ -1169,15 +1258,15 @@ class MCPConfigManager:
|
|
1169
1258
|
):
|
1170
1259
|
return "not_installed"
|
1171
1260
|
|
1172
|
-
# Import errors
|
1261
|
+
# Import errors when using pipx run (cache version)
|
1173
1262
|
if (
|
1174
1263
|
"modulenotfounderror" in combined_output
|
1175
1264
|
or "importerror" in combined_output
|
1176
1265
|
):
|
1177
|
-
#
|
1178
|
-
|
1179
|
-
|
1180
|
-
return "
|
1266
|
+
# Don't report missing_dependency for cache version - it may be missing injected deps
|
1267
|
+
# Just report that service needs to be installed properly
|
1268
|
+
self.logger.debug(f"{service_name} has import errors in pipx run cache - needs proper installation")
|
1269
|
+
return "not_installed"
|
1181
1270
|
|
1182
1271
|
# Path issues
|
1183
1272
|
if "no such file or directory" in combined_output:
|
@@ -1240,6 +1329,11 @@ class MCPConfigManager:
|
|
1240
1329
|
)
|
1241
1330
|
|
1242
1331
|
if install_result.returncode == 0:
|
1332
|
+
# Inject any missing dependencies if needed
|
1333
|
+
if service_name in self.SERVICE_MISSING_DEPENDENCIES:
|
1334
|
+
self.logger.debug(f"Injecting missing dependencies for {service_name}...")
|
1335
|
+
self._inject_missing_dependencies(service_name)
|
1336
|
+
|
1243
1337
|
# Verify the reinstall worked
|
1244
1338
|
issue = self._detect_service_issue(service_name)
|
1245
1339
|
if issue is None:
|
@@ -1258,6 +1352,65 @@ class MCPConfigManager:
|
|
1258
1352
|
self.logger.error(f"Error reinstalling {service_name}: {e}")
|
1259
1353
|
return False
|
1260
1354
|
|
1355
|
+
def _inject_missing_dependencies(self, service_name: str) -> bool:
|
1356
|
+
"""
|
1357
|
+
Inject missing dependencies into a pipx-installed MCP service.
|
1358
|
+
|
1359
|
+
Some MCP services don't properly declare all their dependencies in their
|
1360
|
+
package metadata, which causes import errors when pipx creates isolated
|
1361
|
+
virtual environments. This method injects the missing dependencies using
|
1362
|
+
pipx inject.
|
1363
|
+
|
1364
|
+
Args:
|
1365
|
+
service_name: Name of the MCP service to fix
|
1366
|
+
|
1367
|
+
Returns:
|
1368
|
+
True if dependencies were injected successfully or no injection needed, False otherwise
|
1369
|
+
"""
|
1370
|
+
# Check if this service has known missing dependencies
|
1371
|
+
if service_name not in self.SERVICE_MISSING_DEPENDENCIES:
|
1372
|
+
return True # No dependencies to inject
|
1373
|
+
|
1374
|
+
missing_deps = self.SERVICE_MISSING_DEPENDENCIES[service_name]
|
1375
|
+
if not missing_deps:
|
1376
|
+
return True # No dependencies to inject
|
1377
|
+
|
1378
|
+
self.logger.info(
|
1379
|
+
f" → Injecting missing dependencies for {service_name}: {', '.join(missing_deps)}"
|
1380
|
+
)
|
1381
|
+
|
1382
|
+
all_successful = True
|
1383
|
+
for dep in missing_deps:
|
1384
|
+
try:
|
1385
|
+
self.logger.debug(f" Injecting {dep} into {service_name}...")
|
1386
|
+
result = subprocess.run(
|
1387
|
+
["pipx", "inject", service_name, dep],
|
1388
|
+
capture_output=True,
|
1389
|
+
text=True,
|
1390
|
+
timeout=60,
|
1391
|
+
check=False,
|
1392
|
+
)
|
1393
|
+
|
1394
|
+
if result.returncode == 0:
|
1395
|
+
self.logger.info(f" ✅ Successfully injected {dep}")
|
1396
|
+
# Check if already injected (pipx will complain if package already exists)
|
1397
|
+
elif "already satisfied" in result.stderr.lower() or "already installed" in result.stderr.lower():
|
1398
|
+
self.logger.debug(f" {dep} already present in {service_name}")
|
1399
|
+
else:
|
1400
|
+
self.logger.error(
|
1401
|
+
f" Failed to inject {dep}: {result.stderr}"
|
1402
|
+
)
|
1403
|
+
all_successful = False
|
1404
|
+
|
1405
|
+
except subprocess.TimeoutExpired:
|
1406
|
+
self.logger.error(f" Timeout while injecting {dep}")
|
1407
|
+
all_successful = False
|
1408
|
+
except Exception as e:
|
1409
|
+
self.logger.error(f" Error injecting {dep}: {e}")
|
1410
|
+
all_successful = False
|
1411
|
+
|
1412
|
+
return all_successful
|
1413
|
+
|
1261
1414
|
def _auto_reinstall_mcp_service(self, service_name: str) -> bool:
|
1262
1415
|
"""
|
1263
1416
|
Automatically reinstall an MCP service with missing dependencies.
|
@@ -1312,6 +1465,14 @@ class MCPConfigManager:
|
|
1312
1465
|
)
|
1313
1466
|
return False
|
1314
1467
|
|
1468
|
+
# Inject any missing dependencies that pipx doesn't handle automatically
|
1469
|
+
if service_name in self.SERVICE_MISSING_DEPENDENCIES:
|
1470
|
+
self.logger.info(f" → Fixing missing dependencies for {service_name}...")
|
1471
|
+
if not self._inject_missing_dependencies(service_name):
|
1472
|
+
self.logger.warning(
|
1473
|
+
f"Failed to inject all dependencies for {service_name}, but continuing..."
|
1474
|
+
)
|
1475
|
+
|
1315
1476
|
# Verify the reinstall worked
|
1316
1477
|
self.logger.info(f" → Verifying {service_name} installation...")
|
1317
1478
|
issue = self._detect_service_issue(service_name)
|
@@ -1319,9 +1480,17 @@ class MCPConfigManager:
|
|
1319
1480
|
if issue is None:
|
1320
1481
|
self.logger.info(f" ✅ Successfully reinstalled {service_name}")
|
1321
1482
|
return True
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1483
|
+
|
1484
|
+
# If still has missing dependency issue after injection, log specific instructions
|
1485
|
+
if issue == "missing_dependency" and service_name == "mcp-ticketer":
|
1486
|
+
self.logger.error(
|
1487
|
+
f" {service_name} still has missing dependencies after injection. "
|
1488
|
+
f"Manual fix: pipx inject {service_name} gql"
|
1489
|
+
)
|
1490
|
+
else:
|
1491
|
+
self.logger.warning(
|
1492
|
+
f"Reinstalled {service_name} but still has issue: {issue}"
|
1493
|
+
)
|
1325
1494
|
return False
|
1326
1495
|
|
1327
1496
|
except subprocess.TimeoutExpired:
|
@@ -961,7 +961,6 @@ Generated by Claude MPM Archive Manager
|
|
961
961
|
content,
|
962
962
|
)
|
963
963
|
|
964
|
-
|
965
964
|
def display_review_summary(self, review: Dict) -> None:
|
966
965
|
"""Display a formatted summary of the documentation review."""
|
967
966
|
console.print("\n[bold cyan]📚 Documentation Review Summary[/bold cyan]\n")
|
@@ -979,10 +979,7 @@ This directory is used for {description.lower()}.
|
|
979
979
|
for file in root_files:
|
980
980
|
if file.is_file() and (
|
981
981
|
("test" in file.name.lower() and file.suffix == ".py")
|
982
|
-
or (
|
983
|
-
file.suffix in [".sh", ".bash"]
|
984
|
-
and file.name not in ["Makefile"]
|
985
|
-
)
|
982
|
+
or (file.suffix in [".sh", ".bash"] and file.name not in ["Makefile"])
|
986
983
|
or file.suffix in [".log", ".tmp", ".cache"]
|
987
984
|
):
|
988
985
|
misplaced_count += 1
|
@@ -19,6 +19,7 @@ DESIGN DECISIONS:
|
|
19
19
|
"""
|
20
20
|
|
21
21
|
from datetime import datetime, timezone
|
22
|
+
from threading import Lock
|
22
23
|
from typing import Any, Dict, Optional
|
23
24
|
|
24
25
|
from claude_mpm.core.config import Config
|
@@ -214,12 +215,15 @@ class ResponseTracker:
|
|
214
215
|
logger.info(f"Response tracker session ID set to: {session_id}")
|
215
216
|
|
216
217
|
|
217
|
-
# Singleton instance
|
218
|
+
# Singleton instance with thread-safe initialization
|
218
219
|
_tracker_instance = None
|
220
|
+
_tracker_lock = Lock()
|
219
221
|
|
220
222
|
|
221
223
|
def get_response_tracker(config: Optional[Config] = None) -> ResponseTracker:
|
222
|
-
"""Get the singleton response tracker instance.
|
224
|
+
"""Get the singleton response tracker instance with thread-safe initialization.
|
225
|
+
|
226
|
+
Uses double-checked locking pattern to ensure thread safety.
|
223
227
|
|
224
228
|
Args:
|
225
229
|
config: Optional configuration instance
|
@@ -228,9 +232,16 @@ def get_response_tracker(config: Optional[Config] = None) -> ResponseTracker:
|
|
228
232
|
The shared ResponseTracker instance
|
229
233
|
"""
|
230
234
|
global _tracker_instance
|
231
|
-
|
232
|
-
|
233
|
-
|
235
|
+
|
236
|
+
# Fast path - check without lock
|
237
|
+
if _tracker_instance is not None:
|
238
|
+
return _tracker_instance
|
239
|
+
|
240
|
+
# Slow path - acquire lock and double-check
|
241
|
+
with _tracker_lock:
|
242
|
+
if _tracker_instance is None:
|
243
|
+
_tracker_instance = ResponseTracker(config=config)
|
244
|
+
return _tracker_instance
|
234
245
|
|
235
246
|
|
236
247
|
def track_response(
|
@@ -0,0 +1,174 @@
|
|
1
|
+
"""
|
2
|
+
Session Manager Service
|
3
|
+
|
4
|
+
Centralized session ID management with thread-safe singleton pattern.
|
5
|
+
Ensures a single session ID is generated and used across all components.
|
6
|
+
|
7
|
+
This service addresses race conditions and duplicate session ID generation
|
8
|
+
by providing a single source of truth for session identifiers.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import os
|
12
|
+
from datetime import datetime, timezone
|
13
|
+
from threading import Lock
|
14
|
+
from typing import Optional
|
15
|
+
|
16
|
+
from claude_mpm.core.logging_utils import get_logger
|
17
|
+
|
18
|
+
logger = get_logger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class SessionManager:
|
22
|
+
"""
|
23
|
+
Thread-safe singleton session manager.
|
24
|
+
|
25
|
+
Provides centralized session ID generation and management to prevent
|
26
|
+
duplicate session IDs across different components.
|
27
|
+
|
28
|
+
Uses double-checked locking pattern for thread-safe singleton initialization.
|
29
|
+
"""
|
30
|
+
|
31
|
+
_instance: Optional["SessionManager"] = None
|
32
|
+
_lock = Lock()
|
33
|
+
_initialized = False
|
34
|
+
|
35
|
+
def __new__(cls) -> "SessionManager":
|
36
|
+
"""
|
37
|
+
Create or return the singleton instance using double-checked locking.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
The singleton SessionManager instance
|
41
|
+
"""
|
42
|
+
# First check without lock (fast path)
|
43
|
+
if cls._instance is None:
|
44
|
+
# Acquire lock for thread safety
|
45
|
+
with cls._lock:
|
46
|
+
# Double-check inside lock
|
47
|
+
if cls._instance is None:
|
48
|
+
cls._instance = super().__new__(cls)
|
49
|
+
return cls._instance
|
50
|
+
|
51
|
+
def __init__(self):
|
52
|
+
"""
|
53
|
+
Initialize the session manager (only once).
|
54
|
+
|
55
|
+
This method uses an initialization flag to ensure it only
|
56
|
+
runs once, even if __init__ is called multiple times.
|
57
|
+
"""
|
58
|
+
# Use class-level lock to ensure thread-safe initialization
|
59
|
+
with self.__class__._lock:
|
60
|
+
if self.__class__._initialized:
|
61
|
+
return
|
62
|
+
|
63
|
+
# Generate session ID once during initialization
|
64
|
+
self._session_id = self._generate_session_id()
|
65
|
+
self._session_start_time = datetime.now(timezone.utc)
|
66
|
+
|
67
|
+
# Mark as initialized
|
68
|
+
self.__class__._initialized = True
|
69
|
+
|
70
|
+
logger.info(
|
71
|
+
f"SessionManager initialized with session ID: {self._session_id}"
|
72
|
+
)
|
73
|
+
|
74
|
+
def _generate_session_id(self) -> str:
|
75
|
+
"""
|
76
|
+
Generate or retrieve a session ID.
|
77
|
+
|
78
|
+
Checks environment variables first, then generates a timestamp-based ID.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
A unique session identifier
|
82
|
+
"""
|
83
|
+
# Check environment variables in order of preference
|
84
|
+
env_vars = ["CLAUDE_SESSION_ID", "ANTHROPIC_SESSION_ID", "SESSION_ID"]
|
85
|
+
|
86
|
+
for env_var in env_vars:
|
87
|
+
session_id = os.environ.get(env_var)
|
88
|
+
if session_id:
|
89
|
+
logger.info(f"Using session ID from {env_var}: {session_id}")
|
90
|
+
return session_id
|
91
|
+
|
92
|
+
# Generate timestamp-based session ID
|
93
|
+
session_id = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
94
|
+
logger.info(f"Generated new session ID: {session_id}")
|
95
|
+
return session_id
|
96
|
+
|
97
|
+
def get_session_id(self) -> str:
|
98
|
+
"""
|
99
|
+
Get the current session ID.
|
100
|
+
|
101
|
+
Thread-safe method to retrieve the session ID.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
The current session ID
|
105
|
+
"""
|
106
|
+
return self._session_id
|
107
|
+
|
108
|
+
def get_session_start_time(self) -> datetime:
|
109
|
+
"""
|
110
|
+
Get the session start time.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
The datetime when the session was initialized
|
114
|
+
"""
|
115
|
+
return self._session_start_time
|
116
|
+
|
117
|
+
def set_session_id(self, session_id: str) -> None:
|
118
|
+
"""
|
119
|
+
Override the session ID.
|
120
|
+
|
121
|
+
This should only be used in special circumstances, as it can
|
122
|
+
break the single session ID guarantee.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
session_id: The new session ID to use
|
126
|
+
"""
|
127
|
+
with self.__class__._lock:
|
128
|
+
old_id = self._session_id
|
129
|
+
if old_id != session_id:
|
130
|
+
self._session_id = session_id
|
131
|
+
logger.warning(f"Session ID changed from {old_id} to {session_id}")
|
132
|
+
else:
|
133
|
+
logger.debug(f"Session ID already set to {session_id}, no change needed")
|
134
|
+
|
135
|
+
@classmethod
|
136
|
+
def reset(cls) -> None:
|
137
|
+
"""
|
138
|
+
Reset the singleton instance (mainly for testing).
|
139
|
+
|
140
|
+
This method should not be used in production code.
|
141
|
+
"""
|
142
|
+
with cls._lock:
|
143
|
+
cls._instance = None
|
144
|
+
cls._initialized = False
|
145
|
+
logger.debug("SessionManager singleton reset")
|
146
|
+
|
147
|
+
|
148
|
+
# Global accessor function
|
149
|
+
_manager: Optional[SessionManager] = None
|
150
|
+
|
151
|
+
|
152
|
+
def get_session_manager() -> SessionManager:
|
153
|
+
"""
|
154
|
+
Get the global SessionManager instance.
|
155
|
+
|
156
|
+
Thread-safe accessor that ensures a single SessionManager exists.
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
The singleton SessionManager instance
|
160
|
+
"""
|
161
|
+
global _manager
|
162
|
+
if _manager is None:
|
163
|
+
_manager = SessionManager()
|
164
|
+
return _manager
|
165
|
+
|
166
|
+
|
167
|
+
def get_session_id() -> str:
|
168
|
+
"""
|
169
|
+
Convenience function to get the current session ID.
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
The current session ID
|
173
|
+
"""
|
174
|
+
return get_session_manager().get_session_id()
|
@@ -580,7 +580,6 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
|
|
580
580
|
# In production, you would integrate with vulnerability databases
|
581
581
|
# like npm audit, pip-audit, or safety
|
582
582
|
|
583
|
-
|
584
583
|
def _calculate_statistics(self, results: Dict[str, Any]) -> Dict[str, Any]:
|
585
584
|
"""Calculate dependency statistics."""
|
586
585
|
all_deps = self._flatten_dependencies(results.get("dependencies", {}))
|
@@ -716,9 +716,13 @@ class StructureAnalyzerStrategy(AnalyzerStrategy):
|
|
716
716
|
}
|
717
717
|
|
718
718
|
# Compare architecture
|
719
|
-
if
|
720
|
-
|
721
|
-
|
719
|
+
if (
|
720
|
+
"architecture" in baseline
|
721
|
+
and "architecture" in current
|
722
|
+
and (
|
723
|
+
baseline["architecture"]["pattern"]
|
724
|
+
!= current["architecture"]["pattern"]
|
725
|
+
)
|
722
726
|
):
|
723
727
|
comparison["architecture_change"] = {
|
724
728
|
"baseline": baseline["architecture"]["pattern"],
|
@@ -509,9 +509,7 @@ class CachingContextManager:
|
|
509
509
|
def invalidate_context(self, context_id: str):
|
510
510
|
"""Invalidate all cached values for context"""
|
511
511
|
with self._lock:
|
512
|
-
keys_to_remove = [
|
513
|
-
k for k in self.cache if k.startswith(f"{context_id}:")
|
514
|
-
]
|
512
|
+
keys_to_remove = [k for k in self.cache if k.startswith(f"{context_id}:")]
|
515
513
|
|
516
514
|
for key in keys_to_remove:
|
517
515
|
del self.cache[key]
|