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.
Files changed (31) hide show
  1. {meshcode-2.6.7 → meshcode-2.6.9}/PKG-INFO +1 -1
  2. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/__init__.py +1 -1
  3. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/realtime.py +23 -0
  4. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/server.py +87 -47
  5. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.6.7 → meshcode-2.6.9}/pyproject.toml +1 -1
  7. {meshcode-2.6.7 → meshcode-2.6.9}/README.md +0 -0
  8. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/cli.py +0 -0
  9. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/comms_v4.py +0 -0
  10. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/invites.py +0 -0
  11. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/launcher.py +0 -0
  12. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/backend.py +0 -0
  16. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  19. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/preferences.py +0 -0
  20. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/protocol_v2.py +0 -0
  21. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/run_agent.py +0 -0
  22. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/secrets.py +0 -0
  23. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/self_update.py +0 -0
  24. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode/setup_clients.py +0 -0
  25. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/SOURCES.txt +0 -0
  26. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/dependency_links.txt +0 -0
  27. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/entry_points.txt +0 -0
  28. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/requires.txt +0 -0
  29. {meshcode-2.6.7 → meshcode-2.6.9}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.6.7 → meshcode-2.6.9}/setup.cfg +0 -0
  31. {meshcode-2.6.7 → meshcode-2.6.9}/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.7
3
+ Version: 2.6.9
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.7"
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 timeout. Your idle state.
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 in seconds (default 120, hard cap 120).
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
- result = await _meshcode_wait_inner(actual_timeout=capped_timeout, include_acks=include_acks)
1388
- if result.get("got_message"):
1389
- _set_state("online", "")
1390
- _CONSECUTIVE_IDLE_SECONDS = 0 # reset on message received
1391
- elif result.get("timed_out"):
1392
- _CONSECUTIVE_IDLE_SECONDS += capped_timeout
1393
- if _AUTO_SLEEP_THRESHOLD > 0 and _CONSECUTIVE_IDLE_SECONDS >= _AUTO_SLEEP_THRESHOLD:
1394
- # Auto-sleep: set status to sleeping and signal the agent to exit
1395
- try:
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
- return {
1409
- "auto_sleep": True,
1410
- "idle_seconds": _CONSECUTIVE_IDLE_SECONDS,
1411
- "threshold": _AUTO_SLEEP_THRESHOLD,
1412
- "instruction": "You have been idle for a while. Status set to sleeping. Call meshcode_wait() again to keep listening — do NOT exit the loop or use ScheduleWakeup.",
1413
- }
1414
- # Auto-inject pending tasks so agents don't forget to check
1415
- pending_tasks = _get_pending_tasks_summary()
1416
- if pending_tasks:
1417
- result["pending_tasks"] = pending_tasks
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 # best-effort, never block wait loop
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
- # 2) Real async wait zero CPU, zero Supabase calls.
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 unavailableplain sleep fallback so we still honor timeout.
1528
- await asyncio.sleep(actual_timeout)
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
- # Fallback: poll DB for messages that realtime may have missed
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.6.7
3
+ Version: 2.6.9
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.7"
7
+ version = "2.6.9"
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