meshcode 2.10.54__tar.gz → 2.10.55__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 (32) hide show
  1. {meshcode-2.10.54 → meshcode-2.10.55}/PKG-INFO +1 -1
  2. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/cli.py +1 -1
  4. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/comms_v4.py +31 -31
  5. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/meshcode_mcp/backend.py +42 -36
  6. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/meshcode_mcp/realtime.py +24 -0
  7. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/meshcode_mcp/server.py +194 -57
  8. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode.egg-info/PKG-INFO +1 -1
  9. {meshcode-2.10.54 → meshcode-2.10.55}/pyproject.toml +1 -1
  10. {meshcode-2.10.54 → meshcode-2.10.55}/README.md +0 -0
  11. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/ascii_art.py +0 -0
  12. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/invites.py +0 -0
  13. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/launcher.py +0 -0
  14. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/launcher_install.py +0 -0
  15. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.54 → meshcode-2.10.55}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.54 → meshcode-2.10.55}/setup.cfg +0 -0
  32. {meshcode-2.10.54 → meshcode-2.10.55}/tests/test_status_enum_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.54
3
+ Version: 2.10.55
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.10.54"
2
+ __version__ = "2.10.55"
@@ -34,7 +34,7 @@ def main():
34
34
  exec(compile(f.read(), candidate, "exec"), globs)
35
35
  return
36
36
 
37
- print("[ERROR] comms_v4.py not found. Reinstall: pip install meshcode")
37
+ print("[meshcode] ERROR: comms_v4.py not found. Reinstall: pip install meshcode")
38
38
  sys.exit(1)
39
39
 
40
40
 
@@ -120,12 +120,12 @@ def _request(method, path, *, data=None, prefer=None):
120
120
  if "Daily message limit reached" in msg:
121
121
  print(f"[QUOTA] {msg}", file=sys.stderr)
122
122
  else:
123
- print(f"[ERROR] {method} {path}: {e.code} {msg}", file=sys.stderr)
123
+ print(f"[meshcode] ERROR: {method} {path}: {e.code} {msg}", file=sys.stderr)
124
124
  except Exception:
125
- print(f"[ERROR] {method} {path}: {e.code} {err[:200]}", file=sys.stderr)
125
+ print(f"[meshcode] ERROR: {method} {path}: {e.code} {err[:200]}", file=sys.stderr)
126
126
  return None
127
127
  except URLError as e:
128
- print(f"[ERROR] Network: {e.reason}", file=sys.stderr)
128
+ print(f"[meshcode] ERROR: Network: {e.reason}", file=sys.stderr)
129
129
  return None
130
130
 
131
131
 
@@ -962,7 +962,7 @@ def register(project, name, role=""):
962
962
  ensure_sessions()
963
963
  project_id = get_project_id(project)
964
964
  if not project_id:
965
- print(f"[ERROR] Could not create/find project '{project}'")
965
+ print(f"[meshcode] ERROR: Could not create/find project '{project}'")
966
966
  return
967
967
 
968
968
  ppid = os.getppid()
@@ -992,7 +992,7 @@ def register(project, name, role=""):
992
992
  # the RPC validates owner/member access (no anon bypass).
993
993
  _api_key = _load_api_key_for_cli()
994
994
  if not _api_key:
995
- print("[ERROR] Not authenticated. Run `meshcode login <api_key>` first.")
995
+ print("[meshcode] ERROR: Not authenticated. Run `meshcode login <api_key>` first.")
996
996
  return
997
997
  rpc_result = sb_rpc("mc_register_agent_with_api_key", {
998
998
  "p_api_key": _api_key,
@@ -1003,11 +1003,11 @@ def register(project, name, role=""):
1003
1003
  })
1004
1004
 
1005
1005
  if rpc_result and rpc_result.get("error"):
1006
- print(f"[ERROR] {rpc_result['error']}")
1006
+ print(f"[meshcode] ERROR: {rpc_result['error']}")
1007
1007
  if rpc_result.get("upgrade_url"):
1008
- print(f"[ERROR] Upgrade: {rpc_result['upgrade_url']}")
1008
+ print(f"[meshcode] ERROR: Upgrade: {rpc_result['upgrade_url']}")
1009
1009
  if rpc_result.get("current") is not None:
1010
- print(f"[ERROR] Agentes: {rpc_result['current']} / {rpc_result.get('max', '?')}")
1010
+ print(f"[meshcode] ERROR: Agentes: {rpc_result['current']} / {rpc_result.get('max', '?')}")
1011
1011
  return
1012
1012
 
1013
1013
  # Update tty/pid (RPC doesn't take these — patch separately)
@@ -1094,7 +1094,7 @@ def _resolve_agent_name(project_id, name):
1094
1094
  def send_msg(project, from_agent, to_agent, content, msg_type="msg", compact=False):
1095
1095
  project_id = get_project_id(project)
1096
1096
  if not project_id:
1097
- print(f"[ERROR] Proyecto '{project}' no encontrado")
1097
+ print(f"[meshcode] ERROR: Proyecto '{project}' no encontrado")
1098
1098
  return
1099
1099
 
1100
1100
  # Resolve partial agent names
@@ -1761,7 +1761,7 @@ def connect_terminal(project, name, role=""):
1761
1761
  ensure_sessions()
1762
1762
  project_id = get_project_id(project)
1763
1763
  if not project_id:
1764
- print(f"[ERROR] Could not create/find project '{project}'")
1764
+ print(f"[meshcode] ERROR: Could not create/find project '{project}'")
1765
1765
  return
1766
1766
 
1767
1767
  tty, owning = _capture_tty_for_pid(os.getppid())
@@ -1791,11 +1791,11 @@ def connect_terminal(project, name, role=""):
1791
1791
  "p_instance_id": _instance_id,
1792
1792
  })
1793
1793
  else:
