netra-zen 1.2.0__py3-none-any.whl → 1.2.3__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.2.0.dist-info → netra_zen-1.2.3.dist-info}/METADATA +1 -1
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.3.dist-info}/RECORD +15 -9
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.3.dist-info}/top_level.txt +1 -0
- scripts/agent_cli.py +401 -109
- shared/README.md +47 -0
- shared/TIMING_FIX_COMPLETE.md +80 -0
- shared/__init__.py +12 -0
- shared/types/__init__.py +21 -0
- shared/types/websocket_closure_codes.py +124 -0
- shared/windows_encoding.py +45 -0
- zen/telemetry/embedded_credentials.py +2 -1
- zen_orchestrator.py +14 -14
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.3.dist-info}/WHEEL +0 -0
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.3.dist-info}/entry_points.txt +0 -0
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.3.dist-info}/licenses/LICENSE.md +0 -0
@@ -1,16 +1,22 @@
|
|
1
|
-
zen_orchestrator.py,sha256=
|
1
|
+
zen_orchestrator.py,sha256=BcXb5hucuOq6OVlfWP6055SZEyMNpvW9dWeHmpOifGc,156153
|
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.2.
|
4
|
+
netra_zen-1.2.3.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=SSsmoEzYhPfDpzHEtmTWf300kmd6_l5hL2EM3oygTuA,361760
|
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
|
11
11
|
scripts/embed_release_credentials.py,sha256=dEw3jP5Hvzbd0YUaXRZtiAZSpj315uVWIIjvwcK4g6Y,2103
|
12
12
|
scripts/test_apex_telemetry_debug.py,sha256=UG5pbOmna653S9q83qPjrDAjkLeb6utW8ruXpWYy3aQ,6896
|
13
13
|
scripts/verify_log_transmission.py,sha256=IZBeQos1rMHSdCyml_HL2x1XpLcpOuI0EUr0qA7mkE8,4493
|
14
|
+
shared/README.md,sha256=BmZEHkMCddPKifIL4kEec6WNeX3t4gMp0Fmz8BJ3fC8,2024
|
15
|
+
shared/TIMING_FIX_COMPLETE.md,sha256=KZUAF9OKFqYz7URs4unemNYYJXHFryhohc28c7P5250,3529
|
16
|
+
shared/__init__.py,sha256=oE8UsFia6La8PUhngnjYeJuLD18SiT31sYFMmdXRwaI,412
|
17
|
+
shared/windows_encoding.py,sha256=tAJ42LKsHKcbpRRKDYuRnPrxPovzwvkXjIavy1MNkcU,1395
|
18
|
+
shared/types/__init__.py,sha256=_Ost84d4g3lg7p-MV4yZYiXU-fmJHlNEK4A1KWSzS1A,544
|
19
|
+
shared/types/websocket_closure_codes.py,sha256=HY1UG_9tCK79PpI-b9sTmDiIrUtJsuDYwnEMlsuljmo,4429
|
14
20
|
token_budget/__init__.py,sha256=_2tmi72DGNtbYcZ-rGIxVKMytdkHFjzJaWz8bDhYACc,33
|
15
21
|
token_budget/budget_manager.py,sha256=VRWxKcGDtgJfIRh-ztYQ4-wuhBvddVJJnyoGfxCBlv0,9567
|
16
22
|
token_budget/models.py,sha256=14xFTk2-R1Ql0F9WLDof7vADrKC_5Fj7EE7UmZdoG00,2384
|
@@ -21,10 +27,10 @@ zen/__init__.py,sha256=lVT1vB_ohU1o9-WRPwVsI2jtXB9mb7WaIdfDRfYC1PE,213
|
|
21
27
|
zen/__main__.py,sha256=3Sex-j1o01NGoFnZOO9lPxcNX--M5VdIbjkJ_0z16qU,181
|
22
28
|
zen/telemetry/__init__.py,sha256=zyH7YcK_eWxwaQZW0MRefu1bX5hEgfRljr_dsqNU-tw,452
|
23
29
|
zen/telemetry/apex_telemetry.py,sha256=FAkiHXOuzZFL-51oYetTC4wbjCL5UH-qOghwcaaOVgE,9685
|
24
|
-
zen/telemetry/embedded_credentials.py,sha256=
|
30
|
+
zen/telemetry/embedded_credentials.py,sha256=bSU6XUOFQ7HP6TMOvuHfKz1Oo5v8pljneUjzwE8XprU,4941
|
25
31
|
zen/telemetry/manager.py,sha256=Rdbpnjbjel9xZEJyvLqy2M-4amvkr80abRiWnqHghIQ,9980
|
26
|
-
netra_zen-1.2.
|
27
|
-
netra_zen-1.2.
|
28
|
-
netra_zen-1.2.
|
29
|
-
netra_zen-1.2.
|
30
|
-
netra_zen-1.2.
|
32
|
+
netra_zen-1.2.3.dist-info/METADATA,sha256=VXUzXHtqJxTZNZEOoZi9nepmOjkPLE_-eEJDexwodE4,46283
|
33
|
+
netra_zen-1.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
34
|
+
netra_zen-1.2.3.dist-info/entry_points.txt,sha256=oDehCnPGZezG0m9ZWspxjHLHyQ3eERX87eojR4ljaRo,45
|
35
|
+
netra_zen-1.2.3.dist-info/top_level.txt,sha256=4TUOH4VJrzPxQU4XsvfaKC4_NdfZrFFsgQGbS_pMeX8,84
|
36
|
+
netra_zen-1.2.3.dist-info/RECORD,,
|
scripts/agent_cli.py
CHANGED
@@ -9,6 +9,7 @@ import json
|
|
9
9
|
import sys
|
10
10
|
import os
|
11
11
|
import argparse
|
12
|
+
import time
|
12
13
|
|
13
14
|
import logging
|
14
15
|
|
@@ -372,7 +373,7 @@ class DebugManager:
|
|
372
373
|
Provides comprehensive debugging capabilities with different verbosity levels
|
373
374
|
"""
|
374
375
|
|
375
|
-
def __init__(self, debug_level: DebugLevel = DebugLevel.
|
376
|
+
def __init__(self, debug_level: DebugLevel = DebugLevel.SILENT, log_file: Optional[Path] = None,
|
376
377
|
enable_websocket_diagnostics: bool = False, json_mode: bool = False, ci_mode: bool = False):
|
377
378
|
self.debug_level = debug_level
|
378
379
|
self.log_file = log_file or Path.home() / ".netra" / "cli_debug.log"
|
@@ -1628,7 +1629,7 @@ class Config:
|
|
1628
1629
|
log_level: str = "INFO"
|
1629
1630
|
timeout: int = 30
|
1630
1631
|
default_email: str = "test@netra.ai"
|
1631
|
-
debug_level: DebugLevel = DebugLevel.
|
1632
|
+
debug_level: DebugLevel = DebugLevel.SILENT
|
1632
1633
|
debug_log_file: Optional[Path] = None
|
1633
1634
|
stream_logs: bool = False # Issue #1828: Stream backend logs via WebSocket
|
1634
1635
|
enable_websocket_diagnostics: bool = False # Issue #2478: Enhanced WebSocket error diagnostics
|
@@ -2602,7 +2603,7 @@ class WebSocketClient:
|
|
2602
2603
|
def __init__(self, config: Config, token: str, debug_manager: Optional[DebugManager] = None,
|
2603
2604
|
send_logs: bool = False, logs_count: int = 1, logs_project: Optional[str] = None,
|
2604
2605
|
logs_path: Optional[str] = None, logs_user: Optional[str] = None,
|
2605
|
-
handshake_timeout: float =
|
2606
|
+
handshake_timeout: float = 10.0):
|
2606
2607
|
self.config = config
|
2607
2608
|
self.token = token
|
2608
2609
|
self.debug = debug_manager or DebugManager(config.debug_level, config.debug_log_file, config.enable_websocket_diagnostics)
|
@@ -2643,6 +2644,11 @@ class WebSocketClient:
|
|
2643
2644
|
self.closure_reason: Optional[str] = None
|
2644
2645
|
self.closure_category: Optional[WebSocketClosureCategory] = None
|
2645
2646
|
|
2647
|
+
# Event queuing mechanism: Wait for BOTH handshake AND connection_established
|
2648
|
+
self.connection_established_received = False
|
2649
|
+
self.event_queue: List[Dict[str, Any]] = []
|
2650
|
+
self.ready_to_send_events = False
|
2651
|
+
|
2646
2652
|
def _initialize_timeouts(self) -> None:
|
2647
2653
|
"""Initialize WebSocket timeouts using backend timeout configuration.
|
2648
2654
|
|
@@ -2863,16 +2869,16 @@ class WebSocketClient:
|
|
2863
2869
|
for method_name, method in methods:
|
2864
2870
|
try:
|
2865
2871
|
self.debug.debug_print(
|
2866
|
-
f"
|
2867
|
-
DebugLevel.
|
2872
|
+
f"Initiating WebSocket auth via {method_name}",
|
2873
|
+
DebugLevel.VERBOSE,
|
2868
2874
|
style="cyan"
|
2869
2875
|
)
|
2870
2876
|
self.debug.log_connection_attempt(method_name, self.config.ws_url)
|
2871
2877
|
if await method():
|
2872
2878
|
self.debug.log_connection_attempt(method_name, self.config.ws_url, success=True)
|
2873
2879
|
self.debug.debug_print(
|
2874
|
-
f"
|
2875
|
-
DebugLevel.
|
2880
|
+
f"WebSocket connected using {method_name}",
|
2881
|
+
DebugLevel.VERBOSE,
|
2876
2882
|
style="green"
|
2877
2883
|
)
|
2878
2884
|
|
@@ -2881,14 +2887,35 @@ class WebSocketClient:
|
|
2881
2887
|
if handshake_success:
|
2882
2888
|
safe_console_print(f"✅ Connected with thread ID: {self.current_thread_id}", style="green")
|
2883
2889
|
self.connected = True
|
2890
|
+
|
2891
|
+
# Start listening for events in background immediately
|
2892
|
+
asyncio.create_task(self.receive_events())
|
2893
|
+
|
2894
|
+
# Wait for connection_established event
|
2895
|
+
wait_start = asyncio.get_event_loop().time()
|
2896
|
+
timeout = 5.0
|
2897
|
+
while not self.connection_established_received:
|
2898
|
+
if (asyncio.get_event_loop().time() - wait_start) > timeout:
|
2899
|
+
self.debug.debug_print(
|
2900
|
+
f"⚠️ Timeout waiting for connection_established after {timeout}s",
|
2901
|
+
DebugLevel.BASIC,
|
2902
|
+
style="yellow"
|
2903
|
+
)
|
2904
|
+
break
|
2905
|
+
await asyncio.sleep(0.1)
|
2906
|
+
|
2907
|
+
# Set ready flag if we got the event
|
2908
|
+
if self.connection_established_received:
|
2909
|
+
self.ready_to_send_events = True
|
2910
|
+
self.debug.debug_print(
|
2911
|
+
"✅ Connection fully established - ready to send events",
|
2912
|
+
DebugLevel.BASIC,
|
2913
|
+
style="green"
|
2914
|
+
)
|
2915
|
+
|
2884
2916
|
return True
|
2885
2917
|
else:
|
2886
2918
|
# Handshake failed - server not ready to process messages
|
2887
|
-
self.debug.debug_print(
|
2888
|
-
"WARNING: Server handshake not completed - server not ready",
|
2889
|
-
DebugLevel.BASIC,
|
2890
|
-
style="yellow"
|
2891
|
-
)
|
2892
2919
|
self.debug.debug_print(
|
2893
2920
|
"Server still completing lifecycle phases (Initialize → Authenticate → Handshake → Prepare → Processing)",
|
2894
2921
|
DebugLevel.VERBOSE,
|
@@ -2911,6 +2938,32 @@ class WebSocketClient:
|
|
2911
2938
|
safe_console_print(f"✅ Connected with thread ID: {self.current_thread_id}", style="green",
|
2912
2939
|
json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
2913
2940
|
self.connected = True
|
2941
|
+
|
2942
|
+
# Start listening for events in background immediately
|
2943
|
+
asyncio.create_task(self.receive_events())
|
2944
|
+
|
2945
|
+
# Wait for connection_established event
|
2946
|
+
wait_start = asyncio.get_event_loop().time()
|
2947
|
+
timeout = 5.0
|
2948
|
+
while not self.connection_established_received:
|
2949
|
+
if (asyncio.get_event_loop().time() - wait_start) > timeout:
|
2950
|
+
self.debug.debug_print(
|
2951
|
+
f"⚠️ Timeout waiting for connection_established after {timeout}s",
|
2952
|
+
DebugLevel.BASIC,
|
2953
|
+
style="yellow"
|
2954
|
+
)
|
2955
|
+
break
|
2956
|
+
await asyncio.sleep(0.1)
|
2957
|
+
|
2958
|
+
# Set ready flag if we got the event
|
2959
|
+
if self.connection_established_received:
|
2960
|
+
self.ready_to_send_events = True
|
2961
|
+
self.debug.debug_print(
|
2962
|
+
"✅ Connection fully established - ready to send events",
|
2963
|
+
DebugLevel.BASIC,
|
2964
|
+
style="green"
|
2965
|
+
)
|
2966
|
+
|
2914
2967
|
return True
|
2915
2968
|
else:
|
2916
2969
|
# Server still not ready - fail the connection
|
@@ -2929,7 +2982,7 @@ class WebSocketClient:
|
|
2929
2982
|
except Exception as e:
|
2930
2983
|
self.debug.log_connection_attempt(method_name, self.config.ws_url, success=False, error=str(e))
|
2931
2984
|
self.debug.debug_print(
|
2932
|
-
f"
|
2985
|
+
f"WebSocket authentication via {method_name} failed with {type(e).__name__}: {e}",
|
2933
2986
|
DebugLevel.BASIC,
|
2934
2987
|
style="red"
|
2935
2988
|
)
|
@@ -3133,19 +3186,51 @@ class WebSocketClient:
|
|
3133
3186
|
style="green"
|
3134
3187
|
)
|
3135
3188
|
|
3189
|
+
# Check if we can start sending events
|
3190
|
+
# We need BOTH handshake AND connection_established
|
3191
|
+
if self.connection_established_received:
|
3192
|
+
self.ready_to_send_events = True
|
3193
|
+
self.debug.debug_print(
|
3194
|
+
"✅ Ready to send events: Handshake ✓ | connection_established ✓",
|
3195
|
+
DebugLevel.BASIC,
|
3196
|
+
style="green"
|
3197
|
+
)
|
3198
|
+
safe_console_print(
|
3199
|
+
"✅ Fully connected: Handshake ✓ | connection_established ✓",
|
3200
|
+
style="green",
|
3201
|
+
json_mode=self.config.json_mode,
|
3202
|
+
ci_mode=self.config.ci_mode
|
3203
|
+
)
|
3204
|
+
else:
|
3205
|
+
self.debug.debug_print(
|
3206
|
+
"⏳ Handshake complete, waiting for connection_established event",
|
3207
|
+
DebugLevel.BASIC,
|
3208
|
+
style="yellow"
|
3209
|
+
)
|
3210
|
+
|
3136
3211
|
return result
|
3137
3212
|
|
3138
3213
|
# Handle other message types while waiting
|
3139
3214
|
elif msg_type == 'connection_established':
|
3140
3215
|
# This is from AUTHENTICATING phase completion
|
3141
3216
|
self.debug.debug_print(
|
3142
|
-
"Connection established (AUTH phase) - waiting for HANDSHAKING phase",
|
3217
|
+
"✓ Connection established (AUTH phase) - waiting for HANDSHAKING phase",
|
3143
3218
|
DebugLevel.VERBOSE,
|
3144
3219
|
style="yellow"
|
3145
3220
|
)
|
3221
|
+
# Track that connection_established was received
|
3222
|
+
self.connection_established_received = True
|
3223
|
+
self.debug.debug_print(
|
3224
|
+
"📌 connection_established event received during handshake",
|
3225
|
+
DebugLevel.BASIC,
|
3226
|
+
style="cyan"
|
3227
|
+
)
|
3146
3228
|
# Store the event but continue waiting
|
3147
3229
|
if hasattr(self, 'events'):
|
3148
|
-
self.events.append(WebSocketEvent
|
3230
|
+
self.events.append(WebSocketEvent(
|
3231
|
+
type=response.get('type', 'unknown'),
|
3232
|
+
data=response
|
3233
|
+
))
|
3149
3234
|
|
3150
3235
|
else:
|
3151
3236
|
# Other event types - store but keep waiting for handshake
|
@@ -3155,20 +3240,18 @@ class WebSocketClient:
|
|
3155
3240
|
style="dim"
|
3156
3241
|
)
|
3157
3242
|
if hasattr(self, 'events'):
|
3158
|
-
self.events.append(WebSocketEvent
|
3243
|
+
self.events.append(WebSocketEvent(
|
3244
|
+
type=response.get('type', 'unknown'),
|
3245
|
+
data=response
|
3246
|
+
))
|
3159
3247
|
|
3160
3248
|
except asyncio.TimeoutError:
|
3161
3249
|
pass # Fall through to timeout handling below
|
3162
3250
|
|
3163
3251
|
# Timeout - server didn't send handshake
|
3164
3252
|
self.debug.debug_print(
|
3165
|
-
"
|
3166
|
-
DebugLevel.
|
3167
|
-
style="red"
|
3168
|
-
)
|
3169
|
-
self.debug.debug_print(
|
3170
|
-
"Server may not have HANDSHAKING phase (pre-2025-10-09 version)",
|
3171
|
-
DebugLevel.BASIC,
|
3253
|
+
"Server did not send handshake - may be using pre-2025-10-09 version without HANDSHAKING phase",
|
3254
|
+
DebugLevel.VERBOSE,
|
3172
3255
|
style="yellow"
|
3173
3256
|
)
|
3174
3257
|
return False
|
@@ -3695,6 +3778,101 @@ class WebSocketClient:
|
|
3695
3778
|
|
3696
3779
|
return False
|
3697
3780
|
|
3781
|
+
async def _flush_queued_events(self) -> None:
|
3782
|
+
"""
|
3783
|
+
Flush all queued events after connection_established is received.
|
3784
|
+
|
3785
|
+
This method sends all events that were queued while waiting for
|
3786
|
+
the connection_established event (which comes after handshake).
|
3787
|
+
"""
|
3788
|
+
if not self.event_queue:
|
3789
|
+
self.debug.debug_print(
|
3790
|
+
"No queued events to flush",
|
3791
|
+
DebugLevel.VERBOSE,
|
3792
|
+
style="dim"
|
3793
|
+
)
|
3794
|
+
return
|
3795
|
+
|
3796
|
+
queue_size = len(self.event_queue)
|
3797
|
+
self.debug.debug_print(
|
3798
|
+
f"🚀 Flushing {queue_size} queued event(s) after connection_established",
|
3799
|
+
DebugLevel.BASIC,
|
3800
|
+
style="green"
|
3801
|
+
)
|
3802
|
+
safe_console_print(
|
3803
|
+
f"🚀 Connection fully established! Sending {queue_size} queued event(s)...",
|
3804
|
+
style="green",
|
3805
|
+
json_mode=self.config.json_mode,
|
3806
|
+
ci_mode=self.config.ci_mode
|
3807
|
+
)
|
3808
|
+
|
3809
|
+
# Send all queued events
|
3810
|
+
for i, event_payload in enumerate(self.event_queue, 1):
|
3811
|
+
try:
|
3812
|
+
self.debug.debug_print(
|
3813
|
+
f"Sending queued event {i}/{queue_size}: {event_payload.get('type', 'unknown')}",
|
3814
|
+
DebugLevel.VERBOSE,
|
3815
|
+
style="cyan"
|
3816
|
+
)
|
3817
|
+
await self.ws.send(json.dumps(event_payload))
|
3818
|
+
self.debug.debug_print(
|
3819
|
+
f"✓ Queued event {i}/{queue_size} sent successfully",
|
3820
|
+
DebugLevel.VERBOSE,
|
3821
|
+
style="green"
|
3822
|
+
)
|
3823
|
+
except Exception as e:
|
3824
|
+
self.debug.log_error(e, f"sending queued event {i}/{queue_size}")
|
3825
|
+
safe_console_print(
|
3826
|
+
f"⚠️ Failed to send queued event {i}/{queue_size}: {e}",
|
3827
|
+
style="yellow",
|
3828
|
+
json_mode=self.config.json_mode,
|
3829
|
+
ci_mode=self.config.ci_mode
|
3830
|
+
)
|
3831
|
+
|
3832
|
+
# Clear the queue
|
3833
|
+
self.event_queue.clear()
|
3834
|
+
self.debug.debug_print(
|
3835
|
+
f"✅ All {queue_size} queued events have been sent",
|
3836
|
+
DebugLevel.BASIC,
|
3837
|
+
style="green"
|
3838
|
+
)
|
3839
|
+
|
3840
|
+
def _display_log_collection_info(self, info: dict) -> None:
|
3841
|
+
"""Display log collection information to the console"""
|
3842
|
+
separator = "=" * 60
|
3843
|
+
|
3844
|
+
# Display log collection details
|
3845
|
+
safe_console_print("", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3846
|
+
safe_console_print(separator, style="cyan", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3847
|
+
safe_console_print("SENDING LOGS TO OPTIMIZER", style="bold cyan", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3848
|
+
safe_console_print(separator, style="cyan", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3849
|
+
safe_console_print(f" Total Entries: {len(info['logs'])}", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3850
|
+
safe_console_print(f" Files Read: {info['files_read']}", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3851
|
+
safe_console_print(f" Payload Size: {info['size_str']}", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3852
|
+
|
3853
|
+
if self.logs_project:
|
3854
|
+
safe_console_print(f" Project: {self.logs_project}", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3855
|
+
|
3856
|
+
safe_console_print("", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3857
|
+
safe_console_print(" Files:", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3858
|
+
|
3859
|
+
# Add file details with hashes
|
3860
|
+
for file_info in info['file_info']:
|
3861
|
+
safe_console_print(
|
3862
|
+
f" * {file_info['name']} (hash: {file_info['hash']}, {file_info['entries']} entries)",
|
3863
|
+
json_mode=self.config.json_mode, ci_mode=self.config.ci_mode
|
3864
|
+
)
|
3865
|
+
|
3866
|
+
# Add payload proof
|
3867
|
+
safe_console_print("", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3868
|
+
safe_console_print(" Payload Confirmation:", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3869
|
+
safe_console_print(f" [OK] 'jsonl_logs' key added to payload", style="green", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3870
|
+
safe_console_print(f" [OK] First log entry timestamp: {info['logs'][0].get('timestamp', 'N/A') if info['logs'] else 'N/A'}", style="green", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3871
|
+
safe_console_print(f" [OK] Last log entry timestamp: {info['logs'][-1].get('timestamp', 'N/A') if info['logs'] else 'N/A'}", style="green", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3872
|
+
|
3873
|
+
safe_console_print(separator, style="cyan", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3874
|
+
safe_console_print("", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
3875
|
+
|
3698
3876
|
async def send_message(self, message: str) -> str:
|
3699
3877
|
"""Send a message and return the run_id"""
|
3700
3878
|
if not self.ws:
|
@@ -3721,6 +3899,75 @@ class WebSocketClient:
|
|
3721
3899
|
)
|
3722
3900
|
raise RuntimeError("Connection not ready - handshake incomplete. Server needs to reach Processing phase.")
|
3723
3901
|
|
3902
|
+
# NEW: Check if ready to send events
|
3903
|
+
# Events can only be sent AFTER both handshake AND connection_established
|
3904
|
+
if not self.ready_to_send_events:
|
3905
|
+
# Wait for connection_established event with timeout
|
3906
|
+
self.debug.debug_print(
|
3907
|
+
"⏳ Waiting for connection_established event before sending message...",
|
3908
|
+
DebugLevel.BASIC,
|
3909
|
+
style="yellow"
|
3910
|
+
)
|
3911
|
+
safe_console_print(
|
3912
|
+
"⏳ Waiting for connection_established event (up to 5 seconds)...",
|
3913
|
+
style="yellow",
|
3914
|
+
json_mode=self.config.json_mode,
|
3915
|
+
ci_mode=self.config.ci_mode
|
3916
|
+
)
|
3917
|
+
|
3918
|
+
# Wait up to 30 seconds for connection_established
|
3919
|
+
wait_timeout = 30.0
|
3920
|
+
wait_start = time.time()
|
3921
|
+
wait_interval = 0.1
|
3922
|
+
|
3923
|
+
while not self.ready_to_send_events and (time.time() - wait_start) < wait_timeout:
|
3924
|
+
await asyncio.sleep(wait_interval)
|
3925
|
+
# Check if connection_established arrived
|
3926
|
+
if self.ready_to_send_events:
|
3927
|
+
self.debug.debug_print(
|
3928
|
+
"✅ connection_established received - ready to send events",
|
3929
|
+
DebugLevel.BASIC,
|
3930
|
+
style="green"
|
3931
|
+
)
|
3932
|
+
safe_console_print(
|
3933
|
+
"✅ connection_established received - proceeding with message",
|
3934
|
+
style="green",
|
3935
|
+
json_mode=self.config.json_mode,
|
3936
|
+
ci_mode=self.config.ci_mode
|
3937
|
+
)
|
3938
|
+
break
|
3939
|
+
|
3940
|
+
# If still not ready after timeout, then error
|
3941
|
+
if not self.ready_to_send_events:
|
3942
|
+
elapsed = time.time() - wait_start
|
3943
|
+
self.debug.debug_print(
|
3944
|
+
f"❌ Timeout waiting for connection_established after {elapsed:.1f} seconds",
|
3945
|
+
DebugLevel.BASIC,
|
3946
|
+
style="red"
|
3947
|
+
)
|
3948
|
+
safe_console_print(
|
3949
|
+
f"❌ Timeout: connection_established event not received after {elapsed:.1f} seconds",
|
3950
|
+
style="red",
|
3951
|
+
json_mode=self.config.json_mode,
|
3952
|
+
ci_mode=self.config.ci_mode
|
3953
|
+
)
|
3954
|
+
safe_console_print(
|
3955
|
+
" Handshake complete: ✓ | connection_established: ✗",
|
3956
|
+
style="dim",
|
3957
|
+
json_mode=self.config.json_mode,
|
3958
|
+
ci_mode=self.config.ci_mode
|
3959
|
+
)
|
3960
|
+
safe_console_print(
|
3961
|
+
"\n💡 The server may not be sending the connection_established event.",
|
3962
|
+
style="yellow",
|
3963
|
+
json_mode=self.config.json_mode,
|
3964
|
+
ci_mode=self.config.ci_mode
|
3965
|
+
)
|
3966
|
+
raise RuntimeError(
|
3967
|
+
f"Cannot send message - connection_established event not received after {elapsed:.1f}s timeout. "
|
3968
|
+
"Both handshake and connection_established are required before sending events."
|
3969
|
+
)
|
3970
|
+
|
3724
3971
|
# Generate run_id
|
3725
3972
|
self.run_id = f"cli_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{os.getpid()}"
|
3726
3973
|
|
@@ -3843,51 +4090,13 @@ class WebSocketClient:
|
|
3843
4090
|
else:
|
3844
4091
|
size_str = f"{logs_size_bytes} bytes"
|
3845
4092
|
|
3846
|
-
#
|
3847
|
-
|
3848
|
-
|
3849
|
-
|
3850
|
-
|
3851
|
-
|
3852
|
-
|
3853
|
-
f" Total Entries: {len(logs)}",
|
3854
|
-
f" Files Read: {files_read}",
|
3855
|
-
f" Payload Size: {size_str}",
|
3856
|
-
]
|
3857
|
-
|
3858
|
-
if self.logs_project:
|
3859
|
-
log_msg_parts.append(f" Project: {self.logs_project}")
|
3860
|
-
|
3861
|
-
log_msg_parts.append("")
|
3862
|
-
log_msg_parts.append(" Files:")
|
3863
|
-
|
3864
|
-
# Add file details with hashes
|
3865
|
-
for info in file_info:
|
3866
|
-
log_msg_parts.append(
|
3867
|
-
f" • {info['name']} (hash: {info['hash']}, {info['entries']} entries)"
|
3868
|
-
)
|
3869
|
-
|
3870
|
-
# Add payload proof
|
3871
|
-
log_msg_parts.append("")
|
3872
|
-
log_msg_parts.append(" Payload Confirmation:")
|
3873
|
-
log_msg_parts.append(f" ✓ 'jsonl_logs' key added to payload")
|
3874
|
-
log_msg_parts.append(f" ✓ First log entry timestamp: {logs[0].get('timestamp', 'N/A') if logs else 'N/A'}")
|
3875
|
-
log_msg_parts.append(f" ✓ Last log entry timestamp: {logs[-1].get('timestamp', 'N/A') if logs else 'N/A'}")
|
3876
|
-
|
3877
|
-
log_msg_parts.append(separator)
|
3878
|
-
log_msg_parts.append("")
|
3879
|
-
|
3880
|
-
log_msg = "\n".join(log_msg_parts)
|
3881
|
-
|
3882
|
-
# Log at INFO level
|
3883
|
-
logging.info(log_msg)
|
3884
|
-
|
3885
|
-
# Also print via debug system for consistency
|
3886
|
-
self.debug.debug_print(
|
3887
|
-
log_msg,
|
3888
|
-
DebugLevel.BASIC,
|
3889
|
-
style="cyan"
|
3890
|
-
)
|
4093
|
+
# Save log display info for later (will be displayed after "Sending message:")
|
4094
|
+
self._log_display_info = {
|
4095
|
+
'logs': logs,
|
4096
|
+
'files_read': files_read,
|
4097
|
+
'file_info': file_info,
|
4098
|
+
'size_str': size_str
|
4099
|
+
}
|
3891
4100
|
else:
|
3892
4101
|
self.debug.debug_print(
|
3893
4102
|
"Warning: --send-logs enabled but no logs found",
|
@@ -3903,7 +4112,7 @@ class WebSocketClient:
|
|
3903
4112
|
)
|
3904
4113
|
|
3905
4114
|
self.debug.debug_print(
|
3906
|
-
f"
|
4115
|
+
f"Prepared WebSocket payload for run_id={self.run_id}, thread_id={thread_id}",
|
3907
4116
|
DebugLevel.BASIC,
|
3908
4117
|
style="yellow"
|
3909
4118
|
)
|
@@ -4039,6 +4248,15 @@ class WebSocketClient:
|
|
4039
4248
|
if self.debug.debug_level >= DebugLevel.DIAGNOSTIC:
|
4040
4249
|
self.debug.debug_print(f"SENDING WEBSOCKET MESSAGE: {json.dumps(payload, indent=2)}", DebugLevel.DIAGNOSTIC)
|
4041
4250
|
|
4251
|
+
# Print sending message after logs section (moved from run_cli method)
|
4252
|
+
safe_console_print(f"Sending message: {payload['payload']['content']}", style="cyan", json_mode=self.config.json_mode, ci_mode=self.config.ci_mode)
|
4253
|
+
|
4254
|
+
# Display log collection info after "Sending message:" if logs were collected
|
4255
|
+
if self.send_logs and hasattr(self, '_log_display_info'):
|
4256
|
+
self._display_log_collection_info(self._log_display_info)
|
4257
|
+
# Clean up the temporary display info
|
4258
|
+
del self._log_display_info
|
4259
|
+
|
4042
4260
|
await self.ws.send(payload_json)
|
4043
4261
|
if self.debug.debug_level >= DebugLevel.VERBOSE:
|
4044
4262
|
self.debug.debug_print(f"WEBSOCKET MESSAGE SENT SUCCESSFULLY - run_id: {self.run_id}, thread_id: {thread_id}", DebugLevel.VERBOSE)
|
@@ -4051,7 +4269,7 @@ class WebSocketClient:
|
|
4051
4269
|
|
4052
4270
|
self.debug.debug_print("Listening for WebSocket events...")
|
4053
4271
|
self.debug.debug_print(
|
4054
|
-
"
|
4272
|
+
"Event listener started after successful connection",
|
4055
4273
|
DebugLevel.BASIC,
|
4056
4274
|
style="cyan"
|
4057
4275
|
)
|
@@ -4080,15 +4298,43 @@ class WebSocketClient:
|
|
4080
4298
|
# Handle connection_established as a basic WebSocket connection event
|
4081
4299
|
# Note: handshake_response is now used for thread_id exchange, not connection_established
|
4082
4300
|
if event.type == 'connection_established':
|
4083
|
-
#
|
4084
|
-
self.
|
4085
|
-
|
4086
|
-
|
4087
|
-
|
4088
|
-
|
4301
|
+
# Track that connection_established was received
|
4302
|
+
if not self.connection_established_received:
|
4303
|
+
self.connection_established_received = True
|
4304
|
+
self.debug.debug_print(
|
4305
|
+
"📌 connection_established event received after handshake",
|
4306
|
+
DebugLevel.BASIC,
|
4307
|
+
style="cyan"
|
4308
|
+
)
|
4309
|
+
|
4310
|
+
# Check if we can now start sending events
|
4311
|
+
# We need BOTH handshake (self.connected) AND connection_established
|
4312
|
+
if self.connected and not self.ready_to_send_events:
|
4313
|
+
self.ready_to_send_events = True
|
4314
|
+
self.debug.debug_print(
|
4315
|
+
"✅ Ready to send events: Handshake ✓ | connection_established ✓",
|
4316
|
+
DebugLevel.BASIC,
|
4317
|
+
style="green"
|
4318
|
+
)
|
4319
|
+
safe_console_print(
|
4320
|
+
"✅ Fully connected: Handshake ✓ | connection_established ✓",
|
4321
|
+
style="green",
|
4322
|
+
json_mode=self.config.json_mode,
|
4323
|
+
ci_mode=self.config.ci_mode
|
4324
|
+
)
|
4325
|
+
|
4326
|
+
# Flush any queued events
|
4327
|
+
await self._flush_queued_events()
|
4328
|
+
else:
|
4329
|
+
# This is a duplicate connection_established event
|
4330
|
+
self.debug.debug_print(
|
4331
|
+
f"WebSocket connection_established event received (duplicate, already tracked)",
|
4332
|
+
DebugLevel.VERBOSE,
|
4333
|
+
style="dim"
|
4334
|
+
)
|
4089
4335
|
|
4090
4336
|
self.debug.debug_print(
|
4091
|
-
f"
|
4337
|
+
f"Parsed WebSocket event type={event.type}",
|
4092
4338
|
DebugLevel.BASIC,
|
4093
4339
|
style="green"
|
4094
4340
|
)
|
@@ -4758,7 +5004,7 @@ class AgentCLI:
|
|
4758
5004
|
json_mode: bool = False, ci_mode: bool = False, json_output_file: Optional[str] = None,
|
4759
5005
|
send_logs: bool = False, logs_count: int = 1, logs_project: Optional[str] = None,
|
4760
5006
|
logs_path: Optional[str] = None, logs_user: Optional[str] = None,
|
4761
|
-
handshake_timeout: float =
|
5007
|
+
handshake_timeout: float = 10.0):
|
4762
5008
|
# ISSUE #2766: Store JSON/CI mode flags early for output suppression
|
4763
5009
|
self.json_mode = json_mode
|
4764
5010
|
self.ci_mode = ci_mode
|
@@ -4966,8 +5212,8 @@ class AgentCLI:
|
|
4966
5212
|
formatted_event = event.format_for_display(self.debug)
|
4967
5213
|
safe_console_print(f"[{event.timestamp.strftime('%H:%M:%S')}] {formatted_event}")
|
4968
5214
|
|
4969
|
-
# Update spinner for thinking and tool_executing events
|
4970
|
-
if event.type in ["agent_thinking", "tool_executing"]:
|
5215
|
+
# Update spinner for thinking and tool_executing events (if spinner is enabled)
|
5216
|
+
if spinner_enabled and event.type in ["agent_thinking", "tool_executing"]:
|
4971
5217
|
# Remove old task if exists
|
4972
5218
|
if thinking_task is not None:
|
4973
5219
|
thinking_spinner.remove_task(thinking_task)
|
@@ -4983,8 +5229,8 @@ class AgentCLI:
|
|
4983
5229
|
spinner_text = f"Executing {tool_name}..."
|
4984
5230
|
thinking_task = thinking_spinner.add_task(f"🔧 {spinner_text}", total=None)
|
4985
5231
|
|
4986
|
-
# Clear spinner for any other event type
|
4987
|
-
elif thinking_task is not None:
|
5232
|
+
# Clear spinner for any other event type (if spinner is enabled)
|
5233
|
+
elif spinner_enabled and thinking_task is not None:
|
4988
5234
|
thinking_spinner.remove_task(thinking_task)
|
4989
5235
|
thinking_task = None
|
4990
5236
|
|
@@ -5004,29 +5250,47 @@ class AgentCLI:
|
|
5004
5250
|
|
5005
5251
|
async def _receive_events_with_display(self):
|
5006
5252
|
"""ISSUE #1603 FIX: Enhanced event receiver with better display for single message mode"""
|
5253
|
+
# Debug: Confirm function is called
|
5254
|
+
if self.debug.debug_level >= DebugLevel.SILENT:
|
5255
|
+
safe_console_print("⏳ Waiting for agent response...", style="dim", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5256
|
+
|
5007
5257
|
# Create persistent spinner that stays at bottom
|
5008
|
-
|
5009
|
-
|
5010
|
-
|
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)
|
5258
|
+
# Note: Using Console() without file parameter for better Windows compatibility
|
5259
|
+
thinking_spinner = None
|
5260
|
+
thinking_live = None
|
5015
5261
|
thinking_task = None
|
5262
|
+
spinner_enabled = False
|
5263
|
+
event_count = 0 # Track number of events received
|
5264
|
+
last_event_index = len(self.ws_client.events) # Track where we are in the event list
|
5016
5265
|
|
5017
|
-
|
5018
|
-
|
5266
|
+
try:
|
5267
|
+
thinking_spinner = Progress(
|
5268
|
+
SpinnerColumn(spinner_name="dots"),
|
5269
|
+
TextColumn("[dim]{task.description}"),
|
5270
|
+
console=Console(),
|
5271
|
+
transient=True
|
5272
|
+
)
|
5273
|
+
thinking_live = Live(thinking_spinner, console=Console(), refresh_per_second=10)
|
5274
|
+
# Start the spinner live display
|
5275
|
+
thinking_live.start()
|
5276
|
+
spinner_enabled = True
|
5277
|
+
except Exception as e:
|
5278
|
+
# Spinner failed to start (common on Windows), continue without it
|
5279
|
+
if self.debug.debug_level >= DebugLevel.SILENT:
|
5280
|
+
safe_console_print(f"Note: Live spinner disabled ({str(e)[:50]})", style="dim yellow", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5281
|
+
spinner_enabled = False
|
5019
5282
|
|
5020
|
-
|
5021
|
-
nonlocal thinking_task
|
5283
|
+
def handle_event_with_display(event: WebSocketEvent):
|
5284
|
+
nonlocal thinking_task, event_count
|
5285
|
+
event_count += 1
|
5022
5286
|
|
5023
5287
|
# Display event with enhanced formatting and emojis
|
5024
5288
|
formatted_event = event.format_for_display(self.debug)
|
5025
5289
|
timestamp = event.timestamp.strftime('%H:%M:%S')
|
5026
|
-
safe_console_print(f"[{timestamp}] {formatted_event}")
|
5290
|
+
safe_console_print(f"[{timestamp}] {formatted_event}", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5027
5291
|
|
5028
|
-
# Update spinner for thinking and tool_executing events
|
5029
|
-
if event.type in ["agent_thinking", "tool_executing"]:
|
5292
|
+
# Update spinner for thinking and tool_executing events (if spinner is enabled)
|
5293
|
+
if spinner_enabled and event.type in ["agent_thinking", "tool_executing"]:
|
5030
5294
|
# Remove old task if exists
|
5031
5295
|
if thinking_task is not None:
|
5032
5296
|
thinking_spinner.remove_task(thinking_task)
|
@@ -5042,8 +5306,8 @@ class AgentCLI:
|
|
5042
5306
|
spinner_text = f"Executing {tool_name}..."
|
5043
5307
|
thinking_task = thinking_spinner.add_task(f"🔧 {spinner_text}", total=None)
|
5044
5308
|
|
5045
|
-
# Clear spinner for any other event type
|
5046
|
-
elif thinking_task is not None:
|
5309
|
+
# Clear spinner for any other event type (if spinner is enabled)
|
5310
|
+
elif spinner_enabled and thinking_task is not None:
|
5047
5311
|
thinking_spinner.remove_task(thinking_task)
|
5048
5312
|
thinking_task = None
|
5049
5313
|
|
@@ -5067,15 +5331,15 @@ class AgentCLI:
|
|
5067
5331
|
if event.type == "agent_thinking":
|
5068
5332
|
thought = event.data.get('thought', event.data.get('reasoning', ''))
|
5069
5333
|
if thought and len(thought) > 100:
|
5070
|
-
safe_console_print(f"💭 Full thought: {thought}", style="dim cyan")
|
5334
|
+
safe_console_print(f"💭 Full thought: {thought}", style="dim cyan", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5071
5335
|
elif event.type == "tool_executing":
|
5072
5336
|
tool_input = event.data.get('input', event.data.get('parameters', {}))
|
5073
5337
|
if tool_input:
|
5074
|
-
safe_console_print(f"📥 Tool input: {json.dumps(tool_input, indent=2)[:200]}...", style="dim blue")
|
5338
|
+
safe_console_print(f"📥 Tool input: {json.dumps(tool_input, indent=2)[:200]}...", style="dim blue", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5075
5339
|
elif event.type == "tool_completed":
|
5076
5340
|
tool_output = event.data.get('output', event.data.get('result', ''))
|
5077
5341
|
if tool_output:
|
5078
|
-
safe_console_print(f"📤 Tool output: {str(tool_output)[:200]}...", style="dim green")
|
5342
|
+
safe_console_print(f"📤 Tool output: {str(tool_output)[:200]}...", style="dim green", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5079
5343
|
elif event.type == "agent_completed":
|
5080
5344
|
# Prefer structured result payloads but fall back to legacy keys
|
5081
5345
|
result = (
|
@@ -5120,13 +5384,41 @@ class AgentCLI:
|
|
5120
5384
|
Syntax(json.dumps(event.data, indent=2), "json", word_wrap=True),
|
5121
5385
|
title=f"Event: {event.type}",
|
5122
5386
|
border_style="dim"
|
5123
|
-
))
|
5387
|
+
), json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5124
5388
|
|
5125
5389
|
try:
|
5126
|
-
|
5390
|
+
# Debug: Track if monitoring events
|
5391
|
+
if self.debug.debug_level >= DebugLevel.SILENT:
|
5392
|
+
safe_console_print("📡 Monitoring events from server...", style="dim cyan", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5393
|
+
|
5394
|
+
# Poll for new events from the existing event stream
|
5395
|
+
# The WebSocketClient already has a background task receiving events
|
5396
|
+
while True:
|
5397
|
+
# Check for new events
|
5398
|
+
while last_event_index < len(self.ws_client.events):
|
5399
|
+
event = self.ws_client.events[last_event_index]
|
5400
|
+
last_event_index += 1
|
5401
|
+
handle_event_with_display(event)
|
5402
|
+
|
5403
|
+
# Check if we should stop
|
5404
|
+
if self.ws_client.cleanup_complete or self.ws_client.timeout_occurred:
|
5405
|
+
break
|
5406
|
+
|
5407
|
+
# Small delay to avoid busy waiting
|
5408
|
+
await asyncio.sleep(0.1)
|
5409
|
+
|
5410
|
+
except Exception as e:
|
5411
|
+
# Log any errors
|
5412
|
+
safe_console_print(f"❌ Error monitoring events: {str(e)}", style="red", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5413
|
+
raise
|
5127
5414
|
finally:
|
5128
|
-
# Clean up spinner
|
5129
|
-
thinking_live
|
5415
|
+
# Clean up spinner if it was started
|
5416
|
+
if spinner_enabled and thinking_live:
|
5417
|
+
thinking_live.stop()
|
5418
|
+
|
5419
|
+
# Debug: Report how many events were received
|
5420
|
+
if self.debug.debug_level >= DebugLevel.SILENT and event_count == 0:
|
5421
|
+
safe_console_print("⚠️ No events received from server", style="yellow", json_mode=self.json_mode, ci_mode=self.ci_mode)
|
5130
5422
|
|
5131
5423
|
def _get_event_summary(self, event: WebSocketEvent) -> str:
|
5132
5424
|
"""ISSUE #1603 FIX: Get a concise summary of an event for display"""
|
@@ -5546,7 +5838,7 @@ class AgentCLI:
|
|
5546
5838
|
|
5547
5839
|
try:
|
5548
5840
|
# ISSUE #2766: Suppress output in JSON/CI mode
|
5549
|
-
|
5841
|
+
# Note: "Sending message:" is now printed inside send_message() after logs section
|
5550
5842
|
|
5551
5843
|
async with AuthManager(self.config) as auth_manager:
|
5552
5844
|
self.auth_manager = auth_manager
|
@@ -6406,8 +6698,8 @@ def main(argv=None):
|
|
6406
6698
|
"--debug-level",
|
6407
6699
|
type=str,
|
6408
6700
|
choices=["silent", "basic", "verbose", "trace", "diagnostic"],
|
6409
|
-
default="
|
6410
|
-
help="Debug verbosity level (default:
|
6701
|
+
default="silent",
|
6702
|
+
help="Debug verbosity level (default: silent)"
|
6411
6703
|
)
|
6412
6704
|
|
6413
6705
|
parser.add_argument(
|
@@ -6756,7 +7048,7 @@ def main(argv=None):
|
|
6756
7048
|
safe_console_print("📡 Stream logs enabled - backend logging active", style="cyan")
|
6757
7049
|
else:
|
6758
7050
|
# Keep logging minimal to prevent JSON output noise during CLI operations
|
6759
|
-
log_level = logging.
|
7051
|
+
log_level = logging.INFO
|
6760
7052
|
logging.basicConfig(
|
6761
7053
|
level=log_level,
|
6762
7054
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
shared/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Shared Utilities (Vendored Subset)
|
2
|
+
|
3
|
+
This directory contains a **minimal vendored subset** of the Apex `shared/` package, containing only the files required by `zen --apex` CLI functionality.
|
4
|
+
|
5
|
+
## Included Files
|
6
|
+
|
7
|
+
### 1. `__init__.py`
|
8
|
+
Package initialization stub with basic documentation.
|
9
|
+
|
10
|
+
### 2. `windows_encoding.py`
|
11
|
+
Windows UTF-8 console encoding fixes. The `setup_windows_encoding()` function is called early in `agent_cli.py` startup to ensure proper Unicode handling on Windows platforms.
|
12
|
+
|
13
|
+
### 3. `types/__init__.py`
|
14
|
+
Type definitions package that re-exports WebSocket closure code utilities.
|
15
|
+
|
16
|
+
### 4. `types/websocket_closure_codes.py`
|
17
|
+
WebSocket closure code validation utilities:
|
18
|
+
- `WebSocketClosureCode`: Enum of standard RFC 6455 closure codes
|
19
|
+
- `WebSocketClosureCategory`: Categories for classifying closure types
|
20
|
+
- `categorize_closure_code()`: Categorize a code into normal/client/server/infrastructure
|
21
|
+
- `get_closure_description()`: Human-readable description of closure codes
|
22
|
+
- `is_infrastructure_error()`: Check if a code represents infrastructure failure
|
23
|
+
|
24
|
+
## Maintenance
|
25
|
+
|
26
|
+
⚠️ **Important**: These files are vendored from the Apex repository. If Apex updates its closure-code definitions or Windows encoding logic, these files must be manually synchronized.
|
27
|
+
|
28
|
+
### What's NOT Included
|
29
|
+
|
30
|
+
Everything else under Apex's `shared/` directory is intentionally excluded because it's not referenced by `agent_cli.py`. This keeps the vendored code minimal and avoids pulling in backend logic, secrets, or unnecessary dependencies.
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
These modules are imported by `scripts/agent_cli.py`:
|
35
|
+
|
36
|
+
```python
|
37
|
+
from shared.windows_encoding import setup_windows_encoding
|
38
|
+
from shared.types.websocket_closure_codes import (
|
39
|
+
WebSocketClosureCode,
|
40
|
+
WebSocketClosureCategory,
|
41
|
+
categorize_closure_code,
|
42
|
+
get_closure_description,
|
43
|
+
is_infrastructure_error
|
44
|
+
)
|
45
|
+
```
|
46
|
+
|
47
|
+
The `setup_windows_encoding()` function is called immediately at startup, before any console I/O operations.
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# WebSocket Timing Fix - Complete Implementation
|
2
|
+
|
3
|
+
## Problem Statement
|
4
|
+
The CLI was sending messages too early, before the server completed its lifecycle phases and entered the message processing loop (Phase 5). Messages sent before Phase 5 were lost because the server's message handler wasn't active yet.
|
5
|
+
|
6
|
+
## Root Cause
|
7
|
+
The CLI code had backward compatibility logic that allowed connections to proceed even when the handshake failed, causing messages to be sent before the server was ready.
|
8
|
+
|
9
|
+
## Complete Fix Implementation
|
10
|
+
|
11
|
+
### 1. Connection Method (lines 2879-2928)
|
12
|
+
**REMOVED:** Backward compatibility code that returned `True` even when handshake failed
|
13
|
+
**ADDED:**
|
14
|
+
- Proper failure when handshake doesn't complete
|
15
|
+
- Retry mechanism with 3-second delay
|
16
|
+
- Second handshake attempt after delay
|
17
|
+
- WebSocket closure on failure
|
18
|
+
|
19
|
+
### 2. Handshake Response Processing (lines 3307-3363)
|
20
|
+
**CHANGED:** Message type from `session_acknowledged` to `handshake_acknowledged`
|
21
|
+
**ADDED:**
|
22
|
+
- Wait for `handshake_complete` confirmation
|
23
|
+
- 500ms delay after handshake_complete for server to enter Phase 5
|
24
|
+
- Fallback delay if no handshake_complete received
|
25
|
+
|
26
|
+
### 3. Message Sending Validation (lines 3703-3722)
|
27
|
+
**ADDED:** Check for `self.connected` flag before sending messages
|
28
|
+
- Ensures handshake is fully complete
|
29
|
+
- Prevents messages being sent during server initialization
|
30
|
+
- Clear error messages when connection not ready
|
31
|
+
|
32
|
+
## Order of Operations (Enforced)
|
33
|
+
|
34
|
+
1. **Connect** → WebSocket connection established
|
35
|
+
2. **Wait** for `connection_established` event
|
36
|
+
3. **Handshake** → Wait for `handshake_response`
|
37
|
+
4. **Acknowledge** → Send `handshake_acknowledged` with thread_id
|
38
|
+
5. **Confirm** → Wait for `handshake_complete` message
|
39
|
+
6. **Delay** → Add 500ms for server to enter Phase 5 (Processing)
|
40
|
+
7. **Ready** → NOW messages can be safely sent
|
41
|
+
|
42
|
+
## Server Phases Reference
|
43
|
+
|
44
|
+
```
|
45
|
+
Phase 1: INITIALIZING → Accept connection, assign ID
|
46
|
+
Phase 2: AUTHENTICATING → Validate user credentials
|
47
|
+
Phase 3: HANDSHAKING → Exchange thread IDs
|
48
|
+
Phase 4: READY → Initialize services, register with router
|
49
|
+
Phase 5: PROCESSING → Message loop active ✓ (Messages accepted here!)
|
50
|
+
Phase 6: CLEANING_UP → Coordinated cleanup
|
51
|
+
Phase 7: CLOSED → Terminal state
|
52
|
+
```
|
53
|
+
|
54
|
+
## Key Benefits
|
55
|
+
|
56
|
+
✓ **No Message Loss** - Messages only sent when server is ready to process them
|
57
|
+
✓ **Proper Sequencing** - Enforces documented WebSocket lifecycle
|
58
|
+
✓ **Retry Logic** - Handles transient delays during server startup
|
59
|
+
✓ **Clear Errors** - Descriptive messages when connection fails
|
60
|
+
✓ **Fail Fast** - Connection fails quickly if server isn't ready
|
61
|
+
|
62
|
+
## Testing Verification
|
63
|
+
|
64
|
+
The fix ensures:
|
65
|
+
- `self.connected` is only set to `True` after full handshake completion
|
66
|
+
- `send_message()` validates both `self.connected` and `self.current_thread_id`
|
67
|
+
- Messages cannot be sent until server reaches Phase 5 (Processing)
|
68
|
+
- Connection properly fails if server doesn't complete handshake
|
69
|
+
|
70
|
+
## Files Modified
|
71
|
+
|
72
|
+
- `scripts/agent_cli.py`:
|
73
|
+
- `connect()` method - lines 2879-2928
|
74
|
+
- `_perform_handshake()` method - lines 3057-3058
|
75
|
+
- `_process_handshake_response()` method - lines 3307-3363
|
76
|
+
- `send_message()` method - lines 3703-3722
|
77
|
+
|
78
|
+
## Implementation Complete
|
79
|
+
|
80
|
+
The timing issue is now fully resolved. The CLI will properly wait for the server to complete all initialization phases before sending any messages, preventing message loss and ensuring reliable communication.
|
shared/__init__.py
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
"""
|
2
|
+
Shared utilities package (minimal vendored subset for zen --apex CLI).
|
3
|
+
|
4
|
+
This package contains only the minimal files required by agent_cli.py:
|
5
|
+
- windows_encoding.py: Windows UTF-8 console fixes
|
6
|
+
- types/websocket_closure_codes.py: WebSocket closure code validation
|
7
|
+
|
8
|
+
TODO: Manually sync these files if Apex updates its closure-code definitions
|
9
|
+
or Windows handling logic.
|
10
|
+
"""
|
11
|
+
|
12
|
+
__version__ = "1.0.0"
|
shared/types/__init__.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
"""
|
2
|
+
Shared type definitions (minimal vendored subset for zen --apex CLI).
|
3
|
+
|
4
|
+
This package provides WebSocket closure code utilities required by agent_cli.py.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from shared.types.websocket_closure_codes import (
|
8
|
+
WebSocketClosureCode,
|
9
|
+
WebSocketClosureCategory,
|
10
|
+
categorize_closure_code,
|
11
|
+
get_closure_description,
|
12
|
+
is_infrastructure_error
|
13
|
+
)
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
'WebSocketClosureCode',
|
17
|
+
'WebSocketClosureCategory',
|
18
|
+
'categorize_closure_code',
|
19
|
+
'get_closure_description',
|
20
|
+
'is_infrastructure_error'
|
21
|
+
]
|
@@ -0,0 +1,124 @@
|
|
1
|
+
"""
|
2
|
+
WebSocket closure code definitions and categorization.
|
3
|
+
|
4
|
+
Provides enums and helper functions for validating and categorizing WebSocket
|
5
|
+
closure codes according to RFC 6455 and common extensions.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from enum import IntEnum
|
9
|
+
from typing import Optional
|
10
|
+
|
11
|
+
|
12
|
+
class WebSocketClosureCode(IntEnum):
|
13
|
+
"""
|
14
|
+
Standard WebSocket closure codes from RFC 6455 and extensions.
|
15
|
+
|
16
|
+
See: https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1
|
17
|
+
"""
|
18
|
+
# Standard codes (1000-1015)
|
19
|
+
NORMAL_CLOSURE = 1000
|
20
|
+
GOING_AWAY = 1001
|
21
|
+
PROTOCOL_ERROR = 1002
|
22
|
+
UNSUPPORTED_DATA = 1003
|
23
|
+
# 1004 is reserved
|
24
|
+
NO_STATUS_RECEIVED = 1005
|
25
|
+
ABNORMAL_CLOSURE = 1006
|
26
|
+
INVALID_FRAME_PAYLOAD_DATA = 1007
|
27
|
+
POLICY_VIOLATION = 1008
|
28
|
+
MESSAGE_TOO_BIG = 1009
|
29
|
+
MANDATORY_EXTENSION = 1010
|
30
|
+
INTERNAL_SERVER_ERROR = 1011
|
31
|
+
SERVICE_RESTART = 1012
|
32
|
+
TRY_AGAIN_LATER = 1013
|
33
|
+
BAD_GATEWAY = 1014
|
34
|
+
TLS_HANDSHAKE_FAILURE = 1015
|
35
|
+
|
36
|
+
# Custom application codes (4000-4999)
|
37
|
+
# These are application-specific and can be defined as needed
|
38
|
+
|
39
|
+
|
40
|
+
class WebSocketClosureCategory(IntEnum):
|
41
|
+
"""
|
42
|
+
Categories for WebSocket closure codes to classify failure types.
|
43
|
+
"""
|
44
|
+
NORMAL = 0 # Expected closures (1000, 1001)
|
45
|
+
CLIENT_ERROR = 1 # Client-side errors (1002-1003, 1007-1010)
|
46
|
+
SERVER_ERROR = 2 # Server-side errors (1011-1014)
|
47
|
+
INFRASTRUCTURE = 3 # Infrastructure/network errors (1006, 1015)
|
48
|
+
UNKNOWN = 4 # Unrecognized codes
|
49
|
+
|
50
|
+
|
51
|
+
def categorize_closure_code(code: int) -> WebSocketClosureCategory:
|
52
|
+
"""
|
53
|
+
Categorize a WebSocket closure code into a failure category.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
code: The WebSocket closure code
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
The category of the closure code
|
60
|
+
"""
|
61
|
+
# Normal closures
|
62
|
+
if code in (1000, 1001):
|
63
|
+
return WebSocketClosureCategory.NORMAL
|
64
|
+
|
65
|
+
# Infrastructure/network errors
|
66
|
+
if code in (1006, 1015):
|
67
|
+
return WebSocketClosureCategory.INFRASTRUCTURE
|
68
|
+
|
69
|
+
# Server errors
|
70
|
+
if code in (1011, 1012, 1013, 1014):
|
71
|
+
return WebSocketClosureCategory.SERVER_ERROR
|
72
|
+
|
73
|
+
# Client errors
|
74
|
+
if code in (1002, 1003, 1007, 1008, 1009, 1010):
|
75
|
+
return WebSocketClosureCategory.CLIENT_ERROR
|
76
|
+
|
77
|
+
# Unknown/unrecognized codes
|
78
|
+
return WebSocketClosureCategory.UNKNOWN
|
79
|
+
|
80
|
+
|
81
|
+
def is_infrastructure_error(code: int) -> bool:
|
82
|
+
"""
|
83
|
+
Check if a closure code represents an infrastructure/network error.
|
84
|
+
|
85
|
+
Infrastructure errors are typically transient and may be retryable.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
code: The WebSocket closure code
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
True if the code represents an infrastructure error
|
92
|
+
"""
|
93
|
+
return categorize_closure_code(code) == WebSocketClosureCategory.INFRASTRUCTURE
|
94
|
+
|
95
|
+
|
96
|
+
def get_closure_description(code: int) -> str:
|
97
|
+
"""
|
98
|
+
Get a human-readable description of a WebSocket closure code.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
code: The WebSocket closure code
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
A description of what the closure code means
|
105
|
+
"""
|
106
|
+
descriptions = {
|
107
|
+
1000: "Normal closure - connection completed successfully",
|
108
|
+
1001: "Going away - endpoint is going away (e.g., server shutdown, browser navigation)",
|
109
|
+
1002: "Protocol error - endpoint received a malformed message",
|
110
|
+
1003: "Unsupported data - endpoint received data of unsupported type",
|
111
|
+
1005: "No status received - no status code was provided (internal use only)",
|
112
|
+
1006: "Abnormal closure - connection closed without close frame (network/infrastructure issue)",
|
113
|
+
1007: "Invalid frame payload data - message contains invalid UTF-8 or violates payload requirements",
|
114
|
+
1008: "Policy violation - endpoint received message that violates its policy",
|
115
|
+
1009: "Message too big - endpoint received message that is too large to process",
|
116
|
+
1010: "Mandatory extension - client expected server to negotiate an extension",
|
117
|
+
1011: "Internal server error - server encountered unexpected condition",
|
118
|
+
1012: "Service restart - server is restarting",
|
119
|
+
1013: "Try again later - server is temporarily overloaded or under maintenance",
|
120
|
+
1014: "Bad gateway - server acting as gateway received invalid response",
|
121
|
+
1015: "TLS handshake failure - TLS handshake failed (internal use only)",
|
122
|
+
}
|
123
|
+
|
124
|
+
return descriptions.get(code, f"Unknown closure code: {code}")
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"""
|
2
|
+
Windows UTF-8 console encoding fixes.
|
3
|
+
|
4
|
+
Ensures proper UTF-8 handling on Windows consoles by setting appropriate
|
5
|
+
encoding for stdin, stdout, and stderr streams.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import sys
|
9
|
+
import io
|
10
|
+
|
11
|
+
|
12
|
+
def setup_windows_encoding():
|
13
|
+
"""
|
14
|
+
Configure Windows console to use UTF-8 encoding.
|
15
|
+
|
16
|
+
This function reconfigures sys.stdin, sys.stdout, and sys.stderr to use
|
17
|
+
UTF-8 encoding with error handling on Windows platforms. This prevents
|
18
|
+
Unicode encoding errors when displaying special characters or emoji.
|
19
|
+
|
20
|
+
Must be called early in the startup sequence before any console I/O.
|
21
|
+
"""
|
22
|
+
if sys.platform == "win32":
|
23
|
+
# Reconfigure stdout and stderr to use UTF-8 with error handling
|
24
|
+
if sys.stdout is not None:
|
25
|
+
sys.stdout = io.TextIOWrapper(
|
26
|
+
sys.stdout.buffer,
|
27
|
+
encoding='utf-8',
|
28
|
+
errors='replace',
|
29
|
+
line_buffering=True
|
30
|
+
)
|
31
|
+
|
32
|
+
if sys.stderr is not None:
|
33
|
+
sys.stderr = io.TextIOWrapper(
|
34
|
+
sys.stderr.buffer,
|
35
|
+
encoding='utf-8',
|
36
|
+
errors='replace',
|
37
|
+
line_buffering=True
|
38
|
+
)
|
39
|
+
|
40
|
+
if sys.stdin is not None:
|
41
|
+
sys.stdin = io.TextIOWrapper(
|
42
|
+
sys.stdin.buffer,
|
43
|
+
encoding='utf-8',
|
44
|
+
errors='replace'
|
45
|
+
)
|
@@ -10,7 +10,8 @@ from typing import Optional
|
|
10
10
|
|
11
11
|
from google.oauth2 import service_account
|
12
12
|
|
13
|
-
_ENV_B64 = "COMMUNITY_CREDENTIALS"
|
13
|
+
#_ENV_B64 = "COMMUNITY_CREDENTIALS"
|
14
|
+
_ENV_B64 = "ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAibmV0cmEtdGVsZW1ldHJ5LXB1YmxpYyIsCiAgInByaXZhdGVfa2V5X2lkIjogImVjOWM4ZGNlZGZmMTUzNjM5YTUxOTcyMzc0MjYyNjkwNjZkNzAxYTQiLAogICJwcml2YXRlX2tleSI6ICItLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRREhUcmZFOHlQdUFCTDNcbk5aS3diZ1AwamRyaWRnY0UwMUlLMks1YkZ1bWFrUHVrRGxzV0dVaUswOXEyaVNYTWVUQmJPZDF0VjFoc3VJcUhcbnpQK0pZd0NTcVp4S0pIQS8yWUdVcERqeWhHRVd3QTNtS3laVXVUUS9yUTBUK20yV0cwdEMxUzBpQzB6U211cEpcbjBNeGhUUDRjKzYreEczSDVSWEF1YjdONmx6eFFWVnJSY3FHMTYydlF5SFA2OWIrd2RidTJHM0o5UkJVN1VUK1FcbkU2RHB2K01YaEp3MnRPdHZLbFBQT3BnWm9Sb0pmeXU1WlpzbHlJZCtzY3FhUTY5ZjBaSmpIRjlYQVdlT25mUTRcbmExbmV0LzJqRjZibWpuZmQ2MjhBODA5cEluTXJEL0FwZjJzUWJJdXJIYUI4am5uTEQ0eDMrbVhXRTgyMFJLNktcbjViZ0FQanNoQWdNQkFBRUNnZ0VBQmZERVZMWVlDakFkb0pscnpyOHF0a056cEpnV3Y3ZXlQSEZHcWgraDFSbndcbjdDd0Mza0xnNlFWbFFaZFBKWHZ2dTJwYlBaYnl3MlBST2ppN25adFNNU3pseEFaM3c0bHV2YkRTNHpTYnBiZFJcbityd3F3Mi8xUFJnaCtZaFhjNWZLNjVvcHd4Zmg0VzJkWWRlYnZlTXkrRWR1cmtsV2dYTG13L1dQbkkzdExlbzlcbjV1elZjbU42Qk04YkU3azFYK1M0RURBS0VRWlprUEdzTFQ4RXN4UmdWOWtnT1Zicm5VQ1Z0dXA1Q0NGbUR3U1Bcbmg0U25wMEsvTUp3b1U3NG4reTlFMXYxUXRnajE5TkhaNHJ2dFpnUlVaandHQy9Cc3ZkcE1PazArZTJEMlgvRk9cblZnc29xS2tDaklWUzRMcG5YSEpZbU5oajZWNHRXUnZ1OW1NTXhTL3FBUUtCZ1FEb3hZenlsdEZKL242ZEQvUHlcbnZLOFRaTHd5dFdCcjBXU3ZHU2VzM0JYRGZoKzBFbU4rRHpnZGdUb0ovbkhpazM5R21QM0tLN2htOFVvaFFHRy9cbkh0SFRuS0lBQlhrSU8yd2Z3N0h0V2pPTXRocHp2dFQxcmVEVHVjVk0wc2lCMHpjTldCMjFUamdQL3JYY2Q3NWVcbklERmNBN0hTbUJDLzB4bzk3aC8wV2YvOW9RS0JnUURiTWtnbjlVR2Y2SVRMNmxTcDdrYWJGL0NuaE4yU2VTMVdcbnd3R21iRThxTTU0UitDcVRUeHk2UHBRaFVSczlHM1VpVmQ1SXZWVDhuT095ZVBZVFJEbnFCYjJ4S214SFRodlZcbnVQcTgwQXB3anBMbzh6VkFDSy9iVFVjSmlKVGFBdXFHaXI1Ykc4YUlldVpIc0pLeWJ1NmhoNkhXMldwWXVVV1BcbkZ3TTl4elpOZ1FLQmdRQzg5dHJVZVJFUVE3VGZwbnJBM09JNEdUZ2E1bG1QVFo2eDh2YmRncEY4Y2FBbEhDUitcbnlyWWdaYThMTysrU0kzRllpNHpFR2pnS0FlblBFcWdIY21xZW9uSjFGL3hJYll6NlFIRHFJYWJsblZQZUVOWnJcblY2dkQxZlRReC9FVVM3Wk9jL0V5Slh5bnAzeFZyVFB5ejZtaWJERm9xQ0E0eVpSdElDbjZ3VEZxNFFLQmdFWFJcbnAxQXErOE0rb2dYOTF3ZmxvTkhIOTF5MG9vc0VWQis5cjZuZDkvMWVRYXhCbXZZZkRleDVBRi80WUsrL0xqbElcbmxxd2V1cEpZT3VMZlNxcHFZZlFiN2djZmx5dkRRblI2SGt2RURIODd1cW0reGlobVcvV0RrT3dGZUR4VkQzVFpcbmZyYXdpelZ2eUNmdm8xcDRvVVFNV3MxL3BUTXJtRzl5aWhMRWdKU0JBb0dCQU1GWm50ZUtUUDZrVVdrVmpOcndcbmUvQzBDbjJ6dk1YNXVnZURkS1FWNkwrY25mRWlRSzdzZ3R5eFp5ek5kMC82QXJ0YnBrcS9wcVlaYXpwVzVFMkxcbkxVMUF3MmdHT25GRlh2ZXg4aXpOZXViMGdvUVE4d3BtL3lrMVNVekR6VTV1dCtPbVFFRmpsbUYrNDkza0ZYcC9cbnc1MWh2WjVVL2loL1NYbjN6cjdEWE5QYlxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwKICAiY2xpZW50X2VtYWlsIjogInplbi1jb21tdW5pdHktdGVsZW1ldHJ5QG5ldHJhLXRlbGVtZXRyeS1wdWJsaWMuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICJjbGllbnRfaWQiOiAiMTE0NzAwMDA0NzA1MDUxODg5NTY4IiwKICAiYXV0aF91cmkiOiAiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLAogICJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4iLAogICJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRzIiwKICAiY2xpZW50X3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS96ZW4tY29tbXVuaXR5LXRlbGVtZXRyeSU0MG5ldHJhLXRlbGVtZXRyeS1wdWJsaWMuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICJ1bml2ZXJzZV9kb21haW4iOiAiZ29vZ2xlYXBpcy5jb20iCn0K%"
|
14
15
|
_ENV_PATH = "ZEN_COMMUNITY_TELEMETRY_FILE"
|
15
16
|
_ENV_PROJECT = "ZEN_COMMUNITY_TELEMETRY_PROJECT"
|
16
17
|
_DEFAULT_PROJECT = "netra-telemetry-public"
|
zen_orchestrator.py
CHANGED
@@ -1428,11 +1428,11 @@ class ClaudeInstanceOrchestrator:
|
|
1428
1428
|
content = message.get('content', [])
|
1429
1429
|
if isinstance(content, list):
|
1430
1430
|
tool_types = [item.get('type') for item in content if isinstance(item, dict)]
|
1431
|
-
logger.
|
1431
|
+
logger.info(f"🎯 CONTENT TYPES: {tool_types}")
|
1432
1432
|
|
1433
1433
|
# Check if this looks like a tool usage line
|
1434
1434
|
if 'name' in json_data and ('type' in json_data and json_data['type'] in ['tool_use', 'tool_call']):
|
1435
|
-
logger.
|
1435
|
+
logger.info(f"🎯 POTENTIAL TOOL: type={json_data.get('type')}, name={json_data.get('name')}")
|
1436
1436
|
|
1437
1437
|
# Extract message ID for deduplication
|
1438
1438
|
message_id = self._extract_message_id(json_data)
|
@@ -1560,7 +1560,7 @@ class ClaudeInstanceOrchestrator:
|
|
1560
1560
|
|
1561
1561
|
# Handle tool calls with detailed tracking
|
1562
1562
|
if 'type' in json_data:
|
1563
|
-
logger.
|
1563
|
+
logger.info(f"🔍 TOOL DETECTION: Found type='{json_data['type']}', checking for tool usage...")
|
1564
1564
|
|
1565
1565
|
if json_data['type'] in ['tool_use', 'tool_call', 'tool_execution']:
|
1566
1566
|
# Extract tool name for detailed tracking (ALWAYS track, even without message_id)
|
@@ -1568,7 +1568,7 @@ class ClaudeInstanceOrchestrator:
|
|
1568
1568
|
status.tool_details[tool_name] = status.tool_details.get(tool_name, 0) + 1
|
1569
1569
|
status.tool_calls += 1
|
1570
1570
|
|
1571
|
-
logger.
|
1571
|
+
logger.info(f"🔧 TOOL FOUND: {tool_name} (message_id={message_id})")
|
1572
1572
|
|
1573
1573
|
# Track tool token usage if available
|
1574
1574
|
tool_tokens = 0
|
@@ -1583,14 +1583,14 @@ class ClaudeInstanceOrchestrator:
|
|
1583
1583
|
|
1584
1584
|
if tool_tokens > 0:
|
1585
1585
|
status.tool_tokens[tool_name] = status.tool_tokens.get(tool_name, 0) + tool_tokens
|
1586
|
-
logger.
|
1586
|
+
logger.info(f"🔧 TOOL TRACKED: {tool_name} (uses: {status.tool_details[tool_name]}, tokens: {status.tool_tokens[tool_name]})")
|
1587
1587
|
else:
|
1588
|
-
logger.
|
1588
|
+
logger.info(f"🔧 TOOL TRACKED: {tool_name} (uses: {status.tool_details[tool_name]}, no tokens)")
|
1589
1589
|
return True
|
1590
1590
|
elif json_data['type'] == 'message' and 'tool_calls' in json_data:
|
1591
1591
|
# Count tool calls in message with token tracking
|
1592
1592
|
tool_calls = json_data['tool_calls']
|
1593
|
-
logger.
|
1593
|
+
logger.info(f"🔧 TOOL MESSAGE: Found tool_calls in message: {tool_calls}")
|
1594
1594
|
if isinstance(tool_calls, list):
|
1595
1595
|
for tool in tool_calls:
|
1596
1596
|
if isinstance(tool, dict):
|
@@ -1607,7 +1607,7 @@ class ClaudeInstanceOrchestrator:
|
|
1607
1607
|
|
1608
1608
|
if tool_tokens > 0:
|
1609
1609
|
status.tool_tokens[tool_name] = status.tool_tokens.get(tool_name, 0) + tool_tokens
|
1610
|
-
logger.
|
1610
|
+
logger.info(f"🔧 TOOL FROM MESSAGE: {tool_name} (tokens: {tool_tokens})")
|
1611
1611
|
|
1612
1612
|
status.tool_calls += len(tool_calls)
|
1613
1613
|
elif isinstance(tool_calls, (int, float)):
|
@@ -1635,7 +1635,7 @@ class ClaudeInstanceOrchestrator:
|
|
1635
1635
|
|
1636
1636
|
status.tool_details[tool_name] = status.tool_details.get(tool_name, 0) + 1
|
1637
1637
|
status.tool_calls += 1
|
1638
|
-
logger.
|
1638
|
+
logger.info(f"🔧 TOOL FROM ASSISTANT CONTENT: {tool_name} (id: {tool_use_id})")
|
1639
1639
|
return True
|
1640
1640
|
elif json_data['type'] == 'user' and 'message' in json_data:
|
1641
1641
|
# Handle Claude Code user messages with tool results: {"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"..."}]}}
|
@@ -1663,9 +1663,9 @@ class ClaudeInstanceOrchestrator:
|
|
1663
1663
|
tool_tokens = self._estimate_tool_tokens(item)
|
1664
1664
|
if tool_tokens > 0:
|
1665
1665
|
status.tool_tokens[tool_name] = status.tool_tokens.get(tool_name, 0) + tool_tokens
|
1666
|
-
logger.
|
1666
|
+
logger.info(f"🔧 TOOL FROM USER CONTENT: {tool_name} (tool_use_id: {tool_use_id}, estimated_tokens: {tool_tokens})")
|
1667
1667
|
else:
|
1668
|
-
logger.
|
1668
|
+
logger.info(f"🔧 TOOL FROM USER CONTENT: {tool_name} (tool_use_id: {tool_use_id})")
|
1669
1669
|
# Tool use in user message (request)
|
1670
1670
|
elif item.get('type') == 'tool_use' and 'name' in item:
|
1671
1671
|
tool_name = item.get('name', 'unknown_tool')
|
@@ -1676,9 +1676,9 @@ class ClaudeInstanceOrchestrator:
|
|
1676
1676
|
tool_tokens = self._estimate_tool_tokens(item, is_tool_use=True)
|
1677
1677
|
if tool_tokens > 0:
|
1678
1678
|
status.tool_tokens[tool_name] = status.tool_tokens.get(tool_name, 0) + tool_tokens
|
1679
|
-
logger.
|
1679
|
+
logger.info(f"🔧 TOOL USE FROM USER CONTENT: {tool_name} (estimated_tokens: {tool_tokens})")
|
1680
1680
|
else:
|
1681
|
-
logger.
|
1681
|
+
logger.info(f"🔧 TOOL USE FROM USER CONTENT: {tool_name}")
|
1682
1682
|
return True
|
1683
1683
|
|
1684
1684
|
# Handle direct token fields at root level (without message ID - treat as individual message tokens)
|
@@ -1823,7 +1823,7 @@ class ClaudeInstanceOrchestrator:
|
|
1823
1823
|
elif isinstance(tool_calls, (int, float)):
|
1824
1824
|
status.tool_calls += int(tool_calls)
|
1825
1825
|
|
1826
|
-
logger.
|
1826
|
+
logger.info(f"Parsed JSON final output: tokens={status.total_tokens}, tools={status.tool_calls}")
|
1827
1827
|
|
1828
1828
|
except (json.JSONDecodeError, ValueError) as e:
|
1829
1829
|
logger.debug(f"Failed to parse final output as JSON: {e}")
|
File without changes
|
File without changes
|
File without changes
|