claude-mpm 5.4.85__py3-none-any.whl → 5.6.1__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/CLAUDE_MPM_OUTPUT_STYLE.md +8 -5
- claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
- claude_mpm/agents/PM_INSTRUCTIONS.md +101 -703
- claude_mpm/agents/WORKFLOW.md +2 -0
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- claude_mpm/cli/commands/autotodos.py +566 -0
- claude_mpm/cli/commands/commander.py +46 -0
- claude_mpm/cli/commands/hook_errors.py +60 -60
- claude_mpm/cli/commands/monitor.py +2 -2
- claude_mpm/cli/commands/mpm_init/core.py +2 -2
- claude_mpm/cli/commands/run.py +35 -3
- claude_mpm/cli/executor.py +119 -16
- claude_mpm/cli/parsers/base_parser.py +71 -1
- claude_mpm/cli/parsers/commander_parser.py +83 -0
- claude_mpm/cli/parsers/run_parser.py +10 -0
- claude_mpm/cli/startup.py +54 -16
- claude_mpm/cli/startup_display.py +72 -5
- claude_mpm/cli/startup_logging.py +2 -2
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +72 -0
- claude_mpm/commander/adapters/__init__.py +31 -0
- claude_mpm/commander/adapters/base.py +191 -0
- claude_mpm/commander/adapters/claude_code.py +361 -0
- claude_mpm/commander/adapters/communication.py +366 -0
- claude_mpm/commander/api/__init__.py +16 -0
- claude_mpm/commander/api/app.py +105 -0
- claude_mpm/commander/api/errors.py +112 -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 +215 -0
- claude_mpm/commander/api/routes/work.py +260 -0
- claude_mpm/commander/api/schemas.py +182 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +107 -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/daemon.py +398 -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/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 +404 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +316 -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 +189 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +219 -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 +9 -1
- 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/core/config.py +5 -0
- claude_mpm/core/hook_manager.py +51 -3
- claude_mpm/core/logger.py +10 -7
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/output_style_manager.py +15 -5
- claude_mpm/core/unified_config.py +10 -6
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cs_tUR18.js → 1WZnGYqX.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDuw-vjf.js → 67pF3qNn.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bTOqqlTd.js → 6RxdMKe4.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DwBR2MJi.js → 8cZrfX0h.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{ZGh7QtNv.js → 9a6T2nm-.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D9lljYKQ.js → B443AUzu.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{RJiighC3.js → B8AwtY2H.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uuIeMWc-.js → BF15LAsF.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D3k0OPJN.js → BRcwIQNr.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CyWMqx4W.js → BV6nKitt.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CiIAseT4.js → BViJ8lZt.js} +5 -5
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CBBdVcY8.js → BcQ-Q0FE.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BovzEFCE.js → Bpyvgze_.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{eNVUfhuA.js → C3rbW_a-.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{GYwsonyD.js → C8WYN38h.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BIF9m_hv.js → C9I8FlXH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B0uc0UOD.js → CIQcWgO2.js} +3 -3
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Be7GpZd6.js → CIctN7YN.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Bh0LDWpI.js → CKrS_JZW.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DUrLdbGD.js → CR6P9C4A.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7xVLGWV.js → CRRR9MD_.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dhb8PKl3.js → CSXtMOf0.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BPYeabCQ.js → CT-sbxSk.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{sQeU3Y1z.js → CWm6DJsp.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CnA0NrzZ.js → CpqQ1Kzn.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4B-KCzX.js → D2nGpDRe.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DGkLK5U1.js → D9iCMida.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BofRWZRR.js → D9ykgMoY.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DmxopI1J.js → DL2Ldur1.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C30mlcqg.js → DPfltzjH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Vzk33B_K.js → DR8nis88.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DI7hHRFL.js → DUliQN2b.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4JcI4KD.js → DXlhR01x.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bT1r9zLR.js → D_lyTybS.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DZX00Y4g.js → DngoTTgh.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzZX-COe.js → DqkmHtDC.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7RN905-.js → DsDh8EYs.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DLVjFsZ3.js → DypDmXgd.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{iEWssX7S.js → IPYC-LnN.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DaimHw_p.js → JpevfAFt.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DY1XQ8fi.js → R8CEIRAd.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dle-35c7.js → Zxy7qc-l.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C_Usid8X.js → qtd3IeO4.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzeYkLYB.js → ulBFON_C.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cfqx1Qun.js → wQVh1CoA.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/{app.D6-I5TpK.js → app.Dr7t0z2J.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.m1gL8KXf.js → 0.RgBboRvH.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{1.CgNOuw-d.js → 1.DG-KkbDf.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
- claude_mpm/dashboard/static/svelte-build/index.html +9 -9
- claude_mpm/experimental/cli_enhancements.py +2 -1
- claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
- claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +486 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +250 -11
- claude_mpm/hooks/claude_hooks/hook_handler.py +106 -89
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
- claude_mpm/hooks/claude_hooks/installer.py +69 -5
- claude_mpm/hooks/claude_hooks/response_tracking.py +3 -1
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +20 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +14 -77
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +30 -6
- claude_mpm/hooks/session_resume_hook.py +85 -1
- claude_mpm/init.py +1 -1
- claude_mpm/scripts/claude-hook-handler.sh +36 -10
- claude_mpm/services/agents/agent_recommendation_service.py +8 -8
- claude_mpm/services/agents/cache_git_manager.py +1 -1
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +3 -0
- claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
- claude_mpm/services/cli/__init__.py +3 -0
- claude_mpm/services/cli/incremental_pause_manager.py +561 -0
- claude_mpm/services/cli/session_resume_helper.py +10 -2
- claude_mpm/services/delegation_detector.py +175 -0
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
- claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
- claude_mpm/services/diagnostics/models.py +14 -1
- claude_mpm/services/event_log.py +325 -0
- claude_mpm/services/infrastructure/__init__.py +4 -0
- claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
- claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
- claude_mpm/services/monitor/daemon_manager.py +15 -4
- claude_mpm/services/monitor/management/lifecycle.py +8 -2
- claude_mpm/services/monitor/server.py +106 -16
- claude_mpm/services/pm_skills_deployer.py +259 -87
- claude_mpm/services/skills/git_skill_source_manager.py +51 -2
- claude_mpm/services/skills/selective_skill_deployer.py +114 -16
- claude_mpm/services/skills/skill_discovery_service.py +57 -3
- claude_mpm/services/socketio/handlers/hook.py +14 -7
- claude_mpm/services/socketio/server/main.py +12 -4
- claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
- claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
- claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -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-management/SKILL.md +312 -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/{pm-teaching-mode → mpm-teaching-mode}/SKILL.md +2 -2
- claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
- claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
- claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
- claude_mpm/skills/skill_manager.py +4 -4
- claude_mpm-5.6.1.dist-info/METADATA +391 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/RECORD +244 -145
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +0 -24
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +0 -323
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +0 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +0 -1
- claude_mpm-5.4.85.dist-info/METADATA +0 -1023
- /claude_mpm/skills/bundled/pm/{pm-bug-reporting/pm-bug-reporting.md → mpm-bug-reporting/SKILL.md} +0 -0
- /claude_mpm/skills/bundled/pm/{pm-delegation-patterns → mpm-delegation-patterns}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-git-file-tracking → mpm-git-file-tracking}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-pr-workflow → mpm-pr-workflow}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-ticketing-integration → mpm-ticketing-integration}/SKILL.md +0 -0
- /claude_mpm/skills/bundled/pm/{pm-verification-protocols → mpm-verification-protocols}/SKILL.md +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Event Log Service for persistent event storage.
|
|
2
|
+
|
|
3
|
+
WHY this is needed:
|
|
4
|
+
- Decouple event producers from consumers
|
|
5
|
+
- Persist events for later processing (e.g., autotodos CLI)
|
|
6
|
+
- Enable event-driven architecture patterns
|
|
7
|
+
- Provide audit trail of system events
|
|
8
|
+
|
|
9
|
+
DESIGN DECISION: Simple JSON file storage because:
|
|
10
|
+
- Human-readable and inspectable
|
|
11
|
+
- No additional database dependencies
|
|
12
|
+
- Fast for small event volumes
|
|
13
|
+
- Easy to clear and manage
|
|
14
|
+
- Follows existing pattern (hook_error_memory)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
21
|
+
|
|
22
|
+
from ..core.logger import get_logger
|
|
23
|
+
|
|
24
|
+
# Event status types
|
|
25
|
+
EventStatus = Literal["pending", "resolved", "archived"]
|
|
26
|
+
|
|
27
|
+
# Max message length to prevent file bloat
|
|
28
|
+
MAX_MESSAGE_LENGTH = 2000
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EventLog:
|
|
32
|
+
"""Persistent event log with simple JSON storage.
|
|
33
|
+
|
|
34
|
+
WHY this design:
|
|
35
|
+
- Store events with timestamp, type, payload, status
|
|
36
|
+
- Support filtering by status and event type
|
|
37
|
+
- Prevent file bloat with message truncation
|
|
38
|
+
- Enable mark-as-resolved workflow
|
|
39
|
+
- Keep it simple - no complex queries needed
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, log_file: Optional[Path] = None):
|
|
43
|
+
"""Initialize event log.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
log_file: Path to event log file (default: .claude-mpm/event_log.json)
|
|
47
|
+
"""
|
|
48
|
+
self.logger = get_logger("event_log")
|
|
49
|
+
|
|
50
|
+
# Use default location if not specified
|
|
51
|
+
if log_file is None:
|
|
52
|
+
log_file = Path.cwd() / ".claude-mpm" / "event_log.json"
|
|
53
|
+
|
|
54
|
+
self.log_file = log_file
|
|
55
|
+
self.events: List[Dict[str, Any]] = self._load_events()
|
|
56
|
+
|
|
57
|
+
def _load_events(self) -> List[Dict[str, Any]]:
|
|
58
|
+
"""Load events from disk.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of event records
|
|
62
|
+
"""
|
|
63
|
+
if not self.log_file.exists():
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
content = self.log_file.read_text()
|
|
68
|
+
if not content.strip():
|
|
69
|
+
return []
|
|
70
|
+
data = json.loads(content)
|
|
71
|
+
|
|
72
|
+
# Validate structure
|
|
73
|
+
if not isinstance(data, list):
|
|
74
|
+
self.logger.warning("Event log is not a list, resetting")
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
return data
|
|
78
|
+
except json.JSONDecodeError as e:
|
|
79
|
+
self.logger.warning(f"Failed to parse event log: {e}, resetting")
|
|
80
|
+
return []
|
|
81
|
+
except Exception as e:
|
|
82
|
+
self.logger.error(f"Error loading event log: {e}")
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
def _save_events(self):
|
|
86
|
+
"""Persist events to disk."""
|
|
87
|
+
try:
|
|
88
|
+
# Ensure directory exists
|
|
89
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
|
|
91
|
+
# Write with pretty formatting for human readability
|
|
92
|
+
self.log_file.write_text(json.dumps(self.events, indent=2))
|
|
93
|
+
except Exception as e:
|
|
94
|
+
self.logger.error(f"Failed to save event log: {e}")
|
|
95
|
+
|
|
96
|
+
def _truncate_message(self, message: str) -> str:
|
|
97
|
+
"""Truncate message to prevent file bloat.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
message: Message to truncate
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Truncated message with ellipsis if needed
|
|
104
|
+
"""
|
|
105
|
+
if len(message) <= MAX_MESSAGE_LENGTH:
|
|
106
|
+
return message
|
|
107
|
+
|
|
108
|
+
return message[:MAX_MESSAGE_LENGTH] + "... (truncated)"
|
|
109
|
+
|
|
110
|
+
def append_event(
|
|
111
|
+
self,
|
|
112
|
+
event_type: str,
|
|
113
|
+
payload: Dict[str, Any],
|
|
114
|
+
status: EventStatus = "pending",
|
|
115
|
+
) -> str:
|
|
116
|
+
"""Append a new event to the log.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
event_type: Type of event (e.g., "autotodo.error", "hook.error")
|
|
120
|
+
payload: Event data (will be truncated if too large)
|
|
121
|
+
status: Event status (default: "pending")
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Event ID (timestamp-based for simplicity)
|
|
125
|
+
"""
|
|
126
|
+
# Truncate any message fields in payload
|
|
127
|
+
truncated_payload = payload.copy()
|
|
128
|
+
if "message" in truncated_payload:
|
|
129
|
+
truncated_payload["message"] = self._truncate_message(
|
|
130
|
+
str(truncated_payload["message"])
|
|
131
|
+
)
|
|
132
|
+
if "full_message" in truncated_payload:
|
|
133
|
+
truncated_payload["full_message"] = self._truncate_message(
|
|
134
|
+
str(truncated_payload["full_message"])
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Create event record
|
|
138
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
139
|
+
event = {
|
|
140
|
+
"id": timestamp, # Use timestamp as ID for simplicity
|
|
141
|
+
"timestamp": timestamp,
|
|
142
|
+
"event_type": event_type,
|
|
143
|
+
"payload": truncated_payload,
|
|
144
|
+
"status": status,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Append and save
|
|
148
|
+
self.events.append(event)
|
|
149
|
+
self._save_events()
|
|
150
|
+
|
|
151
|
+
self.logger.debug(f"Appended event: {event_type} (status: {status})")
|
|
152
|
+
return timestamp
|
|
153
|
+
|
|
154
|
+
def list_events(
|
|
155
|
+
self,
|
|
156
|
+
event_type: Optional[str] = None,
|
|
157
|
+
status: Optional[EventStatus] = None,
|
|
158
|
+
limit: Optional[int] = None,
|
|
159
|
+
) -> List[Dict[str, Any]]:
|
|
160
|
+
"""List events with optional filtering.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
event_type: Filter by event type (e.g., "autotodo.error")
|
|
164
|
+
status: Filter by status (e.g., "pending")
|
|
165
|
+
limit: Maximum number of events to return (most recent first)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of matching events
|
|
169
|
+
"""
|
|
170
|
+
# Filter events
|
|
171
|
+
filtered = self.events
|
|
172
|
+
|
|
173
|
+
if event_type:
|
|
174
|
+
filtered = [e for e in filtered if e["event_type"] == event_type]
|
|
175
|
+
|
|
176
|
+
if status:
|
|
177
|
+
filtered = [e for e in filtered if e["status"] == status]
|
|
178
|
+
|
|
179
|
+
# Sort by timestamp (most recent first)
|
|
180
|
+
filtered = sorted(filtered, key=lambda e: e["timestamp"], reverse=True)
|
|
181
|
+
|
|
182
|
+
# Apply limit
|
|
183
|
+
if limit:
|
|
184
|
+
filtered = filtered[:limit]
|
|
185
|
+
|
|
186
|
+
return filtered
|
|
187
|
+
|
|
188
|
+
def mark_resolved(self, event_id: str) -> bool:
|
|
189
|
+
"""Mark an event as resolved.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
event_id: Event ID (timestamp)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if event was found and updated
|
|
196
|
+
"""
|
|
197
|
+
for event in self.events:
|
|
198
|
+
if event["id"] == event_id:
|
|
199
|
+
event["status"] = "resolved"
|
|
200
|
+
event["resolved_at"] = datetime.now(timezone.utc).isoformat()
|
|
201
|
+
self._save_events()
|
|
202
|
+
self.logger.debug(f"Marked event resolved: {event_id}")
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def mark_all_resolved(
|
|
208
|
+
self, event_type: Optional[str] = None, status: EventStatus = "pending"
|
|
209
|
+
) -> int:
|
|
210
|
+
"""Mark multiple events as resolved.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
event_type: Optional filter by event type
|
|
214
|
+
status: Filter by current status (default: "pending")
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Number of events marked as resolved
|
|
218
|
+
"""
|
|
219
|
+
count = 0
|
|
220
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
221
|
+
|
|
222
|
+
for event in self.events:
|
|
223
|
+
# Check filters
|
|
224
|
+
if event["status"] != status:
|
|
225
|
+
continue
|
|
226
|
+
if event_type and event["event_type"] != event_type:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Mark resolved
|
|
230
|
+
event["status"] = "resolved"
|
|
231
|
+
event["resolved_at"] = now
|
|
232
|
+
count += 1
|
|
233
|
+
|
|
234
|
+
if count > 0:
|
|
235
|
+
self._save_events()
|
|
236
|
+
self.logger.debug(f"Marked {count} events as resolved")
|
|
237
|
+
|
|
238
|
+
return count
|
|
239
|
+
|
|
240
|
+
def clear_resolved(self, older_than_days: Optional[int] = None) -> int:
|
|
241
|
+
"""Remove resolved events from the log.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
older_than_days: Only clear events older than N days
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Number of events removed
|
|
248
|
+
"""
|
|
249
|
+
if older_than_days:
|
|
250
|
+
# Calculate cutoff timestamp
|
|
251
|
+
from datetime import timedelta
|
|
252
|
+
|
|
253
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=older_than_days)
|
|
254
|
+
cutoff_iso = cutoff.isoformat()
|
|
255
|
+
|
|
256
|
+
# Keep events that are NOT resolved OR are newer than cutoff
|
|
257
|
+
before_count = len(self.events)
|
|
258
|
+
self.events = [
|
|
259
|
+
e
|
|
260
|
+
for e in self.events
|
|
261
|
+
if e["status"] != "resolved" or e.get("resolved_at", "") > cutoff_iso
|
|
262
|
+
]
|
|
263
|
+
removed = before_count - len(self.events)
|
|
264
|
+
else:
|
|
265
|
+
# Remove all resolved events
|
|
266
|
+
before_count = len(self.events)
|
|
267
|
+
self.events = [e for e in self.events if e["status"] != "resolved"]
|
|
268
|
+
removed = before_count - len(self.events)
|
|
269
|
+
|
|
270
|
+
if removed > 0:
|
|
271
|
+
self._save_events()
|
|
272
|
+
self.logger.debug(f"Cleared {removed} resolved events")
|
|
273
|
+
|
|
274
|
+
return removed
|
|
275
|
+
|
|
276
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
277
|
+
"""Get event log statistics.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Dictionary with event counts by status and type
|
|
281
|
+
"""
|
|
282
|
+
stats = {
|
|
283
|
+
"total_events": len(self.events),
|
|
284
|
+
"by_status": {"pending": 0, "resolved": 0, "archived": 0},
|
|
285
|
+
"by_type": {},
|
|
286
|
+
"log_file": str(self.log_file),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for event in self.events:
|
|
290
|
+
# Count by status
|
|
291
|
+
status = event["status"]
|
|
292
|
+
stats["by_status"][status] = stats["by_status"].get(status, 0) + 1
|
|
293
|
+
|
|
294
|
+
# Count by type
|
|
295
|
+
event_type = event["event_type"]
|
|
296
|
+
stats["by_type"][event_type] = stats["by_type"].get(event_type, 0) + 1
|
|
297
|
+
|
|
298
|
+
return stats
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# Global instance
|
|
302
|
+
_event_log: Optional[EventLog] = None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def get_event_log(log_file: Optional[Path] = None) -> EventLog:
|
|
306
|
+
"""Get the global event log instance.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
log_file: Optional custom log file path
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
EventLog instance
|
|
313
|
+
|
|
314
|
+
Note:
|
|
315
|
+
If log_file is provided and differs from the current instance,
|
|
316
|
+
a new EventLog is created and replaces the global instance.
|
|
317
|
+
This allows hooks to use project-specific event logs.
|
|
318
|
+
"""
|
|
319
|
+
global _event_log
|
|
320
|
+
if _event_log is None:
|
|
321
|
+
_event_log = EventLog(log_file)
|
|
322
|
+
elif log_file is not None and _event_log.log_file != log_file:
|
|
323
|
+
# Create new instance if log file differs from current
|
|
324
|
+
_event_log = EventLog(log_file)
|
|
325
|
+
return _event_log
|
|
@@ -11,8 +11,10 @@ Services:
|
|
|
11
11
|
- LoggingService: Centralized logging with structured output
|
|
12
12
|
- HealthMonitor: System health monitoring and alerting
|
|
13
13
|
- MemoryGuardian: Memory monitoring and process restart management
|
|
14
|
+
- ContextUsageTracker: Token usage tracking across hook invocations
|
|
14
15
|
"""
|
|
15
16
|
|
|
17
|
+
from .context_usage_tracker import ContextUsageState, ContextUsageTracker
|
|
16
18
|
from .logging import LoggingService
|
|
17
19
|
from .monitoring import (
|
|
18
20
|
AdvancedHealthMonitor,
|
|
@@ -36,6 +38,8 @@ except ImportError:
|
|
|
36
38
|
|
|
37
39
|
__all__ = [
|
|
38
40
|
"AdvancedHealthMonitor", # For SocketIO server monitoring
|
|
41
|
+
"ContextUsageState",
|
|
42
|
+
"ContextUsageTracker",
|
|
39
43
|
"LoggingService",
|
|
40
44
|
# New service-based monitoring API
|
|
41
45
|
"MonitoringAggregatorService",
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Context Usage Tracker Service.
|
|
2
|
+
|
|
3
|
+
WHY: Track cumulative token usage across Claude Code hook invocations to prevent
|
|
4
|
+
context window exhaustion and enable intelligent auto-pause behavior.
|
|
5
|
+
|
|
6
|
+
DESIGN DECISIONS:
|
|
7
|
+
- File-based persistence (hooks run in separate processes)
|
|
8
|
+
- Atomic file operations using StateStorage
|
|
9
|
+
- Threshold detection at 70% (caution), 85% (warning), 90% (auto_pause), 95% (critical)
|
|
10
|
+
- Session-scoped tracking with reset capability
|
|
11
|
+
- Compatible with 200k context budget for Claude Sonnet 4.5
|
|
12
|
+
|
|
13
|
+
USAGE:
|
|
14
|
+
tracker = ContextUsageTracker()
|
|
15
|
+
state = tracker.update_usage(input_tokens=15000, output_tokens=2000)
|
|
16
|
+
if tracker.should_auto_pause():
|
|
17
|
+
# Trigger auto-pause workflow
|
|
18
|
+
pass
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import asdict, dataclass, field
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from claude_mpm.core.logger import get_logger
|
|
27
|
+
from claude_mpm.storage.state_storage import StateStorage
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ContextUsageState:
|
|
34
|
+
"""State tracking for cumulative context/token usage.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
session_id: Unique session identifier
|
|
38
|
+
cumulative_input_tokens: Total input tokens across all hook invocations
|
|
39
|
+
cumulative_output_tokens: Total output tokens across all hook invocations
|
|
40
|
+
cache_creation_tokens: Total tokens spent creating prompt cache
|
|
41
|
+
cache_read_tokens: Total tokens read from prompt cache
|
|
42
|
+
percentage_used: Percentage of 200k context budget used (0.0-100.0)
|
|
43
|
+
threshold_reached: Highest threshold crossed ('caution', 'warning', 'auto_pause', 'critical')
|
|
44
|
+
auto_pause_active: Whether auto-pause has been triggered
|
|
45
|
+
last_updated: ISO timestamp of last update
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
session_id: str
|
|
49
|
+
cumulative_input_tokens: int = 0
|
|
50
|
+
cumulative_output_tokens: int = 0
|
|
51
|
+
cache_creation_tokens: int = 0
|
|
52
|
+
cache_read_tokens: int = 0
|
|
53
|
+
percentage_used: float = 0.0
|
|
54
|
+
threshold_reached: Optional[str] = None
|
|
55
|
+
auto_pause_active: bool = False
|
|
56
|
+
last_updated: str = field(
|
|
57
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ContextUsageTracker:
|
|
62
|
+
"""Track cumulative context/token usage across hook invocations.
|
|
63
|
+
|
|
64
|
+
Features:
|
|
65
|
+
- Cumulative tracking across multiple API calls
|
|
66
|
+
- File-based persistence for cross-process state sharing
|
|
67
|
+
- Atomic file operations for concurrent safety
|
|
68
|
+
- Threshold detection (70%, 85%, 90%, 95%)
|
|
69
|
+
- Auto-pause triggering at 90%+ usage
|
|
70
|
+
- Session management with reset capability
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# Claude Sonnet 4.5 context window
|
|
74
|
+
CONTEXT_BUDGET = 200000
|
|
75
|
+
|
|
76
|
+
# Threshold levels for warnings and auto-pause
|
|
77
|
+
THRESHOLDS = {
|
|
78
|
+
"caution": 0.70, # Yellow warning
|
|
79
|
+
"warning": 0.85, # Orange warning
|
|
80
|
+
"auto_pause": 0.90, # Trigger auto-pause
|
|
81
|
+
"critical": 0.95, # Red critical alert
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def __init__(self, project_path: Optional[Path] = None):
|
|
85
|
+
"""Initialize context usage tracker.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
project_path: Project root path (default: current directory)
|
|
89
|
+
"""
|
|
90
|
+
self.project_path = (project_path or Path.cwd()).resolve()
|
|
91
|
+
self.state_dir = self.project_path / ".claude-mpm" / "state"
|
|
92
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
self.state_file = self.state_dir / "context-usage.json"
|
|
94
|
+
|
|
95
|
+
# Use StateStorage for atomic operations
|
|
96
|
+
self.storage = StateStorage(self.state_dir)
|
|
97
|
+
|
|
98
|
+
logger.debug(f"ContextUsageTracker initialized: {self.state_file}")
|
|
99
|
+
|
|
100
|
+
def update_usage(
|
|
101
|
+
self,
|
|
102
|
+
input_tokens: int,
|
|
103
|
+
output_tokens: int,
|
|
104
|
+
cache_creation: int = 0,
|
|
105
|
+
cache_read: int = 0,
|
|
106
|
+
) -> ContextUsageState:
|
|
107
|
+
"""Update cumulative usage from API response.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
input_tokens: Input tokens from this API call
|
|
111
|
+
output_tokens: Output tokens from this API call
|
|
112
|
+
cache_creation: Cache creation tokens (optional)
|
|
113
|
+
cache_read: Cache read tokens (optional)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Updated context usage state
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: If token counts are negative
|
|
120
|
+
"""
|
|
121
|
+
if any(
|
|
122
|
+
t < 0 for t in [input_tokens, output_tokens, cache_creation, cache_read]
|
|
123
|
+
):
|
|
124
|
+
raise ValueError("Token counts cannot be negative")
|
|
125
|
+
|
|
126
|
+
# Load current state
|
|
127
|
+
state = self._load_state()
|
|
128
|
+
|
|
129
|
+
# Update cumulative counters
|
|
130
|
+
state.cumulative_input_tokens += input_tokens
|
|
131
|
+
state.cumulative_output_tokens += output_tokens
|
|
132
|
+
state.cache_creation_tokens += cache_creation
|
|
133
|
+
state.cache_read_tokens += cache_read
|
|
134
|
+
|
|
135
|
+
# Calculate total effective tokens (input + output, cache read is "free")
|
|
136
|
+
total_tokens = state.cumulative_input_tokens + state.cumulative_output_tokens
|
|
137
|
+
state.percentage_used = (total_tokens / self.CONTEXT_BUDGET) * 100
|
|
138
|
+
|
|
139
|
+
# Check thresholds
|
|
140
|
+
state.threshold_reached = self.check_thresholds(state)
|
|
141
|
+
|
|
142
|
+
# Activate auto-pause if threshold reached
|
|
143
|
+
if state.threshold_reached in {"auto_pause", "critical"}:
|
|
144
|
+
state.auto_pause_active = True
|
|
145
|
+
|
|
146
|
+
# Update timestamp
|
|
147
|
+
state.last_updated = datetime.now(timezone.utc).isoformat()
|
|
148
|
+
|
|
149
|
+
# Persist state atomically
|
|
150
|
+
self._save_state(state)
|
|
151
|
+
|
|
152
|
+
logger.debug(
|
|
153
|
+
f"Usage updated: {total_tokens}/{self.CONTEXT_BUDGET} tokens "
|
|
154
|
+
f"({state.percentage_used:.1f}%), threshold: {state.threshold_reached}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return state
|
|
158
|
+
|
|
159
|
+
def check_thresholds(
|
|
160
|
+
self, state: Optional[ContextUsageState] = None
|
|
161
|
+
) -> Optional[str]:
|
|
162
|
+
"""Check which threshold (if any) has been exceeded.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
state: Optional state to check (uses current state if None)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Highest threshold exceeded ('caution', 'warning', 'auto_pause', 'critical')
|
|
169
|
+
or None if no thresholds exceeded
|
|
170
|
+
"""
|
|
171
|
+
if state is None:
|
|
172
|
+
state = self.get_current_state()
|
|
173
|
+
|
|
174
|
+
percentage = state.percentage_used / 100 # Convert to 0.0-1.0
|
|
175
|
+
|
|
176
|
+
# Check thresholds in descending order (highest first)
|
|
177
|
+
for threshold_name in ["critical", "auto_pause", "warning", "caution"]:
|
|
178
|
+
if percentage >= self.THRESHOLDS[threshold_name]:
|
|
179
|
+
return threshold_name
|
|
180
|
+
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def should_auto_pause(self) -> bool:
|
|
184
|
+
"""Check if auto-pause should be triggered.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
True if 90%+ context budget used
|
|
188
|
+
"""
|
|
189
|
+
state = self.get_current_state()
|
|
190
|
+
return state.percentage_used >= (self.THRESHOLDS["auto_pause"] * 100)
|
|
191
|
+
|
|
192
|
+
def get_current_state(self) -> ContextUsageState:
|
|
193
|
+
"""Get current usage state without modifying.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Current context usage state
|
|
197
|
+
"""
|
|
198
|
+
return self._load_state()
|
|
199
|
+
|
|
200
|
+
def reset_session(self, new_session_id: str) -> None:
|
|
201
|
+
"""Reset tracking for a new session.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
new_session_id: New session identifier
|
|
205
|
+
"""
|
|
206
|
+
state = ContextUsageState(session_id=new_session_id)
|
|
207
|
+
self._save_state(state)
|
|
208
|
+
logger.info(f"Context usage reset for new session: {new_session_id}")
|
|
209
|
+
|
|
210
|
+
def _load_state(self) -> ContextUsageState:
|
|
211
|
+
"""Load state from persistence file.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Loaded state or default state if file doesn't exist/is corrupted
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
if not self.state_file.exists():
|
|
218
|
+
# Generate initial session ID
|
|
219
|
+
session_id = (
|
|
220
|
+
f"session-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}"
|
|
221
|
+
)
|
|
222
|
+
logger.debug("No state file found, creating default state")
|
|
223
|
+
return ContextUsageState(session_id=session_id)
|
|
224
|
+
|
|
225
|
+
# Load JSON state
|
|
226
|
+
data = self.storage.read_json(self.state_file)
|
|
227
|
+
|
|
228
|
+
if data is None:
|
|
229
|
+
logger.warning("Failed to read state file, using default state")
|
|
230
|
+
session_id = (
|
|
231
|
+
f"session-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}"
|
|
232
|
+
)
|
|
233
|
+
return ContextUsageState(session_id=session_id)
|
|
234
|
+
|
|
235
|
+
# Reconstruct ContextUsageState from dict
|
|
236
|
+
return ContextUsageState(**data)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error(f"Error loading state, using default: {e}")
|
|
240
|
+
session_id = (
|
|
241
|
+
f"session-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}"
|
|
242
|
+
)
|
|
243
|
+
return ContextUsageState(session_id=session_id)
|
|
244
|
+
|
|
245
|
+
def _save_state(self, state: ContextUsageState) -> None:
|
|
246
|
+
"""Save state to persistence file atomically.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
state: Context usage state to persist
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
RuntimeError: If atomic write fails
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
# Convert dataclass to dict
|
|
256
|
+
state_dict = asdict(state)
|
|
257
|
+
|
|
258
|
+
# Atomic write using StateStorage
|
|
259
|
+
if not self.storage.write_json(state_dict, self.state_file, atomic=True):
|
|
260
|
+
raise RuntimeError(f"Failed to write state to {self.state_file}")
|
|
261
|
+
|
|
262
|
+
logger.debug(f"State saved: {self.state_file}")
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error(f"Error saving state: {e}")
|
|
266
|
+
raise RuntimeError(f"Failed to persist context usage state: {e}") from e
|
|
267
|
+
|
|
268
|
+
def get_usage_summary(self) -> dict:
|
|
269
|
+
"""Get human-readable usage summary.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Dictionary with usage statistics
|
|
273
|
+
"""
|
|
274
|
+
state = self.get_current_state()
|
|
275
|
+
total_tokens = state.cumulative_input_tokens + state.cumulative_output_tokens
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"session_id": state.session_id,
|
|
279
|
+
"total_tokens": total_tokens,
|
|
280
|
+
"budget": self.CONTEXT_BUDGET,
|
|
281
|
+
"percentage_used": round(state.percentage_used, 2),
|
|
282
|
+
"threshold_reached": state.threshold_reached,
|
|
283
|
+
"auto_pause_active": state.auto_pause_active,
|
|
284
|
+
"breakdown": {
|
|
285
|
+
"input_tokens": state.cumulative_input_tokens,
|
|
286
|
+
"output_tokens": state.cumulative_output_tokens,
|
|
287
|
+
"cache_creation_tokens": state.cache_creation_tokens,
|
|
288
|
+
"cache_read_tokens": state.cache_read_tokens,
|
|
289
|
+
},
|
|
290
|
+
"last_updated": state.last_updated,
|
|
291
|
+
}
|
|
@@ -6,7 +6,8 @@ Integrates with session management and response tracking infrastructure.
|
|
|
6
6
|
Triggers:
|
|
7
7
|
- model_context_window_exceeded (stop_reason)
|
|
8
8
|
- Manual pause command
|
|
9
|
-
- 95% token threshold reached
|
|
9
|
+
- 95% token threshold reached (critical)
|
|
10
|
+
- 90% token threshold reached (auto-pause)
|
|
10
11
|
- Session end with high token usage (>85%)
|
|
11
12
|
|
|
12
13
|
Design Principles:
|
|
@@ -71,6 +72,7 @@ class ResumeLogGenerator:
|
|
|
71
72
|
thresholds = self.config.get("context_management", {}).get("thresholds", {})
|
|
72
73
|
self.threshold_caution = thresholds.get("caution", 0.70)
|
|
73
74
|
self.threshold_warning = thresholds.get("warning", 0.85)
|
|
75
|
+
self.threshold_auto_pause = thresholds.get("auto_pause", 0.90)
|
|
74
76
|
self.threshold_critical = thresholds.get("critical", 0.95)
|
|
75
77
|
|
|
76
78
|
logger.info(
|
|
@@ -96,14 +98,14 @@ class ResumeLogGenerator:
|
|
|
96
98
|
if not self.enabled or not self.auto_generate:
|
|
97
99
|
return manual_trigger # Only generate on manual trigger if auto is disabled
|
|
98
100
|
|
|
99
|
-
# Trigger conditions
|
|
101
|
+
# Trigger conditions (ordered by severity)
|
|
100
102
|
triggers = [
|
|
101
103
|
stop_reason == "max_tokens",
|
|
102
104
|
stop_reason == "model_context_window_exceeded",
|
|
103
105
|
manual_trigger,
|
|
104
|
-
token_usage_pct and token_usage_pct >= self.threshold_critical,
|
|
105
|
-
token_usage_pct
|
|
106
|
-
and token_usage_pct >= self.threshold_warning, #
|
|
106
|
+
token_usage_pct and token_usage_pct >= self.threshold_critical, # 95%
|
|
107
|
+
token_usage_pct and token_usage_pct >= self.threshold_auto_pause, # 90%
|
|
108
|
+
token_usage_pct and token_usage_pct >= self.threshold_warning, # 85%
|
|
107
109
|
]
|
|
108
110
|
|
|
109
111
|
should_gen = any(triggers)
|
|
@@ -121,6 +123,22 @@ class ResumeLogGenerator:
|
|
|
121
123
|
|
|
122
124
|
return should_gen
|
|
123
125
|
|
|
126
|
+
def should_auto_pause(self, token_usage_pct: Optional[float]) -> bool:
|
|
127
|
+
"""Check if auto-pause threshold (90%) has been reached.
|
|
128
|
+
|
|
129
|
+
This is a convenience method to check specifically for the 90% threshold
|
|
130
|
+
which triggers automatic session pausing.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
token_usage_pct: Current token usage percentage (0.0-1.0)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if auto-pause threshold has been reached
|
|
137
|
+
"""
|
|
138
|
+
if token_usage_pct is None:
|
|
139
|
+
return False
|
|
140
|
+
return token_usage_pct >= self.threshold_auto_pause
|
|
141
|
+
|
|
124
142
|
def generate_from_session_state(
|
|
125
143
|
self,
|
|
126
144
|
session_id: str,
|
|
@@ -427,6 +445,7 @@ class ResumeLogGenerator:
|
|
|
427
445
|
"thresholds": {
|
|
428
446
|
"caution": f"{self.threshold_caution:.0%}",
|
|
429
447
|
"warning": f"{self.threshold_warning:.0%}",
|
|
448
|
+
"auto_pause": f"{self.threshold_auto_pause:.0%}",
|
|
430
449
|
"critical": f"{self.threshold_critical:.0%}",
|
|
431
450
|
},
|
|
432
451
|
}
|