1794
- print("[ERROR] No API key found — cannot connect without authentication.")
1795
- print("[ERROR] Run: meshcode setup-key")
1794
+ print("[meshcode] ERROR: No API key found — cannot connect without authentication.")
1795
+ print("[meshcode] ERROR: Run: meshcode setup-key")
1796
1796
  return
1797
1797
  if rpc_result and rpc_result.get("error"):
1798
- print(f"[ERROR] {rpc_result['error']}")
1798
+ print(f"[meshcode] ERROR: {rpc_result['error']}")
1799
1799
  return
1800
1800
 
1801
1801
  session_data = {"project": project, "agent": name, "pid": ppid, "tty": tty, "registered_at": now()}
@@ -1831,14 +1831,14 @@ def disconnect_terminal(project, name):
1831
1831
  """Mark agent offline, clear tty/pid, stop heartbeat, remove session file."""
1832
1832
  project_id = get_project_id(project)
1833
1833
  if not project_id:
1834
- print(f"[ERROR] project '{project}' not found")
1834
+ print(f"[meshcode] ERROR: project '{project}' not found")
1835
1835
  return
1836
1836
  rpc_result = sb_rpc("mc_disconnect_agent", {
1837
1837
  "p_project_id": project_id,
1838
1838
  "p_agent_name": name,
1839
1839
  })
1840
1840
  if rpc_result and rpc_result.get("error"):
1841
- print(f"[ERROR] {rpc_result['error']}")
1841
+ print(f"[meshcode] ERROR: {rpc_result['error']}")
1842
1842
  return
1843
1843
  _stop_heartbeat_daemon(project, name)
1844
1844
  sf = SESSIONS_DIR / f"{project}_{name}"
@@ -2242,7 +2242,7 @@ def show_subcommand_help(cmd):
2242
2242
  if text:
2243
2243
  print(text)
2244
2244
  else:
2245
- print(f"[ERROR] No help available for '{cmd}'")
2245
+ print(f"[meshcode] ERROR: No help available for '{cmd}'")
2246
2246
  show_help()
2247
2247
 
2248
2248
 
@@ -2447,7 +2447,7 @@ if __name__ == "__main__":
2447
2447
  elif cmd == "validate-sessions":
2448
2448
  proj = pos[0] if len(pos) > 0 else (sys.argv[2] if len(sys.argv) > 2 else "")
2449
2449
  if not proj:
2450
- print("[ERROR] Uso: meshcode validate-sessions <project>")
2450
+ print("[meshcode] ERROR: Uso: meshcode validate-sessions <project>")
2451
2451
  sys.exit(1)
2452
2452
  ensure_sessions()
2453
2453
  prefix = f"{proj}_"
@@ -2473,11 +2473,11 @@ if __name__ == "__main__":
2473
2473
  proj = pos[0] if len(pos) > 0 else ""
2474
2474
  name = pos[1] if len(pos) > 1 else ""
2475
2475
  if not proj or not name:
2476
- print("[ERROR] Uso: meshcode wake-headless <project> <agent>")
2476
+ print("[meshcode] ERROR: Uso: meshcode wake-headless <project> <agent>")
2477
2477
  sys.exit(1)
2478
2478
  project_id = get_project_id(proj)
2479
2479
  if not project_id:
2480
- print(f"[ERROR] project '{proj}' not found")
2480
+ print(f"[meshcode] ERROR: project '{proj}' not found")
2481
2481
  sys.exit(1)
2482
2482
  ok = spawn_headless_agent(proj, name, project_id,
2483
2483
  "ping: synthetic wake-headless test message", "test-harness")
@@ -2489,16 +2489,16 @@ if __name__ == "__main__":
2489
2489
  proj = pos[0] if len(pos) > 0 else "default"
2490
2490
  name = pos[1] if len(pos) > 1 else ""
2491
2491
  if not name:
2492
- print(f"[ERROR] Uso: meshcode {cmd} <project> <agent>")
2492
+ print(f"[meshcode] ERROR: Uso: meshcode {cmd} <project> <agent>")
2493
2493
  sys.exit(1)
2494
2494
  project_id = get_project_id(proj)
2495
2495
  if not project_id:
2496
- print(f"[ERROR] project '{proj}' not found")
2496
+ print(f"[meshcode] ERROR: project '{proj}' not found")
2497
2497
  sys.exit(1)
2498
2498
  rpc_name = {"kill": "mc_agent_kill", "wake": "mc_agent_wake", "sleep": "mc_agent_sleep"}[cmd]
2499
2499
  result = sb_rpc(rpc_name, {"p_project_id": project_id, "p_agent_name": name})
2500
2500
  if result and result.get("error"):
2501
- print(f"[ERROR] {result['error']}")
2501
+ print(f"[meshcode] ERROR: {result['error']}")
2502
2502
  sys.exit(1)
2503
2503
  print(f"[{proj}] {name}: {cmd} → {json.dumps(result, ensure_ascii=False)}")
2504
2504
 
@@ -2508,11 +2508,11 @@ if __name__ == "__main__":
2508
2508
  proj = pos[1] if len(pos) > 1 else "default"
2509
2509
  name = pos[2] if len(pos) > 2 else ""
2510
2510
  if not name:
2511
- print("[ERROR] Uso: meshcode profile get|set <project> <agent> [flags]")
2511
+ print("[meshcode] ERROR: Uso: meshcode profile get|set <project> <agent> [flags]")
2512
2512
  sys.exit(1)
2513
2513
  project_id = get_project_id(proj)
2514
2514
  if not project_id:
2515
- print(f"[ERROR] project '{proj}' not found")
2515
+ print(f"[meshcode] ERROR: project '{proj}' not found")
2516
2516
  sys.exit(1)
2517
2517
  if sub == "get":
2518
2518
  result = sb_rpc("mc_agent_get_profile", {"p_project_id": project_id, "p_agent_name": name})
@@ -2529,11 +2529,11 @@ if __name__ == "__main__":
2529
2529
  params["p_launch_prompt"] = flags["launch-prompt"]
