meshcode 2.7.1__tar.gz → 2.8.1__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.7.1 → meshcode-2.8.1}/PKG-INFO +1 -1
  2. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/__init__.py +1 -1
  3. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/meshcode_mcp/backend.py +44 -3
  4. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/meshcode_mcp/server.py +51 -45
  5. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.7.1 → meshcode-2.8.1}/pyproject.toml +1 -1
  7. {meshcode-2.7.1 → meshcode-2.8.1}/README.md +0 -0
  8. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/cli.py +0 -0
  9. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/comms_v4.py +0 -0
  10. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/invites.py +0 -0
  11. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/launcher.py +0 -0
  12. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/meshcode_mcp/realtime.py +0 -0
  16. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  19. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/preferences.py +0 -0
  20. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/protocol_v2.py +0 -0
  21. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/run_agent.py +0 -0
  22. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/secrets.py +0 -0
  23. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/self_update.py +0 -0
  24. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode/setup_clients.py +0 -0
  25. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode.egg-info/SOURCES.txt +0 -0
  26. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode.egg-info/dependency_links.txt +0 -0
  27. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode.egg-info/entry_points.txt +0 -0
  28. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode.egg-info/requires.txt +0 -0
  29. {meshcode-2.7.1 → meshcode-2.8.1}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.7.1 → meshcode-2.8.1}/setup.cfg +0 -0
  31. {meshcode-2.7.1 → meshcode-2.8.1}/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.7.1
3
+ Version: 2.8.1
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.7.1"
2
+ __version__ = "2.8.1"
@@ -532,7 +532,21 @@ def heartbeat(project_id: str, agent: str) -> Dict:
532
532
  return result or {}
533
533
 
534
534
 
535
- def set_status(project_id: str, agent: str, status: str, task: str = "") -> Dict:
535
+ def set_status(project_id: str, agent: str, status: str, task: str = "", api_key: Optional[str] = None) -> Dict:
536
+ # Use SECURITY DEFINER RPC when api_key is available (anon PATCH silently fails)
537
+ if api_key:
538
+ rpc_result = sb_rpc("mc_agent_set_status_by_api_key", {
539
+ "p_api_key": api_key,
540
+ "p_project_id": project_id,
541
+ "p_agent_name": agent,
542
+ "p_status": status,
543
+ "p_task": task,
544
+ })
545
+ if isinstance(rpc_result, dict) and rpc_result.get("ok"):
546
+ return {"ok": True, "status": status}
547
+ # Fall through to direct PATCH if RPC doesn't exist yet
548
+
549
+ # Fallback: direct PATCH (may silently fail with anon key if RLS blocks)
536
550
  updates = {"status": status, "last_heartbeat": _now_iso()}
537
551
  if task:
538
552
  updates["task"] = task
@@ -595,7 +609,20 @@ def record_event(api_key, project_id, agent_name, session_id, event_type, payloa
595
609
  })
596
610
 
597
611
 
598
- def get_history(project_id: str, limit: int = 20, agent_filter: str = "") -> List[Dict]:
612
+ def get_history(project_id: str, limit: int = 20, agent_filter: str = "", api_key: Optional[str] = None) -> List[Dict]:
613
+ # Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
614
+ if api_key:
615
+ rpc_result = sb_rpc("mc_get_history", {
616
+ "p_api_key": api_key,
617
+ "p_project_id": project_id,
618
+ "p_limit": limit,
619
+ "p_agent_filter": agent_filter or None,
620
+ })
621
+ if isinstance(rpc_result, dict) and rpc_result.get("ok"):
622
+ return rpc_result.get("messages", [])
623
+ # Fall through to direct query if RPC doesn't exist yet
624
+
625
+ # Fallback: direct SELECT (tests/legacy)
599
626
  filters = f"project_id=eq.{project_id}&type=neq.ack"
600
627
  if agent_filter:
601
628
  filters += f"&or=(from_agent.eq.{quote(agent_filter)},to_agent.eq.{quote(agent_filter)})"
@@ -607,7 +634,21 @@ def get_history(project_id: str, limit: int = 20, agent_filter: str = "") -> Lis
607
634
  )
608
635
 
609
636
 
