meshcode 2.10.50__tar.gz → 2.10.52__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.50 → meshcode-2.10.52}/PKG-INFO +1 -1
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/__init__.py +1 -1
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/backend.py +4 -2
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/realtime.py +7 -1
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/server.py +70 -20
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.50 → meshcode-2.10.52}/pyproject.toml +1 -1
- {meshcode-2.10.50 → meshcode-2.10.52}/README.md +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/cli.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/invites.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/launcher.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/preferences.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/secrets.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/self_update.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/setup.cfg +0 -0
- {meshcode-2.10.50 → meshcode-2.10.52}/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.52"
|
|
@@ -715,9 +715,10 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
|
|
|
715
715
|
# If RPC doesn't exist yet, fall through to direct query
|
|
716
716
|
|
|
717
717
|
# Fallback: direct SELECT (tests/legacy — requires anon RLS bypass)
|
|
718
|
+
# Include broadcasts (to_agent='*') — matches RPC behavior.
|
|
718
719
|
messages = sb_select(
|
|
719
720
|
"mc_messages",
|
|
720
|
-
f"project_id=eq.{project_id}&to_agent=
|
|
721
|
+
f"project_id=eq.{project_id}&to_agent=in.({quote(agent)},*)&read=eq.false",
|
|
721
722
|
order="created_at.asc",
|
|
722
723
|
)
|
|
723
724
|
# Auto-decrypt encrypted messages
|
|
@@ -792,9 +793,10 @@ def count_pending(project_id: str, agent: str, api_key: Optional[str] = None) ->
|
|
|
792
793
|
# Fall through to direct query if RPC doesn't exist yet
|
|
793
794
|
|
|
794
795
|
# Fallback: direct SELECT (tests/legacy)
|
|
796
|
+
# Include broadcasts (to_agent='*') — matches mc_read_inbox behavior.
|
|
795
797
|
pending = sb_select(
|
|
796
798
|
"mc_messages",
|
|
797
|
-
f"project_id=eq.{project_id}&to_agent=
|
|
799
|
+
f"project_id=eq.{project_id}&to_agent=in.({quote(agent)},*)&read=eq.false&type=neq.ack",
|
|
798
800
|
limit=1000,
|
|
799
801
|
)
|
|
800
802
|
return len(pending)
|
|
@@ -261,7 +261,13 @@ class RealtimeListener:
|
|
|
261
261
|
"ref": str(ref),
|
|
262
262
|
}))
|
|
263
263
|
except Exception as e:
|
|
264
|
-
log.warning(f"Realtime heartbeat send failed: {e}")
|
|
264
|
+
log.warning(f"Realtime heartbeat send failed: {e} — closing WS to force reconnect")
|
|
265
|
+
# Close WS to trigger recv loop exit → outer _run() reconnects.
|
|
266
|
+
# Without this, half-dead WS stays open for 45-60s silent blackout.
|
|
267
|
+
try:
|
|
268
|
+
await ws.close()
|
|
269
|
+
except Exception:
|
|
270
|
+
pass
|
|
265
271
|
return
|
|
266
272
|
|
|
267
273
|
async def _handle_message(self, msg: Dict[str, Any]) -> None:
|
|
@@ -104,7 +104,7 @@ def _mc_log(msg: str, level: str = "info") -> None:
|
|
|
104
104
|
_SEEN_MSG_IDS: dict = {} # key -> timestamp (monotonic)
|
|
105
105
|
_SEEN_MSG_ORDER: deque = deque()
|
|
106
106
|
_SEEN_MSG_CAP = 2000
|
|
107
|
-
_SEEN_TTL =
|
|
107
|
+
_SEEN_TTL = 1800.0 # 30 minutes — prevents duplicate delivery during long sessions
|
|
108
108
|
_SEEN_LOCK = _threading.Lock() # Guards _SEEN_MSG_IDS + _SEEN_MSG_ORDER
|
|
109
109
|
|
|
110
110
|
# ============================================================
|
|
@@ -1470,13 +1470,15 @@ async def lifespan(_app):
|
|
|
1470
1470
|
# Without this, the lifespan yields before the WS is ready, and Claude
|
|
1471
1471
|
# Code's handshake can time out on slower network paths — one agent
|
|
1472
1472
|
# fails while siblings on the same box succeed.
|
|
1473
|
-
for _rt_check in range(
|
|
1474
|
-
if getattr(_REALTIME, '
|
|
1475
|
-
log.info(f"Realtime connected for {AGENT_NAME}")
|
|
1473
|
+
for _rt_check in range(20):
|
|
1474
|
+
if getattr(_REALTIME, '_subscription_ok', False):
|
|
1475
|
+
log.info(f"Realtime connected + subscribed for {AGENT_NAME}")
|
|
1476
1476
|
break
|
|
1477
|
+
if _rt_check == 10 and getattr(_REALTIME, '_connected', False):
|
|
1478
|
+
log.info(f"Realtime connected (not yet subscribed) for {AGENT_NAME} — waiting for subscription...")
|
|
1477
1479
|
await asyncio.sleep(0.5)
|
|
1478
1480
|
else:
|
|
1479
|
-
log.warning(f"Realtime not
|
|
1481
|
+
log.warning(f"Realtime not fully subscribed after 10s for {AGENT_NAME} — continuing with polling fallback")
|
|
1480
1482
|
|
|
1481
1483
|
# IMMEDIATE: send first heartbeat + set online status BEFORE any tool calls.
|
|
1482
1484
|
# Without this, the agent appears offline for up to 30s after boot.
|
|
@@ -1887,6 +1889,13 @@ try:
|
|
|
1887
1889
|
except Exception as _e:
|
|
1888
1890
|
print(f"[meshcode] Could not restore last_seen: {_e}", file=sys.stderr)
|
|
1889
1891
|
|
|
1892
|
+
# Fallback: if last_seen is still None, default to now minus 5 minutes
|
|
1893
|
+
# to avoid flooding agent with ancient messages on cold boot.
|
|
1894
|
+
if _LAST_SEEN_TS is None:
|
|
1895
|
+
from datetime import datetime, timezone, timedelta
|
|
1896
|
+
_LAST_SEEN_TS = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
|
|
1897
|
+
print(f"[meshcode] last_seen fallback: {_LAST_SEEN_TS} (now minus 5min)", file=sys.stderr)
|
|
1898
|
+
|
|
1890
1899
|
|
|
1891
1900
|
def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
1892
1901
|
"""Fetch tasks that THIS agent should work on. Returns compact list or None.
|
|
@@ -2092,16 +2101,26 @@ def _mark_realtime_msgs_read_in_db(messages: List[Dict[str, Any]]) -> None:
|
|
|
2092
2101
|
return
|
|
2093
2102
|
|
|
2094
2103
|
def _do():
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2104
|
+
# Batch mark_read: single RPC instead of N individual calls
|
|
2105
|
+
try:
|
|
2106
|
+
be.sb_rpc("mc_mark_messages_read_batch", {
|
|
2107
|
+
"p_api_key": api_key,
|
|
2108
|
+
"p_project_id": _PROJECT_ID,
|
|
2109
|
+
"p_message_ids": msg_ids,
|
|
2110
|
+
})
|
|
2111
|
+
except Exception:
|
|
2112
|
+
# Fallback: individual mark_read if batch RPC not deployed yet
|
|
2113
|
+
for mid in msg_ids:
|
|
2114
|
+
try:
|
|
2115
|
+
be.sb_rpc("mc_mark_message_read", {
|
|
2116
|
+
"p_api_key": api_key,
|
|
2117
|
+
"p_project_id": _PROJECT_ID,
|
|
2118
|
+
"p_message_id": mid,
|
|
2119
|
+
})
|
|
2120
|
+
except Exception as e:
|
|
2121
|
+
log.debug(f"mark_read failed for msg {mid}: {e}")
|
|
2122
|
+
# Non-daemon: survives MCP shutdown so mark_read RPCs complete
|
|
2123
|
+
threading.Thread(target=_do, daemon=False, name="mark_read_bg").start()
|
|
2105
2124
|
|
|
2106
2125
|
|
|
2107
2126
|
async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[str, Any]:
|
|
@@ -2338,9 +2357,14 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark
|
|
|
2338
2357
|
# Don't mark as seen — meshcode_check is a peek, not a consume
|
|
2339
2358
|
deduped = [m for m in realtime_buffered if _seen_key(m) not in _SEEN_MSG_IDS]
|
|
2340
2359
|
|
|
2341
|
-
#
|
|
2342
|
-
#
|
|
2343
|
-
if
|
|
2360
|
+
# When mark_read=True and we have RT-buffered messages, also mark them
|
|
2361
|
+
# as read in DB so they don't inflate pending counts on restart.
|
|
2362
|
+
if mark_read and deduped:
|
|
2363
|
+
_mark_realtime_msgs_read_in_db(deduped)
|
|
2364
|
+
|
|
2365
|
+
# Fetch from DB when: (a) RT buffer empty and DB has pending, OR
|
|
2366
|
+
# (b) mark_read=True and DB has pending (must hit DB to actually mark read).
|
|
2367
|
+
if (not deduped and pending > 0) or (mark_read and pending > 0):
|
|
2344
2368
|
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=mark_read, api_key=_get_api_key())
|
|
2345
2369
|
deduped = [
|
|
2346
2370
|
{
|
|
@@ -3711,6 +3735,7 @@ def run_server():
|
|
|
3711
3735
|
# `mcp.run()` normally, which breaks the loop.
|
|
3712
3736
|
import time as _time_mod
|
|
3713
3737
|
_restart_count = 0
|
|
3738
|
+
_clean_return_count = 0
|
|
3714
3739
|
while True:
|
|
3715
3740
|
try:
|
|
3716
3741
|
mcp.run()
|
|
@@ -3736,5 +3761,30 @@ def run_server():
|
|
|
3736
3761
|
break
|
|
3737
3762
|
_time_mod.sleep(0.3)
|
|
3738
3763
|
continue
|
|
3739
|
-
|
|
3740
|
-
|
|
3764
|
+
|
|
3765
|
+
# mcp.run() returned cleanly (stdin EOF). This can mean:
|
|
3766
|
+
# a) Claude Code intentionally shut us down (process exit)
|
|
3767
|
+
# b) Claude Code closed the pipe temporarily due to ESC/cancel
|
|
3768
|
+
# Instead of exiting immediately, wait briefly and retry — Claude
|
|
3769
|
+
# Code's /mcp reconnect will re-attach stdin within seconds.
|
|
3770
|
+
# Only exit after multiple consecutive clean returns with no
|
|
3771
|
+
# recovery, which means the parent process truly wants us dead.
|
|
3772
|
+
_clean_return_count += 1
|
|
3773
|
+
if _clean_return_count > 5:
|
|
3774
|
+
try:
|
|
3775
|
+
sys.stderr.write("[meshcode-mcp] stdin EOF persisted after 5 retries; exiting\n")
|
|
3776
|
+
sys.stderr.flush()
|
|
3777
|
+
except Exception:
|
|
3778
|
+
pass
|
|
3779
|
+
break
|
|
3780
|
+
try:
|
|
3781
|
+
sys.stderr.write(
|
|
3782
|
+
f"[meshcode-mcp] mcp.run() returned cleanly (stdin EOF, attempt {_clean_return_count}/5); "
|
|
3783
|
+
f"waiting 2s for Claude Code reconnect...\n"
|
|
3784
|
+
)
|
|
3785
|
+
sys.stderr.flush()
|
|
3786
|
+
except Exception:
|
|
3787
|
+
pass
|
|
3788
|
+
_time_mod.sleep(2.0)
|
|
3789
|
+
# Reset exception restart count — this is a clean return, not a crash
|
|
3790
|
+
_restart_count = 0
|
|
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
|