2530
2530
  result = sb_rpc("mc_agent_update_profile", params)
2531
2531
  if result and result.get("error"):
2532
- print(f"[ERROR] {result['error']}")
2532
+ print(f"[meshcode] ERROR: {result['error']}")
2533
2533
  sys.exit(1)
2534
2534
  print(f"[{proj}] profile updated for {name}")
2535
2535
  else:
2536
- print(f"[ERROR] subcomando desconocido: {sub}. Usa 'get' o 'set'.")
2536
+ print(f"[meshcode] ERROR: subcomando desconocido: {sub}. Usa 'get' o 'set'.")
2537
2537
  sys.exit(1)
2538
2538
 
2539
2539
  elif cmd == "connect":
@@ -2721,7 +2721,7 @@ if __name__ == "__main__":
2721
2721
  elif cmd == "login":
2722
2722
  key = sys.argv[2] if len(sys.argv) > 2 else ""
2723
2723
  if not key:
2724
- print("[ERROR] Uso: meshcode login <api_key>")
2724
+ print("[meshcode] ERROR: Uso: meshcode login <api_key>")
2725
2725
  sys.exit(1)
2726
2726
  login(key)
2727
2727
 
@@ -2824,10 +2824,10 @@ if __name__ == "__main__":
2824
2824
  arg = sys.argv[3].lower()
2825
2825
  if arg in ("on", "yes", "y", "true", "1"):
2826
2826
  ok = set_auto_update(True)
2827
- print("[meshcode] auto_update = ON" if ok else "[ERROR] failed to save")
2827
+ print("[meshcode] auto_update = ON" if ok else "[meshcode] ERROR: failed to save")
2828
2828
  elif arg in ("off", "no", "n", "false", "0"):
2829
2829
  ok = set_auto_update(False)
2830
- print("[meshcode] auto_update = OFF" if ok else "[ERROR] failed to save")
2830
+ print("[meshcode] auto_update = OFF" if ok else "[meshcode] ERROR: failed to save")
2831
2831
  elif arg == "reset":
2832
2832
  reset_auto_update()
2833
2833
  print("[meshcode] auto_update preference cleared")
@@ -2845,10 +2845,10 @@ if __name__ == "__main__":
2845
2845
  if len(sys.argv) > 3:
2846
2846
  mode = sys.argv[3].lower()
2847
2847
  if mode not in VALID_PERMISSION_MODES:
2848
- print(f"[ERROR] mode must be one of: {', '.join(sorted(VALID_PERMISSION_MODES))}")
2848
+ print(f"[meshcode] ERROR: mode must be one of: {', '.join(sorted(VALID_PERMISSION_MODES))}")
2849
2849
  sys.exit(1)
2850
2850
  ok = set_permission_mode(mode)
2851
- print(f"[meshcode] permission_mode = {mode}" if ok else "[ERROR] failed to save")
2851
+ print(f"[meshcode] permission_mode = {mode}" if ok else "[meshcode] ERROR: failed to save")
2852
2852
  sys.exit(0 if ok else 1)
2853
2853
  else:
2854
2854
  cur = get_permission_mode()
@@ -266,34 +266,40 @@ def _headers(*, prefer: Optional[str] = None, content_profile: bool = True) -> D
266
266
  return h
267
267
 
268
268
 
269
- def _request(method: str, path: str, *, data: Any = None, prefer: Optional[str] = None) -> Any:
269
+ def _request(method: str, path: str, *, data: Any = None, prefer: Optional[str] = None,
270
+ _max_retries: int = 3) -> Any:
271
+ import random as _random
270
272
  if not _circuit.can_execute():
271
273
  return {"_error": "circuit breaker open — Supabase temporarily unavailable", "_code": 503}
272
274
  rest_path = f"/rest/v1/{path}"
273
275
  body = json.dumps(data).encode("utf-8") if data else None
274
276
  hdrs = _headers(prefer=prefer)
275
- try:
276
- status, raw = _pool.request(method, rest_path, body, hdrs)
277
- if 200 <= status < 300:
278
- _circuit.record_success()
279
- return json.loads(raw) if raw.strip() else None
280
- elif 400 <= status < 500:
281
- _circuit.record_success()
282
- try:
283
- err_obj = json.loads(raw)
284
- return {"_error": err_obj.get("message", raw[:200]), "_code": status}
285
- except Exception:
286
- return {"_error": raw[:200], "_code": status}
287
- else:
288
- _circuit.record_failure()
289
- try:
290
- err_obj = json.loads(raw)
291
- return {"_error": err_obj.get("message", raw[:200]), "_code": status}
292
- except Exception:
293
- return {"_error": raw[:200], "_code": status}
294
- except (URLError, OSError, TimeoutError, http.client.HTTPException) as e:
295
- _circuit.record_failure()
296
- return {"_error": str(getattr(e, 'reason', e)), "_code": 0}
277
+ last_err = None
278
+ for attempt in range(_max_retries):
279
+ try:
280
+ status, raw = _pool.request(method, rest_path, body, hdrs)
281
+ if 200 <= status < 300:
282
+ _circuit.record_success()
283
+ return json.loads(raw) if raw.strip() else None
284
+ elif 400 <= status < 500:
285
+ # Client errors are not transient — don't retry
286
+ _circuit.record_success()
287
+ try:
288
+ err_obj = json.loads(raw)
289
+ return {"_error": err_obj.get("message", raw[:200]), "_code": status}
290
+ except Exception:
291
+ return {"_error": raw[:200], "_code": status}
292
+ else:
293
+ # 5xx transient, retry
294
+ last_err = raw[:200]
295
+ except (URLError, OSError, TimeoutError, http.client.HTTPException) as e:
296
+ last_err = str(getattr(e, 'reason', e))
297
+ # Retry with jitter for transient errors (5xx, network)
298
+ if attempt < _max_retries - 1:
299
+ delay = (2 ** attempt) + _random.uniform(0, 1)
300
+ _time.sleep(delay)
301
+ _circuit.record_failure()
302
+ return {"_error": str(last_err)[:200] if last_err else "request failed after retries", "_code": 0}
297
303
 
