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.
Files changed (32) hide show
  1. {meshcode-2.10.16 → meshcode-2.10.18}/PKG-INFO +1 -1
  2. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/backend.py +110 -40
  4. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/server.py +21 -15
  5. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.10.16 → meshcode-2.10.18}/pyproject.toml +1 -1
  7. {meshcode-2.10.16 → meshcode-2.10.18}/README.md +0 -0
  8. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/ascii_art.py +0 -0
  9. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/cli.py +0 -0
  10. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/comms_v4.py +0 -0
  11. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/invites.py +0 -0
  12. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/launcher.py +0 -0
  13. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/realtime.py +0 -0
  17. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.16 → meshcode-2.10.18}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.16 → meshcode-2.10.18}/setup.cfg +0 -0
  32. {meshcode-2.10.16 → meshcode-2.10.18}/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.16
3
+ Version: 2.10.18
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.16"
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
- url = f"{SUPABASE_URL}/rest/v1/{path}"
191
+ rest_path = f"/rest/v1/{path}"
122
192
  body = json.dumps(data).encode("utf-8") if data else None
123
- req = Request(url, data=body, method=method, headers=_headers(prefer=prefer))
193
+ hdrs = _headers(prefer=prefer)
124
194
  try:
125
- with urlopen(req, timeout=10) as resp:
126
- raw = resp.read().decode("utf-8")
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
- except HTTPError as e:
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
- try:
137
- err_obj = json.loads(err)
138
- return {"_error": err_obj.get("message", err[:200]), "_code": e.code}
139
- except Exception:
140
- return {"_error": err[:200], "_code": e.code}
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
- with urlopen(req, timeout=10) as resp:
209
- raw = resp.read().decode("utf-8")
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
- _circuit.record_success()
212
- # Auto-record tool calls to session events (hot-reloadable)
213
- if _recording_enabled and fn_name not in _SKIP_RECORDING:
214
- _bg_record("tool_call", {"rpc": fn_name})
215
- return result
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(err).get("message", err[:200])}
290
+ result = {"_error": json.loads(raw).get("message", raw[:200])}
223
291
  except Exception:
224
- result = {"_error": err[:200]}
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": str(err)[:200]})
294
+ _bg_record("error", {"rpc": fn_name, "error": raw[:200]})
227
295
  return result
228
- _circuit.record_failure()
229
- last_err = err
230
- except (URLError, OSError, TimeoutError) as e:
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
- url = f"{SUPABASE_URL}/rest/v1/rpc/{fn_name}"
332
+ rpc_path = f"/rest/v1/rpc/{fn_name}"
264
333
  body = json.dumps(params).encode("utf-8")
265
- req = Request(url, data=body, method="POST", headers=_headers(content_profile=False))
334
+ hdrs = _headers(content_profile=False)
266
335
  try:
267
- with urlopen(req, timeout=10) as resp:
268
- raw = resp.read().decode("utf-8")
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}", file=sys.stderr)
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']}", file=sys.stderr)
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}", file=sys.stderr)
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...", file=sys.stderr)
424
- _time.sleep(_wait)
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']}", file=sys.stderr)
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", file=sys.stderr)
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}", file=sys.stderr)
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')}", file=sys.stderr)
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}", file=sys.stderr)
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", file=sys.stderr)
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
- if _SHUTDOWN_LOGGED:
787
- return
788
- _SHUTDOWN_LOGGED = True
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}", file=sys.stderr)
811
+ _mc_log(f" crash logged: {reason}", "warn")
806
812
 
807
813
 
808
814
  def _on_exit() -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.16
3
+ Version: 2.10.18
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.16"
7
+ version = "2.10.18"
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
File without changes