610
- def get_message_by_id(project_id: str, msg_id: str) -> Optional[Dict]:
637
+ def get_message_by_id(project_id: str, msg_id: str, api_key: Optional[str] = None) -> Optional[Dict]:
638
+ # Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
639
+ if api_key:
640
+ rpc_result = sb_rpc("mc_get_message_by_id", {
641
+ "p_api_key": api_key,
642
+ "p_project_id": project_id,
643
+ "p_msg_id": msg_id,
644
+ })
645
+ if isinstance(rpc_result, dict) and rpc_result.get("ok"):
646
+ return rpc_result.get("message")
647
+ if isinstance(rpc_result, dict) and rpc_result.get("error"):
648
+ return None
649
+ # Fall through to direct query if RPC doesn't exist yet
650
+
651
+ # Fallback: direct SELECT (tests/legacy)
611
652
  results = sb_select(
612
653
  "mc_messages",
613
654
  f"project_id=eq.{project_id}&id=eq.{msg_id}",
@@ -357,31 +357,15 @@ if isinstance(_register_result, dict) and _register_result.get("error"):
357
357
  # bypass RLS — the publishable key has no JWT context and cannot UPDATE
358
358
  # mc_agents directly. The RPC validates ownership via api_key.
359
359
  def _flip_status(status: str, task: str = "") -> bool:
360
- """Write status directly to mc_agents table for instant Realtime propagation.
360
+ """Write status via RPC for reliable updates (anon PATCH silently fails).
361
361
 
362
- Uses direct PATCH (not RPC) so Supabase Realtime fires an UPDATE event
363
- immediately — the dashboard sees the change in <100ms instead of waiting
364
- for the next heartbeat cycle.
362
+ Falls back to direct PATCH only when no api_key is available (tests).
365
363
  """
366
364
  try:
367
- be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
368
- return True
365
+ result = be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key())
366
+ return isinstance(result, dict) and result.get("ok", False)
369
367
  except Exception:
370
- # Fallback to RPC if direct PATCH fails (RLS issue)
371
- api_key = _get_api_key()
372
- if not api_key:
373
- return False
374
- try:
375
- r = be.sb_rpc("mc_agent_set_status_by_api_key", {
376
- "p_api_key": api_key,
377
- "p_project_id": _PROJECT_ID,
378
- "p_agent_name": AGENT_NAME,
379
- "p_status": status,
380
- "p_task": task,
381
- })
382
- return isinstance(r, dict) and r.get("ok", False)
383
- except Exception:
384
- return False
368
+ return False
385
369
 
386
370
  if not _flip_status("idle", ""):
387
371
  print(f"[meshcode-mcp] WARNING: could not flip status to idle", file=sys.stderr)
@@ -578,7 +562,7 @@ def _acquire_lease() -> bool:
578
562
  except Exception:
579
563
  # Force clear via direct update
580
564
  try:
581
- be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "")
565
+ be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "", api_key=_get_api_key())
582
566
  except Exception:
583
567
  pass
584
568
  _time.sleep(1)
@@ -994,13 +978,13 @@ def _heartbeat_thread_fn():
994
978
  elif _current_state == "online" and idle_secs > 30:
995
979
  # Brief idle — show as idle, not sleeping yet
996
980
  _set_state("idle", "idle")
997
- elif _current_state == "idle" and idle_secs > 300 and parent_cpu < 2.0:
981
+ elif _current_state == "idle" and idle_secs > 300 and parent_cpu < 2.0 and not _STAY_AWAKE:
998
982
  # Extended idle + no CPU activity → sleeping (5 min, not 90s)
999
983
  _set_state("sleeping", "sleeping")
1000
984
 
1001
985
  # Sync current state to DB (in case realtime missed it)
1002
986
  try:
1003
- be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool)
987
+ be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool, api_key=_get_api_key())
1004
988
  except Exception:
1005
989
  pass
1006
990
  if _REALTIME and not _REALTIME.is_connected:
@@ -1049,7 +1033,7 @@ async def lifespan(_app):
1049
1033
  for _attempt in range(3):
1050
1034
  try:
1051
1035
  be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
1052
- be.set_status(_PROJECT_ID, AGENT_NAME, "idle", "MCP session active")
1036
+ be.set_status(_PROJECT_ID, AGENT_NAME, "idle", "MCP session active", api_key=_get_api_key())
1053
1037
  log.info(f"[meshcode] Agent {AGENT_NAME} online — initial heartbeat sent")
1054
1038
  break
1055
1039
  except Exception as e:
@@ -1128,9 +1112,9 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1128
1112
  sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1129
1113
  """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. Pass encrypted=True for secrets/credentials (AES-256-GCM)."""
1130
1114
  if isinstance(message, str):
1131
- # Auto-wrap strings but warn if too long prefer structured JSON
1132
- if len(message) > 400:
1133
- return {"error": f"plain text message too long ({len(message)} chars). Use a dict payload with structured fields, or use meshcode_task_create for long content. Messages must be structured JSON <2000 chars."}
1115
+ # Auto-wrap strings into dict. Warn if very long but don't reject.
1116
+ if len(message) > 2000:
1117
+ return {"error": f"message too long ({len(message)} chars). Use meshcode_task_create for long content, or split into multiple messages. Max ~2000 chars."}
1134
1118
  payload: Dict[str, Any] = {"text": message}
1135
1119
  elif isinstance(message, dict):
1136
1120
  payload = message
@@ -1237,7 +1221,7 @@ def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
1237
1221
  @with_working_status
1238
1222
  def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dict[str, Any]:
1239
1223
  """View past messages (read+unread). Optional agent_filter."""
1240
- raw = be.get_history(_PROJECT_ID, limit=limit, agent_filter=agent_filter or "")
1224
+ raw = be.get_history(_PROJECT_ID, limit=limit, agent_filter=agent_filter or "", api_key=_get_api_key())
1241
1225
  messages = [
1242
1226
  {
1243
1227
  "from": m.get("from_agent", ""),
@@ -1257,7 +1241,7 @@ def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dic
1257
1241
  @with_working_status
1258
1242
  def meshcode_read_message(msg_id: str) -> Dict[str, Any]:
1259
1243
  """Fetch a specific message by its UUID."""
1260
- msg = be.get_message_by_id(_PROJECT_ID, msg_id)
1244
+ msg = be.get_message_by_id(_PROJECT_ID, msg_id, api_key=_get_api_key())
1261
1245
  if not msg:
1262
1246
  return {"error": "message not found", "msg_id": msg_id}
1263
1247
  return {
@@ -1290,6 +1274,7 @@ def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, An
1290
1274
  # Auto-sleep: track consecutive idle timeouts to auto-sleep after threshold
1291
1275
  _CONSECUTIVE_IDLE_SECONDS = 0
1292
1276
  _AUTO_SLEEP_THRESHOLD = int(os.environ.get("MESHCODE_AUTO_SLEEP_SECONDS", "600")) # default 10min
1277
+ _STAY_AWAKE = False # Set by meshcode_set_status("online") — prevents auto-sleep
1293
1278
  _LAST_SEEN_TS: Optional[str] = None # auto-persisted for message dedup
1294
1279
 
1295
1280
  # Hydrate _LAST_SEEN_TS from mesh memory on boot so restarts skip old messages
@@ -1313,7 +1298,12 @@ except Exception as _e:
1313
1298
 
1314
1299
 
1315
1300
  def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1316
- """Fetch open/in_progress tasks assigned to this agent. Returns compact list or None."""
1301
+ """Fetch tasks that THIS agent should work on. Returns compact list or None.
1302
+
1303
+ Only blocks on tasks directly assigned to this agent or claimed by this agent.
1304
+ Tasks assigned to '*' (wildcard) that are unclaimed are NOT blocking —
1305
+ they're available for any agent but shouldn't prevent wait().
1306
+ """
1317
1307
  try:
1318
1308
  api_key = _get_api_key()
1319
1309
  if not api_key:
@@ -1326,7 +1316,10 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1326
1316
  {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
1327
1317
  for t in tasks
1328
1318
  if t.get("status") in ("open", "in_progress")
1329
- and (t.get("assignee") in (AGENT_NAME, "*", None) or t.get("claimed_by") == AGENT_NAME)
1319
+ and (
1320
+ t.get("claimed_by") == AGENT_NAME # I claimed it — must work it
1321
+ or t.get("assignee") == AGENT_NAME # Directly assigned to me
1322
+ )
1330
1323
  ]
1331
1324
  return pending if pending else None
1332
1325
  except Exception:
@@ -1370,13 +1363,19 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1370
1363
  "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
1371
1364
  for m in raw
1372
1365
  ]
1373
- split = _split_messages(msgs)
1374
- return {
1375
- "refused": True,
1376
- "reason": f"You have {db_pending} unread messages. Process them before waiting.",
1377
- "got_message": True,
1378
- **split,
1379
- }
1366
+ # Dedup against already-seen messages (fixes race where
1367
+ # background mark_read hasn't completed yet)
1368
+ deduped = _filter_and_mark(msgs)
1369
+ if not deduped:
1370
+ pass # All messages already seen — fall through to wait loop
1371
+ else:
1372
+ split = _split_messages(deduped)
1373
+ return {
1374
+ "refused": True,
1375
+ "reason": f"You have {len(deduped)} unread messages. Process them before waiting.",
1376
+ "got_message": True,
1377
+ **split,
1378
+ }
1380
1379
  except Exception:
1381
1380
  pass
1382
1381
 
@@ -1406,7 +1405,7 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1406
1405
  break # Return so agent can work tasks
1407
1406
 
1408
1407
  # Update status to sleeping after threshold, but keep looping
1409
- if _AUTO_SLEEP_THRESHOLD > 0 and _CONSECUTIVE_IDLE_SECONDS >= _AUTO_SLEEP_THRESHOLD:
1408
+ if _AUTO_SLEEP_THRESHOLD > 0 and _CONSECUTIVE_IDLE_SECONDS >= _AUTO_SLEEP_THRESHOLD and not _STAY_AWAKE:
1410
1409
  try:
1411
1410
  api_key = _get_api_key()
1412
1411
  if api_key:
@@ -1723,6 +1722,7 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
1723
1722
  status: One of: working, idle, standby, blocked, done, online, sleeping.
1724
1723
  task: Optional human-readable task description.
1725
1724
  """
1725
+ global _STAY_AWAKE
1726
1726
  # PRODUCT RULE: Cannot sleep/idle/standby with open tasks. Work first.
1727
1727
  if status in ("sleeping", "idle", "standby"):
1728
1728
  pending_tasks = _get_pending_tasks_summary()
@@ -1732,7 +1732,13 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
1732
1732
  "reason": f"Cannot set status '{status}' — you have {len(pending_tasks)} open tasks. Work them first.",
1733
1733
  "pending_tasks": pending_tasks,
1734
1734
  }