298
304
 
299
305
  def sb_select(table: str, filters: str = "", order: Optional[str] = None, limit: Optional[int] = None) -> List[Dict]:
@@ -422,8 +428,10 @@ def sb_rpc_raw(fn_name: str, params: Dict) -> Any:
422
428
  status, raw = _pool.request("POST", rpc_path, body, hdrs)
423
429
  if 200 <= status < 300:
424
430
  return json.loads(raw) if raw.strip() else None
431
+ log.warning("sb_rpc_raw(%s) returned %d: %s", fn_name, status, raw[:200])
425
432
  return None
426
- except Exception:
433
+ except Exception as e:
434
+ log.warning("sb_rpc_raw(%s) failed: %s", fn_name, e)
427
435
  return None
428
436
 
429
437
 
@@ -865,13 +873,16 @@ def encrypt_payload(payload: Dict, hex_key: str, aad: Optional[str] = None) -> s
865
873
  def decrypt_payload(encrypted_b64: str, hex_key: str, aad: Optional[str] = None) -> Optional[Dict]:
866
874
  """Decrypt an AES-256-GCM encrypted payload. Returns None on failure.
867
875
 
868
- Tries with AAD first, falls back to no-AAD for backward compat
869
- with messages encrypted before AAD was added.
876
+ If AAD is provided, it is MANDATORY the message MUST have been
877
+ encrypted with the same AAD. No fallback to no-AAD when AAD is
878
+ present (this prevents swap/replay attacks). Messages without AAD
879
+ (legacy, pre-AAD era) are still decrypted with aad=None.
870
880
  """
871
881
  import base64
872
882
  try:
873
883
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
874
884
  except ImportError:
885
+ log.warning("decrypt_payload: cryptography package not installed")
875
886
  return None
876
887
  try:
877
888
  key = bytes.fromhex(hex_key)
@@ -880,17 +891,12 @@ def decrypt_payload(encrypted_b64: str, hex_key: str, aad: Optional[str] = None)
880
891
  ciphertext = raw[12:]
881
892
  aesgcm = AESGCM(key)
882
893
  aad_bytes = aad.encode("utf-8") if aad else None
883
- # Try with AAD first
884
- if aad_bytes:
885
- try:
886
- plaintext = aesgcm.decrypt(nonce, ciphertext, aad_bytes)
887
- return json.loads(plaintext.decode("utf-8"))
888
- except Exception:
889
- pass # Fall through to no-AAD for backward compat
890
- # Try without AAD (legacy messages)
891
- plaintext = aesgcm.decrypt(nonce, ciphertext, None)
894
+ # AAD is mandatory when provided — no fallback to prevent replay attacks.
895
+ # Legacy messages (aad=None) are decrypted without AAD.
896
+ plaintext = aesgcm.decrypt(nonce, ciphertext, aad_bytes)
892
897
  return json.loads(plaintext.decode("utf-8"))
893
- except Exception:
898
+ except Exception as e:
899
+ log.warning("decrypt_payload failed (aad=%s): %s", aad[:20] if aad else "none", e)
894
900
  return None
895
901
 
896
902
 
@@ -70,6 +70,13 @@ class RealtimeListener:
70
70
  # meshcode_wait awaits this instead of polling -> zero-cost idle.
71
71
  self.message_event: Optional[asyncio.Event] = None
72
72
 
73
+ # Deaf detection: track when we last received ANY event from Realtime
74
+ # (message, task, or heartbeat reply). If this goes stale while the
75
+ # agent is supposedly listening, Realtime is deaf and needs restart.
76
+ import time as _time_mod
77
+ self.last_event_at: float = _time_mod.time()
78
+ self._deaf_restart_count: int = 0
79
+
73
80
  @property
74
81
  def ws_url(self) -> str:
75
82
  host = self.supabase_url.replace("https://", "").replace("http://", "").rstrip("/")
@@ -271,6 +278,8 @@ class RealtimeListener:
271
278
  return
272
279
 
273
280
  async def _handle_message(self, msg: Dict[str, Any]) -> None:
281
+ import time as _time_mod
282
+ self.last_event_at = _time_mod.time()
274
283
  event = msg.get("event")
275
284
  payload = msg.get("payload") or {}
276
285
 
@@ -382,3 +391,18 @@ class RealtimeListener:
382
391
  @property
383
392
  def is_subscribed(self) -> bool:
384
393
  return self._connected and getattr(self, "_subscription_ok", False)
394
+
395
+ @property
396
+ def seconds_since_last_event(self) -> float:
397
+ import time as _time_mod
398
+ return _time_mod.time() - self.last_event_at
399
+
400
+ @property
401
+ def is_deaf(self) -> bool:
402
+ """True if Realtime claims to be subscribed but hasn't received
403
+ any events for >60s. This indicates a silent WebSocket failure."""
404
+ return self.is_subscribed and self.seconds_since_last_event > 60.0
405
+
406
+ @property
407
+ def deaf_restart_count(self) -> int:
408
+ return self._deaf_restart_count
@@ -74,7 +74,7 @@ def _agent_color(name: str) -> str:
74
74
 
75
75
 
76
76
  def _mc_log(msg: str, level: str = "info") -> None:
77
- """Colored [meshcode-mcp] log line. Uses agent color if available.
77
+ """Colored [meshcode] log line. Uses agent color if available.
78
78
 
79
79
  CRITICAL: Must write to stderr, NEVER stdout. MCP protocol uses stdout
80
80
  for JSON-RPC — any non-JSON output to stdout corrupts the stream and
