netra-zen 1.0.11__py3-none-any.whl → 1.2.0__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.
- {netra_zen-1.0.11.dist-info → netra_zen-1.2.0.dist-info}/METADATA +1 -1
- {netra_zen-1.0.11.dist-info → netra_zen-1.2.0.dist-info}/RECORD +7 -7
- scripts/agent_cli.py +344 -189
- {netra_zen-1.0.11.dist-info → netra_zen-1.2.0.dist-info}/WHEEL +0 -0
- {netra_zen-1.0.11.dist-info → netra_zen-1.2.0.dist-info}/entry_points.txt +0 -0
- {netra_zen-1.0.11.dist-info → netra_zen-1.2.0.dist-info}/licenses/LICENSE.md +0 -0
- {netra_zen-1.0.11.dist-info → netra_zen-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,10 @@
|
|
1
1
|
zen_orchestrator.py,sha256=hKeP2dW6eJgEhtUHYylnhIgP_HgaxrDAwCnlS7mKkgU,156167
|
2
2
|
agent_interface/__init__.py,sha256=OsbOKzElHsxhVgak87oOx_u46QNgKmz-Reis-plAMwk,525
|
3
3
|
agent_interface/base_agent.py,sha256=GNskG9VaZgno7X24lQTpFdxUoQE0yJHLh0UPFJvOPn4,11098
|
4
|
-
netra_zen-1.0.
|
4
|
+
netra_zen-1.2.0.dist-info/licenses/LICENSE.md,sha256=PZrP0UDn58i4LjV4zijIQTnsQPvWm4zq9Fet9i7qgwI,80
|
5
5
|
scripts/__init__.py,sha256=r6jX6e9SaisREml6B7uDoV-_rzIW0_yQXMm6OhNgbIw,52
|
6
6
|
scripts/__main__.py,sha256=NSqyN47EijNf7m_8QsoxeYevKYgH2sNtNPFZcwKWMs0,160
|
7
|
-
scripts/agent_cli.py,sha256=
|
7
|
+
scripts/agent_cli.py,sha256=ATQIWoVx7GthFV1WeiV6Co8PzoXYXhlJjOerU-JocJ0,345625
|
8
8
|
scripts/agent_logs.py,sha256=ppseAtUCvS2N-iEDSUQh84I9Ov-gBEw5ElxYajNdpJs,11218
|
9
9
|
scripts/bump_version.py,sha256=fjABzzRVXJ00CbYMpUIUMwcOHwafLYtFL6NvUga-i6M,4183
|
10
10
|
scripts/demo_log_collection.py,sha256=5XladS8j4lz-jJdVzSeDSxopChqHKX7HLNJUzSWR40c,5623
|
@@ -23,8 +23,8 @@ zen/telemetry/__init__.py,sha256=zyH7YcK_eWxwaQZW0MRefu1bX5hEgfRljr_dsqNU-tw,452
|
|
23
23
|
zen/telemetry/apex_telemetry.py,sha256=FAkiHXOuzZFL-51oYetTC4wbjCL5UH-qOghwcaaOVgE,9685
|
24
24
|
zen/telemetry/embedded_credentials.py,sha256=z-j_TfuVIz3nX-Vvh4O6_iDQhtteEyQgfc-xSslVbXU,1716
|
25
25
|
zen/telemetry/manager.py,sha256=Rdbpnjbjel9xZEJyvLqy2M-4amvkr80abRiWnqHghIQ,9980
|
26
|
-
netra_zen-1.0.
|
27
|
-
netra_zen-1.0.
|
28
|
-
netra_zen-1.0.
|
29
|
-
netra_zen-1.0.
|
30
|
-
netra_zen-1.0.
|
26
|
+
netra_zen-1.2.0.dist-info/METADATA,sha256=sb-1CUEfEqK3rmHbqxqrNeMi4t5d_hZ34xhBoPT1a90,46283
|
27
|
+
netra_zen-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
28
|
+
netra_zen-1.2.0.dist-info/entry_points.txt,sha256=oDehCnPGZezG0m9ZWspxjHLHyQ3eERX87eojR4ljaRo,45
|
29
|
+
netra_zen-1.2.0.dist-info/top_level.txt,sha256=OhiyXmoXftBijCF6ck-RS1dN2NBJv9wdd7kBG1Es7zA,77
|
30
|
+
netra_zen-1.2.0.dist-info/RECORD,,
|
scripts/agent_cli.py
CHANGED
@@ -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,17 +2883,49 @@ class WebSocketClient:
|
|
2881
2883
|
self.connected = True
|
2882
2884
|
return True
|
2883
2885
|
else:
|
2884
|
-
# Handshake failed -
|
2886
|
+
# Handshake failed - server not ready to process messages
|
2885
2887
|
self.debug.debug_print(
|
2886
|
-
"WARNING:
|
2888
|
+
"WARNING: Server handshake not completed - server not ready",
|
2887
2889
|
DebugLevel.BASIC,
|
2888
2890
|
style="yellow"
|
2889
2891
|
)
|
2892
|
+
self.debug.debug_print(
|
2893
|
+
"Server still completing lifecycle phases (Initialize → Authenticate → Handshake → Prepare → Processing)",
|
2894
|
+
DebugLevel.VERBOSE,
|
2895
|
+
style="yellow"
|
2896
|
+
)
|
2890
2897
|
|
2891
|
-
#
|
2892
|
-
|
2893
|
-
|
2894
|
-
|
2898
|
+
# Wait briefly for server to complete its phases and retry
|
2899
|
+
safe_console_print("⏳ Waiting for server to complete initialization...", style="yellow",
|
2900
|
+
json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
2901
|
+
await asyncio.sleep(3.0) # Give server time to reach PROCESSING phase
|
2902
|
+
|
2903
|
+
# Retry handshake once more
|
2904
|
+
self.debug.debug_print(
|
2905
|
+
"Retrying handshake after delay...",
|
2906
|
+
DebugLevel.VERBOSE,
|
2907
|
+
style="cyan"
|
2908
|
+
)
|
2909
|
+
handshake_success = await self._perform_handshake()
|
2910
|
+
if handshake_success:
|
2911
|
+
safe_console_print(f"✅ Connected with thread ID: {self.current_thread_id}", style="green",
|
2912
|
+
json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
2913
|
+
self.connected = True
|
2914
|
+
return True
|
2915
|
+
else:
|
2916
|
+
# Server still not ready - fail the connection
|
2917
|
+
self.debug.debug_print(
|
2918
|
+
"ERROR: Server not ready after retry",
|
2919
|
+
DebugLevel.BASIC,
|
2920
|
+
style="red"
|
2921
|
+
)
|
2922
|
+
safe_console_print("❌ Server not ready to process messages. Please try again.", style="red",
|
2923
|
+
json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
2924
|
+
|
2925
|
+
# Close WebSocket and fail gracefully
|
2926
|
+
if self.ws:
|
2927
|
+
await self.ws.close()
|
2928
|
+
return False
|
2895
2929
|
except Exception as e:
|
2896
2930
|
self.debug.log_connection_attempt(method_name, self.config.ws_url, success=False, error=str(e))
|
2897
2931
|
self.debug.debug_print(
|
@@ -2997,77 +3031,150 @@ class WebSocketClient:
|
|
2997
3031
|
|
2998
3032
|
async def _perform_handshake(self) -> bool:
|
2999
3033
|
"""
|
3000
|
-
|
3001
|
-
This ensures both CLI and backend agree on the thread_id for proper event routing.
|
3034
|
+
Wait for proactive handshake from server (as of 2025-10-09).
|
3002
3035
|
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3036
|
+
Server Phase Alignment:
|
3037
|
+
- INITIALIZING: WebSocket connection accepted
|
3038
|
+
- AUTHENTICATING: User validation (happens during connect())
|
3039
|
+
- HANDSHAKING: Server proactively sends handshake_response (we wait here)
|
3040
|
+
- READY: Services initialized
|
3041
|
+
- PROCESSING: Message handling begins
|
3007
3042
|
|
3008
|
-
The
|
3009
|
-
|
3043
|
+
The client waits during server's HANDSHAKING phase to receive the
|
3044
|
+
proactive handshake_response without sending any request.
|
3010
3045
|
"""
|
3011
3046
|
try:
|
3012
3047
|
import asyncio
|
3013
3048
|
|
3014
3049
|
self.debug.debug_print(
|
3015
|
-
"
|
3050
|
+
"Waiting for server to enter HANDSHAKING phase and send handshake...",
|
3016
3051
|
DebugLevel.VERBOSE,
|
3017
3052
|
style="cyan"
|
3018
3053
|
)
|
3019
3054
|
|
3020
|
-
#
|
3055
|
+
# Server enters HANDSHAKING phase after authentication
|
3056
|
+
# and proactively sends handshake_response
|
3057
|
+
# Use configured timeout or default to 10 seconds
|
3058
|
+
handshake_timeout = self.handshake_timeout if hasattr(self, 'handshake_timeout') and self.handshake_timeout else 10.0
|
3059
|
+
start_time = asyncio.get_event_loop().time()
|
3060
|
+
|
3021
3061
|
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
|
-
}
|
3062
|
+
while (asyncio.get_event_loop().time() - start_time) < handshake_timeout:
|
3063
|
+
remaining_time = handshake_timeout - (asyncio.get_event_loop().time() - start_time)
|
3042
3064
|
|
3043
|
-
|
3044
|
-
|
3045
|
-
|
3046
|
-
|
3047
|
-
|
3065
|
+
# Be ready to receive messages during server's HANDSHAKING phase
|
3066
|
+
self.debug.debug_print(
|
3067
|
+
f"Listening for handshake (remaining: {remaining_time:.1f}s)...",
|
3068
|
+
DebugLevel.VERBOSE,
|
3069
|
+
style="dim"
|
3070
|
+
)
|
3048
3071
|
|
3049
|
-
|
3072
|
+
try:
|
3073
|
+
# Use the full remaining time for recv to avoid timeout errors
|
3074
|
+
# This prevents premature connection closure during handshake
|
3075
|
+
response_msg = await asyncio.wait_for(
|
3076
|
+
self.ws.recv(),
|
3077
|
+
timeout=remaining_time
|
3078
|
+
)
|
3079
|
+
response = json.loads(response_msg)
|
3080
|
+
except asyncio.TimeoutError:
|
3081
|
+
# Gracefully handle timeout - don't let it propagate and close connection
|
3082
|
+
self.debug.debug_print(
|
3083
|
+
"Handshake wait timed out after 10 seconds",
|
3084
|
+
DebugLevel.VERBOSE,
|
3085
|
+
style="yellow"
|
3086
|
+
)
|
3087
|
+
break # Exit loop to handle timeout below
|
3088
|
+
except json.JSONDecodeError as e:
|
3089
|
+
# Handle JSON parsing errors gracefully
|
3090
|
+
self.debug.debug_print(
|
3091
|
+
f"Invalid JSON received during handshake: {e}",
|
3092
|
+
DebugLevel.VERBOSE,
|
3093
|
+
style="yellow"
|
3094
|
+
)
|
3095
|
+
continue # Try to receive next message
|
3096
|
+
except websockets.exceptions.ConnectionClosed as e:
|
3097
|
+
# Connection closed during handshake - this is the issue!
|
3098
|
+
self.debug.debug_print(
|
3099
|
+
f"Connection closed during handshake wait: {e}",
|
3100
|
+
DebugLevel.BASIC,
|
3101
|
+
style="red"
|
3102
|
+
)
|
3103
|
+
return False # Connection lost, can't continue
|
3050
3104
|
|
3051
|
-
|
3052
|
-
|
3053
|
-
|
3054
|
-
|
3055
|
-
|
3105
|
+
msg_type = response.get('type', 'unknown')
|
3106
|
+
self.debug.debug_print(
|
3107
|
+
f"Received: {msg_type}",
|
3108
|
+
DebugLevel.VERBOSE,
|
3109
|
+
style="cyan"
|
3110
|
+
)
|
3111
|
+
|
3112
|
+
# Check for the proactive handshake_response
|
3113
|
+
if msg_type == 'handshake_response':
|
3114
|
+
# Server is in HANDSHAKING phase and sent the handshake
|
3115
|
+
self.debug.debug_print(
|
3116
|
+
"✅ Server sent proactive handshake (HANDSHAKING phase)",
|
3117
|
+
DebugLevel.VERBOSE,
|
3118
|
+
style="green"
|
3119
|
+
)
|
3056
3120
|
|
3057
|
-
|
3058
|
-
|
3121
|
+
# Process the handshake
|
3122
|
+
result = await self._process_handshake_response(response)
|
3123
|
+
|
3124
|
+
if result:
|
3125
|
+
self.debug.debug_print(
|
3126
|
+
f"Handshake complete - Thread ID: {self.current_thread_id}",
|
3127
|
+
DebugLevel.BASIC,
|
3128
|
+
style="green"
|
3129
|
+
)
|
3130
|
+
self.debug.debug_print(
|
3131
|
+
"Server phases: AUTH ✓ → HANDSHAKING ✓ → READY",
|
3132
|
+
DebugLevel.VERBOSE,
|
3133
|
+
style="green"
|
3134
|
+
)
|
3135
|
+
|
3136
|
+
return result
|
3137
|
+
|
3138
|
+
# Handle other message types while waiting
|
3139
|
+
elif msg_type == 'connection_established':
|
3140
|
+
# This is from AUTHENTICATING phase completion
|
3141
|
+
self.debug.debug_print(
|
3142
|
+
"Connection established (AUTH phase) - waiting for HANDSHAKING phase",
|
3143
|
+
DebugLevel.VERBOSE,
|
3144
|
+
style="yellow"
|
3145
|
+
)
|
3146
|
+
# Store the event but continue waiting
|
3147
|
+
if hasattr(self, 'events'):
|
3148
|
+
self.events.append(WebSocketEvent.from_dict(response))
|
3149
|
+
|
3150
|
+
else:
|
3151
|
+
# Other event types - store but keep waiting for handshake
|
3152
|
+
self.debug.debug_print(
|
3153
|
+
f"Storing {msg_type} event - still waiting for handshake",
|
3154
|
+
DebugLevel.VERBOSE,
|
3155
|
+
style="dim"
|
3156
|
+
)
|
3157
|
+
if hasattr(self, 'events'):
|
3158
|
+
self.events.append(WebSocketEvent.from_dict(response))
|
3059
3159
|
|
3060
3160
|
except asyncio.TimeoutError:
|
3061
|
-
#
|
3062
|
-
|
3063
|
-
|
3064
|
-
|
3065
|
-
|
3066
|
-
|
3067
|
-
|
3161
|
+
pass # Fall through to timeout handling below
|
3162
|
+
|
3163
|
+
# Timeout - server didn't send handshake
|
3164
|
+
self.debug.debug_print(
|
3165
|
+
"ERROR: Server did not send handshake within 10 seconds",
|
3166
|
+
DebugLevel.BASIC,
|
3167
|
+
style="red"
|
3168
|
+
)
|
3169
|
+
self.debug.debug_print(
|
3170
|
+
"Server may not have HANDSHAKING phase (pre-2025-10-09 version)",
|
3171
|
+
DebugLevel.BASIC,
|
3172
|
+
style="yellow"
|
3173
|
+
)
|
3174
|
+
return False
|
3068
3175
|
|
3069
3176
|
except Exception as e:
|
3070
|
-
# Handshake error
|
3177
|
+
# Handshake error
|
3071
3178
|
error_msg = f"WARNING: Handshake error: {e}"
|
3072
3179
|
self.debug.log_error(e, "handshake protocol")
|
3073
3180
|
self.debug.debug_print(error_msg, DebugLevel.BASIC, style="yellow")
|
@@ -3100,51 +3207,29 @@ class WebSocketClient:
|
|
3100
3207
|
return await self._process_handshake_response(response)
|
3101
3208
|
|
3102
3209
|
elif response_type == 'connection_established':
|
3103
|
-
#
|
3210
|
+
# connection_established is NOT a handshake response!
|
3211
|
+
# It's just a WebSocket connection event. We should NOT acknowledge it.
|
3212
|
+
# We need to wait for the actual handshake_response message.
|
3104
3213
|
self.debug.debug_print(
|
3105
|
-
"
|
3214
|
+
"Received connection_established - waiting for handshake_response",
|
3106
3215
|
DebugLevel.VERBOSE,
|
3107
|
-
style="
|
3216
|
+
style="yellow"
|
3108
3217
|
)
|
3109
3218
|
|
3110
|
-
# Extract connection_id
|
3219
|
+
# Extract connection_id for logging purposes only
|
3111
3220
|
connection_data = response.get('data', {})
|
3112
3221
|
connection_id = connection_data.get('connection_id')
|
3113
3222
|
|
3114
|
-
if
|
3223
|
+
if connection_id:
|
3115
3224
|
self.debug.debug_print(
|
3116
|
-
"
|
3117
|
-
DebugLevel.
|
3118
|
-
style="
|
3225
|
+
f"Connection established with connection_id: {connection_id} (not using as thread_id)",
|
3226
|
+
DebugLevel.VERBOSE,
|
3227
|
+
style="yellow"
|
3119
3228
|
)
|
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
|
-
|
3133
|
-
# Send acknowledgment with the connection_id
|
3134
|
-
ack_message = {
|
3135
|
-
"type": "session_acknowledged",
|
3136
|
-
"thread_id": connection_id,
|
3137
|
-
"timestamp": datetime.now(timezone.utc).isoformat()
|
3138
|
-
}
|
3139
3229
|
|
3140
|
-
|
3141
|
-
|
3142
|
-
|
3143
|
-
DebugLevel.VERBOSE,
|
3144
|
-
style="cyan"
|
3145
|
-
)
|
3146
|
-
|
3147
|
-
return True
|
3230
|
+
# Return False to indicate we're still waiting for handshake_response
|
3231
|
+
# DO NOT send session_acknowledged here!
|
3232
|
+
return False
|
3148
3233
|
|
3149
3234
|
else:
|
3150
3235
|
# Unexpected response type
|
@@ -3217,14 +3302,64 @@ class WebSocketClient:
|
|
3217
3302
|
)
|
3218
3303
|
|
3219
3304
|
# CRITICAL: Send acknowledgment with the SAME thread_id
|
3305
|
+
# Per WebSocket Client Lifecycle Guide, use "handshake_acknowledged"
|
3220
3306
|
ack_message = {
|
3221
|
-
"type": "
|
3307
|
+
"type": "handshake_acknowledged",
|
3222
3308
|
"thread_id": backend_thread_id, # Echo back the same ID
|
3223
3309
|
"timestamp": datetime.now(timezone.utc).isoformat()
|
3224
3310
|
}
|
3225
3311
|
|
3226
3312
|
await self.ws.send(json.dumps(ack_message))
|
3227
3313
|
|
3314
|
+
# Per WebSocket Client Lifecycle Guide, wait for handshake_complete
|
3315
|
+
# and then add delay for server to enter Phase 5 (Processing)
|
3316
|
+
self.debug.debug_print(
|
3317
|
+
"Waiting for handshake_complete confirmation...",
|
3318
|
+
DebugLevel.VERBOSE,
|
3319
|
+
style="cyan"
|
3320
|
+
)
|
3321
|
+
|
3322
|
+
# Wait briefly for handshake_complete message
|
3323
|
+
try:
|
3324
|
+
complete_msg = await asyncio.wait_for(self.ws.recv(), timeout=2.0)
|
3325
|
+
complete_data = json.loads(complete_msg)
|
3326
|
+
if complete_data.get('type') == 'handshake_complete':
|
3327
|
+
self.debug.debug_print(
|
3328
|
+
"✅ Received handshake_complete - Server at Phase 4 (Ready)",
|
3329
|
+
DebugLevel.VERBOSE,
|
3330
|
+
style="green"
|
3331
|
+
)
|
3332
|
+
|
3333
|
+
# CRITICAL: Add delay for server to enter Phase 5 (Processing)
|
3334
|
+
# Per documentation, 500ms is recommended
|
3335
|
+
self.debug.debug_print(
|
3336
|
+
"Waiting 500ms for server to enter Phase 5 (Processing)...",
|
3337
|
+
DebugLevel.VERBOSE,
|
3338
|
+
style="cyan"
|
3339
|
+
)
|
3340
|
+
await asyncio.sleep(0.5)
|
3341
|
+
|
3342
|
+
self.debug.debug_print(
|
3343
|
+
"✅ Server should now be in Phase 5 (Processing) - ready for messages",
|
3344
|
+
DebugLevel.VERBOSE,
|
3345
|
+
style="green"
|
3346
|
+
)
|
3347
|
+
except asyncio.TimeoutError:
|
3348
|
+
# If no handshake_complete, still add a delay to be safe
|
3349
|
+
self.debug.debug_print(
|
3350
|
+
"No handshake_complete received, adding safety delay",
|
3351
|
+
DebugLevel.VERBOSE,
|
3352
|
+
style="yellow"
|
3353
|
+
)
|
3354
|
+
await asyncio.sleep(0.5)
|
3355
|
+
except Exception as e:
|
3356
|
+
self.debug.debug_print(
|
3357
|
+
f"Error waiting for handshake_complete: {e}",
|
3358
|
+
DebugLevel.VERBOSE,
|
3359
|
+
style="yellow"
|
3360
|
+
)
|
3361
|
+
await asyncio.sleep(0.5)
|
3362
|
+
|
3228
3363
|
return True
|
3229
3364
|
|
3230
3365
|
def _get_platform_cache_path(self) -> Path:
|
@@ -3565,6 +3700,27 @@ class WebSocketClient:
|
|
3565
3700
|
if not self.ws:
|
3566
3701
|
raise RuntimeError("WebSocket not connected")
|
3567
3702
|
|
3703
|
+
# Check if connection handshake is fully complete
|
3704
|
+
if not self.connected:
|
3705
|
+
self.debug.debug_print(
|
3706
|
+
"ERROR: Connection not ready - handshake not complete",
|
3707
|
+
DebugLevel.BASIC,
|
3708
|
+
style="red"
|
3709
|
+
)
|
3710
|
+
safe_console_print(
|
3711
|
+
"\n❌ ERROR: Cannot send message - server not ready",
|
3712
|
+
style="red",
|
3713
|
+
json_mode=self.config.json_mode,
|
3714
|
+
ci_mode=self.config.ci_mode
|
3715
|
+
)
|
3716
|
+
safe_console_print(
|
3717
|
+
"The server is still completing its initialization phases.",
|
3718
|
+
style="yellow",
|
3719
|
+
json_mode=self.config.json_mode,
|
3720
|
+
ci_mode=self.config.ci_mode
|
3721
|
+
)
|
3722
|
+
raise RuntimeError("Connection not ready - handshake incomplete. Server needs to reach Processing phase.")
|
3723
|
+
|
3568
3724
|
# Generate run_id
|
3569
3725
|
self.run_id = f"cli_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{os.getpid()}"
|
3570
3726
|
|
@@ -3810,9 +3966,9 @@ class WebSocketClient:
|
|
3810
3966
|
# Check if payload exceeds maximum allowed size
|
3811
3967
|
if payload_size_mb > MAX_SIZE_MB:
|
3812
3968
|
error_msg = f"""
|
3813
|
-
|
3814
|
-
|
3815
|
-
|
3969
|
+
+==============================================================================+
|
3970
|
+
| ❌ PAYLOAD SIZE EXCEEDED |
|
3971
|
+
+==============================================================================+
|
3816
3972
|
|
3817
3973
|
Payload Size: {payload_size_mb:.2f} MB
|
3818
3974
|
Maximum Allowed: {MAX_SIZE_MB:.1f} MB
|
@@ -3840,7 +3996,7 @@ class WebSocketClient:
|
|
3840
3996
|
|
3841
3997
|
✨ OPTIMAL PERFORMANCE: Keep payload under {OPTIMAL_SIZE_MB:.1f} MB for best results
|
3842
3998
|
|
3843
|
-
|
3999
|
+
+==============================================================================+
|
3844
4000
|
"""
|
3845
4001
|
safe_console_print(error_msg, style="red")
|
3846
4002
|
raise RuntimeError(f"Payload size ({payload_size_mb:.2f} MB) exceeds maximum allowed ({MAX_SIZE_MB:.1f} MB)")
|
@@ -3848,9 +4004,9 @@ class WebSocketClient:
|
|
3848
4004
|
# Warn if payload is large but within limits
|
3849
4005
|
if payload_size_mb > WARNING_SIZE_MB:
|
3850
4006
|
warning_msg = f"""
|
3851
|
-
|
3852
|
-
|
3853
|
-
|
4007
|
+
+==============================================================================+
|
4008
|
+
| ⚠️ LARGE PAYLOAD WARNING |
|
4009
|
+
+==============================================================================+
|
3854
4010
|
|
3855
4011
|
Payload Size: {payload_size_mb:.2f} MB
|
3856
4012
|
Maximum Allowed: {MAX_SIZE_MB:.1f} MB
|
@@ -3875,7 +4031,7 @@ class WebSocketClient:
|
|
3875
4031
|
• Keep total payload under {OPTIMAL_SIZE_MB:.1f} MB for best results
|
3876
4032
|
• Larger payloads may take longer to process
|
3877
4033
|
|
3878
|
-
|
4034
|
+
+==============================================================================+
|
3879
4035
|
"""
|
3880
4036
|
safe_console_print(warning_msg, style="yellow")
|
3881
4037
|
|
@@ -3910,7 +4066,16 @@ class WebSocketClient:
|
|
3910
4066
|
type=data.get('type', 'unknown'),
|
3911
4067
|
data=data
|
3912
4068
|
)
|
3913
|
-
|
4069
|
+
# Skip duplicate connection_established events after handshake
|
4070
|
+
if event.type == 'connection_established' and self.connected and self.current_thread_id:
|
4071
|
+
self.debug.debug_print(
|
4072
|
+
"Ignoring duplicate connection_established (already connected with thread_id)",
|
4073
|
+
DebugLevel.VERBOSE,
|
4074
|
+
style="yellow"
|
4075
|
+
)
|
4076
|
+
# Don't append duplicate connection events
|
4077
|
+
else:
|
4078
|
+
self.events.append(event)
|
3914
4079
|
|
3915
4080
|
# Handle connection_established as a basic WebSocket connection event
|
3916
4081
|
# Note: handshake_response is now used for thread_id exchange, not connection_established
|
@@ -4781,51 +4946,47 @@ class AgentCLI:
|
|
4781
4946
|
|
4782
4947
|
async def _receive_events(self):
|
4783
4948
|
"""Background task to receive and display events"""
|
4784
|
-
|
4785
|
-
|
4949
|
+
# Create persistent spinner that stays at bottom
|
4950
|
+
thinking_spinner = Progress(
|
4951
|
+
SpinnerColumn(spinner_name="dots"),
|
4952
|
+
TextColumn("[dim]{task.description}"),
|
4953
|
+
console=Console(file=sys.stderr),
|
4954
|
+
transient=True
|
4955
|
+
)
|
4956
|
+
thinking_live = Live(thinking_spinner, console=Console(file=sys.stderr), refresh_per_second=10)
|
4957
|
+
thinking_task = None
|
4786
4958
|
|
4787
|
-
|
4788
|
-
|
4959
|
+
# Start the spinner live display
|
4960
|
+
thinking_live.start()
|
4789
4961
|
|
4790
|
-
|
4791
|
-
|
4792
|
-
thinking_live.stop()
|
4793
|
-
thinking_live = None
|
4794
|
-
thinking_spinner = None
|
4962
|
+
async def handle_event(event: WebSocketEvent):
|
4963
|
+
nonlocal thinking_task
|
4795
4964
|
|
4796
4965
|
# Display event with enhanced formatting
|
4797
4966
|
formatted_event = event.format_for_display(self.debug)
|
4798
4967
|
safe_console_print(f"[{event.timestamp.strftime('%H:%M:%S')}] {formatted_event}")
|
4799
4968
|
|
4800
|
-
#
|
4801
|
-
if event.type
|
4802
|
-
|
4803
|
-
|
4969
|
+
# Update spinner for thinking and tool_executing events
|
4970
|
+
if event.type in ["agent_thinking", "tool_executing"]:
|
4971
|
+
# Remove old task if exists
|
4972
|
+
if thinking_task is not None:
|
4973
|
+
thinking_spinner.remove_task(thinking_task)
|
4974
|
+
thinking_task = None
|
4804
4975
|
|
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)
|
4976
|
+
# Add new task with latest event
|
4977
|
+
if event.type == "agent_thinking":
|
4978
|
+
thought = event.data.get('thought', event.data.get('reasoning', ''))
|
4979
|
+
spinner_text = truncate_with_ellipsis(thought, 60) if thought else "Processing..."
|
4980
|
+
thinking_task = thinking_spinner.add_task(f"💭 {spinner_text}", total=None)
|
4981
|
+
elif event.type == "tool_executing":
|
4982
|
+
tool_name = event.data.get('tool', event.data.get('tool_name', 'Unknown'))
|
4983
|
+
spinner_text = f"Executing {tool_name}..."
|
4984
|
+
thinking_task = thinking_spinner.add_task(f"🔧 {spinner_text}", total=None)
|
4985
|
+
|
4986
|
+
# Clear spinner for any other event type
|
4987
|
+
elif thinking_task is not None:
|
4988
|
+
thinking_spinner.remove_task(thinking_task)
|
4989
|
+
thinking_task = None
|
4829
4990
|
|
4830
4991
|
# Display raw data in verbose mode
|
4831
4992
|
if self.debug.debug_level >= DebugLevel.DIAGNOSTIC:
|
@@ -4838,58 +4999,53 @@ class AgentCLI:
|
|
4838
4999
|
try:
|
4839
5000
|
await self.ws_client.receive_events(callback=handle_event)
|
4840
5001
|
finally:
|
4841
|
-
# Clean up spinner
|
4842
|
-
|
4843
|
-
thinking_live.stop()
|
5002
|
+
# Clean up spinner
|
5003
|
+
thinking_live.stop()
|
4844
5004
|
|
4845
5005
|
async def _receive_events_with_display(self):
|
4846
5006
|
"""ISSUE #1603 FIX: Enhanced event receiver with better display for single message mode"""
|
4847
|
-
|
4848
|
-
|
5007
|
+
# Create persistent spinner that stays at bottom
|
5008
|
+
thinking_spinner = Progress(
|
5009
|
+
SpinnerColumn(spinner_name="dots"),
|
5010
|
+
TextColumn("[dim]{task.description}"),
|
5011
|
+
console=Console(file=sys.stderr),
|
5012
|
+
transient=True
|
5013
|
+
)
|
5014
|
+
thinking_live = Live(thinking_spinner, console=Console(file=sys.stderr), refresh_per_second=10)
|
5015
|
+
thinking_task = None
|
4849
5016
|
|
4850
|
-
|
4851
|
-
|
5017
|
+
# Start the spinner live display
|
5018
|
+
thinking_live.start()
|
4852
5019
|
|
4853
|
-
|
4854
|
-
|
4855
|
-
thinking_live.stop()
|
4856
|
-
thinking_live = None
|
4857
|
-
thinking_spinner = None
|
5020
|
+
async def handle_event_with_display(event: WebSocketEvent):
|
5021
|
+
nonlocal thinking_task
|
4858
5022
|
|
4859
5023
|
# Display event with enhanced formatting and emojis
|
4860
5024
|
formatted_event = event.format_for_display(self.debug)
|
4861
5025
|
timestamp = event.timestamp.strftime('%H:%M:%S')
|
4862
5026
|
safe_console_print(f"[{timestamp}] {formatted_event}")
|
4863
5027
|
|
4864
|
-
#
|
4865
|
-
if event.type
|
4866
|
-
|
4867
|
-
|
5028
|
+
# Update spinner for thinking and tool_executing events
|
5029
|
+
if event.type in ["agent_thinking", "tool_executing"]:
|
5030
|
+
# Remove old task if exists
|
5031
|
+
if thinking_task is not None:
|
5032
|
+
thinking_spinner.remove_task(thinking_task)
|
5033
|
+
thinking_task = None
|
4868
5034
|
|
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)
|
5035
|
+
# Add new task with latest event
|
5036
|
+
if event.type == "agent_thinking":
|
5037
|
+
thought = event.data.get('thought', event.data.get('reasoning', ''))
|
5038
|
+
spinner_text = truncate_with_ellipsis(thought, 60) if thought else "Processing..."
|
5039
|
+
thinking_task = thinking_spinner.add_task(f"💭 {spinner_text}", total=None)
|
5040
|
+
elif event.type == "tool_executing":
|
5041
|
+
tool_name = event.data.get('tool', event.data.get('tool_name', 'Unknown'))
|
5042
|
+
spinner_text = f"Executing {tool_name}..."
|
5043
|
+
thinking_task = thinking_spinner.add_task(f"🔧 {spinner_text}", total=None)
|
5044
|
+
|
5045
|
+
# Clear spinner for any other event type
|
5046
|
+
elif thinking_task is not None:
|
5047
|
+
thinking_spinner.remove_task(thinking_task)
|
5048
|
+
thinking_task = None
|
4893
5049
|
|
4894
5050
|
# Issue #2177: WebSocket event validation
|
4895
5051
|
if self.validate_events and self.event_validator:
|
@@ -4969,9 +5125,8 @@ class AgentCLI:
|
|
4969
5125
|
try:
|
4970
5126
|
await self.ws_client.receive_events(callback=handle_event_with_display)
|
4971
5127
|
finally:
|
4972
|
-
# Clean up spinner
|
4973
|
-
|
4974
|
-
thinking_live.stop()
|
5128
|
+
# Clean up spinner
|
5129
|
+
thinking_live.stop()
|
4975
5130
|
|
4976
5131
|
def _get_event_summary(self, event: WebSocketEvent) -> str:
|
4977
5132
|
"""ISSUE #1603 FIX: Get a concise summary of an event for display"""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|