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.
Files changed (32) hide show
  1. {meshcode-2.10.50 → meshcode-2.10.52}/PKG-INFO +1 -1
  2. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/backend.py +4 -2
  4. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/realtime.py +7 -1
  5. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/server.py +70 -20
  6. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.10.50 → meshcode-2.10.52}/pyproject.toml +1 -1
  8. {meshcode-2.10.50 → meshcode-2.10.52}/README.md +0 -0
  9. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/ascii_art.py +0 -0
  10. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/cli.py +0 -0
  11. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/comms_v4.py +0 -0
  12. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/invites.py +0 -0
  13. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/launcher.py +0 -0
  14. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/launcher_install.py +0 -0
  15. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.50 → meshcode-2.10.52}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.50 → meshcode-2.10.52}/setup.cfg +0 -0
  32. {meshcode-2.10.50 → meshcode-2.10.52}/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.50
3
+ Version: 2.10.52
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.50"
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=eq.{quote(agent)}&read=eq.false",
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=eq.{quote(agent)}&read=eq.false&type=neq.ack",
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 = 300.0 # 5 minutes
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(10):
1474
- if getattr(_REALTIME, '_connected', False):
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 connected after 5s for {AGENT_NAME} — continuing with polling fallback")
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
- for mid in msg_ids:
2096
- try:
2097
- be.sb_rpc("mc_mark_message_read", {
2098
- "p_api_key": api_key,
2099
- "p_project_id": _PROJECT_ID,
2100
- "p_message_id": mid,
2101
- })
2102
- except Exception as e:
2103
- log.debug(f"mark_read failed for msg {mid}: {e}")
2104
- threading.Thread(target=_do, daemon=True).start()
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
- # Fallback: if realtime buffer is empty but DB has pending messages,
2342
- # fetch them from the DB. mark_read controls whether we consume or peek.
2343
- if not deduped and pending > 0:
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
- # Clean return = stdin EOF = Claude Code closed the pipe = real shutdown
3740
- break
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.50
3
+ Version: 2.10.52
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.50"
7
+ version = "2.10.52"
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