@@ -82,7 +82,7 @@ def _mc_log(msg: str, level: str = "info") -> None:
82
82
  """
83
83
  agent = os.environ.get("MESHCODE_AGENT", "")
84
84
  c = _agent_color(agent) if agent else "\033[36m"
85
- prefix = f"{c}{_ANSI_BOLD}[meshcode-mcp]{_ANSI_RESET}"
85
+ prefix = f"{c}{_ANSI_BOLD}[meshcode]{_ANSI_RESET}"
86
86
  if level == "error":
87
87
  print(f"{prefix} \033[91mERROR:{_ANSI_RESET} {msg}", file=sys.stderr)
88
88
  elif level == "warn":
@@ -368,6 +368,74 @@ def _split_messages(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
368
368
  "count": len(real),
369
369
  }
370
370
 
371
+
372
+ _PAYLOAD_TRUNCATE_LIMIT = 500 # chars — messages longer get truncated
373
+
374
+
375
+ def _compress_message(m: Dict[str, Any]) -> Dict[str, Any]:
376
+ """Compress a single message for token-efficient LLM delivery.
377
+
378
+ - Truncates long text payloads with a hint to use meshcode_read_message
379
+ - Strips null/empty fields (parent_id)
380
+ """
381
+ out: Dict[str, Any] = {"from": m.get("from"), "type": m.get("type", "msg")}
382
+ msg_id = m.get("id")
383
+ if msg_id:
384
+ out["id"] = msg_id
385
+ ts = m.get("ts")
386
+ if ts:
387
+ out["ts"] = ts
388
+ parent = m.get("parent_id")
389
+ if parent:
390
+ out["parent_id"] = parent
391
+ # Truncate large payloads
392
+ payload = m.get("payload", {})
393
+ if isinstance(payload, dict):
394
+ text = payload.get("text")
395
+ if isinstance(text, str) and len(text) > _PAYLOAD_TRUNCATE_LIMIT:
396
+ payload = {**payload, "text": text[:_PAYLOAD_TRUNCATE_LIMIT] + "...",
397
+ "_truncated": True}
398
+ if msg_id:
399
+ payload["_full"] = f"use meshcode_read_message(msg_id='{msg_id}') for full content"
400
+ # Check overall payload size
401
+ import json as _json
402
+ _payload_str = _json.dumps(payload, default=str)
403
+ if len(_payload_str) > _PAYLOAD_TRUNCATE_LIMIT * 2:
404
+ payload = {"_summary": _payload_str[:_PAYLOAD_TRUNCATE_LIMIT] + "...",
405
+ "_truncated": True}
406
+ if msg_id:
407
+ payload["_full"] = f"use meshcode_read_message(msg_id='{msg_id}') for full content"
408
+ out["payload"] = payload
409
+ return out
410
+
411
+
412
+ def _compress_output(result: Dict[str, Any]) -> Dict[str, Any]:
413
+ """Compress the output dict for token efficiency.
414
+
415
+ - Compresses individual messages
416
+ - Omits empty arrays (acks, done_signals)
417
+ - Strips redundant metadata
418
+ """
419
+ out: Dict[str, Any] = {}
420
+ # Copy non-list fields
421
+ for k, v in result.items():
422
+ if k in ("messages", "acks", "done_signals"):
423
+ continue
424
+ if v is not None:
425
+ out[k] = v
426
+ # Compress and include messages
427
+ msgs = result.get("messages", [])
428
+ if msgs:
429
+ out["messages"] = [_compress_message(m) for m in msgs]
430
+ # Only include acks/dones if non-empty
431
+ acks = result.get("acks", [])
432
+ if acks:
433
+ out["acks"] = [_compress_message(m) for m in acks]
434
+ dones = result.get("done_signals", [])
435
+ if dones:
436
+ out["done_signals"] = [_compress_message(m) for m in dones]
437
+ return out
438
+
371
439
  from . import backend as be
372
440
  from .realtime import RealtimeListener
373
441
 
@@ -441,14 +509,14 @@ try:
441
509
  from mcp.server.fastmcp import FastMCP
442
510
  except ImportError:
443
511
  print(
444
- "[meshcode-mcp] ERROR: mcp package not installed. Run: pip install 'mcp[cli]>=1.0'",
512
+ "[meshcode] ERROR: mcp package not installed. Run: pip install 'mcp[cli]>=1.0'",
445
513
  file=sys.stderr,
446
514
  )
447
515
  sys.exit(2)
448
516
 
449
517
  logging.basicConfig(level=logging.INFO, stream=sys.stderr,
450
- format="[meshcode-mcp] %(message)s")
451
- log = logging.getLogger("meshcode-mcp")
518
+ format="[meshcode] %(message)s")
519
+ log = logging.getLogger("meshcode")
452
520
 
453
521
 
454
522
  # ============================================================
@@ -462,7 +530,7 @@ EDITOR_TYPE = os.environ.get("MESHCODE_EDITOR_TYPE", "")
462
530
 
463
531
  if not PROJECT_NAME or not AGENT_NAME:
464
532
  print(
465
- "[meshcode-mcp] ERROR: MESHCODE_PROJECT and MESHCODE_AGENT env vars required",
533
+ "[meshcode] ERROR: MESHCODE_PROJECT and MESHCODE_AGENT env vars required",
466
534
  file=sys.stderr,
467
535
  )
468
536
  sys.exit(2)
@@ -625,6 +693,7 @@ import time as _time
625
693
  _flip_lock = _threading.Lock()
626
694
  _current_state = "online"
627
695
  _last_tool_at = _time.time()
696
+ _last_send_at = 0.0 # timestamp of last message sent (skip heartbeat if recent)
628
697
  _current_tool = ""
629
698
  _IDLE_THRESHOLD_S = 120 # seconds without tool call → IDLE
630
699
  _SLEEPING_THRESHOLD_S = 300 # seconds in waiting without activity → SLEEPING
@@ -1457,6 +1526,22 @@ def _heartbeat_loop_inner():
1457
1526
  asyncio.run_coroutine_threadsafe(_REALTIME.restart(), _MAIN_LOOP)
1458
1527
  except Exception as e:
1459
1528
  log.debug(f"Realtime restart scheduling failed: {e}")
1529
+ elif _REALTIME.is_deaf and in_wait:
1530
+ # DEAF DETECTION: Realtime claims subscribed but no events
1531
+ # for >60s while agent is actively waiting. This is the
1532
+ # "silent deaf" failure mode Samuel flagged. Auto-recover.
1533
+ _deaf_secs = _REALTIME.seconds_since_last_event
1534
+ _REALTIME._deaf_restart_count += 1
1535
+ log.warning(
1536
+ f"DEAF DETECTED: no Realtime events for {_deaf_secs:.0f}s "
1537
+ f"while in_wait=True — forcing restart "
1538
+ f"(deaf_restart #{_REALTIME._deaf_restart_count})"
1539
+ )
1540
+ try:
1541
+ if _MAIN_LOOP and _MAIN_LOOP.is_running():
1542
+ asyncio.run_coroutine_threadsafe(_REALTIME.restart(), _MAIN_LOOP)
1543
+ except Exception as e:
1544
+ log.debug(f"Deaf recovery restart scheduling failed: {e}")
1460
1545
  else:
1461
1546
  log.debug(f"heartbeat ok for {AGENT_NAME}")
1462
1547
  else:
@@ -1478,9 +1563,27 @@ def _heartbeat_loop_inner():
1478
1563
  except Exception as e:
1479
1564
  log.warning(f"lease renewal failed: {e}")
1480
1565
 
1481
- # Adaptive heartbeat: fast for paid plans, moderate for free
1482
- # Free: 15s (~6K req/day/agent). Pro+: 5s (~17K req/day/agent).
1483
- hb_interval = 5 if _PROJECT_PLAN in ("pro", "team", "enterprise", "unlimited") else 15
1566
+ # Adaptive heartbeat intervals based on agent activity state.
1567
+ # Skip heartbeat entirely if a message was sent <5s ago (already proved alive).
1568
+ now = _time.time()
1569
+ since_send = now - _last_send_at
1570
+ if since_send < 5.0:
1571
+ _heartbeat_stop.wait(5.0 - since_send)
1572
+ continue
1573
+
1574
+ # Base interval by plan (free gets slower baseline)
1575
+ base = 5 if _PROJECT_PLAN in ("pro", "team", "enterprise", "unlimited") else 15
1576
+ # Scale up based on idle duration
1577
+ if cur_state in ("working", "waiting"):
1578
+ hb_interval = base # real-time matters
1579
+ elif cur_state == "sleeping":
1580
+ hb_interval = 60
1581
+ elif idle_secs > 120:
1582
+ hb_interval = 30
1583
+ elif idle_secs > 30:
1584
+ hb_interval = 15
1585
+ else:
1586
+ hb_interval = base
1484
1587
  _heartbeat_stop.wait(hb_interval)
1485
1588
 
1486
1589
 
@@ -1623,7 +1726,7 @@ if _DISABLE_LIFESPAN:
1623
1726
  name=f"meshcode-{PROJECT_NAME}-{AGENT_NAME}",
1624
1727
  instructions=_INSTRUCTIONS,
1625
1728
  )
1626
- print("[meshcode-mcp] LIFESPAN DISABLED (MESHCODE_DISABLE_LIFESPAN=1) — diagnostic mode", file=sys.stderr, flush=True)
1729
+ print("[meshcode] LIFESPAN DISABLED (MESHCODE_DISABLE_LIFESPAN=1) — diagnostic mode", file=sys.stderr, flush=True)
1627
1730
  else:
1628
1731
  mcp = FastMCP(
1629
1732
  name=f"meshcode-{PROJECT_NAME}-{AGENT_NAME}",
@@ -1654,7 +1757,7 @@ async def meshcode_debug_sleep(seconds: int = 30) -> Dict[str, Any]:
1654
1757
  await _asyncio.sleep(max(1, int(seconds)))
1655
1758
  return {"ok": True, "slept": int(seconds), "pid": _os.getpid()}
1656
1759
  except BaseException as e:
1657
- sys.stderr.write(f"[meshcode-mcp] debug_sleep cancelled with {type(e).__name__}: {e}\n")
1760
+ sys.stderr.write(f"[meshcode] debug_sleep cancelled with {type(e).__name__}: {e}\n")
1658
1761
  sys.stderr.flush()
1659
1762
  raise
1660
1763
 
@@ -1664,6 +1767,7 @@ async def meshcode_debug_sleep(seconds: int = 30) -> Dict[str, Any]:
1664
1767
  def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1665
1768
  sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1666
1769
  """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. Pass encrypted=True for secrets/credentials (AES-256-GCM)."""
1770
+ global _last_send_at
1667
1771
  if not to or not to.strip():
1668
1772
  return {"error": "recipient 'to' cannot be empty"}
1669
1773
  to = to.strip()
@@ -1719,11 +1823,20 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1719
1823
  })
1720
1824
  if isinstance(result, dict) and result.get("ok") and encrypted:
1721
1825
  result["encrypted"] = True
1826
+ if isinstance(result, dict) and result.get("ok"):
1827
+ _last_send_at = _time.time()
1722
1828
  return result
1723
1829
 
1724
- return be.send_message(_PROJECT_ID, AGENT_NAME, to, payload, msg_type="msg",
1725
- parent_msg_id=in_reply_to, sensitive=sensitive,
1726
- api_key=_get_api_key(), encrypted=encrypted)
1830
+ # Enforce encryption for ALL sensitive messages (within-mesh and cross-mesh)
1831
+ if sensitive and not encrypted:
1832
+ return {"error": "sensitive messages must use encrypted=True"}
1833
+
1834
+ result = be.send_message(_PROJECT_ID, AGENT_NAME, to, payload, msg_type="msg",
1835
+ parent_msg_id=in_reply_to, sensitive=sensitive,
1836
+ api_key=_get_api_key(), encrypted=encrypted)
1837
+ if isinstance(result, dict) and result.get("sent"):
1838
+ _last_send_at = _time.time()
1839
+ return result
1727
1840
 
1728
1841
 
1729
1842
  @mcp.tool()
@@ -1735,6 +1848,7 @@ def meshcode_broadcast(payload: Any) -> Dict[str, Any]:
1735
1848
  - dict (structured): {"type": "...", "detail": "..."}
1736
1849
  - str (shorthand): wrapped internally as {"text": "..."}
1737
1850
  """
