netra-zen 1.0.11__tar.gz → 1.1.2__tar.gz
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.
- {netra_zen-1.0.11/netra_zen.egg-info → netra_zen-1.1.2}/PKG-INFO +1 -1
- {netra_zen-1.0.11 → netra_zen-1.1.2/netra_zen.egg-info}/PKG-INFO +1 -1
- {netra_zen-1.0.11 → netra_zen-1.1.2}/netra_zen.egg-info/SOURCES.txt +9 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/pyproject.toml +1 -1
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/agent_cli.py +241 -185
- {netra_zen-1.0.11 → netra_zen-1.1.2}/setup.py +1 -1
- netra_zen-1.1.2/tests/test_apex_streaming_fix.py +214 -0
- netra_zen-1.1.2/tests/test_apex_telemetry_live.py +221 -0
- netra_zen-1.1.2/tests/test_apex_telemetry_mock.py +197 -0
- netra_zen-1.1.2/tests/test_apex_telemetry_regression.py +202 -0
- netra_zen-1.1.2/tests/test_event_batching_issue.py +253 -0
- netra_zen-1.1.2/tests/test_thread_handshake.py +162 -0
- netra_zen-1.1.2/tests/test_thread_id_fix.py +130 -0
- netra_zen-1.1.2/tests/test_thread_management.py +147 -0
- netra_zen-1.1.2/tests/test_thread_resolution.py +225 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/LICENSE.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/MANIFEST.in +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/README.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/agent_interface/__init__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/agent_interface/base_agent.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/config_example.json +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/docs/CACHE_TOKENS_GUIDE.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/docs/Cost_allocation.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/docs/DOLLAR_BUDGET_USAGE_EXAMPLES.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/docs/EXAMPLES.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/docs/MODEL_COLUMN_GUIDE.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/docs/apex_integration_test_plan.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/docs/zen_agent_cli_parallel_plan.md +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/netra_zen.egg-info/dependency_links.txt +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/netra_zen.egg-info/entry_points.txt +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/netra_zen.egg-info/requires.txt +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/netra_zen.egg-info/top_level.txt +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/prebuilt_commands_example.json +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/requirements-dev.txt +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/requirements.txt +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/__init__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/__main__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/agent_logs.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/bump_version.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/demo_log_collection.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/embed_release_credentials.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/test_apex_telemetry_debug.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/scripts/verify_log_transmission.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/setup.cfg +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/__init__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/htmlcov/status.json +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_agent_interface.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_agent_logs.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_apex_integration.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_apex_telemetry.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_cli_extensions.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_cli_integration.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_direct_command_execution.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_dollar_budget_enhancement.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_permission_fix_windows.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_pricing_engine.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_runner.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_workspace_detection.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_zen_commands.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_zen_integration.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_zen_metrics.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/tests/test_zen_unit.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/token_budget/__init__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/token_budget/budget_manager.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/token_budget/models.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/token_budget/visualization.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/token_transparency/__init__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/token_transparency/claude_pricing_engine.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/zen/__init__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/zen/__main__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/zen/telemetry/__init__.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/zen/telemetry/apex_telemetry.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/zen/telemetry/embedded_credentials.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/zen/telemetry/manager.py +0 -0
- {netra_zen-1.0.11 → netra_zen-1.1.2}/zen_orchestrator.py +0 -0
@@ -36,14 +36,23 @@ tests/__init__.py
|
|
36
36
|
tests/test_agent_interface.py
|
37
37
|
tests/test_agent_logs.py
|
38
38
|
tests/test_apex_integration.py
|
39
|
+
tests/test_apex_streaming_fix.py
|
39
40
|
tests/test_apex_telemetry.py
|
41
|
+
tests/test_apex_telemetry_live.py
|
42
|
+
tests/test_apex_telemetry_mock.py
|
43
|
+
tests/test_apex_telemetry_regression.py
|
40
44
|
tests/test_cli_extensions.py
|
41
45
|
tests/test_cli_integration.py
|
42
46
|
tests/test_direct_command_execution.py
|
43
47
|
tests/test_dollar_budget_enhancement.py
|
48
|
+
tests/test_event_batching_issue.py
|
44
49
|
tests/test_permission_fix_windows.py
|
45
50
|
tests/test_pricing_engine.py
|
46
51
|
tests/test_runner.py
|
52
|
+
tests/test_thread_handshake.py
|
53
|
+
tests/test_thread_id_fix.py
|
54
|
+
tests/test_thread_management.py
|
55
|
+
tests/test_thread_resolution.py
|
47
56
|
tests/test_workspace_detection.py
|
48
57
|
tests/test_zen_commands.py
|
49
58
|
tests/test_zen_integration.py
|
@@ -216,7 +216,9 @@ EMOJI_FALLBACKS = {
|
|
216
216
|
'🔥': '[HOT]',
|
217
217
|
'💭': '[THOUGHT]',
|
218
218
|
'📥': '[INPUT]',
|
219
|
-
'📤': '[OUTPUT]'
|
219
|
+
'📤': '[OUTPUT]',
|
220
|
+
'📍': '[ENV]',
|
221
|
+
'🔗': '[LINK]'
|
220
222
|
}
|
221
223
|
|
222
224
|
def detect_terminal_capabilities(override_mode: str = None) -> str:
|
@@ -1592,18 +1594,18 @@ def show_startup_banner(config):
|
|
1592
1594
|
config: Config object containing environment, backend_url, and auth_url
|
1593
1595
|
"""
|
1594
1596
|
print()
|
1595
|
-
print("
|
1596
|
-
|
1597
|
-
print("
|
1597
|
+
print("=" * 75)
|
1598
|
+
safe_console_print("🤖 Netra Agent CLI - Interactive Mode", display_mode=detect_terminal_capabilities())
|
1599
|
+
print("=" * 75)
|
1598
1600
|
print()
|
1599
|
-
|
1601
|
+
safe_console_print(f"📍 Environment: {config.environment.value.upper()}", display_mode=detect_terminal_capabilities())
|
1600
1602
|
print()
|
1601
1603
|
|
1602
|
-
|
1604
|
+
safe_console_print("🔗 Endpoints: ", display_mode=detect_terminal_capabilities())
|
1603
1605
|
print(f"Backend: {config.backend_url}")
|
1604
1606
|
print(f"Auth: {config.auth_url}")
|
1605
1607
|
print()
|
1606
|
-
print("
|
1608
|
+
print("=" * 75)
|
1607
1609
|
print()
|
1608
1610
|
|
1609
1611
|
|
@@ -2881,15 +2883,20 @@ class WebSocketClient:
|
|
2881
2883
|
self.connected = True
|
2882
2884
|
return True
|
2883
2885
|
else:
|
2884
|
-
# Handshake failed - backend might be old
|
2886
|
+
# Handshake failed - backend might be using old reactive model
|
2885
2887
|
self.debug.debug_print(
|
2886
|
-
"WARNING:
|
2888
|
+
"WARNING: No proactive handshake received from server",
|
2887
2889
|
DebugLevel.BASIC,
|
2888
2890
|
style="yellow"
|
2889
2891
|
)
|
2892
|
+
self.debug.debug_print(
|
2893
|
+
"Server may be using pre-2025-10-09 architecture (reactive handshake)",
|
2894
|
+
DebugLevel.VERBOSE,
|
2895
|
+
style="yellow"
|
2896
|
+
)
|
2890
2897
|
|
2891
2898
|
# Still mark as connected for backward compatibility
|
2892
|
-
# Old backends might work without the handshake
|
2899
|
+
# Old backends might work without the proactive handshake
|
2893
2900
|
self.connected = True
|
2894
2901
|
return True
|
2895
2902
|
except Exception as e:
|
@@ -2997,77 +3004,149 @@ class WebSocketClient:
|
|
2997
3004
|
|
2998
3005
|
async def _perform_handshake(self) -> bool:
|
2999
3006
|
"""
|
3000
|
-
|
3001
|
-
This ensures both CLI and backend agree on the thread_id for proper event routing.
|
3007
|
+
Wait for proactive handshake from server (as of 2025-10-09).
|
3002
3008
|
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3009
|
+
Server Phase Alignment:
|
3010
|
+
- INITIALIZING: WebSocket connection accepted
|
3011
|
+
- AUTHENTICATING: User validation (happens during connect())
|
3012
|
+
- HANDSHAKING: Server proactively sends handshake_response (we wait here)
|
3013
|
+
- READY: Services initialized
|
3014
|
+
- PROCESSING: Message handling begins
|
3007
3015
|
|
3008
|
-
The
|
3009
|
-
|
3016
|
+
The client waits during server's HANDSHAKING phase to receive the
|
3017
|
+
proactive handshake_response without sending any request.
|
3010
3018
|
"""
|
3011
3019
|
try:
|
3012
3020
|
import asyncio
|
3013
3021
|
|
3014
3022
|
self.debug.debug_print(
|
3015
|
-
"
|
3023
|
+
"Waiting for server to enter HANDSHAKING phase and send handshake...",
|
3016
3024
|
DebugLevel.VERBOSE,
|
3017
3025
|
style="cyan"
|
3018
3026
|
)
|
3019
3027
|
|
3020
|
-
#
|
3028
|
+
# Server enters HANDSHAKING phase after authentication
|
3029
|
+
# and proactively sends handshake_response
|
3030
|
+
handshake_timeout = 10.0 # Wait up to 10 seconds
|
3031
|
+
start_time = asyncio.get_event_loop().time()
|
3032
|
+
|
3021
3033
|
try:
|
3022
|
-
|
3023
|
-
|
3024
|
-
initial_timeout = min(2.0, self.handshake_timeout / 6)
|
3025
|
-
response_msg = await asyncio.wait_for(self.ws.recv(), timeout=initial_timeout)
|
3026
|
-
response = json.loads(response_msg)
|
3027
|
-
|
3028
|
-
# SSOT: Process any handshake-related response
|
3029
|
-
return await self._process_any_handshake_response(response)
|
3030
|
-
except asyncio.TimeoutError:
|
3031
|
-
# Backend didn't send immediately, try sending a trigger message
|
3032
|
-
pass # Silent - this is normal for backends that wait for trigger
|
3033
|
-
|
3034
|
-
# If we didn't get handshake_response immediately, send a trigger
|
3035
|
-
# This handles backends that wait for an initial message
|
3036
|
-
trigger_message = {
|
3037
|
-
"type": "handshake_request",
|
3038
|
-
"client_type": "cli",
|
3039
|
-
"client_version": "2.0.0",
|
3040
|
-
"timestamp": datetime.now(timezone.utc).isoformat()
|
3041
|
-
}
|
3034
|
+
while (asyncio.get_event_loop().time() - start_time) < handshake_timeout:
|
3035
|
+
remaining_time = handshake_timeout - (asyncio.get_event_loop().time() - start_time)
|
3042
3036
|
|
3043
|
-
|
3044
|
-
|
3045
|
-
|
3046
|
-
|
3047
|
-
|
3037
|
+
# Be ready to receive messages during server's HANDSHAKING phase
|
3038
|
+
self.debug.debug_print(
|
3039
|
+
f"Listening for handshake (remaining: {remaining_time:.1f}s)...",
|
3040
|
+
DebugLevel.VERBOSE,
|
3041
|
+
style="dim"
|
3042
|
+
)
|
3048
3043
|
|
3049
|
-
|
3044
|
+
try:
|
3045
|
+
# Use the full remaining time for recv to avoid timeout errors
|
3046
|
+
# This prevents premature connection closure during handshake
|
3047
|
+
response_msg = await asyncio.wait_for(
|
3048
|
+
self.ws.recv(),
|
3049
|
+
timeout=remaining_time
|
3050
|
+
)
|
3051
|
+
response = json.loads(response_msg)
|
3052
|
+
except asyncio.TimeoutError:
|
3053
|
+
# Gracefully handle timeout - don't let it propagate and close connection
|
3054
|
+
self.debug.debug_print(
|
3055
|
+
"Handshake wait timed out after 10 seconds",
|
3056
|
+
DebugLevel.VERBOSE,
|
3057
|
+
style="yellow"
|
3058
|
+
)
|
3059
|
+
break # Exit loop to handle timeout below
|
3060
|
+
except json.JSONDecodeError as e:
|
3061
|
+
# Handle JSON parsing errors gracefully
|
3062
|
+
self.debug.debug_print(
|
3063
|
+
f"Invalid JSON received during handshake: {e}",
|
3064
|
+
DebugLevel.VERBOSE,
|
3065
|
+
style="yellow"
|
3066
|
+
)
|
3067
|
+
continue # Try to receive next message
|
3068
|
+
except websockets.exceptions.ConnectionClosed as e:
|
3069
|
+
# Connection closed during handshake - this is the issue!
|
3070
|
+
self.debug.debug_print(
|
3071
|
+
f"Connection closed during handshake wait: {e}",
|
3072
|
+
DebugLevel.BASIC,
|
3073
|
+
style="red"
|
3074
|
+
)
|
3075
|
+
return False # Connection lost, can't continue
|
3050
3076
|
|
3051
|
-
|
3052
|
-
|
3053
|
-
|
3054
|
-
|
3055
|
-
|
3077
|
+
msg_type = response.get('type', 'unknown')
|
3078
|
+
self.debug.debug_print(
|
3079
|
+
f"Received: {msg_type}",
|
3080
|
+
DebugLevel.VERBOSE,
|
3081
|
+
style="cyan"
|
3082
|
+
)
|
3083
|
+
|
3084
|
+
# Check for the proactive handshake_response
|
3085
|
+
if msg_type == 'handshake_response':
|
3086
|
+
# Server is in HANDSHAKING phase and sent the handshake
|
3087
|
+
self.debug.debug_print(
|
3088
|
+
"✅ Server sent proactive handshake (HANDSHAKING phase)",
|
3089
|
+
DebugLevel.VERBOSE,
|
3090
|
+
style="green"
|
3091
|
+
)
|
3056
3092
|
|
3057
|
-
|
3058
|
-
|
3093
|
+
# Process the handshake
|
3094
|
+
result = await self._process_handshake_response(response)
|
3095
|
+
|
3096
|
+
if result:
|
3097
|
+
self.debug.debug_print(
|
3098
|
+
f"Handshake complete - Thread ID: {self.current_thread_id}",
|
3099
|
+
DebugLevel.BASIC,
|
3100
|
+
style="green"
|
3101
|
+
)
|
3102
|
+
self.debug.debug_print(
|
3103
|
+
"Server phases: AUTH ✓ → HANDSHAKING ✓ → READY",
|
3104
|
+
DebugLevel.VERBOSE,
|
3105
|
+
style="green"
|
3106
|
+
)
|
3107
|
+
|
3108
|
+
return result
|
3109
|
+
|
3110
|
+
# Handle other message types while waiting
|
3111
|
+
elif msg_type == 'connection_established':
|
3112
|
+
# This is from AUTHENTICATING phase completion
|
3113
|
+
self.debug.debug_print(
|
3114
|
+
"Connection established (AUTH phase) - waiting for HANDSHAKING phase",
|
3115
|
+
DebugLevel.VERBOSE,
|
3116
|
+
style="yellow"
|
3117
|
+
)
|
3118
|
+
# Store the event but continue waiting
|
3119
|
+
if hasattr(self, 'events'):
|
3120
|
+
self.events.append(WebSocketEvent.from_dict(response))
|
3121
|
+
|
3122
|
+
else:
|
3123
|
+
# Other event types - store but keep waiting for handshake
|
3124
|
+
self.debug.debug_print(
|
3125
|
+
f"Storing {msg_type} event - still waiting for handshake",
|
3126
|
+
DebugLevel.VERBOSE,
|
3127
|
+
style="dim"
|
3128
|
+
)
|
3129
|
+
if hasattr(self, 'events'):
|
3130
|
+
self.events.append(WebSocketEvent.from_dict(response))
|
3059
3131
|
|
3060
3132
|
except asyncio.TimeoutError:
|
3061
|
-
#
|
3062
|
-
|
3063
|
-
|
3064
|
-
|
3065
|
-
|
3066
|
-
|
3067
|
-
|
3133
|
+
pass # Fall through to timeout handling below
|
3134
|
+
|
3135
|
+
# Timeout - server didn't send handshake
|
3136
|
+
self.debug.debug_print(
|
3137
|
+
"ERROR: Server did not send handshake within 10 seconds",
|
3138
|
+
DebugLevel.BASIC,
|
3139
|
+
style="red"
|
3140
|
+
)
|
3141
|
+
self.debug.debug_print(
|
3142
|
+
"Server may not have HANDSHAKING phase (pre-2025-10-09 version)",
|
3143
|
+
DebugLevel.BASIC,
|
3144
|
+
style="yellow"
|
3145
|
+
)
|
3146
|
+
return False
|
3068
3147
|
|
3069
3148
|
except Exception as e:
|
3070
|
-
# Handshake error
|
3149
|
+
# Handshake error
|
3071
3150
|
error_msg = f"WARNING: Handshake error: {e}"
|
3072
3151
|
self.debug.log_error(e, "handshake protocol")
|
3073
3152
|
self.debug.debug_print(error_msg, DebugLevel.BASIC, style="yellow")
|
@@ -3100,51 +3179,29 @@ class WebSocketClient:
|
|
3100
3179
|
return await self._process_handshake_response(response)
|
3101
3180
|
|
3102
3181
|
elif response_type == 'connection_established':
|
3103
|
-
#
|
3182
|
+
# connection_established is NOT a handshake response!
|
3183
|
+
# It's just a WebSocket connection event. We should NOT acknowledge it.
|
3184
|
+
# We need to wait for the actual handshake_response message.
|
3104
3185
|
self.debug.debug_print(
|
3105
|
-
"
|
3186
|
+
"Received connection_established - waiting for handshake_response",
|
3106
3187
|
DebugLevel.VERBOSE,
|
3107
|
-
style="
|
3188
|
+
style="yellow"
|
3108
3189
|
)
|
3109
3190
|
|
3110
|
-
# Extract connection_id
|
3191
|
+
# Extract connection_id for logging purposes only
|
3111
3192
|
connection_data = response.get('data', {})
|
3112
3193
|
connection_id = connection_data.get('connection_id')
|
3113
3194
|
|
3114
|
-
if
|
3195
|
+
if connection_id:
|
3115
3196
|
self.debug.debug_print(
|
3116
|
-
"
|
3117
|
-
DebugLevel.
|
3118
|
-
style="
|
3197
|
+
f"Connection established with connection_id: {connection_id} (not using as thread_id)",
|
3198
|
+
DebugLevel.VERBOSE,
|
3199
|
+
style="yellow"
|
3119
3200
|
)
|
3120
|
-
return False
|
3121
|
-
|
3122
|
-
# Use connection_id as thread_id (SSOT for thread identification)
|
3123
|
-
self.current_thread_id = connection_id
|
3124
|
-
self.run_id = connection_id # Also use as run_id
|
3125
|
-
self._update_thread_cache(connection_id)
|
3126
|
-
|
3127
|
-
self.debug.debug_print(
|
3128
|
-
f"Handshake complete - Using connection_id as thread ID: {connection_id}",
|
3129
|
-
DebugLevel.VERBOSE,
|
3130
|
-
style="green"
|
3131
|
-
)
|
3132
3201
|
|
3133
|
-
#
|
3134
|
-
|
3135
|
-
|
3136
|
-
"thread_id": connection_id,
|
3137
|
-
"timestamp": datetime.now(timezone.utc).isoformat()
|
3138
|
-
}
|
3139
|
-
|
3140
|
-
await self.ws.send(json.dumps(ack_message))
|
3141
|
-
self.debug.debug_print(
|
3142
|
-
f"Sent session_acknowledged with thread_id: {connection_id}",
|
3143
|
-
DebugLevel.VERBOSE,
|
3144
|
-
style="cyan"
|
3145
|
-
)
|
3146
|
-
|
3147
|
-
return True
|
3202
|
+
# Return False to indicate we're still waiting for handshake_response
|
3203
|
+
# DO NOT send session_acknowledged here!
|
3204
|
+
return False
|
3148
3205
|
|
3149
3206
|
else:
|
3150
3207
|
# Unexpected response type
|
@@ -3810,9 +3867,9 @@ class WebSocketClient:
|
|
3810
3867
|
# Check if payload exceeds maximum allowed size
|
3811
3868
|
if payload_size_mb > MAX_SIZE_MB:
|
3812
3869
|
error_msg = f"""
|
3813
|
-
|
3814
|
-
|
3815
|
-
|
3870
|
+
+==============================================================================+
|
3871
|
+
| ❌ PAYLOAD SIZE EXCEEDED |
|
3872
|
+
+==============================================================================+
|
3816
3873
|
|
3817
3874
|
Payload Size: {payload_size_mb:.2f} MB
|
3818
3875
|
Maximum Allowed: {MAX_SIZE_MB:.1f} MB
|
@@ -3840,7 +3897,7 @@ class WebSocketClient:
|
|
3840
3897
|
|
3841
3898
|
✨ OPTIMAL PERFORMANCE: Keep payload under {OPTIMAL_SIZE_MB:.1f} MB for best results
|
3842
3899
|
|
3843
|
-
|
3900
|
+
+==============================================================================+
|
3844
3901
|
"""
|
3845
3902
|
safe_console_print(error_msg, style="red")
|
3846
3903
|
raise RuntimeError(f"Payload size ({payload_size_mb:.2f} MB) exceeds maximum allowed ({MAX_SIZE_MB:.1f} MB)")
|
@@ -3848,9 +3905,9 @@ class WebSocketClient:
|
|
3848
3905
|
# Warn if payload is large but within limits
|
3849
3906
|
if payload_size_mb > WARNING_SIZE_MB:
|
3850
3907
|
warning_msg = f"""
|
3851
|
-
|
3852
|
-
|
3853
|
-
|
3908
|
+
+==============================================================================+
|
3909
|
+
| ⚠️ LARGE PAYLOAD WARNING |
|
3910
|
+
+==============================================================================+
|
3854
3911
|
|
3855
3912
|
Payload Size: {payload_size_mb:.2f} MB
|
3856
3913
|
Maximum Allowed: {MAX_SIZE_MB:.1f} MB
|
@@ -3875,7 +3932,7 @@ class WebSocketClient:
|
|
3875
3932
|
• Keep total payload under {OPTIMAL_SIZE_MB:.1f} MB for best results
|
3876
3933
|
• Larger payloads may take longer to process
|
3877
3934
|
|
3878
|
-
|
3935
|
+
+==============================================================================+
|
3879
3936
|
"""
|
3880
3937
|
safe_console_print(warning_msg, style="yellow")
|
3881
3938
|
|
@@ -3910,7 +3967,16 @@ class WebSocketClient:
|
|
3910
3967
|
type=data.get('type', 'unknown'),
|
3911
3968
|
data=data
|
3912
3969
|
)
|
3913
|
-
|
3970
|
+
# Skip duplicate connection_established events after handshake
|
3971
|
+
if event.type == 'connection_established' and self.connected and self.current_thread_id:
|
3972
|
+
self.debug.debug_print(
|
3973
|
+
"Ignoring duplicate connection_established (already connected with thread_id)",
|
3974
|
+
DebugLevel.VERBOSE,
|
3975
|
+
style="yellow"
|
3976
|
+
)
|
3977
|
+
# Don't append duplicate connection events
|
3978
|
+
else:
|
3979
|
+
self.events.append(event)
|
3914
3980
|
|
3915
3981
|
# Handle connection_established as a basic WebSocket connection event
|
3916
3982
|
# Note: handshake_response is now used for thread_id exchange, not connection_established
|
@@ -4781,51 +4847,47 @@ class AgentCLI:
|
|
4781
4847
|
|
4782
4848
|
async def _receive_events(self):
|
4783
4849
|
"""Background task to receive and display events"""
|
4784
|
-
|
4785
|
-
|
4850
|
+
# Create persistent spinner that stays at bottom
|
4851
|
+
thinking_spinner = Progress(
|
4852
|
+
SpinnerColumn(spinner_name="dots"),
|
4853
|
+
TextColumn("[dim]{task.description}"),
|
4854
|
+
console=Console(file=sys.stderr),
|
4855
|
+
transient=True
|
4856
|
+
)
|
4857
|
+
thinking_live = Live(thinking_spinner, console=Console(file=sys.stderr), refresh_per_second=10)
|
4858
|
+
thinking_task = None
|
4786
4859
|
|
4787
|
-
|
4788
|
-
|
4860
|
+
# Start the spinner live display
|
4861
|
+
thinking_live.start()
|
4789
4862
|
|
4790
|
-
|
4791
|
-
|
4792
|
-
thinking_live.stop()
|
4793
|
-
thinking_live = None
|
4794
|
-
thinking_spinner = None
|
4863
|
+
async def handle_event(event: WebSocketEvent):
|
4864
|
+
nonlocal thinking_task
|
4795
4865
|
|
4796
4866
|
# Display event with enhanced formatting
|
4797
4867
|
formatted_event = event.format_for_display(self.debug)
|
4798
4868
|
safe_console_print(f"[{event.timestamp.strftime('%H:%M:%S')}] {formatted_event}")
|
4799
4869
|
|
4800
|
-
#
|
4801
|
-
if event.type
|
4802
|
-
|
4803
|
-
|
4870
|
+
# Update spinner for thinking and tool_executing events
|
4871
|
+
if event.type in ["agent_thinking", "tool_executing"]:
|
4872
|
+
# Remove old task if exists
|
4873
|
+
if thinking_task is not None:
|
4874
|
+
thinking_spinner.remove_task(thinking_task)
|
4875
|
+
thinking_task = None
|
4804
4876
|
|
4805
|
-
|
4806
|
-
|
4807
|
-
|
4808
|
-
|
4809
|
-
|
4810
|
-
|
4811
|
-
|
4812
|
-
|
4813
|
-
|
4814
|
-
|
4815
|
-
#
|
4816
|
-
elif
|
4817
|
-
|
4818
|
-
|
4819
|
-
|
4820
|
-
thinking_spinner = Progress(
|
4821
|
-
SpinnerColumn(spinner_name="dots"),
|
4822
|
-
TextColumn("[blue]{task.description}"),
|
4823
|
-
console=Console(file=sys.stderr),
|
4824
|
-
transient=True
|
4825
|
-
)
|
4826
|
-
thinking_live = Live(thinking_spinner, console=Console(file=sys.stderr), refresh_per_second=10)
|
4827
|
-
thinking_live.start()
|
4828
|
-
thinking_spinner.add_task(f"🔧 {spinner_text}", total=None)
|
4877
|
+
# Add new task with latest event
|
4878
|
+
if event.type == "agent_thinking":
|
4879
|
+
thought = event.data.get('thought', event.data.get('reasoning', ''))
|
4880
|
+
spinner_text = truncate_with_ellipsis(thought, 60) if thought else "Processing..."
|
4881
|
+
thinking_task = thinking_spinner.add_task(f"💭 {spinner_text}", total=None)
|
4882
|
+
elif event.type == "tool_executing":
|
4883
|
+
tool_name = event.data.get('tool', event.data.get('tool_name', 'Unknown'))
|
4884
|
+
spinner_text = f"Executing {tool_name}..."
|
4885
|
+
thinking_task = thinking_spinner.add_task(f"🔧 {spinner_text}", total=None)
|
4886
|
+
|
4887
|
+
# Clear spinner for any other event type
|
4888
|
+
elif thinking_task is not None:
|
4889
|
+
thinking_spinner.remove_task(thinking_task)
|
4890
|
+
thinking_task = None
|
4829
4891
|
|
4830
4892
|
# Display raw data in verbose mode
|
4831
4893
|
if self.debug.debug_level >= DebugLevel.DIAGNOSTIC:
|
@@ -4838,58 +4900,53 @@ class AgentCLI:
|
|
4838
4900
|
try:
|
4839
4901
|
await self.ws_client.receive_events(callback=handle_event)
|
4840
4902
|
finally:
|
4841
|
-
# Clean up spinner
|
4842
|
-
|
4843
|
-
thinking_live.stop()
|
4903
|
+
# Clean up spinner
|
4904
|
+
thinking_live.stop()
|
4844
4905
|
|
4845
4906
|
async def _receive_events_with_display(self):
|
4846
4907
|
"""ISSUE #1603 FIX: Enhanced event receiver with better display for single message mode"""
|
4847
|
-
|
4848
|
-
|
4908
|
+
# Create persistent spinner that stays at bottom
|
4909
|
+
thinking_spinner = Progress(
|
4910
|
+
SpinnerColumn(spinner_name="dots"),
|
4911
|
+
TextColumn("[dim]{task.description}"),
|
4912
|
+
console=Console(file=sys.stderr),
|
4913
|
+
transient=True
|
4914
|
+
)
|
4915
|
+
thinking_live = Live(thinking_spinner, console=Console(file=sys.stderr), refresh_per_second=10)
|
4916
|
+
thinking_task = None
|
4849
4917
|
|
4850
|
-
|
4851
|
-
|
4918
|
+
# Start the spinner live display
|
4919
|
+
thinking_live.start()
|
4852
4920
|
|
4853
|
-
|
4854
|
-
|
4855
|
-
thinking_live.stop()
|
4856
|
-
thinking_live = None
|
4857
|
-
thinking_spinner = None
|
4921
|
+
async def handle_event_with_display(event: WebSocketEvent):
|
4922
|
+
nonlocal thinking_task
|
4858
4923
|
|
4859
4924
|
# Display event with enhanced formatting and emojis
|
4860
4925
|
formatted_event = event.format_for_display(self.debug)
|
4861
4926
|
timestamp = event.timestamp.strftime('%H:%M:%S')
|
4862
4927
|
safe_console_print(f"[{timestamp}] {formatted_event}")
|
4863
4928
|
|
4864
|
-
#
|
4865
|
-
if event.type
|
4866
|
-
|
4867
|
-
|
4929
|
+
# Update spinner for thinking and tool_executing events
|
4930
|
+
if event.type in ["agent_thinking", "tool_executing"]:
|
4931
|
+
# Remove old task if exists
|
4932
|
+
if thinking_task is not None:
|
4933
|
+
thinking_spinner.remove_task(thinking_task)
|
4934
|
+
thinking_task = None
|
4868
4935
|
|
4869
|
-
|
4870
|
-
|
4871
|
-
|
4872
|
-
|
4873
|
-
|
4874
|
-
|
4875
|
-
|
4876
|
-
|
4877
|
-
|
4878
|
-
|
4879
|
-
#
|
4880
|
-
elif
|
4881
|
-
|
4882
|
-
|
4883
|
-
|
4884
|
-
thinking_spinner = Progress(
|
4885
|
-
SpinnerColumn(spinner_name="dots"),
|
4886
|
-
TextColumn("[blue]{task.description}"),
|
4887
|
-
console=Console(file=sys.stderr),
|
4888
|
-
transient=True
|
4889
|
-
)
|
4890
|
-
thinking_live = Live(thinking_spinner, console=Console(file=sys.stderr), refresh_per_second=10)
|
4891
|
-
thinking_live.start()
|
4892
|
-
thinking_spinner.add_task(f"🔧 {spinner_text}", total=None)
|
4936
|
+
# Add new task with latest event
|
4937
|
+
if event.type == "agent_thinking":
|
4938
|
+
thought = event.data.get('thought', event.data.get('reasoning', ''))
|
4939
|
+
spinner_text = truncate_with_ellipsis(thought, 60) if thought else "Processing..."
|
4940
|
+
thinking_task = thinking_spinner.add_task(f"💭 {spinner_text}", total=None)
|
4941
|
+
elif event.type == "tool_executing":
|
4942
|
+
tool_name = event.data.get('tool', event.data.get('tool_name', 'Unknown'))
|
4943
|
+
spinner_text = f"Executing {tool_name}..."
|
4944
|
+
thinking_task = thinking_spinner.add_task(f"🔧 {spinner_text}", total=None)
|
4945
|
+
|
4946
|
+
# Clear spinner for any other event type
|
4947
|
+
elif thinking_task is not None:
|
4948
|
+
thinking_spinner.remove_task(thinking_task)
|
4949
|
+
thinking_task = None
|
4893
4950
|
|
4894
4951
|
# Issue #2177: WebSocket event validation
|
4895
4952
|
if self.validate_events and self.event_validator:
|
@@ -4969,9 +5026,8 @@ class AgentCLI:
|
|
4969
5026
|
try:
|
4970
5027
|
await self.ws_client.receive_events(callback=handle_event_with_display)
|
4971
5028
|
finally:
|
4972
|
-
# Clean up spinner
|
4973
|
-
|
4974
|
-
thinking_live.stop()
|
5029
|
+
# Clean up spinner
|
5030
|
+
thinking_live.stop()
|
4975
5031
|
|
4976
5032
|
def _get_event_summary(self, event: WebSocketEvent) -> str:
|
4977
5033
|
"""ISSUE #1603 FIX: Get a concise summary of an event for display"""
|
@@ -8,7 +8,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
8
8
|
|
9
9
|
setup(
|
10
10
|
name="netra-zen",
|
11
|
-
version="1.
|
11
|
+
version="1.1.2",
|
12
12
|
author=" Systems",
|
13
13
|
author_email="pypi@netrasystems.ai",
|
14
14
|
description="Multi-instance Claude orchestrator for parallel task execution",
|