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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netra-zen
3
- Version: 1.2.0
3
+ Version: 1.2.3
4
4
  Summary: Multi-instance Claude orchestrator for parallel task execution
5
5
  Home-page: https://github.com/netra-systems/zen
6
6
  Author: Systems
@@ -1,16 +1,22 @@
1
- zen_orchestrator.py,sha256=hKeP2dW6eJgEhtUHYylnhIgP_HgaxrDAwCnlS7mKkgU,156167
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.0.dist-info/licenses/LICENSE.md,sha256=PZrP0UDn58i4LjV4zijIQTnsQPvWm4zq9Fet9i7qgwI,80
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=ATQIWoVx7GthFV1WeiV6Co8PzoXYXhlJjOerU-JocJ0,345625
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=z-j_TfuVIz3nX-Vvh4O6_iDQhtteEyQgfc-xSslVbXU,1716
30
+ zen/telemetry/embedded_credentials.py,sha256=bSU6XUOFQ7HP6TMOvuHfKz1Oo5v8pljneUjzwE8XprU,4941
25
31
  zen/telemetry/manager.py,sha256=Rdbpnjbjel9xZEJyvLqy2M-4amvkr80abRiWnqHghIQ,9980
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,,
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,,
@@ -1,5 +1,6 @@
1
1
  agent_interface
2
2
  scripts
3
+ shared
3
4
  token_budget
4
5
  token_transparency
5
6
  zen
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.BASIC, log_file: Optional[Path] = None,
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.BASIC
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 = 2.0):
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"GOLDEN PATH TRACE: Initiating WebSocket auth via {method_name}",
2867
- DebugLevel.BASIC,
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"GOLDEN PATH TRACE: WebSocket connected using {method_name}",
2875
- DebugLevel.BASIC,
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"GOLDEN PATH TRACE: WebSocket authentication via {method_name} failed with {type(e).__name__}: {e}",
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.from_dict(response))
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.from_dict(response))
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
- "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,
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
- # Create prominent, formatted log message
3847
- separator = "=" * 60
3848
- log_msg_parts = [
3849
- "",
3850
- separator,
3851
- f"📤 SENDING LOGS TO OPTIMIZER",
3852
- separator,
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"GOLDEN PATH TRACE: Prepared WebSocket payload for run_id={self.run_id}, thread_id={thread_id}",
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
- "GOLDEN PATH TRACE: Event listener started after successful connection",
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
- # This is now just a basic connection confirmation, not part of handshake
4084
- self.debug.debug_print(
4085
- f"WebSocket connection established event received",
4086
- DebugLevel.VERBOSE,
4087
- style="dim"
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"GOLDEN PATH TRACE: Parsed WebSocket event type={event.type}",
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 = 2.0):
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
- 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)
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
- # Start the spinner live display
5018
- thinking_live.start()
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
- async def handle_event_with_display(event: WebSocketEvent):
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
- await self.ws_client.receive_events(callback=handle_event_with_display)
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.stop()
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
- safe_console_print(f"Sending message: {message}", style="cyan", json_mode=self.json_mode, ci_mode=self.ci_mode)
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="basic",
6410
- help="Debug verbosity level (default: basic)"
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.WARNING
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"
@@ -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.debug(f"🎯 CONTENT TYPES: {tool_types}")
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.debug(f"🎯 POTENTIAL TOOL: type={json_data.get('type')}, name={json_data.get('name')}")
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.debug(f"🔍 TOOL DETECTION: Found type='{json_data['type']}', checking for tool usage...")
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.debug(f"🔧 TOOL FOUND: {tool_name} (message_id={message_id})")
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.debug(f"🔧 TOOL TRACKED: {tool_name} (uses: {status.tool_details[tool_name]}, tokens: {status.tool_tokens[tool_name]})")
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.debug(f"🔧 TOOL TRACKED: {tool_name} (uses: {status.tool_details[tool_name]}, no tokens)")
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.debug(f"🔧 TOOL MESSAGE: Found tool_calls in message: {tool_calls}")
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.debug(f"🔧 TOOL FROM MESSAGE: {tool_name} (tokens: {tool_tokens})")
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.debug(f"🔧 TOOL FROM ASSISTANT CONTENT: {tool_name} (id: {tool_use_id})")
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.debug(f"🔧 TOOL FROM USER CONTENT: {tool_name} (tool_use_id: {tool_use_id}, estimated_tokens: {tool_tokens})")
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.debug(f"🔧 TOOL FROM USER CONTENT: {tool_name} (tool_use_id: {tool_use_id})")
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.debug(f"🔧 TOOL USE FROM USER CONTENT: {tool_name} (estimated_tokens: {tool_tokens})")
1679
+ logger.info(f"🔧 TOOL USE FROM USER CONTENT: {tool_name} (estimated_tokens: {tool_tokens})")
1680
1680
  else:
1681
- logger.debug(f"🔧 TOOL USE FROM USER CONTENT: {tool_name}")
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.debug(f"Parsed JSON final output: tokens={status.total_tokens}, tools={status.tool_calls}")
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}")