1851
+ global _last_send_at
1738
1852
  if isinstance(payload, str):
1739
1853
  payload = {"text": payload}
1740
1854
  elif not isinstance(payload, dict):
@@ -1745,6 +1859,7 @@ def meshcode_broadcast(payload: Any) -> Dict[str, Any]:
1745
1859
  # N-row fanout that caused duplicate-render bugs.
1746
1860
  be.send_message(_PROJECT_ID, AGENT_NAME, "*", payload, msg_type="broadcast",
1747
1861
  api_key=_get_api_key())
1862
+ _last_send_at = _time.time()
1748
1863
  return {"broadcast": True, "to": "*"}
1749
1864
 
1750
1865
 
@@ -2388,7 +2503,7 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2388
2503
  out["got_done"] = True
2389
2504
  out["reason"] = done["reason"]
2390
2505
  out["from"] = done["from"]
2391
- return out
2506
+ return _compress_output(out)
2392
2507
 
2393
2508
  # 1) Drain anything already buffered from before this call.
2394
2509
  if _REALTIME:
@@ -2403,14 +2518,23 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2403
2518
 
2404
2519
  if _rt_live:
2405
2520
  # 2a) Realtime wait with DB safety net.
2406
- # Split into 5s sub-waits. Between each sub-wait, also check DB
2407
- # as a safety net Realtime WS can die silently and is_subscribed
2408
- # stays True, so we must not rely on it exclusively.
2409
- _rt_sub_timeout = 5.0
2521
+ # Split into sub-waits with ADAPTIVE safety-net polling.
2522
+ # When Realtime is healthy (safety net finds nothing), we back off
2523
+ # the DB poll interval exponentially: 5s 10s 20s → 40s (cap).
2524
+ # If safety net catches a message Realtime missed, snap back to 5s.
2525
+ # This reduces DB load from ~48 roundtrips/120s to ~8-10 when healthy.
2526
+ _RT_SAFETY_BASE = 5.0
2527
+ _RT_SAFETY_CAP = 40.0
2528
+ _rt_safety_interval = _RT_SAFETY_BASE
2410
2529
  _rt_elapsed = 0.0
