meshcode 2.10.53__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.53 → meshcode-2.10.55}/PKG-INFO +1 -1
  2. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/cli.py +1 -1
  4. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/comms_v4.py +31 -31
  5. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/backend.py +42 -36
  6. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/realtime.py +24 -0
  7. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/server.py +313 -61
  8. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/PKG-INFO +1 -1
  9. {meshcode-2.10.53 → meshcode-2.10.55}/pyproject.toml +1 -1
  10. {meshcode-2.10.53 → meshcode-2.10.55}/README.md +0 -0
  11. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/ascii_art.py +0 -0
  12. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/invites.py +0 -0
  13. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/launcher.py +0 -0
  14. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/launcher_install.py +0 -0
  15. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.53 → meshcode-2.10.55}/setup.cfg +0 -0
  32. {meshcode-2.10.53 → 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.53
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.53"
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
@@ -721,6 +790,52 @@ def with_working_status(func):
721
790
  # to prevent cascade through FastMCP into the event loop.
722
791
  # Return an error dict instead of propagating BaseException.
723
792
  log.debug(f"[meshcode] tool {name} cancelled by client (ESC)")
793
+ # 2.10.54 deaf-agent fix: for meshcode_wait specifically, the
794
+ # inner cancel handler already tries to drain pending messages.
795
+ # If we still got CancelledError here, the inner drain failed —
796
+ # do a last-ditch DB read so the LLM sees pending mail instead
797
+ # of a generic error and goes silent.
798
+ if name == "meshcode_wait":
799
+ try:
800
+ _t = asyncio.current_task()
801
+ if _t is not None and hasattr(_t, "uncancel"):
802
+ _t.uncancel()
803
+ except Exception:
804
+ pass
805
+ try:
806
+ _ak = _get_api_key()
807
+ if _ak:
808
+ _raw = be.read_inbox(
809
+ _PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_ak
810
+ )
811
+ if _raw:
812
+ _msgs = [
813
+ {
814
+ "from": m["from_agent"],
815
+ "type": m.get("type", "msg"),
816
+ "ts": m.get("created_at"),
817
+ "payload": m.get("payload", {}),
818
+ "id": m.get("id"),
819
+ "parent_id": m.get("parent_msg_id"),
820
+ }
821
+ for m in _raw
822
+ ]
823
+ _filter_and_mark(_msgs)
824
+ _split = _split_messages(_msgs)
825
+ if _split["messages"] or _split["done_signals"]:
826
+ log.info(
827
+ f"[meshcode] wrapper-level cancel-drain rescued "
828
+ f"{_split['count']} messages"
829
+ )
830
+ return {
831
+ "got_message": True,
832
+ "cancelled_during_wait": True,
833
+ **_split,
834
+ }
835
+ except Exception as _final_err:
836
+ log.debug(
837
+ f"[meshcode] wrapper cancel-drain failed: {_final_err}"
838
+ )
724
839
  return {"error": "cancelled_by_client", "tool": name}
725
840
  except Exception as e:
726
841
  if not skip:
@@ -1411,6 +1526,22 @@ def _heartbeat_loop_inner():
1411
1526
  asyncio.run_coroutine_threadsafe(_REALTIME.restart(), _MAIN_LOOP)
1412
1527
  except Exception as e:
1413
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}")
1414
1545
  else:
1415
1546
  log.debug(f"heartbeat ok for {AGENT_NAME}")
1416
1547
  else:
@@ -1432,9 +1563,27 @@ def _heartbeat_loop_inner():
1432
1563
  except Exception as e:
1433
1564
  log.warning(f"lease renewal failed: {e}")
1434
1565
 
1435
- # Adaptive heartbeat: fast for paid plans, moderate for free
1436
- # Free: 15s (~6K req/day/agent). Pro+: 5s (~17K req/day/agent).
1437
- 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
1438
1587
  _heartbeat_stop.wait(hb_interval)
1439
1588
 
1440
1589
 
@@ -1577,7 +1726,7 @@ if _DISABLE_LIFESPAN:
1577
1726
  name=f"meshcode-{PROJECT_NAME}-{AGENT_NAME}",
1578
1727
  instructions=_INSTRUCTIONS,
1579
1728
  )
1580
- 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)
1581
1730
  else:
1582
1731
  mcp = FastMCP(
1583
1732
  name=f"meshcode-{PROJECT_NAME}-{AGENT_NAME}",
@@ -1608,7 +1757,7 @@ async def meshcode_debug_sleep(seconds: int = 30) -> Dict[str, Any]:
1608
1757
  await _asyncio.sleep(max(1, int(seconds)))
1609
1758
  return {"ok": True, "slept": int(seconds), "pid": _os.getpid()}
1610
1759
  except BaseException as e:
1611
- 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")
1612
1761
  sys.stderr.flush()
1613
1762
  raise
1614
1763
 
@@ -1618,6 +1767,7 @@ async def meshcode_debug_sleep(seconds: int = 30) -> Dict[str, Any]:
1618
1767
  def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1619
1768
  sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1620
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
1621
1771
  if not to or not to.strip():
1622
1772
  return {"error": "recipient 'to' cannot be empty"}
1623
1773
  to = to.strip()
@@ -1673,11 +1823,20 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1673
1823
  })
1674
1824
  if isinstance(result, dict) and result.get("ok") and encrypted:
1675
1825
  result["encrypted"] = True
1826
+ if isinstance(result, dict) and result.get("ok"):
1827
+ _last_send_at = _time.time()
1676
1828
  return result
1677
1829
 
