netra-zen 1.0.11__tar.gz → 1.1.2__tar.gz

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