2530
+ _rt_since_last_safety = 0.0
2531
+ _rt_since_last_task_check = 0.0
2532
+ _TASK_CHECK_INTERVAL = 15.0 # check tasks every 15s, not every 5s
2411
2533
  woke = False
2412
2534
  while _rt_elapsed < actual_timeout:
2413
- _this_wait = min(_rt_sub_timeout, actual_timeout - _rt_elapsed)
2535
+ _this_wait = min(_rt_safety_interval - _rt_since_last_safety,
2536
+ actual_timeout - _rt_elapsed, 5.0)
2537
+ _this_wait = max(_this_wait, 1.0)
2414
2538
  try:
2415
2539
  woke = await asyncio.shield(
2416
2540
  _REALTIME.wait_for_message(timeout=_this_wait)
@@ -2425,38 +2549,51 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2425
2549
  if woke:
2426
2550
  break
2427
2551
  _rt_elapsed += _this_wait
2428
- # Check for new tasks between sub-iterations
2429
- _sub_tasks = _get_pending_tasks_summary()
2430
- if _sub_tasks:
2431
- return {"timed_out": False, "got_message": False, "pending_tasks": _sub_tasks, "reason": "task_detected_mid_wait"}
2432
- # DB safety net: check for unread messages even when Realtime
2433
- # claims to be alive. Realtime WS can die silently.
2434
- try:
2435
- _safety_key = _get_api_key()
2436
- if _safety_key:
2437
- _safety_cnt = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_safety_key)
2438
- if _safety_cnt and _safety_cnt > 0:
2439
- log.info(f"[meshcode] DB safety net: {_safety_cnt} unread msgs despite Realtime — reading from DB")
2440
- _safety_raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_safety_key)
2441
- if _safety_raw:
2442
- _safety_msgs = [
2443
- {"from": m["from_agent"], "type": m.get("type", "msg"),
2444
- "ts": m.get("created_at"), "payload": m.get("payload", {}),
2445
- "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
2446
- for m in _safety_raw
2447
- ]
2448
- _filter_and_mark(_safety_msgs)
2449
- _safety_split = _split_messages(_safety_msgs)
2450
- if not include_acks:
2451
- _safety_split["acks"] = []
2452
- if _safety_split["messages"] or _safety_split["done_signals"]:
2453
- return {"got_message": True, **_safety_split}
2454
- except Exception as _db_err:
2455
- log.warning(f"[meshcode] DB safety net error: {_db_err}")
2456
- # Health check: if subscription dropped, switch to DB poll
2552
+ _rt_since_last_safety += _this_wait
2553
+ _rt_since_last_task_check += _this_wait
2554
+ # Check for new tasks at a relaxed interval (15s instead of 5s)
2555
+ if _rt_since_last_task_check >= _TASK_CHECK_INTERVAL:
2556
+ _rt_since_last_task_check = 0.0
2557
+ _sub_tasks = _get_pending_tasks_summary()
2558
+ if _sub_tasks:
2559
+ return {"timed_out": False, "got_message": False, "pending_tasks": _sub_tasks, "reason": "task_detected_mid_wait"}
2560
+ # DB safety net: only poll when the adaptive interval has elapsed
2561
+ if _rt_since_last_safety >= _rt_safety_interval:
2562
+ _rt_since_last_safety = 0.0
2563
+ try:
2564
+ _safety_key = _get_api_key()
2565
+ if _safety_key:
2566
+ _safety_cnt = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_safety_key)
2567
+ if _safety_cnt and _safety_cnt > 0:
2568
+ # Safety net caught something — snap back to base interval
2569
+ _rt_safety_interval = _RT_SAFETY_BASE
2570
+ log.info(f"[meshcode] DB safety net: {_safety_cnt} unread msgs despite Realtime — reading from DB")
2571
+ _safety_raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_safety_key)
2572
+ if _safety_raw:
2573
+ _safety_msgs = [
2574
+ {"from": m["from_agent"], "type": m.get("type", "msg"),
2575
+ "ts": m.get("created_at"), "payload": m.get("payload", {}),
2576
+ "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
2577
+ for m in _safety_raw
2578
+ ]
2579
+ _filter_and_mark(_safety_msgs)
2580
+ _safety_split = _split_messages(_safety_msgs)
2581
+ if not include_acks:
2582
+ _safety_split["acks"] = []
2583
+ if _safety_split["messages"] or _safety_split["done_signals"]:
2584
+ return _compress_output({"got_message": True, **_safety_split})
2585
+ else:
2586
+ # Realtime is healthy — back off safety net interval
2587
+ _rt_safety_interval = min(_rt_safety_interval * 2, _RT_SAFETY_CAP)
2588
+ except Exception as _db_err:
2589
+ log.warning(f"[meshcode] DB safety net error: {_db_err}")
2590
+ # Health check: if subscription dropped or deaf, switch to DB poll
2457
2591
  if not (_REALTIME and _REALTIME.is_subscribed):
2458
2592
  log.info("[meshcode] Realtime subscription lost mid-wait — switching to DB poll")
2459
2593
  break
2594
+ if _REALTIME and _REALTIME.is_deaf:
2595
+ log.warning(f"[meshcode] Realtime deaf mid-wait ({_REALTIME.seconds_since_last_event:.0f}s silent) — switching to DB poll")
2596
+ break
2460
2597
  if woke:
2461
2598
  buffered = _REALTIME.drain()
2462
2599
  if buffered:
@@ -2496,7 +2633,7 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2496
2633
  if not include_acks:
2497
2634
  split["acks"] = []
2498
2635
  if split["messages"] or split["done_signals"]:
2499
- return {"got_message": True, **split}
2636
+ return _compress_output({"got_message": True, **split})
2500
2637
  except Exception as e:
2501
2638
  log.warning(f"DB poll fallback error: {e}")
2502
2639
 
@@ -2520,7 +2657,7 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2520
2657
  if not include_acks:
2521
2658
  split["acks"] = []
2522
2659
  if split["messages"] or split["done_signals"]:
2523
- return {"got_message": True, **split}
2660
+ return _compress_output({"got_message": True, **split})
2524
2661
  except Exception as e:
2525
2662
  log.warning(f"final DB fallback error: {e}")
2526
2663
 
@@ -2631,7 +2768,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark
2631
2768
  pending_tasks = _get_pending_tasks_summary()
2632
2769
  if pending_tasks:
2633
2770
  result["pending_tasks"] = pending_tasks
2634
- return result
2771
+ return _compress_output(result)
2635
2772
 
2636
2773
 
2637
2774
  @mcp.tool()
@@ -3892,7 +4029,7 @@ def run_server():
3892
4029
  """Start the MCP server on stdio (default for Claude Code)."""
3893
4030
  _auto_update()
3894
4031
  print(
3895
- f"[meshcode-mcp] Starting server for {AGENT_NAME}@{PROJECT_NAME}",
4032
+ f"[meshcode] Starting server for {AGENT_NAME}@{PROJECT_NAME}",
3896
4033
  file=sys.stderr,
3897
4034
  )
3898
4035
  # Restore real stdout for FastMCP's JSON-RPC transport.
@@ -3927,7 +4064,7 @@ def run_server():
3927
4064
  except Exception:
3928
4065
  name = f"signal-{signum}"
3929
4066
  try:
3930
- sys.stderr.write(f"[meshcode-mcp] received {name} ({signum}); ignored to keep MCP alive during tool cancellation\n")
4067
+ sys.stderr.write(f"[meshcode] received {name} ({signum}); ignored to keep MCP alive during tool cancellation\n")
3931
4068
  sys.stderr.flush()
3932
4069
  except Exception:
3933
4070
  pass
@@ -3971,7 +4108,7 @@ def run_server():
3971
4108
  _restart_count += 1
3972
4109
  try:
3973
4110
  sys.stderr.write(
3974
- f"[meshcode-mcp] mcp.run() raised {type(_e).__name__}: {_e} "
4111
+ f"[meshcode] mcp.run() raised {type(_e).__name__}: {_e} "
3975
4112
  f"(restart #{_restart_count}); re-entering event loop in 0.3s\n"
3976
4113
  )
3977
4114
  sys.stderr.flush()
@@ -3979,7 +4116,7 @@ def run_server():
3979
4116
  pass
3980
4117
  if _restart_count > 20:
3981
4118
  try:
3982
- sys.stderr.write("[meshcode-mcp] too many restarts; exiting\n")
4119
+ sys.stderr.write("[meshcode] too many restarts; exiting\n")
3983
4120
  except Exception:
3984
4121
  pass
3985
4122
  break
@@ -4011,7 +4148,7 @@ def run_server():
4011
4148
  pass
4012
4149
  try:
4013
4150
  sys.stderr.write(
4014
- "[meshcode-mcp] stdin EOF — Claude Code closed pipe (ESC, "
4151
+ "[meshcode] stdin EOF — Claude Code closed pipe (ESC, "
4015
4152
  "/mcp disconnect, or exit). State persisted to disk; next "
4016
4153
  "MCP spawn will resume seamlessly. Exiting cleanly.\n"
4017
4154
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.54
3
+ Version: 2.10.55
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.10.54"
7
+ version = "2.10.55"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes