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.
- {meshcode-2.6.8 → meshcode-2.7.0}/PKG-INFO +1 -1
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/__init__.py +1 -1
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/backend.py +8 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/realtime.py +40 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/server.py +37 -5
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.6.8 → meshcode-2.7.0}/pyproject.toml +1 -1
- {meshcode-2.6.8 → meshcode-2.7.0}/README.md +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/cli.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/comms_v4.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/invites.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/launcher.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/launcher_install.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/preferences.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/run_agent.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/secrets.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/self_update.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode/setup_clients.py +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/setup.cfg +0 -0
- {meshcode-2.6.8 → meshcode-2.7.0}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.
|
|
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
|
-
|
|
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
|
|
1538
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|