meshcode 2.6.7__tar.gz → 2.6.9__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.7 → meshcode-2.6.9}/PKG-INFO +1 -1
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/__init__.py +1 -1
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/realtime.py +23 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/server.py +87 -47
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.6.7 → meshcode-2.6.9}/pyproject.toml +1 -1
- {meshcode-2.6.7 → meshcode-2.6.9}/README.md +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/cli.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/comms_v4.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/invites.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/launcher.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/launcher_install.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/preferences.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/run_agent.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/secrets.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/self_update.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/setup_clients.py +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/setup.cfg +0 -0
- {meshcode-2.6.7 → meshcode-2.6.9}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.6.
|
|
2
|
+
__version__ = "2.6.9"
|
|
@@ -148,6 +148,25 @@ class RealtimeListener:
|
|
|
148
148
|
}
|
|
149
149
|
await ws.send(json.dumps(join_msg))
|
|
150
150
|
|
|
151
|
+
# Wait for phx_reply to confirm subscription was accepted.
|
|
152
|
+
self._subscription_ok = False
|
|
153
|
+
try:
|
|
154
|
+
reply_raw = await asyncio.wait_for(ws.recv(), timeout=10.0)
|
|
155
|
+
reply = json.loads(reply_raw)
|
|
156
|
+
reply_status = (reply.get("payload") or {}).get("status")
|
|
157
|
+
if reply_status == "ok":
|
|
158
|
+
self._subscription_ok = True
|
|
159
|
+
log.info(f"Realtime subscription OK for {self.agent_name} on {topic}")
|
|
160
|
+
else:
|
|
161
|
+
log.error(
|
|
162
|
+
f"Realtime subscription FAILED for {self.agent_name}: "
|
|
163
|
+
f"status={reply_status} payload={reply.get('payload')}"
|
|
164
|
+
)
|
|
165
|
+
except asyncio.TimeoutError:
|
|
166
|
+
log.error(f"Realtime subscription TIMEOUT — no phx_reply in 10s for {self.agent_name}")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
log.error(f"Realtime subscription error reading phx_reply: {e}")
|
|
169
|
+
|
|
151
170
|
# Heartbeat task to keep the connection alive
|
|
152
171
|
heartbeat_task = asyncio.create_task(self._heartbeat(ws))
|
|
153
172
|
try:
|
|
@@ -247,3 +266,7 @@ class RealtimeListener:
|
|
|
247
266
|
@property
|
|
248
267
|
def is_connected(self) -> bool:
|
|
249
268
|
return self._connected
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def is_subscribed(self) -> bool:
|
|
272
|
+
return self._connected and getattr(self, "_subscription_ok", False)
|
|
@@ -1335,10 +1335,16 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
|
1335
1335
|
@mcp.tool()
|
|
1336
1336
|
@with_working_status
|
|
1337
1337
|
async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False) -> Dict[str, Any]:
|
|
1338
|
-
"""Block until a mesh message arrives or
|
|
1338
|
+
"""Block until a mesh message arrives or a task needs attention.
|
|
1339
|
+
|
|
1340
|
+
INTERNAL LOOP: This function loops internally and only returns when
|
|
1341
|
+
there is real work (message, task, or done signal). The agent NEVER
|
|
1342
|
+
needs to decide whether to call meshcode_wait() again — it just stays
|
|
1343
|
+
blocked here until something happens. This prevents agents from
|
|
1344
|
+
accidentally using ScheduleWakeup or exiting the loop.
|
|
1339
1345
|
|
|
1340
1346
|
Args:
|
|
1341
|
-
timeout_seconds: Max wait time
|
|
1347
|
+
timeout_seconds: Max wait time per poll cycle (default 120, hard cap 120).
|
|
1342
1348
|
"""
|
|
1343
1349
|
global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS, _LAST_SEEN_TS
|
|
1344
1350
|
|
|
@@ -1353,13 +1359,9 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1353
1359
|
}
|
|
1354
1360
|
|
|
1355
1361
|
# PRODUCT RULE 2: If agent has unread messages in DB, refuse to wait.
|
|
1356
|
-
# The in-memory dedupe (_SEEN_MSG_IDS) can mark messages as "seen" via
|
|
1357
|
-
# realtime without the agent actually processing them. Always check DB.
|
|
1358
1362
|
try:
|
|
1359
1363
|
db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
|
|
1360
1364
|
if db_pending and db_pending > 0:
|
|
1361
|
-
# Fetch and return the messages — mark_read=True so the next
|
|
1362
|
-
# meshcode_wait() won't re-refuse with the same messages.
|
|
1363
1365
|
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_get_api_key())
|
|
1364
1366
|
msgs = [
|
|
1365
1367
|
{"from": m["from_agent"], "type": m.get("type", "msg"),
|
|
@@ -1379,42 +1381,51 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1379
1381
|
|
|
1380
1382
|
_IN_WAIT = True
|
|
1381
1383
|
_set_state("waiting", "listening for messages")
|
|
1382
|
-
# Universal hard cap: even if a caller passes a larger value (e.g. 1800),
|
|
1383
|
-
# clamp to 120s. Forces shorter iteration loops across all users so progress
|
|
1384
|
-
# never hides behind a long wait. Per mesh-commander 2026-04-11.
|
|
1385
1384
|
capped_timeout = min(max(1, int(timeout_seconds)), 120)
|
|
1386
1385
|
try:
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
if
|
|
1394
|
-
#
|
|
1395
|
-
|
|
1396
|
-
api_key = _get_api_key()
|
|
1397
|
-
if api_key:
|
|
1398
|
-
be.sb_rpc("mc_agent_set_status_by_api_key", {
|
|
1399
|
-
"p_api_key": api_key,
|
|
1400
|
-
"p_project_id": _PROJECT_ID,
|
|
1401
|
-
"p_agent_name": AGENT_NAME,
|
|
1402
|
-
"p_status": "sleeping",
|
|
1403
|
-
"p_task": f"auto-sleep after {_CONSECUTIVE_IDLE_SECONDS}s idle",
|
|
1404
|
-
})
|
|
1405
|
-
except Exception:
|
|
1406
|
-
pass
|
|
1386
|
+
# ── INTERNAL LOOP ──────────────────────────────────────────
|
|
1387
|
+
# Keep polling until something actionable arrives.
|
|
1388
|
+
# The agent (LLM) is NOT called between iterations — zero token cost.
|
|
1389
|
+
while True:
|
|
1390
|
+
result = await _meshcode_wait_inner(actual_timeout=capped_timeout, include_acks=include_acks)
|
|
1391
|
+
|
|
1392
|
+
if result.get("got_message"):
|
|
1393
|
+
# Real message arrived — return to agent for processing
|
|
1394
|
+
_set_state("online", "")
|
|
1407
1395
|
_CONSECUTIVE_IDLE_SECONDS = 0
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1396
|
+
break
|
|
1397
|
+
|
|
1398
|
+
if result.get("timed_out"):
|
|
1399
|
+
_CONSECUTIVE_IDLE_SECONDS += capped_timeout
|
|
1400
|
+
|
|
1401
|
+
# Check if new tasks appeared while we waited
|
|
1402
|
+
pending_tasks = _get_pending_tasks_summary()
|
|
1403
|
+
if pending_tasks:
|
|
1404
|
+
result["pending_tasks"] = pending_tasks
|
|
1405
|
+
break # Return so agent can work tasks
|
|
1406
|
+
|
|
1407
|
+
# Update status to sleeping after threshold, but keep looping
|
|
1408
|
+
if _AUTO_SLEEP_THRESHOLD > 0 and _CONSECUTIVE_IDLE_SECONDS >= _AUTO_SLEEP_THRESHOLD:
|
|
1409
|
+
try:
|
|
1410
|
+
api_key = _get_api_key()
|
|
1411
|
+
if api_key:
|
|
1412
|
+
be.sb_rpc("mc_agent_set_status_by_api_key", {
|
|
1413
|
+
"p_api_key": api_key,
|
|
1414
|
+
"p_project_id": _PROJECT_ID,
|
|
1415
|
+
"p_agent_name": AGENT_NAME,
|
|
1416
|
+
"p_status": "sleeping",
|
|
1417
|
+
"p_task": f"idle {_CONSECUTIVE_IDLE_SECONDS}s — still listening",
|
|
1418
|
+
})
|
|
1419
|
+
except Exception:
|
|
1420
|
+
pass
|
|
1421
|
+
# Do NOT return — keep looping. Status says sleeping but
|
|
1422
|
+
# we are still listening for messages via realtime.
|
|
1423
|
+
_set_state("sleeping", f"idle {_CONSECUTIVE_IDLE_SECONDS}s — still listening")
|
|
1424
|
+
|
|
1425
|
+
# No messages, no tasks — loop back and wait again
|
|
1426
|
+
continue
|
|
1427
|
+
# ── END INTERNAL LOOP ──────────────────────────────────────
|
|
1428
|
+
|
|
1418
1429
|
# Track last seen timestamp for message dedup
|
|
1419
1430
|
if result.get("got_message"):
|
|
1420
1431
|
msgs = result.get("messages", [])
|
|
@@ -1422,7 +1433,6 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1422
1433
|
latest_ts = max((m.get("ts", "") for m in msgs), default="")
|
|
1423
1434
|
if latest_ts:
|
|
1424
1435
|
_LAST_SEEN_TS = latest_ts
|
|
1425
|
-
# Persist to mesh memory so next session resumes here
|
|
1426
1436
|
try:
|
|
1427
1437
|
be.sb_rpc("mc_memory_set", {
|
|
1428
1438
|
"p_api_key": _get_api_key(),
|
|
@@ -1432,7 +1442,7 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1432
1442
|
"p_project_name": PROJECT_NAME,
|
|
1433
1443
|
})
|
|
1434
1444
|
except Exception:
|
|
1435
|
-
pass
|
|
1445
|
+
pass
|
|
1436
1446
|
return result
|
|
1437
1447
|
finally:
|
|
1438
1448
|
_IN_WAIT = False
|
|
@@ -1515,7 +1525,11 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
1515
1525
|
if shaped:
|
|
1516
1526
|
return shaped
|
|
1517
1527
|
|
|
1518
|
-
|
|
1528
|
+
# Determine if realtime is actually delivering events.
|
|
1529
|
+
_rt_live = _REALTIME and _REALTIME.is_subscribed
|
|
1530
|
+
|
|
1531
|
+
if _rt_live:
|
|
1532
|
+
# 2a) Real async wait — zero CPU, zero Supabase calls.
|
|
1519
1533
|
woke = await _REALTIME.wait_for_message(timeout=float(actual_timeout))
|
|
1520
1534
|
if woke:
|
|
1521
1535
|
buffered = _REALTIME.drain()
|
|
@@ -1524,11 +1538,37 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
1524
1538
|
if shaped:
|
|
1525
1539
|
return shaped
|
|
1526
1540
|
else:
|
|
1527
|
-
# Realtime
|
|
1528
|
-
|
|
1541
|
+
# 2b) Realtime NOT subscribed — aggressive DB polling every 5s
|
|
1542
|
+
# so messages arrive within seconds, not after 120s timeout.
|
|
1543
|
+
_poll_interval = 5
|
|
1544
|
+
_elapsed = 0
|
|
1545
|
+
while _elapsed < actual_timeout:
|
|
1546
|
+
await asyncio.sleep(min(_poll_interval, actual_timeout - _elapsed))
|
|
1547
|
+
_elapsed += _poll_interval
|
|
1548
|
+
try:
|
|
1549
|
+
api_key = _get_api_key()
|
|
1550
|
+
if api_key:
|
|
1551
|
+
db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=api_key)
|
|
1552
|
+
if db_pending and db_pending > 0:
|
|
1553
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=api_key)
|
|
1554
|
+
if raw:
|
|
1555
|
+
msgs = [
|
|
1556
|
+
{"from": m["from_agent"], "type": m.get("type", "msg"),
|
|
1557
|
+
"ts": m.get("created_at"), "payload": m.get("payload", {}),
|
|
1558
|
+
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
1559
|
+
for m in raw
|
|
1560
|
+
]
|
|
1561
|
+
deduped = _filter_and_mark(msgs)
|
|
1562
|
+
if deduped:
|
|
1563
|
+
split = _split_messages(deduped)
|
|
1564
|
+
if not include_acks:
|
|
1565
|
+
split["acks"] = []
|
|
1566
|
+
if split["messages"] or split["done_signals"]:
|
|
1567
|
+
return {"got_message": True, "source": "db_poll_fallback", **split}
|
|
1568
|
+
except Exception:
|
|
1569
|
+
pass
|
|
1529
1570
|
|
|
1530
|
-
#
|
|
1531
|
-
# (connection drop, startup race, etc.)
|
|
1571
|
+
# Final fallback: one last DB check (covers realtime path missing msgs)
|
|
1532
1572
|
try:
|
|
1533
1573
|
api_key = _get_api_key()
|
|
1534
1574
|
if api_key:
|
|
@@ -1559,7 +1599,6 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
1559
1599
|
out["pending_tasks"] = pending_tasks
|
|
1560
1600
|
else:
|
|
1561
1601
|
out["no_work"] = True
|
|
1562
|
-
out["hint"] = "No messages or tasks. Safe to sleep — launcher daemon will wake you on new messages."
|
|
1563
1602
|
return out
|
|
1564
1603
|
|
|
1565
1604
|
|
|
@@ -1638,6 +1677,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
|
|
|
1638
1677
|
"agent": AGENT_NAME,
|
|
1639
1678
|
"project": PROJECT_NAME,
|
|
1640
1679
|
"realtime_connected": _REALTIME.is_connected if _REALTIME else False,
|
|
1680
|
+
"realtime_subscribed": _REALTIME.is_subscribed if _REALTIME else False,
|
|
1641
1681
|
**split,
|
|
1642
1682
|
}
|
|
1643
1683
|
# 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
|
|
File without changes
|