claude-mpm 5.4.85__py3-none-any.whl → 5.4.88__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 +90 -9
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B7S5qgOx.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.D3t4z6uz.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DmxopI1J.js → 14Ru8gxt.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDuw-vjf.js → 7ZAeO_Uj.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/AeivYILh.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DaimHw_p.js → B06ALsCS.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bTOqqlTd.js → BCWDw8BF.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cs_tUR18.js → BS0ej2w8.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DwBR2MJi.js → BVFqgd56.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzeYkLYB.js → BXs4CVzO.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BovzEFCE.js → B_fnSNFx.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{iEWssX7S.js → BfMC7wDI.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CyWMqx4W.js → BfXd4Xj4.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CBBdVcY8.js → BnFPFynJ.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B0uc0UOD.js → BwpSELyW.js} +3 -3
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BPYeabCQ.js → C5Dg_JxJ.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DLVjFsZ3.js → C7i47te_.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CnA0NrzZ.js → C86quetY.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{ZGh7QtNv.js → CDNOxKrg.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BIF9m_hv.js → CDi5wzaD.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Be7GpZd6.js → CQ94FMOU.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzZX-COe.js → CT5eAo1x.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{GYwsonyD.js → C_l0vq62.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cn4nXAfg.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DI7hHRFL.js → CvWciI1W.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{sQeU3Y1z.js → CyrTH56Q.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Bh0LDWpI.js → CzkNB1Vu.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DUrLdbGD.js → D0Fj1OdD.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dle-35c7.js → D1ARDjz0.js} +2 -2
- 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/{BofRWZRR.js → DJN4AVXS.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7RN905-.js → DJuK4-OP.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C30mlcqg.js → DLeM8wSV.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uuIeMWc-.js → DNI1jw9S.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4JcI4KD.js → DOViuQX_.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D3k0OPJN.js → DOeJfApz.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cfqx1Qun.js → DTgfNBV9.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CiIAseT4.js → DWDi9IaK.js} +5 -5
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D_vpdI7l.js +325 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dhb8PKl3.js → DdIDcQsD.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DnL7ky1O.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DGkLK5U1.js → Dqtg3hb8.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{eNVUfhuA.js → EKp_wsKE.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D9lljYKQ.js → MJf6AOIJ.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7xVLGWV.js → NsEh4Ivo.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/URSAF6IJ.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DZX00Y4g.js → WiqB4NUY.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bT1r9zLR.js → s04HIjWg.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C_Usid8X.js → vJiSSdpk.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/{app.D6-I5TpK.js → app.CnXU_fEX.js} +2 -2
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.CUaAfoQJ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{1.CgNOuw-d.js → 1.znyTz9u3.js} +1 -1
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.CLVHDDxl.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
- claude_mpm/dashboard/static/svelte-build/index.html +7 -7
- 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/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +486 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +74 -1
- claude_mpm/hooks/claude_hooks/hook_handler.py +25 -3
- claude_mpm/hooks/claude_hooks/response_tracking.py +17 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.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__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +30 -6
- claude_mpm/hooks/session_resume_hook.py +85 -1
- claude_mpm/services/cli/__init__.py +3 -0
- claude_mpm/services/cli/incremental_pause_manager.py +561 -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-5.4.85.dist-info → claude_mpm-5.4.88.dist-info}/METADATA +1 -1
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.4.88.dist-info}/RECORD +93 -73
- 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/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/chunks/RJiighC3.js +0 -1
- 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/dashboard/static/svelte-build/_app/immutable/nodes/{0.m1gL8KXf.js → 0.BuxSUm_s.js} +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.4.88.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.4.88.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.4.88.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.4.88.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.85.dist-info → claude_mpm-5.4.88.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Auto-pause handler for Claude Code hooks.
|
|
3
|
+
|
|
4
|
+
WHY: Automatically pause Claude sessions when context usage reaches 90% to prevent
|
|
5
|
+
context window exhaustion. Integrates with existing hook infrastructure to monitor
|
|
6
|
+
token usage and trigger incremental pause capture.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISIONS:
|
|
9
|
+
- Integrates with ContextUsageTracker for token tracking across hook invocations
|
|
10
|
+
- Uses IncrementalPauseManager for capturing actions during pause mode
|
|
11
|
+
- Thread-safe - handles hook calls from multiple processes via file-based state
|
|
12
|
+
- Emits warnings to stderr for visibility without breaking hook flow
|
|
13
|
+
- Only triggers auto-pause on NEW threshold crossings (prevents duplicate warnings)
|
|
14
|
+
- Graceful error handling - auto-pause failures don't break main hook processing
|
|
15
|
+
|
|
16
|
+
USAGE:
|
|
17
|
+
# Initialize handler in hook handler
|
|
18
|
+
auto_pause = AutoPauseHandler()
|
|
19
|
+
|
|
20
|
+
# Monitor token usage from API responses
|
|
21
|
+
if "usage" in metadata:
|
|
22
|
+
threshold_crossed = auto_pause.on_usage_update(metadata["usage"])
|
|
23
|
+
if threshold_crossed:
|
|
24
|
+
warning = auto_pause.emit_threshold_warning(threshold_crossed)
|
|
25
|
+
print(f"\n⚠️ {warning}", file=sys.stderr)
|
|
26
|
+
|
|
27
|
+
# Record actions during pause mode
|
|
28
|
+
if auto_pause.is_pause_active():
|
|
29
|
+
auto_pause.on_tool_call(tool_name, tool_args)
|
|
30
|
+
auto_pause.on_assistant_response(response_summary)
|
|
31
|
+
|
|
32
|
+
# Finalize on session end
|
|
33
|
+
session_file = auto_pause.on_session_end()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import os
|
|
37
|
+
import sys
|
|
38
|
+
from datetime import datetime, timezone
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Dict, Optional
|
|
41
|
+
|
|
42
|
+
from claude_mpm.core.logger import get_logger
|
|
43
|
+
from claude_mpm.services.cli.incremental_pause_manager import IncrementalPauseManager
|
|
44
|
+
from claude_mpm.services.infrastructure.context_usage_tracker import (
|
|
45
|
+
ContextUsageTracker,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logger = get_logger(__name__)
|
|
49
|
+
|
|
50
|
+
# Debug mode
|
|
51
|
+
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
|
|
52
|
+
|
|
53
|
+
# Warning messages for threshold crossings
|
|
54
|
+
THRESHOLD_WARNINGS = {
|
|
55
|
+
"caution": "Context usage at 70%. Consider wrapping up current work.",
|
|
56
|
+
"warning": "Context usage at 85%. Session nearing capacity.",
|
|
57
|
+
"auto_pause": "Context usage at 90%. Auto-pause activated. Actions are being recorded for session continuity.",
|
|
58
|
+
"critical": "Context usage at 95%. Session nearly exhausted. Wrapping up...",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Maximum length for summaries to avoid storing full responses
|
|
62
|
+
MAX_SUMMARY_LENGTH = 500
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AutoPauseHandler:
|
|
66
|
+
"""Handler for automatic session pausing based on context usage thresholds.
|
|
67
|
+
|
|
68
|
+
Integrates with Claude Code hooks to:
|
|
69
|
+
1. Track cumulative token usage from API responses
|
|
70
|
+
2. Trigger auto-pause when 90% context used
|
|
71
|
+
3. Capture all subsequent actions during pause mode
|
|
72
|
+
4. Emit warnings/notifications to user
|
|
73
|
+
|
|
74
|
+
Features:
|
|
75
|
+
- File-based state persistence (works across hook process restarts)
|
|
76
|
+
- Thread-safe through atomic file operations
|
|
77
|
+
- Graceful error handling (failures don't break main hook flow)
|
|
78
|
+
- Only emits warnings on NEW threshold crossings
|
|
79
|
+
- Summarizes long content to prevent memory bloat
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, project_path: Optional[Path] = None):
|
|
83
|
+
"""Initialize auto-pause handler.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
project_path: Project root path (default: current directory)
|
|
87
|
+
"""
|
|
88
|
+
self.project_path = (project_path or Path.cwd()).resolve()
|
|
89
|
+
|
|
90
|
+
# Initialize services
|
|
91
|
+
self.tracker = ContextUsageTracker(self.project_path)
|
|
92
|
+
self.pause_manager = IncrementalPauseManager(self.project_path)
|
|
93
|
+
|
|
94
|
+
# Track previous threshold to detect NEW crossings
|
|
95
|
+
self._previous_threshold: Optional[str] = None
|
|
96
|
+
|
|
97
|
+
# Load initial state
|
|
98
|
+
try:
|
|
99
|
+
current_state = self.tracker.get_current_state()
|
|
100
|
+
self._previous_threshold = current_state.threshold_reached
|
|
101
|
+
|
|
102
|
+
if DEBUG:
|
|
103
|
+
print(
|
|
104
|
+
f"AutoPauseHandler initialized: "
|
|
105
|
+
f"{current_state.percentage_used:.1f}% context used, "
|
|
106
|
+
f"threshold: {current_state.threshold_reached}",
|
|
107
|
+
file=sys.stderr,
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to initialize AutoPauseHandler: {e}")
|
|
111
|
+
# Continue with None - will initialize on first update
|
|
112
|
+
|
|
113
|
+
def on_usage_update(self, usage: Dict[str, Any]) -> Optional[str]:
|
|
114
|
+
"""Process token usage from a Claude API response.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
usage: Dict with 'input_tokens', 'output_tokens',
|
|
118
|
+
'cache_creation_input_tokens', 'cache_read_input_tokens'
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Threshold name if a NEW threshold was crossed ('caution', 'warning',
|
|
122
|
+
'auto_pause', 'critical'), or None if no new threshold crossed.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValueError: If usage data is invalid
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
# Extract token counts
|
|
129
|
+
input_tokens = usage.get("input_tokens", 0)
|
|
130
|
+
output_tokens = usage.get("output_tokens", 0)
|
|
131
|
+
cache_creation = usage.get("cache_creation_input_tokens", 0)
|
|
132
|
+
cache_read = usage.get("cache_read_input_tokens", 0)
|
|
133
|
+
|
|
134
|
+
# Validate token counts
|
|
135
|
+
if any(
|
|
136
|
+
t < 0 for t in [input_tokens, output_tokens, cache_creation, cache_read]
|
|
137
|
+
):
|
|
138
|
+
raise ValueError("Token counts cannot be negative")
|
|
139
|
+
|
|
140
|
+
# Update cumulative usage
|
|
141
|
+
state = self.tracker.update_usage(
|
|
142
|
+
input_tokens=input_tokens,
|
|
143
|
+
output_tokens=output_tokens,
|
|
144
|
+
cache_creation=cache_creation,
|
|
145
|
+
cache_read=cache_read,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Check if we crossed a NEW threshold
|
|
149
|
+
current_threshold = state.threshold_reached
|
|
150
|
+
new_threshold_crossed = None
|
|
151
|
+
|
|
152
|
+
if current_threshold != self._previous_threshold:
|
|
153
|
+
# Determine if this is a higher threshold
|
|
154
|
+
threshold_order = ["caution", "warning", "auto_pause", "critical"]
|
|
155
|
+
|
|
156
|
+
prev_idx = (
|
|
157
|
+
threshold_order.index(self._previous_threshold)
|
|
158
|
+
if self._previous_threshold in threshold_order
|
|
159
|
+
else -1
|
|
160
|
+
)
|
|
161
|
+
curr_idx = (
|
|
162
|
+
threshold_order.index(current_threshold)
|
|
163
|
+
if current_threshold in threshold_order
|
|
164
|
+
else -1
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if curr_idx > prev_idx:
|
|
168
|
+
new_threshold_crossed = current_threshold
|
|
169
|
+
self._previous_threshold = current_threshold
|
|
170
|
+
|
|
171
|
+
if DEBUG:
|
|
172
|
+
print(
|
|
173
|
+
f"Context threshold crossed: {current_threshold} "
|
|
174
|
+
f"({state.percentage_used:.1f}%)",
|
|
175
|
+
file=sys.stderr,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Trigger auto-pause if threshold reached
|
|
179
|
+
if current_threshold in ["auto_pause", "critical"]:
|
|
180
|
+
self._trigger_auto_pause(state)
|
|
181
|
+
|
|
182
|
+
return new_threshold_crossed
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Failed to update usage: {e}")
|
|
186
|
+
if DEBUG:
|
|
187
|
+
print(f"❌ Usage update failed: {e}", file=sys.stderr)
|
|
188
|
+
# Don't propagate error - auto-pause is optional
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def on_tool_call(self, tool_name: str, tool_args: Dict[str, Any]) -> None:
|
|
192
|
+
"""Record a tool call if auto-pause is active.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
tool_name: Name of the tool being called
|
|
196
|
+
tool_args: Tool arguments dictionary
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
RuntimeError: If append operation fails (optional, logged only)
|
|
200
|
+
"""
|
|
201
|
+
if not self.is_pause_active():
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Summarize tool args to avoid storing large data
|
|
206
|
+
args_summary = self._summarize_dict(tool_args)
|
|
207
|
+
|
|
208
|
+
# Get current context percentage
|
|
209
|
+
state = self.tracker.get_current_state()
|
|
210
|
+
|
|
211
|
+
# Record action
|
|
212
|
+
self.pause_manager.append_action(
|
|
213
|
+
action_type="tool_call",
|
|
214
|
+
action_data={
|
|
215
|
+
"tool": tool_name,
|
|
216
|
+
"args_summary": args_summary,
|
|
217
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
218
|
+
},
|
|
219
|
+
context_percentage=state.percentage_used / 100,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if DEBUG:
|
|
223
|
+
print(f"Recorded tool call during pause: {tool_name}", file=sys.stderr)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Failed to record tool call: {e}")
|
|
227
|
+
if DEBUG:
|
|
228
|
+
print(f"❌ Failed to record tool call: {e}", file=sys.stderr)
|
|
229
|
+
|
|
230
|
+
def on_assistant_response(self, response_summary: str) -> None:
|
|
231
|
+
"""Record an assistant response if auto-pause is active.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
response_summary: Summary of assistant response (will be truncated)
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
RuntimeError: If append operation fails (optional, logged only)
|
|
238
|
+
"""
|
|
239
|
+
if not self.is_pause_active():
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
# Truncate long responses
|
|
244
|
+
summary = self._truncate_text(response_summary, MAX_SUMMARY_LENGTH)
|
|
245
|
+
|
|
246
|
+
# Get current context percentage
|
|
247
|
+
state = self.tracker.get_current_state()
|
|
248
|
+
|
|
249
|
+
# Record action
|
|
250
|
+
self.pause_manager.append_action(
|
|
251
|
+
action_type="assistant_response",
|
|
252
|
+
action_data={
|
|
253
|
+
"summary": summary,
|
|
254
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
255
|
+
},
|
|
256
|
+
context_percentage=state.percentage_used / 100,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if DEBUG:
|
|
260
|
+
print(
|
|
261
|
+
f"Recorded assistant response during pause (length: {len(summary)})",
|
|
262
|
+
file=sys.stderr,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"Failed to record assistant response: {e}")
|
|
267
|
+
if DEBUG:
|
|
268
|
+
print(f"❌ Failed to record assistant response: {e}", file=sys.stderr)
|
|
269
|
+
|
|
270
|
+
def on_user_message(self, message_summary: str) -> None:
|
|
271
|
+
"""Record a user message if auto-pause is active.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
message_summary: Summary of user message (will be truncated)
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
RuntimeError: If append operation fails (optional, logged only)
|
|
278
|
+
"""
|
|
279
|
+
if not self.is_pause_active():
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
# Truncate long messages
|
|
284
|
+
summary = self._truncate_text(message_summary, MAX_SUMMARY_LENGTH)
|
|
285
|
+
|
|
286
|
+
# Get current context percentage
|
|
287
|
+
state = self.tracker.get_current_state()
|
|
288
|
+
|
|
289
|
+
# Record action
|
|
290
|
+
self.pause_manager.append_action(
|
|
291
|
+
action_type="user_message",
|
|
292
|
+
action_data={
|
|
293
|
+
"summary": summary,
|
|
294
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
295
|
+
},
|
|
296
|
+
context_percentage=state.percentage_used / 100,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if DEBUG:
|
|
300
|
+
print(
|
|
301
|
+
f"Recorded user message during pause (length: {len(summary)})",
|
|
302
|
+
file=sys.stderr,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"Failed to record user message: {e}")
|
|
307
|
+
if DEBUG:
|
|
308
|
+
print(f"❌ Failed to record user message: {e}", file=sys.stderr)
|
|
309
|
+
|
|
310
|
+
def on_session_end(self) -> Optional[Path]:
|
|
311
|
+
"""Called when session ends. Finalizes any active pause.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Path to finalized session file, or None if no pause was active.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
RuntimeError: If finalization fails
|
|
318
|
+
"""
|
|
319
|
+
if not self.is_pause_active():
|
|
320
|
+
if DEBUG:
|
|
321
|
+
print("No active pause to finalize", file=sys.stderr)
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
# Finalize the pause session
|
|
326
|
+
session_path = self.pause_manager.finalize_pause(create_full_snapshot=True)
|
|
327
|
+
|
|
328
|
+
if session_path and DEBUG:
|
|
329
|
+
print(f"✅ Session finalized: {session_path.name}", file=sys.stderr)
|
|
330
|
+
|
|
331
|
+
return session_path
|
|
332
|
+
|
|
333
|
+
except Exception as e:
|
|
334
|
+
logger.error(f"Failed to finalize pause session: {e}")
|
|
335
|
+
if DEBUG:
|
|
336
|
+
print(f"❌ Failed to finalize pause: {e}", file=sys.stderr)
|
|
337
|
+
raise
|
|
338
|
+
|
|
339
|
+
def is_pause_active(self) -> bool:
|
|
340
|
+
"""Check if auto-pause mode is currently active.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
True if auto-pause has been triggered and is capturing actions
|
|
344
|
+
"""
|
|
345
|
+
return self.pause_manager.is_pause_active()
|
|
346
|
+
|
|
347
|
+
def get_status(self) -> Dict[str, Any]:
|
|
348
|
+
"""Get current status for display/logging.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Dict with: context_percentage, threshold_reached,
|
|
352
|
+
pause_active, actions_recorded, etc.
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
state = self.tracker.get_current_state()
|
|
356
|
+
pause_summary = self.pause_manager.get_pause_summary()
|
|
357
|
+
|
|
358
|
+
status = {
|
|
359
|
+
"context_percentage": round(state.percentage_used, 2),
|
|
360
|
+
"threshold_reached": state.threshold_reached,
|
|
361
|
+
"auto_pause_active": state.auto_pause_active,
|
|
362
|
+
"pause_active": self.is_pause_active(),
|
|
363
|
+
"session_id": state.session_id,
|
|
364
|
+
"total_tokens": (
|
|
365
|
+
state.cumulative_input_tokens + state.cumulative_output_tokens
|
|
366
|
+
),
|
|
367
|
+
"budget": ContextUsageTracker.CONTEXT_BUDGET,
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# Add pause details if active
|
|
371
|
+
if pause_summary:
|
|
372
|
+
status["pause_details"] = {
|
|
373
|
+
"action_count": pause_summary["action_count"],
|
|
374
|
+
"duration_seconds": pause_summary["duration_seconds"],
|
|
375
|
+
"context_range": pause_summary["context_range"],
|
|
376
|
+
"last_action_type": pause_summary["last_action_type"],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return status
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.error(f"Failed to get status: {e}")
|
|
383
|
+
return {"error": str(e)}
|
|
384
|
+
|
|
385
|
+
def emit_threshold_warning(self, threshold: str) -> str:
|
|
386
|
+
"""Generate a warning message for threshold crossing.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
threshold: Threshold name ('caution', 'warning', 'auto_pause', 'critical')
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
User-friendly warning message string
|
|
393
|
+
"""
|
|
394
|
+
warning = THRESHOLD_WARNINGS.get(
|
|
395
|
+
threshold, f"Context usage threshold reached: {threshold}"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Add context percentage to warning
|
|
399
|
+
try:
|
|
400
|
+
state = self.tracker.get_current_state()
|
|
401
|
+
warning = f"{warning} ({state.percentage_used:.1f}%)"
|
|
402
|
+
except Exception:
|
|
403
|
+
pass # nosec B110 - Intentionally ignore formatting errors, warning is already constructed
|
|
404
|
+
|
|
405
|
+
return warning
|
|
406
|
+
|
|
407
|
+
def _trigger_auto_pause(self, state) -> None:
|
|
408
|
+
"""Trigger auto-pause and start recording actions.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
state: Current context usage state
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
RuntimeError: If pause cannot be started
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
417
|
+
# Check if pause is already active
|
|
418
|
+
if self.is_pause_active():
|
|
419
|
+
if DEBUG:
|
|
420
|
+
print(
|
|
421
|
+
"Auto-pause already active, skipping trigger", file=sys.stderr
|
|
422
|
+
)
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
# Start incremental pause
|
|
426
|
+
session_id = self.pause_manager.start_incremental_pause(
|
|
427
|
+
context_percentage=state.percentage_used / 100,
|
|
428
|
+
initial_state=state.__dict__,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if DEBUG:
|
|
432
|
+
print(
|
|
433
|
+
f"✅ Auto-pause triggered: {session_id} "
|
|
434
|
+
f"({state.percentage_used:.1f}% context used)",
|
|
435
|
+
file=sys.stderr,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"Failed to trigger auto-pause: {e}")
|
|
440
|
+
if DEBUG:
|
|
441
|
+
print(f"❌ Failed to trigger auto-pause: {e}", file=sys.stderr)
|
|
442
|
+
# Don't propagate - auto-pause is optional
|
|
443
|
+
|
|
444
|
+
def _summarize_dict(
|
|
445
|
+
self, data: Dict[str, Any], max_items: int = 10
|
|
446
|
+
) -> Dict[str, Any]:
|
|
447
|
+
"""Create a summary of a dictionary by limiting items and truncating values.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
data: Dictionary to summarize
|
|
451
|
+
max_items: Maximum number of items to include
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Summarized dictionary
|
|
455
|
+
"""
|
|
456
|
+
summary = {}
|
|
457
|
+
|
|
458
|
+
for i, (key, value) in enumerate(data.items()):
|
|
459
|
+
if i >= max_items:
|
|
460
|
+
summary["..."] = f"({len(data) - max_items} more items)"
|
|
461
|
+
break
|
|
462
|
+
|
|
463
|
+
# Truncate string values
|
|
464
|
+
if isinstance(value, str):
|
|
465
|
+
summary[key] = self._truncate_text(value, 100)
|
|
466
|
+
elif isinstance(value, (list, dict)):
|
|
467
|
+
summary[key] = f"<{type(value).__name__} with {len(value)} items>"
|
|
468
|
+
else:
|
|
469
|
+
summary[key] = str(value)[:100]
|
|
470
|
+
|
|
471
|
+
return summary
|
|
472
|
+
|
|
473
|
+
def _truncate_text(self, text: str, max_length: int) -> str:
|
|
474
|
+
"""Truncate text to maximum length with ellipsis.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
text: Text to truncate
|
|
478
|
+
max_length: Maximum length
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Truncated text with "..." suffix if truncated
|
|
482
|
+
"""
|
|
483
|
+
if len(text) <= max_length:
|
|
484
|
+
return text
|
|
485
|
+
|
|
486
|
+
return text[: max_length - 3] + "..."
|
|
@@ -7,7 +7,7 @@ Claude Code hook events.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
-
import subprocess
|
|
10
|
+
import subprocess # nosec B404 - subprocess used for safe claude CLI version checking only
|
|
11
11
|
import sys
|
|
12
12
|
import uuid
|
|
13
13
|
from datetime import datetime, timezone
|
|
@@ -189,6 +189,15 @@ class EventHandlers:
|
|
|
189
189
|
if tool_name == "Task" and isinstance(tool_input, dict):
|
|
190
190
|
self._handle_task_delegation(tool_input, pre_tool_data, session_id)
|
|
191
191
|
|
|
192
|
+
# Record tool call for auto-pause if active
|
|
193
|
+
auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
|
|
194
|
+
if auto_pause and auto_pause.is_pause_active():
|
|
195
|
+
try:
|
|
196
|
+
auto_pause.on_tool_call(tool_name, tool_input)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
if DEBUG:
|
|
199
|
+
print(f"Auto-pause tool recording error: {e}", file=sys.stderr)
|
|
200
|
+
|
|
192
201
|
self.hook_handler._emit_socketio_event("", "pre_tool", pre_tool_data)
|
|
193
202
|
|
|
194
203
|
def _handle_task_delegation(
|
|
@@ -852,6 +861,21 @@ class EventHandlers:
|
|
|
852
861
|
file=sys.stderr,
|
|
853
862
|
)
|
|
854
863
|
|
|
864
|
+
# Record assistant response for auto-pause if active
|
|
865
|
+
auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
|
|
866
|
+
if auto_pause and auto_pause.is_pause_active():
|
|
867
|
+
try:
|
|
868
|
+
# Summarize response to first 200 chars
|
|
869
|
+
summary = (
|
|
870
|
+
response_text[:200] + "..."
|
|
871
|
+
if len(response_text) > 200
|
|
872
|
+
else response_text
|
|
873
|
+
)
|
|
874
|
+
auto_pause.on_assistant_response(summary)
|
|
875
|
+
except Exception as e:
|
|
876
|
+
if DEBUG:
|
|
877
|
+
print(f"Auto-pause response recording error: {e}", file=sys.stderr)
|
|
878
|
+
|
|
855
879
|
# Emit normalized event
|
|
856
880
|
self.hook_handler._emit_socketio_event(
|
|
857
881
|
"", "assistant_response", assistant_response_data
|
|
@@ -886,3 +910,52 @@ class EventHandlers:
|
|
|
886
910
|
|
|
887
911
|
# Emit normalized event
|
|
888
912
|
self.hook_handler._emit_socketio_event("", "session_start", session_start_data)
|
|
913
|
+
|
|
914
|
+
def handle_subagent_start_fast(self, event):
|
|
915
|
+
"""Handle SubagentStart events with proper agent type extraction.
|
|
916
|
+
|
|
917
|
+
WHY separate from SessionStart:
|
|
918
|
+
- SubagentStart contains agent-specific information
|
|
919
|
+
- Frontend needs agent_type to create distinct agent nodes
|
|
920
|
+
- Multiple engineers should show as separate nodes in hierarchy
|
|
921
|
+
- Research agents must appear in the agent hierarchy
|
|
922
|
+
|
|
923
|
+
Unlike SessionStart, SubagentStart events contain agent-specific
|
|
924
|
+
information that must be preserved and emitted to the dashboard.
|
|
925
|
+
"""
|
|
926
|
+
session_id = event.get("session_id", "")
|
|
927
|
+
|
|
928
|
+
# Extract agent type from event - Claude provides this in SubagentStart
|
|
929
|
+
# Try multiple possible field names for compatibility
|
|
930
|
+
agent_type = event.get("agent_type") or event.get("subagent_type") or "unknown"
|
|
931
|
+
|
|
932
|
+
# Generate unique agent ID combining type and session
|
|
933
|
+
agent_id = event.get("agent_id", f"{agent_type}_{session_id[:8]}")
|
|
934
|
+
|
|
935
|
+
# Get working directory and git branch
|
|
936
|
+
working_dir = event.get("cwd", "")
|
|
937
|
+
git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
|
|
938
|
+
|
|
939
|
+
# Build subagent start data with all required fields
|
|
940
|
+
subagent_start_data = {
|
|
941
|
+
"session_id": session_id,
|
|
942
|
+
"agent_type": agent_type,
|
|
943
|
+
"agent_id": agent_id,
|
|
944
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
945
|
+
"hook_event_name": "SubagentStart", # Preserve correct hook name
|
|
946
|
+
"working_directory": working_dir,
|
|
947
|
+
"git_branch": git_branch,
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
# Debug logging
|
|
951
|
+
if DEBUG:
|
|
952
|
+
print(
|
|
953
|
+
f"Hook handler: SubagentStart - agent_type='{agent_type}', "
|
|
954
|
+
f"agent_id='{agent_id}', session_id='{session_id[:16]}...'",
|
|
955
|
+
file=sys.stderr,
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
# Emit to /hook namespace as subagent_start (NOT session_start!)
|
|
959
|
+
self.hook_handler._emit_socketio_event(
|
|
960
|
+
"", "subagent_start", subagent_start_data
|
|
961
|
+
)
|
|
@@ -31,6 +31,7 @@ from typing import Optional, Tuple
|
|
|
31
31
|
# Import extracted modules with fallback for direct execution
|
|
32
32
|
try:
|
|
33
33
|
# Try relative imports first (when imported as module)
|
|
34
|
+
from .auto_pause_handler import AutoPauseHandler
|
|
34
35
|
from .event_handlers import EventHandlers
|
|
35
36
|
from .memory_integration import MemoryHookManager
|
|
36
37
|
from .response_tracking import ResponseTrackingManager
|
|
@@ -47,6 +48,7 @@ except ImportError:
|
|
|
47
48
|
# Add parent directory to path
|
|
48
49
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
49
50
|
|
|
51
|
+
from auto_pause_handler import AutoPauseHandler
|
|
50
52
|
from event_handlers import EventHandlers
|
|
51
53
|
from memory_integration import MemoryHookManager
|
|
52
54
|
from response_tracking import ResponseTrackingManager
|
|
@@ -153,7 +155,7 @@ def check_claude_version() -> Tuple[bool, Optional[str]]:
|
|
|
153
155
|
"""
|
|
154
156
|
try:
|
|
155
157
|
# Try to detect Claude Code version
|
|
156
|
-
result = subprocess.run(
|
|
158
|
+
result = subprocess.run( # nosec B603 - Safe: hardcoded claude CLI with --version flag, no user input
|
|
157
159
|
["claude", "--version"],
|
|
158
160
|
capture_output=True,
|
|
159
161
|
text=True,
|
|
@@ -230,6 +232,19 @@ class ClaudeHookHandler:
|
|
|
230
232
|
self.state_manager, self.response_tracking_manager, self.connection_manager
|
|
231
233
|
)
|
|
232
234
|
|
|
235
|
+
# Initialize auto-pause handler
|
|
236
|
+
try:
|
|
237
|
+
self.auto_pause_handler = AutoPauseHandler()
|
|
238
|
+
# Pass reference to ResponseTrackingManager so it can call auto_pause
|
|
239
|
+
if hasattr(self, "response_tracking_manager"):
|
|
240
|
+
self.response_tracking_manager.auto_pause_handler = (
|
|
241
|
+
self.auto_pause_handler
|
|
242
|
+
)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
self.auto_pause_handler = None
|
|
245
|
+
if DEBUG:
|
|
246
|
+
print(f"Auto-pause initialization failed: {e}", file=sys.stderr)
|
|
247
|
+
|
|
233
248
|
# Backward compatibility properties for tests
|
|
234
249
|
self.connection_pool = self.connection_manager.connection_pool
|
|
235
250
|
|
|
@@ -419,7 +434,7 @@ class ClaudeHookHandler:
|
|
|
419
434
|
"Notification": self.event_handlers.handle_notification_fast,
|
|
420
435
|
"Stop": self.event_handlers.handle_stop_fast,
|
|
421
436
|
"SubagentStop": self.event_handlers.handle_subagent_stop_fast,
|
|
422
|
-
"SubagentStart": self.event_handlers.
|
|
437
|
+
"SubagentStart": self.event_handlers.handle_subagent_start_fast,
|
|
423
438
|
"SessionStart": self.event_handlers.handle_session_start_fast,
|
|
424
439
|
"AssistantResponse": self.event_handlers.handle_assistant_response,
|
|
425
440
|
}
|
|
@@ -628,12 +643,19 @@ class ClaudeHookHandler:
|
|
|
628
643
|
|
|
629
644
|
def __del__(self):
|
|
630
645
|
"""Cleanup on handler destruction."""
|
|
646
|
+
# Finalize any active auto-pause session
|
|
647
|
+
if hasattr(self, "auto_pause_handler") and self.auto_pause_handler:
|
|
648
|
+
try:
|
|
649
|
+
self.auto_pause_handler.on_session_end()
|
|
650
|
+
except Exception:
|
|
651
|
+
pass # nosec B110 - Intentionally ignore cleanup errors during handler destruction
|
|
652
|
+
|
|
631
653
|
# Clean up connection manager if it exists
|
|
632
654
|
if hasattr(self, "connection_manager") and self.connection_manager:
|
|
633
655
|
try:
|
|
634
656
|
self.connection_manager.cleanup()
|
|
635
657
|
except Exception:
|
|
636
|
-
pass #
|
|
658
|
+
pass # nosec B110 - Intentionally ignore cleanup errors during handler destruction
|
|
637
659
|
|
|
638
660
|
|
|
639
661
|
def main():
|
|
@@ -130,7 +130,7 @@ class ResponseTrackingManager:
|
|
|
130
130
|
|
|
131
131
|
try:
|
|
132
132
|
# Get the original request data stored during pre-tool
|
|
133
|
-
request_info = delegation_requests.get(session_id)
|
|
133
|
+
request_info = delegation_requests.get(session_id) # nosec B113 - False positive: dict.get(), not requests library
|
|
134
134
|
if not request_info:
|
|
135
135
|
if DEBUG:
|
|
136
136
|
print(
|
|
@@ -327,6 +327,22 @@ class ResponseTrackingManager:
|
|
|
327
327
|
file=sys.stderr,
|
|
328
328
|
)
|
|
329
329
|
|
|
330
|
+
# Auto-pause integration
|
|
331
|
+
auto_pause = getattr(self, "auto_pause_handler", None)
|
|
332
|
+
if auto_pause and metadata.get("usage"):
|
|
333
|
+
try:
|
|
334
|
+
threshold_crossed = auto_pause.on_usage_update(
|
|
335
|
+
metadata["usage"]
|
|
336
|
+
)
|
|
337
|
+
if threshold_crossed:
|
|
338
|
+
warning = auto_pause.emit_threshold_warning(
|
|
339
|
+
threshold_crossed
|
|
340
|
+
)
|
|
341
|
+
print(f"\n⚠️ {warning}", file=sys.stderr)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
if DEBUG:
|
|
344
|
+
print(f"Auto-pause error: {e}", file=sys.stderr)
|
|
345
|
+
|
|
330
346
|
# Track the main Claude response
|
|
331
347
|
file_path = self.response_tracker.track_response(
|
|
332
348
|
agent_name="claude_main",
|
|
Binary file
|