meshcode 2.10.17__tar.gz → 2.10.19__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.17 → meshcode-2.10.19}/PKG-INFO +1 -1
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/__init__.py +1 -1
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/meshcode_mcp/backend.py +110 -40
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/meshcode_mcp/server.py +45 -22
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.17 → meshcode-2.10.19}/pyproject.toml +1 -1
- {meshcode-2.10.17 → meshcode-2.10.19}/README.md +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/cli.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/invites.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/launcher.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/preferences.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/secrets.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/self_update.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/setup.cfg +0 -0
- {meshcode-2.10.17 → meshcode-2.10.19}/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.19"
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"""Thin Supabase REST backend used by both the MCP server and tests.
|
|
2
2
|
|
|
3
3
|
Reuses the helpers from comms_v4.py without going through subprocess.
|
|
4
|
-
Zero deps beyond stdlib (urllib).
|
|
4
|
+
Zero deps beyond stdlib (urllib + http.client for connection pooling).
|
|
5
5
|
"""
|
|
6
|
+
import http.client
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
9
|
+
import ssl
|
|
8
10
|
import time as _time
|
|
9
11
|
import threading as _threading
|
|
10
12
|
from datetime import datetime
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Any, Dict, List, Optional
|
|
13
15
|
from urllib.error import HTTPError, URLError
|
|
14
|
-
from urllib.parse import quote
|
|
16
|
+
from urllib.parse import quote, urlparse
|
|
15
17
|
from urllib.request import Request, urlopen
|
|
16
18
|
|
|
17
19
|
|
|
@@ -97,6 +99,74 @@ SUPABASE_SERVICE_ROLE_KEY = (
|
|
|
97
99
|
SCHEMA = "meshcode"
|
|
98
100
|
|
|
99
101
|
|
|
102
|
+
# ── Persistent HTTPS Connection Pool ──────────────────────────────
|
|
103
|
+
# Each urlopen() opens a new TCP+TLS connection (~1-2s overhead).
|
|
104
|
+
# This pool reuses connections, cutting typical latency from ~3s to ~100ms.
|
|
105
|
+
class _ConnectionPool:
|
|
106
|
+
"""Thread-safe persistent HTTPS connection to Supabase."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, url: str, max_idle: float = 30.0):
|
|
109
|
+
parsed = urlparse(url)
|
|
110
|
+
self._host = parsed.hostname
|
|
111
|
+
self._port = parsed.port or 443
|
|
112
|
+
self._conn: Optional[http.client.HTTPSConnection] = None
|
|
113
|
+
self._lock = _threading.Lock()
|
|
114
|
+
self._max_idle = max_idle
|
|
115
|
+
self._last_used = 0.0
|
|
116
|
+
self._ctx = ssl.create_default_context()
|
|
117
|
+
|
|
118
|
+
def _get_conn(self) -> http.client.HTTPSConnection:
|
|
119
|
+
now = _time.monotonic()
|
|
120
|
+
if self._conn is not None:
|
|
121
|
+
# Close stale connections
|
|
122
|
+
if now - self._last_used > self._max_idle:
|
|
123
|
+
try:
|
|
124
|
+
self._conn.close()
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
self._conn = None
|
|
128
|
+
if self._conn is None:
|
|
129
|
+
self._conn = http.client.HTTPSConnection(
|
|
130
|
+
self._host, self._port, timeout=10, context=self._ctx
|
|
131
|
+
)
|
|
132
|
+
return self._conn
|
|
133
|
+
|
|
134
|
+
def request(self, method: str, path: str, body: Optional[bytes], headers: Dict[str, str]) -> tuple:
|
|
135
|
+
"""Returns (status, response_body_str). Thread-safe with retry on broken pipe."""
|
|
136
|
+
with self._lock:
|
|
137
|
+
for attempt in range(2):
|
|
138
|
+
conn = self._get_conn()
|
|
139
|
+
try:
|
|
140
|
+
conn.request(method, path, body=body, headers=headers)
|
|
141
|
+
resp = conn.getresponse()
|
|
142
|
+
data = resp.read().decode("utf-8")
|
|
143
|
+
self._last_used = _time.monotonic()
|
|
144
|
+
return resp.status, data
|
|
145
|
+
except (http.client.RemoteDisconnected, BrokenPipeError,
|
|
146
|
+
ConnectionResetError, OSError) as e:
|
|
147
|
+
# Connection went stale — close and retry once
|
|
148
|
+
try:
|
|
149
|
+
self._conn.close()
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
self._conn = None
|
|
153
|
+
if attempt == 0:
|
|
154
|
+
continue
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
def close(self):
|
|
158
|
+
with self._lock:
|
|
159
|
+
if self._conn:
|
|
160
|
+
try:
|
|
161
|
+
self._conn.close()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
self._conn = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
_pool = _ConnectionPool(SUPABASE_URL)
|
|
168
|
+
|
|
169
|
+
|
|
100
170
|
def _now_iso() -> str:
|
|
101
171
|
return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
|
102
172
|
|
|
@@ -118,27 +188,29 @@ def _headers(*, prefer: Optional[str] = None, content_profile: bool = True) -> D
|
|
|
118
188
|
def _request(method: str, path: str, *, data: Any = None, prefer: Optional[str] = None) -> Any:
|
|
119
189
|
if not _circuit.can_execute():
|
|
120
190
|
return {"_error": "circuit breaker open — Supabase temporarily unavailable", "_code": 503}
|
|
121
|
-
|
|
191
|
+
rest_path = f"/rest/v1/{path}"
|
|
122
192
|
body = json.dumps(data).encode("utf-8") if data else None
|
|
123
|
-
|
|
193
|
+
hdrs = _headers(prefer=prefer)
|
|
124
194
|
try:
|
|
125
|
-
|
|
126
|
-
|
|
195
|
+
status, raw = _pool.request(method, rest_path, body, hdrs)
|
|
196
|
+
if 200 <= status < 300:
|
|
127
197
|
_circuit.record_success()
|
|
128
198
|
return json.loads(raw) if raw.strip() else None
|
|
129
|
-
|
|
130
|
-
err = e.read().decode("utf-8", errors="replace")
|
|
131
|
-
# 4xx = client error (not a backend failure), don't trip breaker
|
|
132
|
-
if 400 <= e.code < 500:
|
|
199
|
+
elif 400 <= status < 500:
|
|
133
200
|
_circuit.record_success()
|
|
201
|
+
try:
|
|
202
|
+
err_obj = json.loads(raw)
|
|
203
|
+
return {"_error": err_obj.get("message", raw[:200]), "_code": status}
|
|
204
|
+
except Exception:
|
|
205
|
+
return {"_error": raw[:200], "_code": status}
|
|
134
206
|
else:
|
|
135
207
|
_circuit.record_failure()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
except (URLError, OSError, TimeoutError) as e:
|
|
208
|
+
try:
|
|
209
|
+
err_obj = json.loads(raw)
|
|
210
|
+
return {"_error": err_obj.get("message", raw[:200]), "_code": status}
|
|
211
|
+
except Exception:
|
|
212
|
+
return {"_error": raw[:200], "_code": status}
|
|
213
|
+
except (URLError, OSError, TimeoutError, http.client.HTTPException) as e:
|
|
142
214
|
_circuit.record_failure()
|
|
143
215
|
return {"_error": str(getattr(e, 'reason', e)), "_code": 0}
|
|
144
216
|
|
|
@@ -200,34 +272,31 @@ def sb_rpc(fn_name: str, params: Dict, *, _max_retries: int = 3) -> Any:
|
|
|
200
272
|
if not _circuit.can_execute():
|
|
201
273
|
return {"_error": "circuit breaker open — Supabase temporarily unavailable", "_circuit": "open"}
|
|
202
274
|
last_err = None
|
|
275
|
+
rpc_path = f"/rest/v1/rpc/{fn_name}"
|
|
276
|
+
body = json.dumps(params).encode("utf-8")
|
|
277
|
+
hdrs = _headers(content_profile=False)
|
|
203
278
|
for attempt in range(_max_retries):
|
|
204
|
-
url = f"{SUPABASE_URL}/rest/v1/rpc/{fn_name}"
|
|
205
|
-
body = json.dumps(params).encode("utf-8")
|
|
206
|
-
req = Request(url, data=body, method="POST", headers=_headers(content_profile=False))
|
|
207
279
|
try:
|
|
208
|
-
|
|
209
|
-
|
|
280
|
+
status, raw = _pool.request("POST", rpc_path, body, hdrs)
|
|
281
|
+
if 200 <= status < 300:
|
|
210
282
|
result = json.loads(raw) if raw.strip() else None
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
except HTTPError as e:
|
|
217
|
-
err = e.read().decode("utf-8", errors="replace")
|
|
218
|
-
# 4xx errors are not transient — don't retry, don't trip breaker
|
|
219
|
-
if 400 <= e.code < 500:
|
|
283
|
+
_circuit.record_success()
|
|
284
|
+
if _recording_enabled and fn_name not in _SKIP_RECORDING:
|
|
285
|
+
_bg_record("tool_call", {"rpc": fn_name})
|
|
286
|
+
return result
|
|
287
|
+
elif 400 <= status < 500:
|
|
220
288
|
_circuit.record_success()
|
|
221
289
|
try:
|
|
222
|
-
result = {"_error": json.loads(
|
|
290
|
+
result = {"_error": json.loads(raw).get("message", raw[:200])}
|
|
223
291
|
except Exception:
|
|
224
|
-
result = {"_error":
|
|
292
|
+
result = {"_error": raw[:200]}
|
|
225
293
|
if _recording_enabled and fn_name not in _SKIP_RECORDING:
|
|
226
|
-
_bg_record("error", {"rpc": fn_name, "error":
|
|
294
|
+
_bg_record("error", {"rpc": fn_name, "error": raw[:200]})
|
|
227
295
|
return result
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
296
|
+
else:
|
|
297
|
+
_circuit.record_failure()
|
|
298
|
+
last_err = raw[:200]
|
|
299
|
+
except (URLError, OSError, TimeoutError, http.client.HTTPException) as e:
|
|
231
300
|
_circuit.record_failure()
|
|
232
301
|
last_err = str(getattr(e, 'reason', e))
|
|
233
302
|
# Retry with jitter for transient errors (5xx, network)
|
|
@@ -260,13 +329,14 @@ def _bg_record(event_type: str, payload: dict):
|
|
|
260
329
|
|
|
261
330
|
def sb_rpc_raw(fn_name: str, params: Dict) -> Any:
|
|
262
331
|
"""Raw RPC call without recording (to avoid infinite recursion)."""
|
|
263
|
-
|
|
332
|
+
rpc_path = f"/rest/v1/rpc/{fn_name}"
|
|
264
333
|
body = json.dumps(params).encode("utf-8")
|
|
265
|
-
|
|
334
|
+
hdrs = _headers(content_profile=False)
|
|
266
335
|
try:
|
|
267
|
-
|
|
268
|
-
|
|
336
|
+
status, raw = _pool.request("POST", rpc_path, body, hdrs)
|
|
337
|
+
if 200 <= status < 300:
|
|
269
338
|
return json.loads(raw) if raw.strip() else None
|
|
339
|
+
return None
|
|
270
340
|
except Exception:
|
|
271
341
|
return None
|
|
272
342
|
|
|
@@ -85,11 +85,11 @@ def _mc_log(msg: str, level: str = "info") -> None:
|
|
|
85
85
|
c = _agent_color(agent) if agent else "\033[36m"
|
|
86
86
|
prefix = f"{c}{_ANSI_BOLD}[meshcode-mcp]{_ANSI_RESET}"
|
|
87
87
|
if level == "error":
|
|
88
|
-
print(f"{prefix} \033[91mERROR:{_ANSI_RESET} {msg}",
|
|
88
|
+
print(f"{prefix} \033[91mERROR:{_ANSI_RESET} {msg}", file=sys.stderr)
|
|
89
89
|
elif level == "warn":
|
|
90
|
-
print(f"{prefix} \033[33mWARNING:{_ANSI_RESET} {msg}",
|
|
90
|
+
print(f"{prefix} \033[33mWARNING:{_ANSI_RESET} {msg}", file=sys.stderr)
|
|
91
91
|
else:
|
|
92
|
-
print(f"{prefix} {c}{msg}{_ANSI_RESET}",
|
|
92
|
+
print(f"{prefix} {c}{msg}{_ANSI_RESET}", file=sys.stderr)
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
# ============================================================
|
|
@@ -576,6 +576,11 @@ def with_working_status(func):
|
|
|
576
576
|
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
|
|
577
577
|
try:
|
|
578
578
|
return await func(*args, **kwargs)
|
|
579
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
580
|
+
# User pressed ESC or MCP client cancelled the request.
|
|
581
|
+
# Return clean dict — never propagate BaseException to FastMCP.
|
|
582
|
+
_mc_log(f"tool {name} cancelled by client", "warn")
|
|
583
|
+
return {"cancelled": True, "tool": name}
|
|
579
584
|
except Exception as e:
|
|
580
585
|
if not skip:
|
|
581
586
|
_auto_learn_error(name, e, list(kwargs.keys()))
|
|
@@ -713,7 +718,7 @@ def _boot_diagnostic() -> None:
|
|
|
713
718
|
be.sb_select("mc_projects", f"id=eq.{_PROJECT_ID}", limit=1)
|
|
714
719
|
checks_passed += 1
|
|
715
720
|
except Exception as e:
|
|
716
|
-
print(f"[meshcode] BOOT CHECK FAILED: Supabase API unreachable ({e}). Fix: check network/VPN.",
|
|
721
|
+
print(f"[meshcode] BOOT CHECK FAILED: Supabase API unreachable ({e}). Fix: check network/VPN.", file=sys.stderr)
|
|
717
722
|
|
|
718
723
|
# Check 2: Lease valid
|
|
719
724
|
try:
|
|
@@ -723,11 +728,11 @@ def _boot_diagnostic() -> None:
|
|
|
723
728
|
if agent.get("instance_id") == _INSTANCE_ID:
|
|
724
729
|
checks_passed += 1
|
|
725
730
|
else:
|
|
726
|
-
print(f"[meshcode] BOOT CHECK FAILED: Lease mismatch — expected {_INSTANCE_ID}, got {agent.get('instance_id')}. Fix: restart agent.",
|
|
731
|
+
print(f"[meshcode] BOOT CHECK FAILED: Lease mismatch — expected {_INSTANCE_ID}, got {agent.get('instance_id')}. Fix: restart agent.", file=sys.stderr)
|
|
727
732
|
else:
|
|
728
|
-
print(f"[meshcode] BOOT CHECK FAILED: Agent '{AGENT_NAME}' not found in project. Fix: register agent first.",
|
|
733
|
+
print(f"[meshcode] BOOT CHECK FAILED: Agent '{AGENT_NAME}' not found in project. Fix: register agent first.", file=sys.stderr)
|
|
729
734
|
except Exception as e:
|
|
730
|
-
print(f"[meshcode] BOOT CHECK FAILED: Could not verify lease ({e}).",
|
|
735
|
+
print(f"[meshcode] BOOT CHECK FAILED: Could not verify lease ({e}).", file=sys.stderr)
|
|
731
736
|
|
|
732
737
|
# Check 3: Heartbeat recent
|
|
733
738
|
try:
|
|
@@ -736,7 +741,7 @@ def _boot_diagnostic() -> None:
|
|
|
736
741
|
if hb:
|
|
737
742
|
checks_passed += 1
|
|
738
743
|
else:
|
|
739
|
-
print(f"[meshcode] BOOT CHECK WARNING: No heartbeat recorded yet.",
|
|
744
|
+
print(f"[meshcode] BOOT CHECK WARNING: No heartbeat recorded yet.", file=sys.stderr)
|
|
740
745
|
else:
|
|
741
746
|
checks_passed += 1 # skip if no agent data
|
|
742
747
|
except Exception:
|
|
@@ -753,9 +758,9 @@ def _boot_diagnostic() -> None:
|
|
|
753
758
|
checks_passed += 1 # non-critical
|
|
754
759
|
|
|
755
760
|
if checks_passed == checks_total:
|
|
756
|
-
print(f"[meshcode] All boot checks passed ({checks_passed}/{checks_total}).",
|
|
761
|
+
print(f"[meshcode] All boot checks passed ({checks_passed}/{checks_total}).", file=sys.stderr)
|
|
757
762
|
else:
|
|
758
|
-
print(f"[meshcode] Boot checks: {checks_passed}/{checks_total} passed. Agent starting anyway.",
|
|
763
|
+
print(f"[meshcode] Boot checks: {checks_passed}/{checks_total} passed. Agent starting anyway.", file=sys.stderr)
|
|
759
764
|
|
|
760
765
|
|
|
761
766
|
_boot_diagnostic()
|
|
@@ -783,9 +788,11 @@ _SHUTDOWN_LOGGED = False
|
|
|
783
788
|
def _log_crash_to_db(reason: str = "unknown", error_detail: str = "") -> None:
|
|
784
789
|
"""Best-effort crash log to mc_agent_crash_logs table. Non-fatal if table doesn't exist."""
|
|
785
790
|
global _SHUTDOWN_LOGGED
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
791
|
+
# Only suppress duplicates for process-level shutdown events, not tool exceptions
|
|
792
|
+
if reason in ("process_exit", "signal", "keyboard_interrupt", "system_exit", "unhandled_exception"):
|
|
793
|
+
if _SHUTDOWN_LOGGED:
|
|
794
|
+
return
|
|
795
|
+
_SHUTDOWN_LOGGED = True
|
|
789
796
|
try:
|
|
790
797
|
be.sb_rpc("mc_log_error", {
|
|
791
798
|
"p_api_key": _get_api_key(),
|
|
@@ -812,15 +819,24 @@ def _on_exit() -> None:
|
|
|
812
819
|
|
|
813
820
|
|
|
814
821
|
def _on_signal(signum, frame) -> None:
|
|
815
|
-
"""Signal handler for SIGTERM/SIGINT —
|
|
822
|
+
"""Signal handler for SIGTERM/SIGINT — graceful shutdown.
|
|
823
|
+
|
|
824
|
+
CRITICAL: Do NOT call sys.exit() here. sys.exit() inside a signal handler
|
|
825
|
+
can corrupt the asyncio event loop (raises SystemExit mid-await), which
|
|
826
|
+
crashes FastMCP's stdio transport. Instead, just log + release lease and
|
|
827
|
+
let the normal shutdown path handle process exit.
|
|
828
|
+
"""
|
|
816
829
|
sig_name = signal.Signals(signum).name if hasattr(signal, 'Signals') else str(signum)
|
|
830
|
+
_mc_log(f"Received {sig_name} — releasing lease", "warn")
|
|
817
831
|
_log_crash_to_db("signal", f"Received {sig_name}")
|
|
818
832
|
_release_lease()
|
|
819
|
-
sys.exit(
|
|
833
|
+
# Do NOT sys.exit() — let FastMCP's event loop shut down cleanly.
|
|
834
|
+
# The process will exit naturally when the event loop finishes.
|
|
820
835
|
|
|
821
836
|
|
|
822
837
|
atexit.register(_on_exit)
|
|
823
838
|
signal.signal(signal.SIGTERM, _on_signal)
|
|
839
|
+
signal.signal(signal.SIGINT, _on_signal)
|
|
824
840
|
|
|
825
841
|
|
|
826
842
|
# ============================================================
|
|
@@ -1462,9 +1478,9 @@ try:
|
|
|
1462
1478
|
elif isinstance(_ls_val, str):
|
|
1463
1479
|
_LAST_SEEN_TS = _ls_val
|
|
1464
1480
|
if _LAST_SEEN_TS:
|
|
1465
|
-
print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from mesh memory.",
|
|
1481
|
+
print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from mesh memory.", file=sys.stderr)
|
|
1466
1482
|
except Exception as _e:
|
|
1467
|
-
print(f"[meshcode] Could not restore last_seen: {_e}",
|
|
1483
|
+
print(f"[meshcode] Could not restore last_seen: {_e}", file=sys.stderr)
|
|
1468
1484
|
|
|
1469
1485
|
|
|
1470
1486
|
def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
@@ -1615,6 +1631,13 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1615
1631
|
except Exception:
|
|
1616
1632
|
pass
|
|
1617
1633
|
return result
|
|
1634
|
+
except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
|
|
1635
|
+
# User pressed ESC or Claude Code cancelled the tool call.
|
|
1636
|
+
# Return a clean dict instead of propagating — this prevents
|
|
1637
|
+
# FastMCP/anyio event loop corruption and keeps the MCP server alive.
|
|
1638
|
+
_mc_log("meshcode_wait cancelled by client (ESC) — returning cleanly", "warn")
|
|
1639
|
+
_set_state("online", "interrupted")
|
|
1640
|
+
return {"cancelled": True, "reason": "Tool call cancelled by user"}
|
|
1618
1641
|
finally:
|
|
1619
1642
|
_IN_WAIT = False
|
|
1620
1643
|
|
|
@@ -2624,7 +2647,7 @@ def _auto_update() -> None:
|
|
|
2624
2647
|
return
|
|
2625
2648
|
|
|
2626
2649
|
# 3. Install the new version (blocking, 60s timeout)
|
|
2627
|
-
print(f"[meshcode] Updating {current} → {latest}...",
|
|
2650
|
+
print(f"[meshcode] Updating {current} → {latest}...", file=sys.stderr)
|
|
2628
2651
|
try:
|
|
2629
2652
|
result = subprocess.run(
|
|
2630
2653
|
[sys.executable, "-m", "pip", "install", "--upgrade",
|
|
@@ -2644,11 +2667,11 @@ def _auto_update() -> None:
|
|
|
2644
2667
|
# 4. In MCP mode, NEVER re-exec — it kills the stdio pipe to Claude Code.
|
|
2645
2668
|
# The new version will load on the next clean boot.
|
|
2646
2669
|
if os.environ.get("MESHCODE_MCP_SERVE") == "1":
|
|
2647
|
-
print(f"[meshcode] Updated {current} → {latest}. Will load on next boot (MCP mode — cannot restart).",
|
|
2670
|
+
print(f"[meshcode] Updated {current} → {latest}. Will load on next boot (MCP mode — cannot restart).", file=sys.stderr)
|
|
2648
2671
|
return
|
|
2649
2672
|
|
|
2650
2673
|
# CLI mode: safe to re-exec
|
|
2651
|
-
print(f"[meshcode] Updated to {latest}, restarting...",
|
|
2674
|
+
print(f"[meshcode] Updated to {latest}, restarting...", file=sys.stderr)
|
|
2652
2675
|
os.environ["MESHCODE_UPDATED"] = "1"
|
|
2653
2676
|
try:
|
|
2654
2677
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
@@ -2683,6 +2706,6 @@ def run_server():
|
|
|
2683
2706
|
import traceback as _tb
|
|
2684
2707
|
tb_str = _tb.format_exc()
|
|
2685
2708
|
_log_crash_to_db("unhandled_exception", f"{type(e).__name__}: {e}\n{tb_str}")
|
|
2686
|
-
print(f"[meshcode-mcp] FATAL: {e}",
|
|
2687
|
-
print(f"[meshcode-mcp] Stack trace logged to mc_agent_crash_logs",
|
|
2709
|
+
print(f"[meshcode-mcp] FATAL: {e}", file=sys.stderr)
|
|
2710
|
+
print(f"[meshcode-mcp] Stack trace logged to mc_agent_crash_logs", file=sys.stderr)
|
|
2688
2711
|
sys.exit(1)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|