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.
- {meshcode-2.10.53 → meshcode-2.10.55}/PKG-INFO +1 -1
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/__init__.py +1 -1
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/cli.py +1 -1
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/comms_v4.py +31 -31
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/backend.py +42 -36
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/realtime.py +24 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/server.py +313 -61
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.53 → meshcode-2.10.55}/pyproject.toml +1 -1
- {meshcode-2.10.53 → meshcode-2.10.55}/README.md +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/invites.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/launcher.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/preferences.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/secrets.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/self_update.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/setup.cfg +0 -0
- {meshcode-2.10.53 → meshcode-2.10.55}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.10.
|
|
2
|
+
__version__ = "2.10.55"
|
|
@@ -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"[
|
|
123
|
+
print(f"[meshcode] ERROR: {method} {path}: {e.code} {msg}", file=sys.stderr)
|
|
124
124
|
except Exception:
|
|
125
|
-
print(f"[
|
|
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"[
|
|
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"[
|
|
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("[
|
|
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"[
|
|
1006
|
+
print(f"[meshcode] ERROR: {rpc_result['error']}")
|
|
1007
1007
|
if rpc_result.get("upgrade_url"):
|
|
1008
|
-
print(f"[
|
|
1008
|
+
print(f"[meshcode] ERROR: Upgrade: {rpc_result['upgrade_url']}")
|
|
1009
1009
|
if rpc_result.get("current") is not None:
|
|
1010
|
-
print(f"[
|
|
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"[
|
|
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"[
|
|
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("[
|
|
1795
|
-
print("[
|
|
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"[
|
|
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"[
|
|
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"[
|
|
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"[
|
|
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("[
|
|
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("[
|
|
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"[
|
|
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"[
|
|
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"[
|
|
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"[
|
|
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("[
|
|
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"[
|
|
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"[
|
|
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"[
|
|
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("[
|
|
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 "[
|
|
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 "[
|
|
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"[
|
|
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 "[
|
|
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
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
869
|
-
with
|
|
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
|
-
#
|
|
884
|
-
|
|
885
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
451
|
-
log = logging.getLogger("meshcode
|
|
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
|
|
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
|
|
1436
|
-
#
|
|
1437
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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
|
-
#
|
|
2124
|
-
#
|
|
2125
|
-
|
|
2126
|
-
|
|
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
|
|
2292
|
-
#
|
|
2293
|
-
#
|
|
2294
|
-
|
|
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(
|
|
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
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|