claude-mpm 5.4.96__py3-none-any.whl → 5.6.17__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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
- claude_mpm/agents/PM_INSTRUCTIONS.md +44 -10
- claude_mpm/agents/WORKFLOW.md +2 -0
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- claude_mpm/cli/commands/autotodos.py +45 -5
- claude_mpm/cli/commands/commander.py +216 -0
- claude_mpm/cli/commands/hook_errors.py +60 -60
- claude_mpm/cli/commands/run.py +35 -3
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +32 -17
- claude_mpm/cli/parsers/base_parser.py +17 -0
- claude_mpm/cli/parsers/commander_parser.py +116 -0
- claude_mpm/cli/parsers/run_parser.py +10 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +124 -3
- claude_mpm/cli/startup_display.py +2 -1
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +78 -0
- claude_mpm/commander/adapters/__init__.py +60 -0
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +288 -0
- claude_mpm/commander/adapters/claude_code.py +392 -0
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/communication.py +366 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/__init__.py +16 -0
- claude_mpm/commander/api/app.py +121 -0
- claude_mpm/commander/api/errors.py +133 -0
- claude_mpm/commander/api/routes/__init__.py +8 -0
- claude_mpm/commander/api/routes/events.py +184 -0
- claude_mpm/commander/api/routes/inbox.py +171 -0
- claude_mpm/commander/api/routes/messages.py +148 -0
- claude_mpm/commander/api/routes/projects.py +271 -0
- claude_mpm/commander/api/routes/sessions.py +226 -0
- claude_mpm/commander/api/routes/work.py +296 -0
- claude_mpm/commander/api/schemas.py +186 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +111 -0
- claude_mpm/commander/chat/commands.py +96 -0
- claude_mpm/commander/chat/repl.py +310 -0
- claude_mpm/commander/config.py +49 -0
- claude_mpm/commander/config_loader.py +115 -0
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +594 -0
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/__init__.py +26 -0
- claude_mpm/commander/events/manager.py +332 -0
- claude_mpm/commander/frameworks/__init__.py +12 -0
- claude_mpm/commander/frameworks/base.py +143 -0
- claude_mpm/commander/frameworks/claude_code.py +58 -0
- claude_mpm/commander/frameworks/mpm.py +62 -0
- claude_mpm/commander/inbox/__init__.py +16 -0
- claude_mpm/commander/inbox/dedup.py +128 -0
- claude_mpm/commander/inbox/inbox.py +224 -0
- claude_mpm/commander/inbox/models.py +70 -0
- claude_mpm/commander/instance_manager.py +337 -0
- claude_mpm/commander/llm/__init__.py +6 -0
- claude_mpm/commander/llm/openrouter_client.py +167 -0
- claude_mpm/commander/llm/summarizer.py +70 -0
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/__init__.py +18 -0
- claude_mpm/commander/models/events.py +121 -0
- claude_mpm/commander/models/project.py +162 -0
- claude_mpm/commander/models/work.py +214 -0
- claude_mpm/commander/parsing/__init__.py +20 -0
- claude_mpm/commander/parsing/extractor.py +132 -0
- claude_mpm/commander/parsing/output_parser.py +270 -0
- claude_mpm/commander/parsing/patterns.py +100 -0
- claude_mpm/commander/persistence/__init__.py +11 -0
- claude_mpm/commander/persistence/event_store.py +274 -0
- claude_mpm/commander/persistence/state_store.py +309 -0
- claude_mpm/commander/persistence/work_store.py +164 -0
- claude_mpm/commander/polling/__init__.py +13 -0
- claude_mpm/commander/polling/event_detector.py +104 -0
- claude_mpm/commander/polling/output_buffer.py +49 -0
- claude_mpm/commander/polling/output_poller.py +153 -0
- claude_mpm/commander/project_session.py +268 -0
- claude_mpm/commander/proxy/__init__.py +12 -0
- claude_mpm/commander/proxy/formatter.py +89 -0
- claude_mpm/commander/proxy/output_handler.py +191 -0
- claude_mpm/commander/proxy/relay.py +155 -0
- claude_mpm/commander/registry.py +410 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +346 -0
- claude_mpm/commander/session/__init__.py +6 -0
- claude_mpm/commander/session/context.py +81 -0
- claude_mpm/commander/session/manager.py +59 -0
- claude_mpm/commander/tmux_orchestrator.py +361 -0
- claude_mpm/commander/web/__init__.py +1 -0
- claude_mpm/commander/work/__init__.py +30 -0
- claude_mpm/commander/work/executor.py +207 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +241 -0
- claude_mpm/commander/workflow/notifier.py +146 -0
- claude_mpm/commands/mpm-config.md +8 -0
- claude_mpm/commands/mpm-doctor.md +8 -0
- claude_mpm/commands/mpm-help.md +8 -0
- claude_mpm/commands/mpm-init.md +8 -0
- claude_mpm/commands/mpm-monitor.md +8 -0
- claude_mpm/commands/mpm-organize.md +8 -0
- claude_mpm/commands/mpm-postmortem.md +8 -0
- claude_mpm/commands/mpm-session-resume.md +8 -0
- claude_mpm/commands/mpm-status.md +8 -0
- claude_mpm/commands/mpm-ticket-view.md +8 -0
- claude_mpm/commands/mpm-version.md +8 -0
- claude_mpm/commands/mpm.md +8 -0
- claude_mpm/config/agent_presets.py +8 -7
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/core/claude_runner.py +143 -0
- claude_mpm/core/config.py +32 -19
- claude_mpm/core/logger.py +26 -9
- claude_mpm/core/logging_utils.py +35 -11
- claude_mpm/core/output_style_manager.py +49 -12
- claude_mpm/core/unified_config.py +10 -6
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/experimental/cli_enhancements.py +2 -1
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
- claude_mpm/hooks/claude_hooks/event_handlers.py +112 -99
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
- claude_mpm/hooks/claude_hooks/installer.py +116 -8
- claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
- claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
- claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
- claude_mpm/scripts/claude-hook-handler.sh +43 -16
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_recommendation_service.py +8 -8
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/event_log.py +8 -0
- claude_mpm/services/pm_skills_deployer.py +84 -6
- claude_mpm/services/skills/git_skill_source_manager.py +130 -10
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +74 -4
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
- claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
- claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
- claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
- claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
- claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
- claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
- claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
- claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
- claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
- claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +22 -6
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +213 -83
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Event deduplication logic for inbox system.
|
|
2
|
+
|
|
3
|
+
Prevents duplicate events within a configurable time window using
|
|
4
|
+
content hashing and time-based expiration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from typing import Dict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class DedupEntry:
|
|
15
|
+
"""Deduplication cache entry.
|
|
16
|
+
|
|
17
|
+
Tracks when an event was first seen and how many times it's appeared.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
key: Unique deduplication key
|
|
21
|
+
first_seen: When this key was first encountered
|
|
22
|
+
count: Number of times this key has been seen
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
key: str
|
|
26
|
+
first_seen: datetime
|
|
27
|
+
count: int = 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EventDeduplicator:
|
|
31
|
+
"""Prevent duplicate events within a time window.
|
|
32
|
+
|
|
33
|
+
Uses content hashing to detect duplicates and time-based expiration
|
|
34
|
+
to automatically clean up old entries.
|
|
35
|
+
|
|
36
|
+
The deduplication key is constructed from:
|
|
37
|
+
- Project ID
|
|
38
|
+
- Event type
|
|
39
|
+
- Title hash (first 8 chars of MD5)
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
>>> dedup = EventDeduplicator(window_seconds=60)
|
|
43
|
+
>>> dedup.is_duplicate("proj_123", "error", "Connection failed")
|
|
44
|
+
False # First occurrence
|
|
45
|
+
>>> dedup.is_duplicate("proj_123", "error", "Connection failed")
|
|
46
|
+
True # Duplicate within 60 seconds
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, window_seconds: int = 60):
|
|
50
|
+
"""Initialize deduplicator with time window.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
window_seconds: Duration in seconds to consider events as duplicates.
|
|
54
|
+
Default is 60 seconds.
|
|
55
|
+
"""
|
|
56
|
+
self.window = timedelta(seconds=window_seconds)
|
|
57
|
+
self._seen: Dict[str, DedupEntry] = {}
|
|
58
|
+
|
|
59
|
+
def make_key(self, project_id: str, event_type: str, title: str) -> str:
|
|
60
|
+
"""Create deduplication key from event attributes.
|
|
61
|
+
|
|
62
|
+
Generates a key in the format: {project_id}:{event_type}:{title_hash}
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
project_id: Unique project identifier
|
|
66
|
+
event_type: Type of event (from EventType enum)
|
|
67
|
+
title: Event title text
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Composite key string for deduplication
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> dedup = EventDeduplicator()
|
|
74
|
+
>>> dedup.make_key("proj_123", "error", "Connection failed")
|
|
75
|
+
'proj_123:error:a1b2c3d4'
|
|
76
|
+
"""
|
|
77
|
+
title_hash = hashlib.md5(title.encode(), usedforsecurity=False).hexdigest()[:8]
|
|
78
|
+
return f"{project_id}:{event_type}:{title_hash}"
|
|
79
|
+
|
|
80
|
+
def is_duplicate(self, project_id: str, event_type: str, title: str) -> bool:
|
|
81
|
+
"""Check if this event is a duplicate within the window.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
project_id: Unique project identifier
|
|
85
|
+
event_type: Type of event
|
|
86
|
+
title: Event title
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if this is a duplicate, False if it's new
|
|
90
|
+
|
|
91
|
+
Side Effects:
|
|
92
|
+
- Increments count for duplicates
|
|
93
|
+
- Creates new entry for new events
|
|
94
|
+
- Cleans up expired entries
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
>>> dedup = EventDeduplicator(window_seconds=60)
|
|
98
|
+
>>> dedup.is_duplicate("proj_123", "error", "Timeout")
|
|
99
|
+
False # First occurrence
|
|
100
|
+
>>> dedup.is_duplicate("proj_123", "error", "Timeout")
|
|
101
|
+
True # Duplicate within window
|
|
102
|
+
"""
|
|
103
|
+
self._cleanup_expired()
|
|
104
|
+
key = self.make_key(project_id, event_type, title)
|
|
105
|
+
|
|
106
|
+
if key in self._seen:
|
|
107
|
+
self._seen[key].count += 1
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
self._seen[key] = DedupEntry(
|
|
111
|
+
key=key,
|
|
112
|
+
first_seen=datetime.now(timezone.utc),
|
|
113
|
+
)
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _cleanup_expired(self) -> None:
|
|
117
|
+
"""Remove entries older than the deduplication window.
|
|
118
|
+
|
|
119
|
+
Called automatically before each is_duplicate check to prevent
|
|
120
|
+
unbounded memory growth.
|
|
121
|
+
|
|
122
|
+
Side Effects:
|
|
123
|
+
Removes all entries where (now - first_seen) > window
|
|
124
|
+
"""
|
|
125
|
+
now = datetime.now(timezone.utc)
|
|
126
|
+
expired = [k for k, v in self._seen.items() if now - v.first_seen > self.window]
|
|
127
|
+
for k in expired:
|
|
128
|
+
del self._seen[k]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Centralized inbox for MPM Commander.
|
|
2
|
+
|
|
3
|
+
Aggregates events from all projects with filtering, sorting, and pagination.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from ..events.manager import EventManager
|
|
11
|
+
from ..models.events import Event, EventPriority, EventType
|
|
12
|
+
from ..registry import ProjectRegistry
|
|
13
|
+
from .dedup import EventDeduplicator
|
|
14
|
+
from .models import InboxItem
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class InboxCounts:
|
|
21
|
+
"""Count of events by priority.
|
|
22
|
+
|
|
23
|
+
Provides quick summary statistics for inbox state.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
critical: Count of CRITICAL priority events
|
|
27
|
+
high: Count of HIGH priority events
|
|
28
|
+
normal: Count of NORMAL priority events
|
|
29
|
+
low: Count of LOW priority events
|
|
30
|
+
info: Count of INFO priority events
|
|
31
|
+
total: Total count of all events
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
critical: int = 0
|
|
35
|
+
high: int = 0
|
|
36
|
+
normal: int = 0
|
|
37
|
+
low: int = 0
|
|
38
|
+
info: int = 0
|
|
39
|
+
total: int = 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Inbox:
|
|
43
|
+
"""Centralized inbox aggregating events from all projects.
|
|
44
|
+
|
|
45
|
+
Provides a unified view of all pending events with:
|
|
46
|
+
- Multi-level filtering (priority, project, event type)
|
|
47
|
+
- Pagination support
|
|
48
|
+
- Priority-based sorting
|
|
49
|
+
- Project metadata enrichment
|
|
50
|
+
- Deduplication
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> inbox = Inbox(event_manager, project_registry)
|
|
54
|
+
>>> items = inbox.get_items(limit=10, priority=EventPriority.HIGH)
|
|
55
|
+
>>> counts = inbox.get_counts()
|
|
56
|
+
>>> print(f"Total pending: {counts.total}")
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
event_manager: EventManager,
|
|
62
|
+
project_registry: ProjectRegistry,
|
|
63
|
+
dedup_window: int = 60,
|
|
64
|
+
):
|
|
65
|
+
"""Initialize inbox with dependencies.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
event_manager: Event lifecycle manager
|
|
69
|
+
project_registry: Project registration system
|
|
70
|
+
dedup_window: Deduplication window in seconds (default: 60)
|
|
71
|
+
"""
|
|
72
|
+
self.events = event_manager
|
|
73
|
+
self.projects = project_registry
|
|
74
|
+
self.deduplicator = EventDeduplicator(window_seconds=dedup_window)
|
|
75
|
+
|
|
76
|
+
def get_items(
|
|
77
|
+
self,
|
|
78
|
+
limit: int = 50,
|
|
79
|
+
offset: int = 0,
|
|
80
|
+
priority: Optional[EventPriority] = None,
|
|
81
|
+
project_id: Optional[str] = None,
|
|
82
|
+
event_type: Optional[EventType] = None,
|
|
83
|
+
) -> List[InboxItem]:
|
|
84
|
+
"""Get inbox items with optional filtering and pagination.
|
|
85
|
+
|
|
86
|
+
Applies filters in this order:
|
|
87
|
+
1. Get all pending events (optionally for specific project)
|
|
88
|
+
2. Filter by priority if specified
|
|
89
|
+
3. Filter by event type if specified
|
|
90
|
+
4. Sort by priority (high to low) then created_at (old to new)
|
|
91
|
+
5. Paginate with limit and offset
|
|
92
|
+
6. Enrich with project metadata
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
limit: Maximum number of items to return (default: 50)
|
|
96
|
+
offset: Number of items to skip for pagination (default: 0)
|
|
97
|
+
priority: Filter to specific priority level (optional)
|
|
98
|
+
project_id: Filter to specific project (optional)
|
|
99
|
+
event_type: Filter to specific event type (optional)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of enriched InboxItems, sorted and paginated
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
# Get first 20 critical events
|
|
106
|
+
>>> items = inbox.get_items(limit=20, priority=EventPriority.CRITICAL)
|
|
107
|
+
|
|
108
|
+
# Get high-priority errors for specific project
|
|
109
|
+
>>> items = inbox.get_items(
|
|
110
|
+
... priority=EventPriority.HIGH,
|
|
111
|
+
... project_id="proj_123",
|
|
112
|
+
... event_type=EventType.ERROR
|
|
113
|
+
... )
|
|
114
|
+
|
|
115
|
+
# Pagination: get items 50-100
|
|
116
|
+
>>> items = inbox.get_items(limit=50, offset=50)
|
|
117
|
+
"""
|
|
118
|
+
# Get all pending events (optionally filtered by project)
|
|
119
|
+
pending = self.events.get_pending(project_id)
|
|
120
|
+
|
|
121
|
+
# Filter by priority
|
|
122
|
+
if priority:
|
|
123
|
+
pending = [e for e in pending if e.priority == priority]
|
|
124
|
+
|
|
125
|
+
# Filter by event type
|
|
126
|
+
if event_type:
|
|
127
|
+
pending = [e for e in pending if e.type == event_type]
|
|
128
|
+
|
|
129
|
+
# Sort by priority (CRITICAL first) then created_at (oldest first)
|
|
130
|
+
priority_order = [
|
|
131
|
+
EventPriority.CRITICAL,
|
|
132
|
+
EventPriority.HIGH,
|
|
133
|
+
EventPriority.NORMAL,
|
|
134
|
+
EventPriority.LOW,
|
|
135
|
+
EventPriority.INFO,
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
def sort_key(event: Event):
|
|
139
|
+
pri_idx = (
|
|
140
|
+
priority_order.index(event.priority)
|
|
141
|
+
if event.priority in priority_order
|
|
142
|
+
else 99
|
|
143
|
+
)
|
|
144
|
+
return (pri_idx, event.created_at)
|
|
145
|
+
|
|
146
|
+
sorted_events = sorted(pending, key=sort_key)
|
|
147
|
+
|
|
148
|
+
# Paginate
|
|
149
|
+
paginated = sorted_events[offset : offset + limit]
|
|
150
|
+
|
|
151
|
+
# Enrich with project metadata
|
|
152
|
+
items = []
|
|
153
|
+
for event in paginated:
|
|
154
|
+
project = self.projects.get(event.project_id)
|
|
155
|
+
if project:
|
|
156
|
+
session_runtime = None
|
|
157
|
+
if event.session_id and event.session_id in project.sessions:
|
|
158
|
+
session_runtime = project.sessions[event.session_id].runtime
|
|
159
|
+
|
|
160
|
+
items.append(
|
|
161
|
+
InboxItem(
|
|
162
|
+
event=event,
|
|
163
|
+
project_name=project.name,
|
|
164
|
+
project_path=project.path,
|
|
165
|
+
session_runtime=session_runtime,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return items
|
|
170
|
+
|
|
171
|
+
def get_counts(self, project_id: Optional[str] = None) -> InboxCounts:
|
|
172
|
+
"""Get count of pending events by priority.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
project_id: If provided, only count events for this project
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
InboxCounts with breakdown by priority and total
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
>>> counts = inbox.get_counts()
|
|
182
|
+
>>> print(f"Critical: {counts.critical}, Total: {counts.total}")
|
|
183
|
+
|
|
184
|
+
>>> project_counts = inbox.get_counts(project_id="proj_123")
|
|
185
|
+
"""
|
|
186
|
+
pending = self.events.get_pending(project_id)
|
|
187
|
+
|
|
188
|
+
counts = InboxCounts()
|
|
189
|
+
for event in pending:
|
|
190
|
+
counts.total += 1
|
|
191
|
+
if event.priority == EventPriority.CRITICAL:
|
|
192
|
+
counts.critical += 1
|
|
193
|
+
elif event.priority == EventPriority.HIGH:
|
|
194
|
+
counts.high += 1
|
|
195
|
+
elif event.priority == EventPriority.NORMAL:
|
|
196
|
+
counts.normal += 1
|
|
197
|
+
elif event.priority == EventPriority.LOW:
|
|
198
|
+
counts.low += 1
|
|
199
|
+
elif event.priority == EventPriority.INFO:
|
|
200
|
+
counts.info += 1
|
|
201
|
+
|
|
202
|
+
return counts
|
|
203
|
+
|
|
204
|
+
def should_create_event(
|
|
205
|
+
self, project_id: str, event_type: EventType, title: str
|
|
206
|
+
) -> bool:
|
|
207
|
+
"""Check if event should be created (not a duplicate).
|
|
208
|
+
|
|
209
|
+
Uses deduplication to prevent creating duplicate events within
|
|
210
|
+
the configured time window.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
project_id: Project raising the event
|
|
214
|
+
event_type: Type of event
|
|
215
|
+
title: Event title
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if event should be created, False if it's a duplicate
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
>>> if inbox.should_create_event("proj_123", EventType.ERROR, "Timeout"):
|
|
222
|
+
... event = event_manager.create(...)
|
|
223
|
+
"""
|
|
224
|
+
return not self.deduplicator.is_duplicate(project_id, event_type.value, title)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Inbox item model for event display enrichment.
|
|
2
|
+
|
|
3
|
+
Combines event data with project information for inbox rendering.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..models.events import Event
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class InboxItem:
|
|
15
|
+
"""Enriched event for inbox display.
|
|
16
|
+
|
|
17
|
+
Combines Event with project metadata and computed display fields.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
event: The underlying event
|
|
21
|
+
project_name: Human-readable project name
|
|
22
|
+
project_path: Filesystem path to project
|
|
23
|
+
session_runtime: Optional runtime identifier for the session
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> item = InboxItem(
|
|
27
|
+
... event=event,
|
|
28
|
+
... project_name="My App",
|
|
29
|
+
... project_path="/Users/masa/Projects/my-app",
|
|
30
|
+
... session_runtime="python-agent"
|
|
31
|
+
... )
|
|
32
|
+
>>> item.age_display
|
|
33
|
+
'5m ago'
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
event: Event
|
|
37
|
+
project_name: str
|
|
38
|
+
project_path: str
|
|
39
|
+
session_runtime: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def age(self) -> timedelta:
|
|
43
|
+
"""Time since event was created.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Timedelta representing event age
|
|
47
|
+
"""
|
|
48
|
+
return datetime.now(timezone.utc) - self.event.created_at
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def age_display(self) -> str:
|
|
52
|
+
"""Human-readable age string.
|
|
53
|
+
|
|
54
|
+
Returns age formatted as:
|
|
55
|
+
- "Xs ago" for under 60 seconds
|
|
56
|
+
- "Xm ago" for under 60 minutes
|
|
57
|
+
- "Xh ago" for under 24 hours
|
|
58
|
+
- "Xd ago" for 24+ hours
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Formatted age string (e.g., "5m ago", "2h ago", "3d ago")
|
|
62
|
+
"""
|
|
63
|
+
seconds = int(self.age.total_seconds())
|
|
64
|
+
if seconds < 60:
|
|
65
|
+
return f"{seconds}s ago"
|
|
66
|
+
if seconds < 3600:
|
|
67
|
+
return f"{seconds // 60}m ago"
|
|
68
|
+
if seconds < 86400:
|
|
69
|
+
return f"{seconds // 3600}h ago"
|
|
70
|
+
return f"{seconds // 86400}d ago"
|