claude-mpm 4.3.12__py3-none-any.whl → 4.3.13__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/PM_INSTRUCTIONS.md +390 -28
- claude_mpm/agents/templates/data_engineer.json +39 -14
- claude_mpm/cli/commands/agent_manager.py +3 -3
- claude_mpm/cli/commands/agents.py +2 -2
- claude_mpm/cli/commands/aggregate.py +1 -1
- claude_mpm/cli/commands/config.py +2 -2
- claude_mpm/cli/commands/configure.py +5 -5
- claude_mpm/cli/commands/configure_tui.py +7 -7
- claude_mpm/cli/commands/dashboard.py +1 -1
- claude_mpm/cli/commands/debug.py +5 -5
- claude_mpm/cli/commands/mcp.py +1 -1
- claude_mpm/cli/commands/mcp_command_router.py +1 -1
- claude_mpm/cli/commands/mcp_config.py +7 -10
- claude_mpm/cli/commands/mcp_external_commands.py +40 -32
- claude_mpm/cli/commands/mcp_install_commands.py +38 -10
- claude_mpm/cli/commands/mcp_setup_external.py +143 -102
- claude_mpm/cli/commands/monitor.py +2 -2
- claude_mpm/cli/commands/mpm_init_handler.py +1 -1
- claude_mpm/cli/commands/run.py +46 -2
- claude_mpm/cli/commands/search.py +41 -34
- claude_mpm/cli/interactive/agent_wizard.py +2 -2
- claude_mpm/cli/parsers/mcp_parser.py +1 -3
- claude_mpm/cli/parsers/search_parser.py +10 -4
- claude_mpm/cli/startup_logging.py +3 -5
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/core/agent_registry.py +12 -8
- claude_mpm/core/agent_session_manager.py +8 -8
- claude_mpm/core/api_validator.py +4 -4
- claude_mpm/core/base_service.py +10 -10
- claude_mpm/core/cache.py +5 -5
- claude_mpm/core/config_constants.py +1 -1
- claude_mpm/core/container.py +1 -1
- claude_mpm/core/error_handler.py +2 -2
- claude_mpm/core/file_utils.py +1 -1
- claude_mpm/core/framework_loader.py +3 -3
- claude_mpm/core/hook_manager.py +8 -6
- claude_mpm/core/instruction_reinforcement_hook.py +2 -2
- claude_mpm/core/interactive_session.py +1 -1
- claude_mpm/core/lazy.py +3 -3
- claude_mpm/core/log_manager.py +16 -12
- claude_mpm/core/logger.py +16 -11
- claude_mpm/core/logging_config.py +4 -2
- claude_mpm/core/oneshot_session.py +1 -1
- claude_mpm/core/optimized_agent_loader.py +6 -6
- claude_mpm/core/output_style_manager.py +1 -1
- claude_mpm/core/pm_hook_interceptor.py +3 -3
- claude_mpm/core/service_registry.py +1 -1
- claude_mpm/core/session_manager.py +11 -9
- claude_mpm/core/socketio_pool.py +13 -13
- claude_mpm/core/types.py +2 -2
- claude_mpm/core/unified_agent_registry.py +2 -2
- claude_mpm/core/unified_paths.py +1 -1
- claude_mpm/dashboard/analysis_runner.py +4 -4
- claude_mpm/dashboard/api/simple_directory.py +1 -1
- claude_mpm/generators/agent_profile_generator.py +4 -2
- claude_mpm/hooks/base_hook.py +2 -2
- claude_mpm/hooks/claude_hooks/connection_pool.py +4 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +12 -12
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -4
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +3 -3
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +15 -14
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +4 -4
- claude_mpm/hooks/claude_hooks/installer.py +3 -3
- claude_mpm/hooks/claude_hooks/memory_integration.py +3 -3
- claude_mpm/hooks/claude_hooks/response_tracking.py +3 -3
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +5 -5
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +3 -3
- claude_mpm/hooks/claude_hooks/services/state_manager.py +8 -7
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +3 -3
- claude_mpm/hooks/claude_hooks/tool_analysis.py +2 -2
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/hooks/tool_call_interceptor.py +2 -2
- claude_mpm/models/agent_session.py +5 -5
- claude_mpm/services/__init__.py +1 -1
- claude_mpm/services/agent_capabilities_service.py +1 -1
- claude_mpm/services/agents/agent_builder.py +3 -3
- claude_mpm/services/agents/deployment/agent_deployment.py +2 -1
- claude_mpm/services/agents/deployment/agent_discovery_service.py +9 -3
- claude_mpm/services/agents/deployment/agent_filesystem_manager.py +7 -5
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +3 -1
- claude_mpm/services/agents/deployment/agent_metrics_collector.py +1 -1
- claude_mpm/services/agents/deployment/agent_operation_service.py +2 -2
- claude_mpm/services/agents/deployment/agent_state_service.py +2 -2
- claude_mpm/services/agents/deployment/agent_template_builder.py +1 -1
- claude_mpm/services/agents/deployment/agent_versioning.py +1 -1
- claude_mpm/services/agents/deployment/deployment_wrapper.py +2 -3
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +1 -1
- claude_mpm/services/agents/loading/agent_profile_loader.py +5 -3
- claude_mpm/services/agents/loading/base_agent_manager.py +2 -2
- claude_mpm/services/agents/local_template_manager.py +6 -6
- claude_mpm/services/agents/management/agent_management_service.py +3 -3
- claude_mpm/services/agents/memory/content_manager.py +3 -3
- claude_mpm/services/agents/memory/memory_format_service.py +2 -2
- claude_mpm/services/agents/memory/template_generator.py +3 -3
- claude_mpm/services/agents/registry/__init__.py +1 -1
- claude_mpm/services/agents/registry/modification_tracker.py +2 -2
- claude_mpm/services/async_session_logger.py +3 -3
- claude_mpm/services/claude_session_logger.py +4 -4
- claude_mpm/services/cli/agent_listing_service.py +1 -1
- claude_mpm/services/cli/agent_validation_service.py +1 -0
- claude_mpm/services/cli/memory_crud_service.py +11 -6
- claude_mpm/services/cli/memory_output_formatter.py +1 -1
- claude_mpm/services/cli/session_manager.py +15 -11
- claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
- claude_mpm/services/core/memory_manager.py +81 -23
- claude_mpm/services/core/path_resolver.py +2 -2
- claude_mpm/services/diagnostics/checks/installation_check.py +1 -1
- claude_mpm/services/event_aggregator.py +4 -2
- claude_mpm/services/event_bus/direct_relay.py +5 -3
- claude_mpm/services/event_bus/event_bus.py +3 -3
- claude_mpm/services/event_bus/relay.py +6 -4
- claude_mpm/services/events/consumers/dead_letter.py +5 -3
- claude_mpm/services/events/core.py +3 -3
- claude_mpm/services/events/producers/hook.py +6 -6
- claude_mpm/services/events/producers/system.py +8 -8
- claude_mpm/services/exceptions.py +5 -5
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +2 -2
- claude_mpm/services/hook_installer_service.py +1 -1
- claude_mpm/services/infrastructure/context_preservation.py +6 -4
- claude_mpm/services/infrastructure/daemon_manager.py +2 -2
- claude_mpm/services/infrastructure/logging.py +2 -2
- claude_mpm/services/mcp_config_manager.py +175 -30
- claude_mpm/services/mcp_gateway/__init__.py +1 -1
- claude_mpm/services/mcp_gateway/auto_configure.py +3 -3
- claude_mpm/services/mcp_gateway/config/config_loader.py +1 -1
- claude_mpm/services/mcp_gateway/config/configuration.py +1 -1
- claude_mpm/services/mcp_gateway/core/base.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +21 -7
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +10 -8
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +4 -4
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +1 -1
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -3
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +15 -15
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +7 -5
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +190 -137
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +5 -5
- claude_mpm/services/mcp_gateway/tools/hello_world.py +9 -9
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +16 -16
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +17 -17
- claude_mpm/services/memory/builder.py +7 -5
- claude_mpm/services/memory/indexed_memory.py +4 -4
- claude_mpm/services/memory/optimizer.py +6 -6
- claude_mpm/services/memory/router.py +3 -3
- claude_mpm/services/monitor/daemon.py +1 -1
- claude_mpm/services/monitor/daemon_manager.py +6 -6
- claude_mpm/services/monitor/event_emitter.py +2 -2
- claude_mpm/services/monitor/handlers/file.py +1 -1
- claude_mpm/services/monitor/management/lifecycle.py +1 -1
- claude_mpm/services/monitor/server.py +4 -4
- claude_mpm/services/monitor_build_service.py +2 -2
- claude_mpm/services/port_manager.py +2 -2
- claude_mpm/services/response_tracker.py +2 -2
- claude_mpm/services/session_management_service.py +3 -2
- claude_mpm/services/socketio/client_proxy.py +2 -2
- claude_mpm/services/socketio/dashboard_server.py +4 -3
- claude_mpm/services/socketio/event_normalizer.py +12 -8
- claude_mpm/services/socketio/handlers/base.py +2 -2
- claude_mpm/services/socketio/handlers/connection.py +10 -10
- claude_mpm/services/socketio/handlers/connection_handler.py +13 -10
- claude_mpm/services/socketio/handlers/file.py +1 -1
- claude_mpm/services/socketio/handlers/git.py +1 -1
- claude_mpm/services/socketio/handlers/hook.py +16 -15
- claude_mpm/services/socketio/migration_utils.py +1 -1
- claude_mpm/services/socketio/monitor_client.py +5 -5
- claude_mpm/services/socketio/server/broadcaster.py +9 -7
- claude_mpm/services/socketio/server/connection_manager.py +2 -2
- claude_mpm/services/socketio/server/core.py +7 -5
- claude_mpm/services/socketio/server/eventbus_integration.py +18 -11
- claude_mpm/services/socketio/server/main.py +13 -13
- claude_mpm/services/socketio_client_manager.py +4 -4
- claude_mpm/services/system_instructions_service.py +2 -2
- claude_mpm/services/ticket_services/validation_service.py +1 -1
- claude_mpm/services/utility_service.py +5 -2
- claude_mpm/services/version_control/branch_strategy.py +2 -2
- claude_mpm/services/version_control/git_operations.py +22 -20
- claude_mpm/services/version_control/semantic_versioning.py +3 -3
- claude_mpm/services/version_control/version_parser.py +7 -5
- claude_mpm/services/visualization/mermaid_generator.py +1 -1
- claude_mpm/storage/state_storage.py +1 -1
- claude_mpm/tools/code_tree_analyzer.py +19 -18
- claude_mpm/tools/code_tree_builder.py +2 -2
- claude_mpm/tools/code_tree_events.py +10 -8
- claude_mpm/tools/socketio_debug.py +3 -3
- claude_mpm/utils/agent_dependency_loader.py +2 -2
- claude_mpm/utils/dependency_strategies.py +8 -3
- claude_mpm/utils/environment_context.py +2 -2
- claude_mpm/utils/error_handler.py +2 -2
- claude_mpm/utils/file_utils.py +1 -1
- claude_mpm/utils/imports.py +1 -1
- claude_mpm/utils/log_cleanup.py +21 -7
- claude_mpm/validation/agent_validator.py +2 -2
- {claude_mpm-4.3.12.dist-info → claude_mpm-4.3.13.dist-info}/METADATA +1 -1
- {claude_mpm-4.3.12.dist-info → claude_mpm-4.3.13.dist-info}/RECORD +199 -199
- {claude_mpm-4.3.12.dist-info → claude_mpm-4.3.13.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.12.dist-info → claude_mpm-4.3.13.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.12.dist-info → claude_mpm-4.3.13.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.12.dist-info → claude_mpm-4.3.13.dist-info}/top_level.txt +0 -0
|
@@ -17,7 +17,7 @@ DESIGN DECISIONS:
|
|
|
17
17
|
|
|
18
18
|
import os
|
|
19
19
|
from abc import ABC, abstractmethod
|
|
20
|
-
from datetime import datetime
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from typing import Any, Dict, List, Optional
|
|
23
23
|
|
|
@@ -223,7 +223,9 @@ class MemoryCRUDService(IMemoryCRUDService):
|
|
|
223
223
|
stat = memory_file.stat()
|
|
224
224
|
file_stats = {
|
|
225
225
|
"size_kb": stat.st_size / 1024,
|
|
226
|
-
"modified": datetime.fromtimestamp(
|
|
226
|
+
"modified": datetime.fromtimestamp(
|
|
227
|
+
stat.st_mtime, tz=timezone.utc
|
|
228
|
+
).isoformat(),
|
|
227
229
|
"path": str(memory_file),
|
|
228
230
|
}
|
|
229
231
|
|
|
@@ -256,7 +258,7 @@ class MemoryCRUDService(IMemoryCRUDService):
|
|
|
256
258
|
"file_stats": {
|
|
257
259
|
"size_kb": stat.st_size / 1024,
|
|
258
260
|
"modified": datetime.fromtimestamp(
|
|
259
|
-
stat.st_mtime
|
|
261
|
+
stat.st_mtime, tz=timezone.utc
|
|
260
262
|
).isoformat(),
|
|
261
263
|
"path": str(memory_file),
|
|
262
264
|
},
|
|
@@ -431,10 +433,10 @@ class MemoryCRUDService(IMemoryCRUDService):
|
|
|
431
433
|
{
|
|
432
434
|
"size_kb": stat.st_size / 1024,
|
|
433
435
|
"modified": datetime.fromtimestamp(
|
|
434
|
-
stat.st_mtime
|
|
436
|
+
stat.st_mtime, tz=timezone.utc
|
|
435
437
|
).isoformat(),
|
|
436
438
|
"created": datetime.fromtimestamp(
|
|
437
|
-
stat.st_ctime
|
|
439
|
+
stat.st_ctime, tz=timezone.utc
|
|
438
440
|
).isoformat(),
|
|
439
441
|
}
|
|
440
442
|
)
|
|
@@ -499,7 +501,10 @@ class MemoryCRUDService(IMemoryCRUDService):
|
|
|
499
501
|
continue
|
|
500
502
|
|
|
501
503
|
stat = memory_file.stat()
|
|
502
|
-
age_days = (
|
|
504
|
+
age_days = (
|
|
505
|
+
datetime.now(timezone.utc)
|
|
506
|
+
- datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
|
|
507
|
+
).days
|
|
503
508
|
|
|
504
509
|
# Identify files older than 30 days as candidates
|
|
505
510
|
if age_days > 30:
|
|
@@ -200,7 +200,7 @@ class MemoryOutputFormatter(IMemoryOutputFormatter):
|
|
|
200
200
|
try:
|
|
201
201
|
dt = datetime.fromisoformat(last_modified.replace("Z", "+00:00"))
|
|
202
202
|
last_modified_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
203
|
-
except:
|
|
203
|
+
except Exception:
|
|
204
204
|
last_modified_str = last_modified
|
|
205
205
|
|
|
206
206
|
# Status indicator based on usage
|
|
@@ -18,7 +18,7 @@ import json
|
|
|
18
18
|
import uuid
|
|
19
19
|
from abc import ABC, abstractmethod
|
|
20
20
|
from dataclasses import dataclass, field
|
|
21
|
-
from datetime import datetime, timedelta
|
|
21
|
+
from datetime import datetime, timedelta, timezone
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any, Dict, List, Optional
|
|
24
24
|
|
|
@@ -151,8 +151,12 @@ class SessionInfo:
|
|
|
151
151
|
|
|
152
152
|
id: str
|
|
153
153
|
context: str = "default"
|
|
154
|
-
created_at: str = field(
|
|
155
|
-
|
|
154
|
+
created_at: str = field(
|
|
155
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
156
|
+
)
|
|
157
|
+
last_used: str = field(
|
|
158
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
159
|
+
)
|
|
156
160
|
use_count: int = 0
|
|
157
161
|
agents_run: List[Dict[str, Any]] = field(default_factory=list)
|
|
158
162
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
@@ -175,8 +179,8 @@ class SessionInfo:
|
|
|
175
179
|
return cls(
|
|
176
180
|
id=data["id"],
|
|
177
181
|
context=data.get("context", "default"),
|
|
178
|
-
created_at=data.get("created_at", datetime.now().isoformat()),
|
|
179
|
-
last_used=data.get("last_used", datetime.now().isoformat()),
|
|
182
|
+
created_at=data.get("created_at", datetime.now(timezone.utc).isoformat()),
|
|
183
|
+
last_used=data.get("last_used", datetime.now(timezone.utc).isoformat()),
|
|
180
184
|
use_count=data.get("use_count", 0),
|
|
181
185
|
agents_run=data.get("agents_run", []),
|
|
182
186
|
metadata=data.get("metadata", {}),
|
|
@@ -284,7 +288,7 @@ class SessionManager(ISessionManager):
|
|
|
284
288
|
# Check session age
|
|
285
289
|
try:
|
|
286
290
|
created = datetime.fromisoformat(session.created_at)
|
|
287
|
-
age = datetime.now() - created
|
|
291
|
+
age = datetime.now(timezone.utc) - created
|
|
288
292
|
|
|
289
293
|
if age > timedelta(days=7):
|
|
290
294
|
validation.warnings.append(f"Session is {age.days} days old")
|
|
@@ -355,10 +359,10 @@ class SessionManager(ISessionManager):
|
|
|
355
359
|
{
|
|
356
360
|
"agent": agent,
|
|
357
361
|
"task": task[:100], # Truncate long tasks
|
|
358
|
-
"timestamp": datetime.now().isoformat(),
|
|
362
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
359
363
|
}
|
|
360
364
|
)
|
|
361
|
-
session.last_used = datetime.now().isoformat()
|
|
365
|
+
session.last_used = datetime.now(timezone.utc).isoformat()
|
|
362
366
|
session.use_count += 1
|
|
363
367
|
|
|
364
368
|
self.save_session(session)
|
|
@@ -370,7 +374,7 @@ class SessionManager(ISessionManager):
|
|
|
370
374
|
|
|
371
375
|
WHY: Prevents unbounded growth of session data and improves performance.
|
|
372
376
|
"""
|
|
373
|
-
now = datetime.now()
|
|
377
|
+
now = datetime.now(timezone.utc)
|
|
374
378
|
max_age = timedelta(hours=max_age_hours)
|
|
375
379
|
|
|
376
380
|
expired_ids = []
|
|
@@ -418,7 +422,7 @@ class SessionManager(ISessionManager):
|
|
|
418
422
|
return True
|
|
419
423
|
|
|
420
424
|
# Create timestamped archive file
|
|
421
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
425
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
422
426
|
archive_name = f"sessions_archive_{timestamp}.json.gz"
|
|
423
427
|
archive_path = archive_dir / archive_name
|
|
424
428
|
|
|
@@ -509,5 +513,5 @@ class ManagedSession:
|
|
|
509
513
|
"""Exit session context with cleanup."""
|
|
510
514
|
if self.session:
|
|
511
515
|
# Update last used time
|
|
512
|
-
self.session.last_used = datetime.now().isoformat()
|
|
516
|
+
self.session.last_used = datetime.now(timezone.utc).isoformat()
|
|
513
517
|
self.manager.save_session(self.session)
|
|
@@ -81,7 +81,7 @@ class UnifiedDashboardManager(IUnifiedDashboardManager):
|
|
|
81
81
|
background: bool = False,
|
|
82
82
|
open_browser: bool = True,
|
|
83
83
|
force_restart: bool = False,
|
|
84
|
-
) -> Tuple[bool, bool]:
|
|
84
|
+
) -> Tuple[bool, bool]: # noqa: PLR0911
|
|
85
85
|
"""
|
|
86
86
|
Start the dashboard using unified daemon.
|
|
87
87
|
|
|
@@ -21,7 +21,7 @@ ARCHITECTURE:
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
import logging
|
|
24
|
-
from datetime import datetime
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
from typing import Any, Dict, List, Optional, Set
|
|
27
27
|
|
|
@@ -169,7 +169,7 @@ class MemoryManager(IMemoryManager):
|
|
|
169
169
|
]
|
|
170
170
|
|
|
171
171
|
# Add new memory as a bullet point
|
|
172
|
-
timestamp = datetime.now().isoformat()
|
|
172
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
173
173
|
lines.append(f"- [{timestamp}] {key}: {value}")
|
|
174
174
|
|
|
175
175
|
# Write back
|
|
@@ -335,19 +335,37 @@ class MemoryManager(IMemoryManager):
|
|
|
335
335
|
if pm_memories:
|
|
336
336
|
aggregated_pm = self._aggregate_memories(pm_memories)
|
|
337
337
|
result["actual_memories"] = aggregated_pm
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
# Count actual memory items in aggregated content
|
|
339
|
+
memory_items = [
|
|
340
|
+
line.strip()
|
|
341
|
+
for line in aggregated_pm.split("\n")
|
|
342
|
+
if line.strip().startswith("-")
|
|
343
|
+
]
|
|
344
|
+
if memory_items:
|
|
345
|
+
self.logger.info(
|
|
346
|
+
f"Aggregated PM memory: {len(memory_items)} total items from {len(pm_memories)} source(s)"
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
self.logger.debug(
|
|
350
|
+
f"Aggregated PM memory from {len(pm_memories)} source(s) (no items)"
|
|
351
|
+
)
|
|
342
352
|
|
|
343
353
|
# Store agent memories (already aggregated per agent)
|
|
344
354
|
if agent_memories_dict:
|
|
345
355
|
result["agent_memories"] = agent_memories_dict
|
|
346
356
|
for agent_name, memory_content in agent_memories_dict.items():
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
357
|
+
# Count actual memory items
|
|
358
|
+
memory_items = [
|
|
359
|
+
line.strip()
|
|
360
|
+
for line in memory_content.split("\n")
|
|
361
|
+
if line.strip().startswith("-")
|
|
362
|
+
]
|
|
363
|
+
if memory_items:
|
|
364
|
+
self.logger.debug(
|
|
365
|
+
f"Aggregated {agent_name} memory: {len(memory_items)} items"
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
self.logger.debug(f"Aggregated {agent_name} memory: no items")
|
|
351
369
|
|
|
352
370
|
# Log summary
|
|
353
371
|
if self._stats["loaded_count"] > 0 or self._stats["skipped_count"] > 0:
|
|
@@ -410,10 +428,21 @@ class MemoryManager(IMemoryManager):
|
|
|
410
428
|
"path": pm_memory_path,
|
|
411
429
|
}
|
|
412
430
|
)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
431
|
+
# Count actual memory items (lines starting with "-")
|
|
432
|
+
memory_items = [
|
|
433
|
+
line.strip()
|
|
434
|
+
for line in loaded_content.split("\n")
|
|
435
|
+
if line.strip().startswith("-")
|
|
436
|
+
]
|
|
437
|
+
if memory_items:
|
|
438
|
+
self.logger.info(
|
|
439
|
+
f"Loaded {source} PM memory: {len(memory_items)} items"
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
# Skip logging if no actual memory items
|
|
443
|
+
self.logger.debug(
|
|
444
|
+
f"Skipped {source} PM memory: {pm_memory_path} (no memory items)"
|
|
445
|
+
)
|
|
417
446
|
self._stats["loaded_count"] += 1
|
|
418
447
|
except Exception as e:
|
|
419
448
|
self.logger.error(
|
|
@@ -471,20 +500,47 @@ class MemoryManager(IMemoryManager):
|
|
|
471
500
|
}
|
|
472
501
|
)
|
|
473
502
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
503
|
+
# Count actual memory items (lines starting with "-")
|
|
504
|
+
memory_items = [
|
|
505
|
+
line.strip()
|
|
506
|
+
for line in loaded_content.split("\n")
|
|
507
|
+
if line.strip().startswith("-")
|
|
508
|
+
]
|
|
509
|
+
if memory_items:
|
|
510
|
+
self.logger.info(
|
|
511
|
+
f"Loaded {source} memory for {agent_name}: {len(memory_items)} items"
|
|
512
|
+
)
|
|
513
|
+
else:
|
|
514
|
+
# Skip logging if no actual memory items
|
|
515
|
+
self.logger.debug(
|
|
516
|
+
f"Skipped {source} memory for {agent_name}: {memory_file.name} (no memory items)"
|
|
517
|
+
)
|
|
478
518
|
self._stats["loaded_count"] += 1
|
|
479
519
|
except Exception as e:
|
|
480
520
|
self.logger.error(
|
|
481
521
|
f"Failed to load agent memory from {memory_file}: {e}"
|
|
482
522
|
)
|
|
483
523
|
else:
|
|
484
|
-
# Log skipped memories
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
524
|
+
# Log skipped memories only if they contain actual items
|
|
525
|
+
try:
|
|
526
|
+
loaded_content = memory_file.read_text(encoding="utf-8")
|
|
527
|
+
memory_items = [
|
|
528
|
+
line.strip()
|
|
529
|
+
for line in loaded_content.split("\n")
|
|
530
|
+
if line.strip().startswith("-")
|
|
531
|
+
]
|
|
532
|
+
if memory_items:
|
|
533
|
+
self.logger.info(
|
|
534
|
+
f"Skipped {source} memory: {memory_file.name} (agent '{agent_name}' not deployed, {len(memory_items)} items)"
|
|
535
|
+
)
|
|
536
|
+
else:
|
|
537
|
+
# Don't log if file has no actual memory items
|
|
538
|
+
self.logger.debug(
|
|
539
|
+
f"Skipped {source} memory: {memory_file.name} (agent '{agent_name}' not deployed, no items)"
|
|
540
|
+
)
|
|
541
|
+
except Exception:
|
|
542
|
+
# If we can't read the file, just skip silently
|
|
543
|
+
pass
|
|
488
544
|
|
|
489
545
|
# Detect naming mismatches
|
|
490
546
|
alt_name = (
|
|
@@ -570,7 +626,9 @@ class MemoryManager(IMemoryManager):
|
|
|
570
626
|
lines.append("# Agent Memory")
|
|
571
627
|
|
|
572
628
|
# Add latest timestamp
|
|
573
|
-
lines.append(
|
|
629
|
+
lines.append(
|
|
630
|
+
f"<!-- Last Updated: {datetime.now(timezone.utc).isoformat()}Z -->"
|
|
631
|
+
)
|
|
574
632
|
lines.append("")
|
|
575
633
|
|
|
576
634
|
# Add all unique items (sorted for consistency)
|
|
@@ -163,7 +163,7 @@ class PathResolver(IPathResolver):
|
|
|
163
163
|
self.logger.debug(f"No project root found from {start_path}")
|
|
164
164
|
return None
|
|
165
165
|
|
|
166
|
-
def detect_framework_path(self) -> Optional[Path]:
|
|
166
|
+
def detect_framework_path(self) -> Optional[Path]: # noqa: PLR0911
|
|
167
167
|
"""
|
|
168
168
|
Auto-detect claude-mpm framework using unified path management.
|
|
169
169
|
|
|
@@ -322,8 +322,8 @@ class PathResolver(IPathResolver):
|
|
|
322
322
|
"""Detect framework path using unified path management."""
|
|
323
323
|
try:
|
|
324
324
|
# Import here to avoid circular dependencies
|
|
325
|
+
from ...core.unified_paths import DeploymentContext as UnifiedContext
|
|
325
326
|
from ...core.unified_paths import (
|
|
326
|
-
DeploymentContext as UnifiedContext,
|
|
327
327
|
get_path_manager,
|
|
328
328
|
)
|
|
329
329
|
|
|
@@ -136,7 +136,7 @@ class InstallationCheck(BaseDiagnosticCheck):
|
|
|
136
136
|
details={"error": str(e)},
|
|
137
137
|
)
|
|
138
138
|
|
|
139
|
-
def _check_installation_method(self) -> DiagnosticResult:
|
|
139
|
+
def _check_installation_method(self) -> DiagnosticResult: # noqa: PLR0911
|
|
140
140
|
"""Detect how claude-mpm was installed."""
|
|
141
141
|
methods_found = []
|
|
142
142
|
details = {}
|
|
@@ -18,7 +18,7 @@ import sys
|
|
|
18
18
|
import threading
|
|
19
19
|
import time
|
|
20
20
|
from collections import defaultdict
|
|
21
|
-
from datetime import datetime
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
22
|
from typing import Any, Dict, List, Optional
|
|
23
23
|
|
|
24
24
|
try:
|
|
@@ -267,7 +267,9 @@ class EventAggregator:
|
|
|
267
267
|
try:
|
|
268
268
|
# Extract event metadata
|
|
269
269
|
event_type = event_data.get("type", "unknown")
|
|
270
|
-
timestamp = event_data.get(
|
|
270
|
+
timestamp = event_data.get(
|
|
271
|
+
"timestamp", datetime.now(timezone.utc).isoformat() + "Z"
|
|
272
|
+
)
|
|
271
273
|
data = event_data.get("data", {})
|
|
272
274
|
|
|
273
275
|
# Update statistics
|
|
@@ -15,7 +15,7 @@ DO NOT use "event" or "type" fields - use "hook_event_name" instead!
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
-
from datetime import datetime
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
21
|
from .event_bus import EventBus
|
|
@@ -223,7 +223,9 @@ class DirectSocketIORelay:
|
|
|
223
223
|
event_type, broadcast_data
|
|
224
224
|
)
|
|
225
225
|
self.stats["events_relayed"] += 1
|
|
226
|
-
self.stats["last_relay_time"] = datetime.now(
|
|
226
|
+
self.stats["last_relay_time"] = datetime.now(
|
|
227
|
+
timezone.utc
|
|
228
|
+
).isoformat()
|
|
227
229
|
|
|
228
230
|
# Reset retry counter on successful broadcast
|
|
229
231
|
if self.connection_retries > 0:
|
|
@@ -258,7 +260,7 @@ class DirectSocketIORelay:
|
|
|
258
260
|
logger.info(
|
|
259
261
|
f"[DirectRelay] Retry successful for {event_type}"
|
|
260
262
|
)
|
|
261
|
-
except:
|
|
263
|
+
except Exception:
|
|
262
264
|
pass # Already counted as failed
|
|
263
265
|
else:
|
|
264
266
|
# Enhanced logging when broadcaster is not available
|
|
@@ -11,7 +11,7 @@ WHY pyee over alternatives:
|
|
|
11
11
|
import asyncio
|
|
12
12
|
import logging
|
|
13
13
|
import threading
|
|
14
|
-
from datetime import datetime
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
15
|
from typing import Any, Callable, Dict, List, Optional, Set
|
|
16
16
|
|
|
17
17
|
from pyee.asyncio import AsyncIOEventEmitter
|
|
@@ -210,7 +210,7 @@ class EventBus:
|
|
|
210
210
|
|
|
211
211
|
# Update stats
|
|
212
212
|
self._stats["events_published"] += 1
|
|
213
|
-
self._stats["last_event_time"] = datetime.now().isoformat()
|
|
213
|
+
self._stats["last_event_time"] = datetime.now(timezone.utc).isoformat()
|
|
214
214
|
|
|
215
215
|
if self._debug:
|
|
216
216
|
logger.debug(f"Published event: {event_type}")
|
|
@@ -302,7 +302,7 @@ class EventBus:
|
|
|
302
302
|
data: The event data
|
|
303
303
|
"""
|
|
304
304
|
event_record = {
|
|
305
|
-
"timestamp": datetime.now().isoformat(),
|
|
305
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
306
306
|
"type": event_type,
|
|
307
307
|
"data": data,
|
|
308
308
|
}
|
|
@@ -11,7 +11,7 @@ WHY separate relay component:
|
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
13
|
import time
|
|
14
|
-
from datetime import datetime
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
15
|
from typing import Any, Dict, Optional
|
|
16
16
|
|
|
17
17
|
# Socket.IO imports
|
|
@@ -151,7 +151,7 @@ class SocketIORelay:
|
|
|
151
151
|
# Verify connection is still alive
|
|
152
152
|
if self.client.connected:
|
|
153
153
|
return True
|
|
154
|
-
except:
|
|
154
|
+
except Exception:
|
|
155
155
|
pass
|
|
156
156
|
|
|
157
157
|
# Need to create or reconnect
|
|
@@ -187,7 +187,9 @@ class SocketIORelay:
|
|
|
187
187
|
"subtype": (
|
|
188
188
|
event_type.split(".", 1)[1] if "." in event_type else "generic"
|
|
189
189
|
),
|
|
190
|
-
"timestamp": data.get(
|
|
190
|
+
"timestamp": data.get(
|
|
191
|
+
"timestamp", datetime.now(timezone.utc).isoformat()
|
|
192
|
+
),
|
|
191
193
|
"data": data,
|
|
192
194
|
"source": "event_bus",
|
|
193
195
|
},
|
|
@@ -195,7 +197,7 @@ class SocketIORelay:
|
|
|
195
197
|
|
|
196
198
|
# Update statistics
|
|
197
199
|
self.stats["events_relayed"] += 1
|
|
198
|
-
self.stats["last_relay_time"] = datetime.now().isoformat()
|
|
200
|
+
self.stats["last_relay_time"] = datetime.now(timezone.utc).isoformat()
|
|
199
201
|
|
|
200
202
|
if self.debug:
|
|
201
203
|
logger.debug(f"Relayed event to Socket.IO: {event_type}")
|
|
@@ -6,7 +6,7 @@ Handles events that failed processing in other consumers.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
-
from datetime import datetime
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
@@ -284,7 +284,7 @@ class DeadLetterConsumer(IEventConsumer):
|
|
|
284
284
|
|
|
285
285
|
def _rotate_file(self) -> None:
|
|
286
286
|
"""Rotate to a new output file."""
|
|
287
|
-
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
287
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
288
288
|
self._current_file = self.output_dir / f"dead-letter-{timestamp}.jsonl"
|
|
289
289
|
self._current_file_size = 0
|
|
290
290
|
self._metrics["files_created"] += 1
|
|
@@ -293,7 +293,9 @@ class DeadLetterConsumer(IEventConsumer):
|
|
|
293
293
|
|
|
294
294
|
async def _cleanup_old_files(self) -> None:
|
|
295
295
|
"""Remove files older than retention period."""
|
|
296
|
-
cutoff_time = datetime.now().timestamp() - (
|
|
296
|
+
cutoff_time = datetime.now(timezone.utc).timestamp() - (
|
|
297
|
+
self.retention_days * 86400
|
|
298
|
+
)
|
|
297
299
|
|
|
298
300
|
for file_path in self.output_dir.glob("dead-letter-*.jsonl"):
|
|
299
301
|
try:
|
|
@@ -12,7 +12,7 @@ import time
|
|
|
12
12
|
import uuid
|
|
13
13
|
from collections import defaultdict, deque
|
|
14
14
|
from dataclasses import dataclass, field
|
|
15
|
-
from datetime import datetime
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
16
|
from enum import Enum
|
|
17
17
|
from typing import Any, Deque, Dict, List, Optional, Set
|
|
18
18
|
|
|
@@ -225,7 +225,7 @@ class EventBus(IEventBus):
|
|
|
225
225
|
|
|
226
226
|
# Add metadata
|
|
227
227
|
if event.metadata:
|
|
228
|
-
event.metadata.published_at = datetime.now()
|
|
228
|
+
event.metadata.published_at = datetime.now(timezone.utc)
|
|
229
229
|
|
|
230
230
|
# Queue event
|
|
231
231
|
self._event_queues[event.priority].append(event)
|
|
@@ -353,7 +353,7 @@ class EventBus(IEventBus):
|
|
|
353
353
|
# Update metrics
|
|
354
354
|
if events_processed > 0:
|
|
355
355
|
self._metrics["events_processed"] += events_processed
|
|
356
|
-
self._metrics["last_event_time"] = datetime.now()
|
|
356
|
+
self._metrics["last_event_time"] = datetime.now(timezone.utc)
|
|
357
357
|
self._metrics["queue_size"] = sum(
|
|
358
358
|
len(q) for q in self._event_queues.values()
|
|
359
359
|
)
|
|
@@ -7,7 +7,7 @@ This replaces direct Socket.IO emission in the hook handler.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import uuid
|
|
10
|
-
from datetime import datetime
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
11
|
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
13
|
from claude_mpm.core.logging_config import get_logger
|
|
@@ -96,7 +96,7 @@ class HookEventProducer(IEventProducer):
|
|
|
96
96
|
id=str(uuid.uuid4()),
|
|
97
97
|
topic="hook.response",
|
|
98
98
|
type="AssistantResponse",
|
|
99
|
-
timestamp=datetime.now(),
|
|
99
|
+
timestamp=datetime.now(timezone.utc),
|
|
100
100
|
source=self.source_name,
|
|
101
101
|
data=response_data,
|
|
102
102
|
correlation_id=correlation_id,
|
|
@@ -128,7 +128,7 @@ class HookEventProducer(IEventProducer):
|
|
|
128
128
|
id=str(uuid.uuid4()),
|
|
129
129
|
topic="hook.tool",
|
|
130
130
|
type="ToolUse",
|
|
131
|
-
timestamp=datetime.now(),
|
|
131
|
+
timestamp=datetime.now(timezone.utc),
|
|
132
132
|
source=self.source_name,
|
|
133
133
|
data={
|
|
134
134
|
"tool": tool_name,
|
|
@@ -164,7 +164,7 @@ class HookEventProducer(IEventProducer):
|
|
|
164
164
|
id=str(uuid.uuid4()),
|
|
165
165
|
topic="hook.error",
|
|
166
166
|
type="Error",
|
|
167
|
-
timestamp=datetime.now(),
|
|
167
|
+
timestamp=datetime.now(timezone.utc),
|
|
168
168
|
source=self.source_name,
|
|
169
169
|
data={
|
|
170
170
|
"error_type": error_type,
|
|
@@ -200,7 +200,7 @@ class HookEventProducer(IEventProducer):
|
|
|
200
200
|
id=str(uuid.uuid4()),
|
|
201
201
|
topic=f"hook.subagent.{event_type.lower()}",
|
|
202
202
|
type=f"Subagent{event_type}",
|
|
203
|
-
timestamp=datetime.now(),
|
|
203
|
+
timestamp=datetime.now(timezone.utc),
|
|
204
204
|
source=self.source_name,
|
|
205
205
|
data={
|
|
206
206
|
"subagent": subagent_name,
|
|
@@ -255,7 +255,7 @@ class HookEventProducer(IEventProducer):
|
|
|
255
255
|
id=str(uuid.uuid4()),
|
|
256
256
|
topic=topic,
|
|
257
257
|
type=hook_type,
|
|
258
|
-
timestamp=datetime.now(),
|
|
258
|
+
timestamp=datetime.now(timezone.utc),
|
|
259
259
|
source=self.source_name,
|
|
260
260
|
data=hook_data,
|
|
261
261
|
correlation_id=correlation_id,
|
|
@@ -6,7 +6,7 @@ Publishes system-level events to the event bus.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import uuid
|
|
9
|
-
from datetime import datetime
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
10
|
from typing import Any, Dict, List, Optional
|
|
11
11
|
|
|
12
12
|
from claude_mpm.core.logging_config import get_logger
|
|
@@ -100,7 +100,7 @@ class SystemEventProducer(IEventProducer):
|
|
|
100
100
|
id=str(uuid.uuid4()),
|
|
101
101
|
topic="system.lifecycle.startup",
|
|
102
102
|
type="ServiceStartup",
|
|
103
|
-
timestamp=datetime.now(),
|
|
103
|
+
timestamp=datetime.now(timezone.utc),
|
|
104
104
|
source=self.source_name,
|
|
105
105
|
data={
|
|
106
106
|
"service": service_name,
|
|
@@ -133,7 +133,7 @@ class SystemEventProducer(IEventProducer):
|
|
|
133
133
|
id=str(uuid.uuid4()),
|
|
134
134
|
topic="system.lifecycle.shutdown",
|
|
135
135
|
type="ServiceShutdown",
|
|
136
|
-
timestamp=datetime.now(),
|
|
136
|
+
timestamp=datetime.now(timezone.utc),
|
|
137
137
|
source=self.source_name,
|
|
138
138
|
data={
|
|
139
139
|
"service": service_name,
|
|
@@ -168,7 +168,7 @@ class SystemEventProducer(IEventProducer):
|
|
|
168
168
|
id=str(uuid.uuid4()),
|
|
169
169
|
topic="system.health",
|
|
170
170
|
type="HealthStatus",
|
|
171
|
-
timestamp=datetime.now(),
|
|
171
|
+
timestamp=datetime.now(timezone.utc),
|
|
172
172
|
source=self.source_name,
|
|
173
173
|
data={
|
|
174
174
|
"service": service_name,
|
|
@@ -206,7 +206,7 @@ class SystemEventProducer(IEventProducer):
|
|
|
206
206
|
id=str(uuid.uuid4()),
|
|
207
207
|
topic="system.config",
|
|
208
208
|
type="ConfigChange",
|
|
209
|
-
timestamp=datetime.now(),
|
|
209
|
+
timestamp=datetime.now(timezone.utc),
|
|
210
210
|
source=self.source_name,
|
|
211
211
|
data={
|
|
212
212
|
"service": service_name,
|
|
@@ -238,7 +238,7 @@ class SystemEventProducer(IEventProducer):
|
|
|
238
238
|
id=str(uuid.uuid4()),
|
|
239
239
|
topic="system.performance",
|
|
240
240
|
type="PerformanceMetrics",
|
|
241
|
-
timestamp=datetime.now(),
|
|
241
|
+
timestamp=datetime.now(timezone.utc),
|
|
242
242
|
source=self.source_name,
|
|
243
243
|
data={
|
|
244
244
|
"service": service_name,
|
|
@@ -274,7 +274,7 @@ class SystemEventProducer(IEventProducer):
|
|
|
274
274
|
id=str(uuid.uuid4()),
|
|
275
275
|
topic="system.error",
|
|
276
276
|
type="SystemError",
|
|
277
|
-
timestamp=datetime.now(),
|
|
277
|
+
timestamp=datetime.now(timezone.utc),
|
|
278
278
|
source=self.source_name,
|
|
279
279
|
data={
|
|
280
280
|
"service": service_name,
|
|
@@ -311,7 +311,7 @@ class SystemEventProducer(IEventProducer):
|
|
|
311
311
|
id=str(uuid.uuid4()),
|
|
312
312
|
topic="system.warning",
|
|
313
313
|
type="SystemWarning",
|
|
314
|
-
timestamp=datetime.now(),
|
|
314
|
+
timestamp=datetime.now(timezone.utc),
|
|
315
315
|
source=self.source_name,
|
|
316
316
|
data={
|
|
317
317
|
"service": service_name,
|
|
@@ -15,7 +15,7 @@ Design Principles:
|
|
|
15
15
|
|
|
16
16
|
import platform
|
|
17
17
|
import time
|
|
18
|
-
from datetime import datetime
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
19
|
from typing import Any, Dict, List, Optional
|
|
20
20
|
|
|
21
21
|
|
|
@@ -43,7 +43,7 @@ class SocketIOServerError(Exception):
|
|
|
43
43
|
self.message = message
|
|
44
44
|
self.error_code = error_code or self.__class__.__name__.lower()
|
|
45
45
|
self.context = context or {}
|
|
46
|
-
self.timestamp = datetime.
|
|
46
|
+
self.timestamp = datetime.now(timezone.utc).isoformat() + "Z"
|
|
47
47
|
|
|
48
48
|
def to_dict(self) -> Dict[str, Any]:
|
|
49
49
|
"""Convert error to dictionary format for structured logging/handling."""
|
|
@@ -125,9 +125,9 @@ class DaemonConflictError(SocketIOServerError):
|
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
if create_time:
|
|
128
|
-
start_time = datetime.fromtimestamp(
|
|
129
|
-
|
|
130
|
-
)
|
|
128
|
+
start_time = datetime.fromtimestamp(
|
|
129
|
+
create_time, tz=timezone.utc
|
|
130
|
+
).strftime("%Y-%m-%d %H:%M:%S")
|
|
131
131
|
uptime = time.time() - create_time
|
|
132
132
|
lines.append(f" • Started: {start_time} (uptime: {uptime:.0f}s)")
|
|
133
133
|
|