meshcode 2.10.36__tar.gz → 2.10.38__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.36 → meshcode-2.10.38}/PKG-INFO +1 -1
  2. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/comms_v4.py +39 -16
  4. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/backend.py +41 -29
  5. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/realtime.py +17 -5
  6. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/server.py +167 -23
  7. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.10.36 → meshcode-2.10.38}/pyproject.toml +1 -1
  9. {meshcode-2.10.36 → meshcode-2.10.38}/README.md +0 -0
  10. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/ascii_art.py +0 -0
  11. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/cli.py +0 -0
  12. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/invites.py +0 -0
  13. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/launcher.py +0 -0
  14. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/launcher_install.py +0 -0
  15. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.36 → meshcode-2.10.38}/setup.cfg +0 -0
  32. {meshcode-2.10.36 → meshcode-2.10.38}/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.36
3
+ Version: 2.10.38
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.36"
2
+ __version__ = "2.10.38"
@@ -615,7 +615,7 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
615
615
  # The agent is identity-only at rest; next message wakes it again.
616
616
  sb_update("mc_agents",
617
617
  f"project_id=eq.{project_id}&name=eq.{quote(name)}",
618
- {"status": "sleeping", "task": "Esperando mensajes...",
618
+ {"status": "sleeping", "task": "Waiting for messages...",
619
619
  "last_active_at": now_iso(), "last_heartbeat": now_iso()})
620
620
  except Exception:
621
621
  pass
@@ -865,7 +865,7 @@ def register(project, name, role=""):
865
865
  ensure_sessions()
866
866
  project_id = get_project_id(project)
867
867
  if not project_id:
868
- print(f"[ERROR] No se pudo crear/encontrar proyecto '{project}'")
868
+ print(f"[ERROR] Could not create/find project '{project}'")
869
869
  return
870
870
 
871
871
  ppid = os.getppid()
@@ -946,7 +946,7 @@ def register(project, name, role=""):
946
946
  order="created_at.desc", limit=10)
947
947
  if recent:
948
948
  recent.reverse()
949
- print(f"\n[COMMS] Últimos {len(recent)} mensajes del equipo:")
949
+ print(f"\n[COMMS] Last {len(recent)} team messages:")
950
950
  for msg in recent:
951
951
  ts = msg.get("created_at", "")[-8:]
952
952
  fr = msg["from_agent"]
@@ -1132,7 +1132,7 @@ def watch(project, name, interval=10, timeout=0):
1132
1132
  # Update status to standby
1133
1133
  sb_update("mc_agents",
1134
1134
  f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1135
- {"status": "standby", "task": "Esperando mensajes...", "last_heartbeat": now_iso()})
1135
+ {"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1136
1136
 
1137
1137
  cycle = timeout if timeout > 0 else 600
1138
1138
  print(f"[{project}] {name} en watch — poll cada {interval}s, ciclo {cycle}s (auto-loop)")
@@ -1161,10 +1161,10 @@ def watch(project, name, interval=10, timeout=0):
1161
1161
  time.sleep(interval)
1162
1162
 
1163
1163
  # Cycle ended without messages — check user context, then AUTO-RESTART watch
1164
- print(f"[{project}] Otro ciclo sin mensajes. Watch activo, sigue corriendo. En standby.")
1164
+ print(f"[{project}] No messages this cycle. Watch active, still running. Standby.")
1165
1165
  sb_update("mc_agents",
1166
1166
  f"project_id=eq.{project_id}&name=eq.{quote(name)}",
1167
- {"status": "standby", "task": "Esperando mensajes...", "last_heartbeat": now_iso()})
1167
+ {"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
1168
1168
 
1169
1169
 
1170
1170
  def update_board(project, name, status, task=""):
@@ -1183,7 +1183,7 @@ def update_board(project, name, status, task=""):
1183
1183
  print(f"[{project}] {name}: NO puedes estar idle — tienes {count} mensaje(s) pendientes.")
1184
1184
  print(f"[{project}] Ejecuta: python3 ~/Desktop/meshcode/comms_v4.py read {project} {name}")
1185
1185
  status = "working"
1186
- task = f"{count} mensajes pendientes"
1186
+ task = f"{count} pending messages"
1187
1187
 
1188
1188
  updates = {"status": status, "last_heartbeat": now_iso()}
1189
1189
  if task:
@@ -1197,7 +1197,7 @@ def update_board(project, name, status, task=""):
1197
1197
  def show_board(project):
1198
1198
  project_id = get_project_id(project)
1199
1199
  if not project_id:
1200
- print(f"[{project}] Sin proyecto")
1200
+ print(f"[{project}] Project not found")
1201
1201
  return
1202
1202
 
1203
1203
  agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
@@ -1221,7 +1221,7 @@ def show_status(project=None):
1221
1221
  projects_data = sb_select("mc_projects", "", order="created_at.asc")
1222
1222
 
1223
1223
  if not projects_data:
1224
- print("[COMMS] Sin proyectos activos")
1224
+ print("[COMMS] No active projects")
1225
1225
  return
1226
1226
 
1227
1227
  print(f"\n{'='*60}")
@@ -1266,7 +1266,7 @@ def show_history(project, last_n=20, between=None):
1266
1266
  messages.reverse()
1267
1267
 
1268
1268
  print(f"\n{'='*60}")
1269
- print(f" {project.upper()} — Historial ({len(messages)} mensajes)")
1269
+ print(f" {project.upper()} — History ({len(messages)} messages)")
1270
1270
  if between:
1271
1271
  print(f" Filtro: {between}")
1272
1272
  print(f"{'='*60}\n")
@@ -1472,7 +1472,7 @@ def connect_terminal(project, name, role=""):
1472
1472
  ensure_sessions()
1473
1473
  project_id = get_project_id(project)
1474
1474
  if not project_id:
1475
- print(f"[ERROR] No se pudo crear/encontrar proyecto '{project}'")
1475
+ print(f"[ERROR] Could not create/find project '{project}'")
1476
1476
  return
1477
1477
 
1478
1478
  tty, owning = _capture_tty_for_pid(os.getppid())
@@ -1542,7 +1542,7 @@ def disconnect_terminal(project, name):
1542
1542
  """Mark agent offline, clear tty/pid, stop heartbeat, remove session file."""
1543
1543
  project_id = get_project_id(project)
1544
1544
  if not project_id:
1545
- print(f"[ERROR] proyecto '{project}' no encontrado")
1545
+ print(f"[ERROR] project '{project}' not found")
1546
1546
  return
1547
1547
  rpc_result = sb_rpc("mc_disconnect_agent", {
1548
1548
  "p_project_id": project_id,
@@ -2177,7 +2177,7 @@ if __name__ == "__main__":
2177
2177
  sys.exit(1)
2178
2178
  project_id = get_project_id(proj)
2179
2179
  if not project_id:
2180
- print(f"[ERROR] proyecto '{proj}' no encontrado")
2180
+ print(f"[ERROR] project '{proj}' not found")
2181
2181
  sys.exit(1)
2182
2182
  ok = spawn_headless_agent(proj, name, project_id,
2183
2183
  "ping: synthetic wake-headless test message", "test-harness")
@@ -2193,7 +2193,7 @@ if __name__ == "__main__":
2193
2193
  sys.exit(1)
2194
2194
  project_id = get_project_id(proj)
2195
2195
  if not project_id:
2196
- print(f"[ERROR] proyecto '{proj}' no encontrado")
2196
+ print(f"[ERROR] project '{proj}' not found")
2197
2197
  sys.exit(1)
2198
2198
  rpc_name = {"kill": "mc_agent_kill", "wake": "mc_agent_wake", "sleep": "mc_agent_sleep"}[cmd]
2199
2199
  result = sb_rpc(rpc_name, {"p_project_id": project_id, "p_agent_name": name})
@@ -2212,7 +2212,7 @@ if __name__ == "__main__":
2212
2212
  sys.exit(1)
2213
2213
  project_id = get_project_id(proj)
2214
2214
  if not project_id:
2215
- print(f"[ERROR] proyecto '{proj}' no encontrado")
2215
+ print(f"[ERROR] project '{proj}' not found")
2216
2216
  sys.exit(1)
2217
2217
  if sub == "get":
2218
2218
  result = sb_rpc("mc_agent_get_profile", {"p_project_id": project_id, "p_agent_name": name})
@@ -2425,7 +2425,30 @@ if __name__ == "__main__":
2425
2425
  sys.exit(1)
2426
2426
  login(key)
2427
2427
 
2428
- elif cmd in ("profiles", "whoami"):
2428
+ elif cmd == "whoami":
2429
+ # Show the logged-in user identity from profile_meta.json
2430
+ meta_path = Path.home() / ".meshcode" / "profile_meta.json"
2431
+ if meta_path.exists():
2432
+ try:
2433
+ meta = json.loads(meta_path.read_text(encoding="utf-8"))
2434
+ print()
2435
+ print(f"[meshcode] Logged in as:")
2436
+ if meta.get("email"):
2437
+ print(f" email: {meta['email']}")
2438
+ if meta.get("display_name"):
2439
+ print(f" display name: {meta['display_name']}")
2440
+ if meta.get("user_id"):
2441
+ print(f" user ID: {meta['user_id']}")
2442
+ active = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
2443
+ print(f" profile: {active}")
2444
+ print()
2445
+ except Exception:
2446
+ print("[meshcode] Could not read profile. Run `meshcode login <api_key>`.")
2447
+ else:
2448
+ print("[meshcode] Not logged in. Run `meshcode login <api_key>`.")
2449
+ sys.exit(0)
2450
+
2451
+ elif cmd == "profiles":
2429
2452
  # List all stored keychain profiles with metadata
2430
2453
  try:
2431
2454
  import importlib as _il
@@ -105,69 +105,81 @@ SCHEMA = "meshcode"
105
105
  # ── Persistent HTTPS Connection Pool ──────────────────────────────
106
106
  # Each urlopen() opens a new TCP+TLS connection (~1-2s overhead).
107
107
  # This pool reuses connections, cutting typical latency from ~3s to ~100ms.
108
+ # Pool of N connections (default 3) to avoid lock contention between the
109
+ # heartbeat thread, tool call handlers, and background recording thread.
108
110
  class _ConnectionPool:
109
- """Thread-safe persistent HTTPS connection to Supabase."""
111
+ """Thread-safe HTTPS connection pool to Supabase (N connections, round-robin)."""
110
112
 
111
- def __init__(self, url: str, max_idle: float = 30.0):
113
+ def __init__(self, url: str, pool_size: int = 3, max_idle: float = 30.0):
112
114
  parsed = urlparse(url)
113
115
  self._host = parsed.hostname
114
116
  self._port = parsed.port or 443
115
- self._conn: Optional[http.client.HTTPSConnection] = None
116
- self._lock = _threading.Lock()
117
+ self._pool_size = pool_size
118
+ self._conns: List[Optional[http.client.HTTPSConnection]] = [None] * pool_size
119
+ self._locks = [_threading.Lock() for _ in range(pool_size)]
120
+ self._last_used = [0.0] * pool_size
117
121
  self._max_idle = max_idle
118
- self._last_used = 0.0
119
122
  self._ctx = ssl.create_default_context()
123
+ self._counter = 0
124
+ self._counter_lock = _threading.Lock()
125
+
126
+ def _pick_slot(self) -> int:
127
+ """Round-robin slot selection."""
128
+ with self._counter_lock:
129
+ slot = self._counter % self._pool_size
130
+ self._counter += 1
131
+ return slot
120
132
 
121
- def _get_conn(self) -> http.client.HTTPSConnection:
133
+ def _get_conn(self, slot: int) -> http.client.HTTPSConnection:
122
134
  now = _time.monotonic()
123
- if self._conn is not None:
124
- # Close stale connections
125
- if now - self._last_used > self._max_idle:
135
+ if self._conns[slot] is not None:
136
+ if now - self._last_used[slot] > self._max_idle:
126
137
  try:
127
- self._conn.close()
138
+ self._conns[slot].close()
128
139
  except Exception:
129
140
  pass
130
- self._conn = None
131
- if self._conn is None:
132
- self._conn = http.client.HTTPSConnection(
141
+ self._conns[slot] = None
142
+ if self._conns[slot] is None:
143
+ self._conns[slot] = http.client.HTTPSConnection(
133
144
  self._host, self._port, timeout=10, context=self._ctx
134
145
  )
135
- return self._conn
146
+ return self._conns[slot]
136
147
 
137
148
  def request(self, method: str, path: str, body: Optional[bytes], headers: Dict[str, str]) -> tuple:
138
- """Returns (status, response_body_str). Thread-safe with retry on broken pipe."""
139
- with self._lock:
149
+ """Returns (status, response_body_str). Thread-safe with per-slot locking."""
150
+ slot = self._pick_slot()
151
+ with self._locks[slot]:
140
152
  for attempt in range(2):
141
- conn = self._get_conn()
153
+ conn = self._get_conn(slot)
142
154
  try:
143
155
  conn.request(method, path, body=body, headers=headers)
144
156
  resp = conn.getresponse()
145
157
  data = resp.read().decode("utf-8")
146
- self._last_used = _time.monotonic()
158
+ self._last_used[slot] = _time.monotonic()
147
159
  return resp.status, data
148
160
  except (http.client.RemoteDisconnected, BrokenPipeError,
149
161
  ConnectionResetError, OSError) as e:
150
- # Connection went stale — close and retry once
151
162
  try:
152
- self._conn.close()
163
+ self._conns[slot].close()
153
164
  except Exception:
154
165
  pass
155
- self._conn = None
166
+ self._conns[slot] = None
156
167
  if attempt == 0:
157
168
  continue
158
169
  raise
159
170
 
160
171
  def close(self):
161
- with self._lock:
162
- if self._conn:
163
- try:
164
- self._conn.close()
165
- except Exception:
166
- pass
167
- self._conn = None
172
+ for i in range(self._pool_size):
173
+ with self._locks[i]:
174
+ if self._conns[i]:
175
+ try:
176
+ self._conns[i].close()
177
+ except Exception:
178
+ pass
179
+ self._conns[i] = None
168
180
 
169
181
 
170
- _pool = _ConnectionPool(SUPABASE_URL)
182
+ _pool = _ConnectionPool(SUPABASE_URL, pool_size=3)
171
183
 
172
184
 
173
185
  def _now_iso() -> str:
@@ -53,9 +53,12 @@ class RealtimeListener:
53
53
  self.notify_callback = notify_callback
54
54
  self.service_role_key = service_role_key
55
55
 
56
- # Last 500 unread messages — drained by meshcode_check tool
57
- self.queue: Deque[Dict] = deque(maxlen=500)
56
+ # Message queue — drained by meshcode_check/meshcode_wait.
57
+ # Cap at 2000 (was 500). When full, oldest messages are dropped
58
+ # and _dropped_count is incremented so the agent knows.
59
+ self.queue: Deque[Dict] = deque(maxlen=2000)
58
60
  self._overflow_warned = False
61
+ self._dropped_count = 0
59
62
  self._task: Optional[asyncio.Task] = None
60
63
  # asyncio.Event() in Py3.10+ no longer requires a running loop, but
61
64
  # on older Python or certain Windows event-loop policies it can
@@ -272,9 +275,14 @@ class RealtimeListener:
272
275
  "id": record.get("id"),
273
276
  "parent_id": record.get("parent_msg_id"),
274
277
  }
275
- if len(self.queue) >= 400 and not self._overflow_warned:
276
- log.warning(f"Message queue at {len(self.queue)}/500risk of dropping messages")
277
- self._overflow_warned = True
278
+ if len(self.queue) >= self.queue.maxlen:
279
+ # Queue is fullthis append will drop the oldest message
280
+ self._dropped_count += 1
281
+ if not self._overflow_warned:
282
+ log.warning(f"Message queue FULL ({self.queue.maxlen}) — dropping oldest messages ({self._dropped_count} dropped so far)")
283
+ self._overflow_warned = True
284
+ elif len(self.queue) >= self.queue.maxlen * 0.8 and not self._overflow_warned:
285
+ log.warning(f"Message queue at {len(self.queue)}/{self.queue.maxlen} — approaching limit")
278
286
  self.queue.append(enriched)
279
287
  # Wake any meshcode_wait blocked on this event.
280
288
  try:
@@ -327,6 +335,10 @@ class RealtimeListener:
327
335
  await self.stop()
328
336
  await self.start()
329
337
 
338
+ @property
339
+ def dropped_count(self) -> int:
340
+ return self._dropped_count
341
+
330
342
  @property
331
343
  def is_connected(self) -> bool:
332
344
  return self._connected
@@ -601,7 +601,9 @@ def with_working_status(func):
601
601
  global _CONSECUTIVE_IDLE_SECONDS
602
602
  _CONSECUTIVE_IDLE_SECONDS = 0 # any non-wait tool resets idle timer
603
603
  _set_state("working", name)
604
- _record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
604
+ # Estimate input tokens (chars/4) for usage tracking
605
+ _est_tokens = sum(len(str(v)) for v in kwargs.values()) // 4
606
+ _record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys()), "estimated_tokens": _est_tokens})
605
607
  try:
606
608
  return await func(*args, **kwargs)
607
609
  except Exception as e:
@@ -622,7 +624,8 @@ def with_working_status(func):
622
624
  global _CONSECUTIVE_IDLE_SECONDS
623
625
  _CONSECUTIVE_IDLE_SECONDS = 0 # any non-wait tool resets idle timer
624
626
  _set_state("working", name)
625
- _record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
627
+ _est_tokens = sum(len(str(v)) for v in kwargs.values()) // 4
628
+ _record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys()), "estimated_tokens": _est_tokens})
626
629
  try:
627
630
  return func(*args, **kwargs)
628
631
  except Exception as e:
@@ -864,6 +867,19 @@ except Exception as _e:
864
867
  log.warning(f"could not fetch agent profile: {_e}")
865
868
 
866
869
 
870
+ # ── Leader/commander detection (single source of truth) ───────
871
+ _LEADER_KEYWORDS = (
872
+ 'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
873
+ 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
874
+ 'chief', 'captain', 'boss', 'head agent',
875
+ )
876
+
877
+ def _is_leader_agent() -> bool:
878
+ """Check if this agent is a leader/commander based on name + role keywords."""
879
+ haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
880
+ return any(k in haystack for k in _LEADER_KEYWORDS)
881
+
882
+
867
883
  def _build_instructions() -> str:
868
884
  """Build the system-instructions block injected via the MCP InitializeResult.
869
885
 
@@ -939,15 +955,7 @@ what CLI command to run next (e.g. "meshcode run backend in a new terminal").
939
955
  Setup help → README.md or https://meshcode.io/docs
940
956
  """
941
957
  # Inject commander protocol if this agent is a leader
942
- # Match leader-like agent names and roles across languages.
943
- # Substring match: "orchestrator" hits on "orchestrat", "coordinador" hits on "coordinat".
944
- _leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
945
- _LEADER_KEYWORDS = (
946
- 'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
947
- 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
948
- 'chief', 'captain', 'boss', 'head agent',
949
- )
950
- is_leader = any(k in _leader_haystack for k in _LEADER_KEYWORDS)
958
+ is_leader = _is_leader_agent()
951
959
  if is_leader:
952
960
  base += """
953
961
  COMMANDER PROTOCOL (you are the team lead):
@@ -1507,6 +1515,88 @@ def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
1507
1515
  return split
1508
1516
 
1509
1517
 
1518
+ @mcp.tool()
1519
+ @with_working_status
1520
+ async def meshcode_call(to: str, function: str, args: Any = None, timeout_seconds: int = 30) -> Dict[str, Any]:
1521
+ """Synchronous RPC call to another agent. Blocks until response (max 30s).
1522
+
1523
+ Sends a structured request, waits for the callee to respond with a matching
1524
+ call_id. Like gRPC over the mesh — enables agents to ask each other questions
1525
+ and get structured answers.
1526
+
1527
+ Args:
1528
+ to: Target agent name.
1529
+ function: Function/action name the callee should execute.
1530
+ args: Arguments (any JSON-serializable value).
1531
+ timeout_seconds: Max wait time (default 30, max 60).
1532
+ """
1533
+ import uuid as _uuid
1534
+ if not to or not to.strip():
1535
+ return {"error": "recipient 'to' cannot be empty"}
1536
+ call_id = str(_uuid.uuid4())
1537
+ timeout_seconds = min(max(timeout_seconds, 5), 60)
1538
+
1539
+ # Send the RPC request
1540
+ request_payload = {
1541
+ "type": "rpc_request",
1542
+ "call_id": call_id,
1543
+ "function": function,
1544
+ "args": args,
1545
+ "from": AGENT_NAME,
1546
+ "timeout": timeout_seconds,
1547
+ }
1548
+ send_result = be.send_message(
1549
+ _PROJECT_ID, AGENT_NAME, to, request_payload,
1550
+ msg_type="rpc", api_key=_get_api_key()
1551
+ )
1552
+ if not send_result.get("sent") and send_result.get("error"):
1553
+ return {"error": f"failed to send RPC request: {send_result['error']}"}
1554
+
1555
+ # Poll for response with matching call_id
1556
+ import asyncio as _asyncio
1557
+ poll_interval = 1.0
1558
+ elapsed = 0.0
1559
+ while elapsed < timeout_seconds:
1560
+ await _asyncio.sleep(poll_interval)
1561
+ elapsed += poll_interval
1562
+
1563
+ # Check Realtime buffer first
1564
+ if _REALTIME:
1565
+ for msg in _REALTIME.peek():
1566
+ p = msg.get("payload") or {}
1567
+ if isinstance(p, dict) and p.get("type") == "rpc_response" and p.get("call_id") == call_id:
1568
+ # Found response — drain it
1569
+ _REALTIME.drain()
1570
+ return {
1571
+ "ok": True,
1572
+ "call_id": call_id,
1573
+ "from": msg.get("from"),
1574
+ "result": p.get("result"),
1575
+ "error": p.get("error"),
1576
+ }
1577
+
1578
+ # Fallback: check DB
1579
+ try:
1580
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_get_api_key())
1581
+ for m in (raw if isinstance(raw, list) else raw.get("messages", []) if isinstance(raw, dict) else []):
1582
+ p = m.get("payload") or {}
1583
+ if isinstance(p, dict) and p.get("type") == "rpc_response" and p.get("call_id") == call_id:
1584
+ return {
1585
+ "ok": True,
1586
+ "call_id": call_id,
1587
+ "from": m.get("from_agent") or m.get("from"),
1588
+ "result": p.get("result"),
1589
+ "error": p.get("error"),
1590
+ }
1591
+ except Exception:
1592
+ pass
1593
+
1594
+ # Exponential backoff on polling (1s → 2s → 3s, cap 3s)
1595
+ poll_interval = min(poll_interval + 0.5, 3.0)
1596
+
1597
+ return {"error": f"RPC call to {to}.{function} timed out after {timeout_seconds}s", "call_id": call_id}
1598
+
1599
+
1510
1600
  @mcp.tool()
1511
1601
  @with_working_status
1512
1602
  def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dict[str, Any]:
@@ -1612,11 +1702,7 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1612
1702
  ]
1613
1703
  # For leader agents: also include unclaimed '*' tasks as pending
1614
1704
  # so commanders auto-triage them instead of letting them pile up.
1615
- _leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
1616
- _LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
1617
- 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
1618
- 'chief', 'captain', 'boss', 'head agent')
1619
- is_leader = any(k in _leader_haystack for k in _LEADER_KW)
1705
+ is_leader = _is_leader_agent()
1620
1706
  if is_leader:
1621
1707
  wildcard_tasks = [
1622
1708
  {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
@@ -1626,6 +1712,9 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1626
1712
  and not t.get("claimed_by")
1627
1713
  ]
1628
1714
  pending.extend(wildcard_tasks)
1715
+ # Sort by priority so urgent tasks surface first
1716
+ _PRIORITY_ORDER = {"urgent": 0, "high": 1, "normal": 2, "low": 3}
1717
+ pending.sort(key=lambda t: _PRIORITY_ORDER.get(t.get("priority", "normal"), 2))
1629
1718
  return pending if pending else None
1630
1719
  except Exception:
1631
1720
  return None
@@ -1655,12 +1744,7 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1655
1744
  # delegate tasks and need to stay in the wait loop to receive reports.
1656
1745
  pending_tasks = _get_pending_tasks_summary()
1657
1746
  if pending_tasks:
1658
- _leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
1659
- _LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
1660
- 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
1661
- 'chief', 'captain', 'boss', 'head agent')
1662
- _is_leader = any(k in _leader_haystack for k in _LEADER_KW)
1663
- if not _is_leader:
1747
+ if not _is_leader_agent():
1664
1748
  return {
1665
1749
  "refused": True,
1666
1750
  "reason": "You have open tasks. Work them before entering wait.",
@@ -2267,6 +2351,49 @@ def meshcode_task_reassign(task_id: str, new_assignee: str) -> Dict[str, Any]:
2267
2351
  })
2268
2352
 
2269
2353
 
2354
+ # ----------------- SCHEDULED TASKS -----------------
2355
+
2356
+ @mcp.tool()
2357
+ @with_working_status
2358
+ def meshcode_schedule(title: str, cron_expression: str, assignee: str = "*",
2359
+ description: str = "", priority: str = "normal") -> Dict[str, Any]:
2360
+ """Create a recurring scheduled task. Fires on cron schedule.
2361
+
2362
+ Args:
2363
+ title: Task title (created each time it fires).
2364
+ cron_expression: Standard cron (e.g. "0 9 * * 1-5" = 9am weekdays).
2365
+ assignee: Agent name or "*" for any.
2366
+ description: Task description template.
2367
+ priority: low/normal/high/urgent.
2368
+ """
2369
+ if not title or not title.strip():
2370
+ return {"error": "title cannot be empty"}
2371
+ if not cron_expression or not cron_expression.strip():
2372
+ return {"error": "cron_expression cannot be empty"}
2373
+ api_key = _get_api_key()
2374
+ return be.sb_rpc("mc_schedule_create", {
2375
+ "p_api_key": api_key,
2376
+ "p_project_id": _PROJECT_ID,
2377
+ "p_creator_agent": AGENT_NAME,
2378
+ "p_title": title.strip(),
2379
+ "p_cron_expression": cron_expression.strip(),
2380
+ "p_description": description,
2381
+ "p_assignee": assignee,
2382
+ "p_priority": priority,
2383
+ })
2384
+
2385
+
2386
+ @mcp.tool()
2387
+ @with_working_status
2388
+ def meshcode_schedule_list() -> Dict[str, Any]:
2389
+ """List all scheduled/recurring tasks for this meshwork."""
2390
+ api_key = _get_api_key()
2391
+ return be.sb_rpc("mc_schedule_list", {
2392
+ "p_api_key": api_key,
2393
+ "p_project_id": _PROJECT_ID,
2394
+ })
2395
+
2396
+
2270
2397
  # ----------------- PROACTIVE HEALTH SCAN -----------------
2271
2398
 
2272
2399
  @mcp.tool()
@@ -2671,12 +2798,29 @@ def meshcode_recall(key: Optional[str] = None) -> Dict[str, Any]:
2671
2798
  "p_key": key,
2672
2799
  })
2673
2800
  else:
2674
- return be.sb_rpc("mc_memory_list", {
2801
+ # Return critical tier (full content) + counts of other tiers.
2802
+ # Episodic memories are large and should be searched on demand
2803
+ # via meshcode_recall_search(), not dumped into context.
2804
+ critical = be.sb_rpc("mc_memory_list", {
2675
2805
  "p_api_key": api_key,
2676
2806
  "p_agent_name": AGENT_NAME,
2677
2807
  "p_tier": "critical",
2678
2808
  "p_project_name": PROJECT_NAME,
2679
2809
  })
2810
+ # Also fetch reference tier keys (lightweight, useful context)
2811
+ reference = be.sb_rpc("mc_memory_list", {
2812
+ "p_api_key": api_key,
2813
+ "p_agent_name": AGENT_NAME,
2814
+ "p_tier": "reference",
2815
+ "p_project_name": PROJECT_NAME,
2816
+ })
2817
+ if isinstance(critical, dict) and critical.get("ok"):
2818
+ ref_keys = []
2819
+ if isinstance(reference, dict) and reference.get("ok"):
2820
+ ref_keys = [m.get("key") for m in reference.get("memories", [])]
2821
+ critical["reference_keys"] = ref_keys
2822
+ critical["tip"] = "Use meshcode_recall_search(query) to search episodic memories."
2823
+ return critical
2680
2824
 
2681
2825
 
2682
2826
  @mcp.tool()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.36
3
+ Version: 2.10.38
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.36"
7
+ version = "2.10.38"
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