meshcode 2.6.8__tar.gz → 2.7.0__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 (31) hide show
  1. {meshcode-2.6.8 → meshcode-2.7.0}/PKG-INFO +1 -1
  2. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/__init__.py +1 -1
  3. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/backend.py +8 -0
  4. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/realtime.py +40 -0
  5. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/server.py +37 -5
  6. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.6.8 → meshcode-2.7.0}/pyproject.toml +1 -1
  8. {meshcode-2.6.8 → meshcode-2.7.0}/README.md +0 -0
  9. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/cli.py +0 -0
  10. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/comms_v4.py +0 -0
  11. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/invites.py +0 -0
  12. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/launcher.py +0 -0
  13. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  19. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/preferences.py +0 -0
  20. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/protocol_v2.py +0 -0
  21. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/run_agent.py +0 -0
  22. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/secrets.py +0 -0
  23. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/self_update.py +0 -0
  24. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/setup_clients.py +0 -0
  25. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/SOURCES.txt +0 -0
  26. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/dependency_links.txt +0 -0
  27. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/entry_points.txt +0 -0
  28. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/requires.txt +0 -0
  29. {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.6.8 → meshcode-2.7.0}/setup.cfg +0 -0
  31. {meshcode-2.6.8 → meshcode-2.7.0}/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.6.8
3
+ Version: 2.7.0
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.6.8"
2
+ __version__ = "2.7.0"
@@ -36,6 +36,14 @@ def _load_env_file() -> Dict[str, str]:
36
36
  _env_file = _load_env_file()
37
37
  SUPABASE_URL = os.environ.get("SUPABASE_URL") or _env_file.get("SUPABASE_URL") or _DEFAULT_SUPABASE_URL
38
38
  SUPABASE_KEY = os.environ.get("SUPABASE_KEY") or _env_file.get("SUPABASE_KEY") or _DEFAULT_SUPABASE_KEY
39
+ SUPABASE_SERVICE_ROLE_KEY = (
40
+ os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
41
+ or os.environ.get("SUPABASE_SECRET_KEY")
42
+ or _env_file.get("SUPABASE_SERVICE_ROLE_KEY")
43
+ or _env_file.get("SUPABASE_SECRET_KEY")
44
+ or _env_file.get("NEW_SUPABASE_SECRET")
45
+ or ""
46
+ )
39
47
  SCHEMA = "meshcode"
40
48
 
41
49
 
@@ -44,12 +44,14 @@ class RealtimeListener:
44
44
  project_id: str,
45
45
  agent_name: str,
46
46
  notify_callback: Optional[Callable[[Dict], Awaitable[None]]] = None,
47
+ service_role_key: Optional[str] = None,
47
48
  ):
48
49
  self.supabase_url = supabase_url
49
50
  self.supabase_key = supabase_key
50
51
  self.project_id = project_id
51
52
  self.agent_name = agent_name
52
53
  self.notify_callback = notify_callback
54
+ self.service_role_key = service_role_key
53
55
 
54
56
  # Last 100 unread messages — drained by meshcode_check tool
55
57
  self.queue: Deque[Dict] = deque(maxlen=100)
@@ -127,6 +129,21 @@ class RealtimeListener:
127
129
  self._connected = True
128
130
  log.info(f"Realtime connected for agent={self.agent_name}")
129
131
 
132
+ # Elevate auth context if service_role_key is available.
133
+ # This lets Supabase Realtime bypass RLS for INSERT event delivery,
134
+ # so mc_user_has_project_access (which needs auth.uid()) is not required.
135
+ if self.service_role_key:
136
+ try:
137
+ await ws.send(json.dumps({
138
+ "topic": "realtime:*",
139
+ "event": "access_token",
140
+ "payload": {"access_token": self.service_role_key},
141
+ "ref": "auth",
142
+ }))
143
+ log.info("Realtime auth elevated with service_role_key")
144
+ except Exception as e:
145
+ log.warning(f"Failed to send access_token: {e}")
146
+
130
147
  # Phoenix channel join: phoenix realtime topic
131
148
  topic = f"realtime:{self.project_id}-{self.agent_name}"
132
149
  join_msg = {
@@ -148,6 +165,25 @@ class RealtimeListener:
148
165
  }
149
166
  await ws.send(json.dumps(join_msg))
150
167
 