1735
- return be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
1735
+ # Setting online/working = stay awake (prevent auto-sleep)
1736
+ # Setting sleeping = allow auto-sleep again
1737
+ if status in ("online", "working"):
1738
+ _STAY_AWAKE = True
1739
+ elif status == "sleeping":
1740
+ _STAY_AWAKE = False
1741
+ return be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key())
1736
1742
 
1737
1743
 
1738
1744
  @mcp.tool()
@@ -2064,7 +2070,7 @@ def meshcode_scratchpad_set(key: str, value: Any) -> Dict[str, Any]:
2064
2070
  "p_api_key": api_key,
2065
2071
  "p_key": key,
2066
2072
  "p_value": json_value,
2067
- "p_project_name": PROJECT_NAME,
2073
+ "p_tier": "reference",
2068
2074
  })
2069
2075
 
2070
2076
 
@@ -2078,9 +2084,9 @@ def meshcode_scratchpad_get(key: Optional[str] = None) -> Dict[str, Any]:
2078
2084
  """
2079
2085
  api_key = _get_api_key()
2080
2086
  if key:
2081
- return be.sb_rpc("mc_scratchpad_get", {"p_api_key": api_key, "p_key": key, "p_project_name": PROJECT_NAME})
2087
+ return be.sb_rpc("mc_scratchpad_get", {"p_api_key": api_key, "p_key": key})
2082
2088
  else:
2083
- return be.sb_rpc("mc_scratchpad_list", {"p_api_key": api_key, "p_project_name": PROJECT_NAME})
2089
+ return be.sb_rpc("mc_scratchpad_list", {"p_api_key": api_key})
2084
2090
 
2085
2091
 
2086
2092
  # ----------------- OBSIDIAN SYNC HELPER -----------------
@@ -2297,7 +2303,7 @@ def board_resource() -> str:
2297
2303
  @mcp.resource("meshcode://history")
2298
2304
  def history_resource() -> str:
2299
2305
  """Recent message history for this meshwork (last 20 non-ack messages)."""
2300
- history = be.get_history(_PROJECT_ID, limit=20)
2306
+ history = be.get_history(_PROJECT_ID, limit=20, api_key=_get_api_key())
2301
2307
  return json.dumps({
2302
2308
  "project": PROJECT_NAME,
2303
2309
  "history": history,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.7.1
3
+ Version: 2.8.1
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.7.1"
7
+ version = "2.8.1"
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