1678
- return be.send_message(_PROJECT_ID, AGENT_NAME, to, payload, msg_type="msg",
1679
- parent_msg_id=in_reply_to, sensitive=sensitive,
1680
- 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
1681
1840
 
1682
1841
 
1683
1842
  @mcp.tool()
@@ -1689,6 +1848,7 @@ def meshcode_broadcast(payload: Any) -> Dict[str, Any]:
1689
1848
  - dict (structured): {"type": "...", "detail": "..."}
1690
1849
  - str (shorthand): wrapped internally as {"text": "..."}
1691
1850
  """
1851
+ global _last_send_at
1692
1852
  if isinstance(payload, str):
1693
1853
  payload = {"text": payload}
1694
1854
  elif not isinstance(payload, dict):
@@ -1699,6 +1859,7 @@ def meshcode_broadcast(payload: Any) -> Dict[str, Any]:
1699
1859
  # N-row fanout that caused duplicate-render bugs.
1700
1860
  be.send_message(_PROJECT_ID, AGENT_NAME, "*", payload, msg_type="broadcast",
1701
1861
  api_key=_get_api_key())
1862
+ _last_send_at = _time.time()
1702
1863
  return {"broadcast": True, "to": "*"}
1703
1864
 
1704
1865
 
@@ -2120,10 +2281,79 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
2120
2281
  try:
2121
2282
  result = await _meshcode_wait_inner(actual_timeout=capped_timeout, include_acks=include_acks)
2122
2283
  except asyncio.CancelledError:
2123
- # Safety net: if CancelledError escapes _meshcode_wait_inner
2124
- # despite the inner shield, catch it here to prevent cascade.
2125
- log.debug("[meshcode] meshcode_wait outer loop caught CancelledError")
2126
- result = {"timed_out": True, "reason": "cancelled_by_client"}
2284
+ # 2.10.54 deaf-agent fix: when ESC fires during meshcode_wait,
2285
+ # Python keeps the task in cancelled state every subsequent
2286
+ # await re-raises CancelledError, even if we caught the first
2287
+ # one. That trapped us in a loop that eventually returned
2288
+ # `{"error": "cancelled_by_client"}` to the LLM, which never
2289
+ # saw the messages that arrived during the cancel window —
2290
+ # they sat in the queue/DB unread (the "deaf agents" bug).
2291
+ #
2292
+ # Fix: uncancel the current task so we can run a final drain
2293
+ # cycle without re-raising. Then aggressively pull from BOTH
2294
+ # the realtime buffer AND the DB and return whatever we find
2295
+ # to the LLM. If nothing is pending, return cancelled-timeout
2296
+ # cleanly so the LLM's wait loop continues normally.
2297
+ log.info("[meshcode] meshcode_wait caught CancelledError — uncancelling + final drain")
2298
+ try:
2299
+ _t = asyncio.current_task()
2300
+ if _t is not None and hasattr(_t, "uncancel"):
2301
+ _t.uncancel()
2302
+ except Exception:
2303
+ pass
2304
+
2305
+ _final_msgs: List[Dict[str, Any]] = []
2306
+ # 1) Drain realtime buffer (messages received during cancel window)
2307
+ if _REALTIME:
2308
+ try:
2309
+ _rt_buf = _REALTIME.drain()
2310
+ if _rt_buf:
2311
+ _final_msgs.extend(_rt_buf)
2312
+ except Exception as _drain_err:
2313
+ log.debug(f"[meshcode] cancel-drain realtime failed: {_drain_err}")
2314
+ # 2) Pull from DB (catches messages realtime missed)
2315
+ try:
2316
+ _api_key = _get_api_key()
2317
+ if _api_key:
2318
+ _db_unread = be.read_inbox(
2319
+ _PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_api_key
2320
+ )
2321
+ if _db_unread:
2322
+ _final_msgs.extend([
2323
+ {
2324
+ "from": m["from_agent"],
2325
+ "type": m.get("type", "msg"),
2326
+ "ts": m.get("created_at"),
2327
+ "payload": m.get("payload", {}),
2328
+ "id": m.get("id"),
2329
+ "parent_id": m.get("parent_msg_id"),
2330
+ }
2331
+ for m in _db_unread
2332
+ ])
2333
+ except Exception as _db_err:
2334
+ log.debug(f"[meshcode] cancel-drain DB failed: {_db_err}")
2335
+
2336
+ if _final_msgs:
2337
+ # Dedupe + filter, then return as got_message so the LLM
2338
+ # processes them instead of seeing a generic cancel error.
2339
+ _filter_and_mark(_final_msgs)
2340
+ _split = _split_messages(_final_msgs)
2341
+ if not include_acks:
2342
+ _split["acks"] = []
2343
+ if _split["messages"] or _split["done_signals"]:
2344
+ log.info(
2345
+ f"[meshcode] cancel-drain rescued {_split['count']} pending "
2346
+ f"messages from going deaf"
2347
+ )
2348
+ result = {
2349
+ "got_message": True,
2350
+ "cancelled_during_wait": True,
2351
+ **_split,
2352
+ }
2353
+ else:
2354
+ result = {"timed_out": True, "reason": "cancelled_by_client"}
2355
+ else:
2356
+ result = {"timed_out": True, "reason": "cancelled_by_client"}
2127
2357
 
2128
2358
  if result.get("got_message"):
2129
2359
  # Real message arrived — return to agent for processing
@@ -2273,7 +2503,7 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2273
2503
  out["got_done"] = True
2274
2504
  out["reason"] = done["reason"]
2275
2505
  out["from"] = done["from"]
2276
- return out
2506
+ return _compress_output(out)
2277
2507
 
2278
2508
  # 1) Drain anything already buffered from before this call.
2279
2509
  if _REALTIME:
@@ -2288,14 +2518,23 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2288
2518
 
2289
2519
  if _rt_live:
2290
2520
  # 2a) Realtime wait with DB safety net.
2291
- # Split into 5s sub-waits. Between each sub-wait, also check DB
2292
- # as a safety net Realtime WS can die silently and is_subscribed
2293
- # stays True, so we must not rely on it exclusively.
2294
- _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
2295
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
2296
2533
  woke = False
2297
2534
  while _rt_elapsed < actual_timeout:
2298
- _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)
2299
2538
  try:
2300
2539
  woke = await asyncio.shield(
2301
2540
  _REALTIME.wait_for_message(timeout=_this_wait)
@@ -2310,38 +2549,51 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2310
2549
  if woke:
2311
2550
  break
2312
2551
  _rt_elapsed += _this_wait
2313
- # Check for new tasks between sub-iterations
2314
- _sub_tasks = _get_pending_tasks_summary()
2315
- if _sub_tasks:
2316
- return {"timed_out": False, "got_message": False, "pending_tasks": _sub_tasks, "reason": "task_detected_mid_wait"}
2317
- # DB safety net: check for unread messages even when Realtime
2318
- # claims to be alive. Realtime WS can die silently.
2319
- try:
2320
- _safety_key = _get_api_key()
2321
- if _safety_key:
2322
- _safety_cnt = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_safety_key)
2323
- if _safety_cnt and _safety_cnt > 0:
2324
- log.info(f"[meshcode] DB safety net: {_safety_cnt} unread msgs despite Realtime — reading from DB")
2325
- _safety_raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_safety_key)
2326
- if _safety_raw:
2327
- _safety_msgs = [
2328
- {"from": m["from_agent"], "type": m.get("type", "msg"),
2329
- "ts": m.get("created_at"), "payload": m.get("payload", {}),
2330
- "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
2331
- for m in _safety_raw
2332
- ]
2333
- _filter_and_mark(_safety_msgs)
2334
- _safety_split = _split_messages(_safety_msgs)
2335
- if not include_acks:
2336
- _safety_split["acks"] = []
2337
- if _safety_split["messages"] or _safety_split["done_signals"]:
2338
- return {"got_message": True, **_safety_split}
2339
- except Exception as _db_err:
2340
- log.warning(f"[meshcode] DB safety net error: {_db_err}")
2341
- # 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
2342
2591
  if not (_REALTIME and _REALTIME.is_subscribed):
2343
2592
  log.info("[meshcode] Realtime subscription lost mid-wait — switching to DB poll")
2344
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
2345
2597
  if woke:
2346
2598
  buffered = _REALTIME.drain()
2347
2599
  if buffered:
@@ -2381,7 +2633,7 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2381
2633
  if not include_acks:
2382
2634
  split["acks"] = []
2383
2635
  if split["messages"] or split["done_signals"]:
2384
- return {"got_message": True, **split}
2636
+ return _compress_output({"got_message": True, **split})
2385
2637
  except Exception as e:
2386
2638
  log.warning(f"DB poll fallback error: {e}")
2387
2639
 
@@ -2405,7 +2657,7 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2405
2657
  if not include_acks:
2406
2658
  split["acks"] = []
2407
2659
  if split["messages"] or split["done_signals"]:
2408
- return {"got_message": True, **split}
2660
+ return _compress_output({"got_message": True, **split})
2409
2661
  except Exception as e:
2410
2662
  log.warning(f"final DB fallback error: {e}")
2411
2663
 
@@ -2516,7 +2768,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark
2516
2768
  pending_tasks = _get_pending_tasks_summary()
2517
2769
  if pending_tasks:
2518
2770
  result["pending_tasks"] = pending_tasks
2519
- return result
2771
+ return _compress_output(result)
2520
2772
 
2521
2773
 
2522
2774
  @mcp.tool()
@@ -3777,7 +4029,7 @@ def run_server():
3777
4029
  """Start the MCP server on stdio (default for Claude Code)."""
3778
4030
  _auto_update()
3779
4031
  print(
3780
- f"[meshcode-mcp] Starting server for {AGENT_NAME}@{PROJECT_NAME}",
4032
+ f"[meshcode] Starting server for {AGENT_NAME}@{PROJECT_NAME}",
3781
4033
  file=sys.stderr,
3782
4034
  )
3783
4035
  # Restore real stdout for FastMCP's JSON-RPC transport.
@@ -3812,7 +4064,7 @@ def run_server():
3812
4064
  except Exception:
3813
4065
  name = f"signal-{signum}"
3814
4066
  try:
3815
- 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")
3816
4068
  sys.stderr.flush()
3817
4069
  except Exception:
3818
4070
  pass
@@ -3856,7 +4108,7 @@ def run_server():
3856
4108
  _restart_count += 1
3857
4109
  try:
3858
4110
  sys.stderr.write(
3859
- f"[meshcode-mcp] mcp.run() raised {type(_e).__name__}: {_e} "
4111
+ f"[meshcode] mcp.run() raised {type(_e).__name__}: {_e} "
3860
4112
  f"(restart #{_restart_count}); re-entering event loop in 0.3s\n"
3861
4113
  )
3862
4114
  sys.stderr.flush()
@@ -3864,7 +4116,7 @@ def run_server():
3864
4116
  pass
3865
4117
  if _restart_count > 20:
3866
4118
  try:
3867
- sys.stderr.write("[meshcode-mcp] too many restarts; exiting\n")
4119
+ sys.stderr.write("[meshcode] too many restarts; exiting\n")
3868
4120
  except Exception:
3869
4121
  pass
3870
4122
  break
@@ -3896,7 +4148,7 @@ def run_server():
3896
4148
  pass
3897
4149
  try:
3898
4150
  sys.stderr.write(
3899
- "[meshcode-mcp] stdin EOF — Claude Code closed pipe (ESC, "
4151
+ "[meshcode] stdin EOF — Claude Code closed pipe (ESC, "
3900
4152
  "/mcp disconnect, or exit). State persisted to disk; next "
3901
4153
  "MCP spawn will resume seamlessly. Exiting cleanly.\n"
3902
4154
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.53
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.53"
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