168
+ # Wait for phx_reply to confirm subscription was accepted.
169
+ self._subscription_ok = False
170
+ try:
171
+ reply_raw = await asyncio.wait_for(ws.recv(), timeout=10.0)
172
+ reply = json.loads(reply_raw)
173
+ reply_status = (reply.get("payload") or {}).get("status")
174
+ if reply_status == "ok":
175
+ self._subscription_ok = True
176
+ log.info(f"Realtime subscription OK for {self.agent_name} on {topic}")
177
+ else:
178
+ log.error(
179
+ f"Realtime subscription FAILED for {self.agent_name}: "
180
+ f"status={reply_status} payload={reply.get('payload')}"
181
+ )
182
+ except asyncio.TimeoutError:
183
+ log.error(f"Realtime subscription TIMEOUT — no phx_reply in 10s for {self.agent_name}")
184
+ except Exception as e:
185
+ log.error(f"Realtime subscription error reading phx_reply: {e}")
186
+
151
187
  # Heartbeat task to keep the connection alive
152
188
  heartbeat_task = asyncio.create_task(self._heartbeat(ws))
153
189
  try:
@@ -247,3 +283,7 @@ class RealtimeListener:
247
283
  @property
248
284
  def is_connected(self) -> bool:
249
285
  return self._connected
286
+
287
+ @property
288
+ def is_subscribed(self) -> bool:
289
+ return self._connected and getattr(self, "_subscription_ok", False)
@@ -1040,6 +1040,7 @@ async def lifespan(_app):
1040
1040
  project_id=_PROJECT_ID,
1041
1041
  agent_name=AGENT_NAME,
1042
1042
  notify_callback=_on_new_message,
1043
+ service_role_key=be.SUPABASE_SERVICE_ROLE_KEY or None,
1043
1044
  )
1044
1045
  await _REALTIME.start()
1045
1046
 
@@ -1525,7 +1526,11 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
1525
1526
  if shaped:
1526
1527
  return shaped
1527
1528
 
1528
- # 2) Real async wait zero CPU, zero Supabase calls.
1529
+ # Determine if realtime is actually delivering events.
1530
+ _rt_live = _REALTIME and _REALTIME.is_subscribed
1531
+
1532
+ if _rt_live:
1533
+ # 2a) Real async wait — zero CPU, zero Supabase calls.
1529
1534
  woke = await _REALTIME.wait_for_message(timeout=float(actual_timeout))
1530
1535
  if woke:
1531
1536
  buffered = _REALTIME.drain()
@@ -1534,11 +1539,37 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
1534
1539
  if shaped:
1535
1540
  return shaped
1536
1541
  else:
1537
- # Realtime unavailableplain sleep fallback so we still honor timeout.
1538
- await asyncio.sleep(actual_timeout)
1542
+ # 2b) Realtime NOT subscribed aggressive DB polling every 5s
1543
+ # so messages arrive within seconds, not after 120s timeout.
1544
+ _poll_interval = 5
1545
+ _elapsed = 0
1546
+ while _elapsed < actual_timeout:
1547
+ await asyncio.sleep(min(_poll_interval, actual_timeout - _elapsed))
1548
+ _elapsed += _poll_interval
1549
+ try:
1550
+ api_key = _get_api_key()
1551
+ if api_key:
1552
+ db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=api_key)
1553
+ if db_pending and db_pending > 0:
1554
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=api_key)
1555
+ if raw:
1556
+ msgs = [
1557
+ {"from": m["from_agent"], "type": m.get("type", "msg"),
1558
+ "ts": m.get("created_at"), "payload": m.get("payload", {}),
1559
+ "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
1560
+ for m in raw
1561
+ ]
1562
+ deduped = _filter_and_mark(msgs)
1563
+ if deduped:
1564
+ split = _split_messages(deduped)
1565
+ if not include_acks:
1566
+ split["acks"] = []
1567
+ if split["messages"] or split["done_signals"]:
1568
+ return {"got_message": True, "source": "db_poll_fallback", **split}
1569
+ except Exception:
1570
+ pass
1539
1571
 
1540
- # Fallback: poll DB for messages that realtime may have missed
1541
- # (connection drop, startup race, etc.)
1572
+ # Final fallback: one last DB check (covers realtime path missing msgs)
1542
1573
  try:
1543
1574
  api_key = _get_api_key()
1544
1575
  if api_key:
@@ -1647,6 +1678,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
1647
1678
  "agent": AGENT_NAME,
1648
1679
  "project": PROJECT_NAME,
1649
1680
  "realtime_connected": _REALTIME.is_connected if _REALTIME else False,
1681
+ "realtime_subscribed": _REALTIME.is_subscribed if _REALTIME else False,
1650
1682
  **split,
1651
1683
  }
1652
1684
  # Auto-inject pending tasks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.6.8
3
+ Version: 2.7.0
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.6.8"
7
+ version = "2.7.0"
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
File without changes
File without changes
File without changes
File without changes
File without changes