meshcode 2.10.16__tar.gz → 2.10.18__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.16 → meshcode-2.10.18}/PKG-INFO +1 -1
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/__init__.py +1 -1
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/backend.py +110 -40
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/server.py +21 -15
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.16 → meshcode-2.10.18}/pyproject.toml +1 -1
- {meshcode-2.10.16 → meshcode-2.10.18}/README.md +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/cli.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/invites.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/launcher.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/preferences.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/secrets.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/self_update.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/setup.cfg +0 -0
- {meshcode-2.10.16 → meshcode-2.10.18}/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.18"
|
|
@@ -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
|
|
|
@@ -386,7 +386,7 @@ def _get_api_key() -> str:
|
|
|
386
386
|
_API_KEY_CACHE = kc_val
|
|
387
387
|
return kc_val
|
|
388
388
|
except Exception as e:
|
|
389
|
-
_mc_log(f" keychain lookup failed for profile '{profile}': {e}",
|
|
389
|
+
_mc_log(f" keychain lookup failed for profile '{profile}': {e}", "warn")
|
|
390
390
|
_API_KEY_CACHE = ""
|
|
391
391
|
return ""
|
|
392
392
|
|
|
@@ -411,17 +411,17 @@ if not _PROJECT_ID:
|
|
|
411
411
|
_PROJECT_ID = _r["project_id"]
|
|
412
412
|
break
|
|
413
413
|
elif isinstance(_r, dict) and _r.get("error"):
|
|
414
|
-
_mc_log(f" mc_resolve_project: {_r['error']}",
|
|
414
|
+
_mc_log(f" mc_resolve_project: {_r['error']}", "warn")
|
|
415
415
|
except Exception as _e:
|
|
416
|
-
_mc_log(f" mc_resolve_project failed: {_e}",
|
|
416
|
+
_mc_log(f" mc_resolve_project failed: {_e}", "warn")
|
|
417
417
|
if not _PROJECT_ID:
|
|
418
418
|
_PROJECT_ID = be.get_project_id(PROJECT_NAME)
|
|
419
419
|
if _PROJECT_ID:
|
|
420
420
|
break
|
|
421
421
|
if _boot_attempt < _BOOT_MAX_RETRIES - 1:
|
|
422
422
|
_wait = _BOOT_BACKOFF[_boot_attempt]
|
|
423
|
-
_mc_log(f" project resolution failed (attempt {_boot_attempt+1}/{_BOOT_MAX_RETRIES}), retrying in {_wait}s...",
|
|
424
|
-
|
|
423
|
+
_mc_log(f" project resolution failed (attempt {_boot_attempt+1}/{_BOOT_MAX_RETRIES}), retrying in {_wait}s...", "warn")
|
|
424
|
+
import time; time.sleep(_wait)
|
|
425
425
|
if not _PROJECT_ID:
|
|
426
426
|
_mc_log(f"project '{PROJECT_NAME}' not found after {_BOOT_MAX_RETRIES} attempts (check MESHCODE_KEYCHAIN_PROFILE / MESHCODE_API_KEY)", "error")
|
|
427
427
|
sys.exit(2)
|
|
@@ -437,7 +437,7 @@ except Exception:
|
|
|
437
437
|
|
|
438
438
|
_register_result = be.register_agent(PROJECT_NAME, AGENT_NAME, AGENT_ROLE or "MCP-connected agent", api_key=_get_api_key())
|
|
439
439
|
if isinstance(_register_result, dict) and _register_result.get("error"):
|
|
440
|
-
_mc_log(f" register failed: {_register_result['error']}",
|
|
440
|
+
_mc_log(f" register failed: {_register_result['error']}", "warn")
|
|
441
441
|
|
|
442
442
|
# ── Fetch profile color from dashboard (single source of truth) ──
|
|
443
443
|
try:
|
|
@@ -471,7 +471,7 @@ def _flip_status(status: str, task: str = "") -> bool:
|
|
|
471
471
|
return False
|
|
472
472
|
|
|
473
473
|
if not _flip_status("idle", ""):
|
|
474
|
-
_mc_log(f" could not flip status to idle",
|
|
474
|
+
_mc_log(f" could not flip status to idle", "warn")
|
|
475
475
|
|
|
476
476
|
|
|
477
477
|
# ============================================================
|
|
@@ -576,6 +576,10 @@ 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:
|
|
580
|
+
# User pressed ESC or MCP client cancelled the request.
|
|
581
|
+
# Let it propagate cleanly — FastMCP handles cancellation.
|
|
582
|
+
raise
|
|
579
583
|
except Exception as e:
|
|
580
584
|
if not skip:
|
|
581
585
|
_auto_learn_error(name, e, list(kwargs.keys()))
|
|
@@ -636,7 +640,7 @@ def _acquire_lease() -> bool:
|
|
|
636
640
|
})
|
|
637
641
|
except Exception as e:
|
|
638
642
|
# Non-fatal: RPC might not exist on older servers.
|
|
639
|
-
_mc_log(f"stale-lease pre-clean skipped: {e}",
|
|
643
|
+
_mc_log(f"stale-lease pre-clean skipped: {e}", "warn")
|
|
640
644
|
for attempt in range(3):
|
|
641
645
|
try:
|
|
642
646
|
r = be.sb_rpc("mc_acquire_agent_lease", {
|
|
@@ -688,14 +692,14 @@ def _acquire_lease() -> bool:
|
|
|
688
692
|
_mc_log(f"Could not start — agent '{AGENT_NAME}' is running in another window.", "error")
|
|
689
693
|
_mc_log("Close the other window first, or use a different agent name.", "error")
|
|
690
694
|
return False
|
|
691
|
-
_mc_log(f"lease attempt {attempt+1}: {r.get('error')}",
|
|
695
|
+
_mc_log(f"lease attempt {attempt+1}: {r.get('error')}", "warn")
|
|
692
696
|
else:
|
|
693
697
|
return True
|
|
694
698
|
except Exception as e:
|
|
695
|
-
_mc_log(f"lease attempt {attempt+1} failed: {e}",
|
|
699
|
+
_mc_log(f"lease attempt {attempt+1} failed: {e}", "warn")
|
|
696
700
|
if attempt < 2:
|
|
697
701
|
_time.sleep(2)
|
|
698
|
-
_mc_log(f" lease failed after 3 attempts — proceeding anyway",
|
|
702
|
+
_mc_log(f" lease failed after 3 attempts — proceeding anyway", "warn")
|
|
699
703
|
return True
|
|
700
704
|
|
|
701
705
|
if not _acquire_lease():
|
|
@@ -783,9 +787,11 @@ _SHUTDOWN_LOGGED = False
|
|
|
783
787
|
def _log_crash_to_db(reason: str = "unknown", error_detail: str = "") -> None:
|
|
784
788
|
"""Best-effort crash log to mc_agent_crash_logs table. Non-fatal if table doesn't exist."""
|
|
785
789
|
global _SHUTDOWN_LOGGED
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
790
|
+
# Only suppress duplicates for process-level shutdown events, not tool exceptions
|
|
791
|
+
if reason in ("process_exit", "signal", "keyboard_interrupt", "system_exit", "unhandled_exception"):
|
|
792
|
+
if _SHUTDOWN_LOGGED:
|
|
793
|
+
return
|
|
794
|
+
_SHUTDOWN_LOGGED = True
|
|
789
795
|
try:
|
|
790
796
|
be.sb_rpc("mc_log_error", {
|
|
791
797
|
"p_api_key": _get_api_key(),
|
|
@@ -802,7 +808,7 @@ def _log_crash_to_db(reason: str = "unknown", error_detail: str = "") -> None:
|
|
|
802
808
|
f"crashed: {reason[:100]}", api_key=_get_api_key())
|
|
803
809
|
except Exception:
|
|
804
810
|
pass
|
|
805
|
-
_mc_log(f" crash logged: {reason}",
|
|
811
|
+
_mc_log(f" crash logged: {reason}", "warn")
|
|
806
812
|
|
|
807
813
|
|
|
808
814
|
def _on_exit() -> None:
|
|
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
|