meshcode 2.10.35__tar.gz → 2.10.37__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.35 → meshcode-2.10.37}/PKG-INFO +1 -1
  2. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/comms_v4.py +108 -8
  4. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/backend.py +42 -30
  5. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/realtime.py +17 -5
  6. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/server.py +73 -39
  7. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.10.35 → meshcode-2.10.37}/pyproject.toml +1 -1
  9. {meshcode-2.10.35 → meshcode-2.10.37}/README.md +0 -0
  10. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/ascii_art.py +0 -0
  11. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/cli.py +0 -0
  12. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/invites.py +0 -0
  13. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/launcher.py +0 -0
  14. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/launcher_install.py +0 -0
  15. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.35 → meshcode-2.10.37}/setup.cfg +0 -0
  32. {meshcode-2.10.35 → meshcode-2.10.37}/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.35
3
+ Version: 2.10.37
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.35"
2
+ __version__ = "2.10.37"
@@ -1730,10 +1730,21 @@ SETUP (advanced):
1730
1730
  register <proj> <name> [role] Register agent manually
1731
1731
  setup <client> <proj> <name> [role] Legacy: global MCP config
1732
1732
 
1733
+ AGENT CONTROL:
1734
+ scan Scan identicon from clipboard → run
1735
+ kill <proj> <name> Force disconnect agent
1736
+ wake <proj> <name> Send wake signal
1737
+ sleep <proj> <name> Send sleep signal
1738
+ disconnect <proj> <name> Graceful disconnect
1739
+ whoami Show logged-in identity
1740
+ profile [agent] Show/set agent profile
1741
+ connect <proj> <name> Connect existing agent
1742
+
1733
1743
  ADMIN:
1734
1744
  clear <proj> <name> Clear inbox
1735
1745
  unregister <proj> <name> Leave project
1736
1746
  prefs View/set preferences
1747
+ update Check for package updates
1737
1748
 
1738
1749
  PROFILES (multi-account):
1739
1750
  profiles List stored keychain profiles
@@ -1856,6 +1867,80 @@ Clear inbox: marks all unread messages as read for an agent.
1856
1867
  "unregister": """meshcode unregister <project> <name>
1857
1868
 
1858
1869
  Remove an agent from a meshwork (deletes the row from mc_agents).
1870
+ """,
1871
+ "setup": """meshcode setup <project> <agent> [role]
1872
+
1873
+ Create an isolated workspace at ~/meshcode/<project>-<agent>/ with
1874
+ .mcp.json configured for the agent's MCP server. Usually auto-created
1875
+ by `meshcode go`.
1876
+
1877
+ EXAMPLES:
1878
+ meshcode setup my-app backend "Backend Engineer"
1879
+ """,
1880
+ "run": """meshcode run <agent> [--project <name>] [--editor claude|cursor|code]
1881
+
1882
+ Launch an agent in your preferred editor. Detects Claude Code, Cursor,
1883
+ VS Code, Windsurf, or Codex. Use <project>/<agent> to disambiguate.
1884
+
1885
+ EXAMPLES:
1886
+ meshcode run backend
1887
+ meshcode run my-app/backend --editor cursor
1888
+ """,
1889
+ "go": """meshcode go <agent> [--project <name>]
1890
+
1891
+ Shortcut for setup + run. Creates workspace if needed, then launches.
1892
+
1893
+ EXAMPLES:
1894
+ meshcode go backend
1895
+ meshcode go my-app/frontend
1896
+ """,
1897
+ "scan": """meshcode scan
1898
+
1899
+ Read an agent identicon from clipboard (or stdin) and launch the agent.
1900
+ The identicon must contain a ⟨ project/agent ⟩ tag.
1901
+
1902
+ EXAMPLES:
1903
+ meshcode scan # reads from clipboard
1904
+ cat identicon.txt | meshcode scan
1905
+ """,
1906
+ "invite": """meshcode invite <project> <agent> [--role "..."] [--days 7]
1907
+
1908
+ Generate an invite token for a teammate to join as a specific agent.
1909
+ --days 0 = permanent (never expires).
1910
+
1911
+ EXAMPLES:
1912
+ meshcode invite my-app frontend --role "React Developer" --days 7
1913
+ """,
1914
+ "join": """meshcode join <token> [--display-name "alice"]
1915
+
1916
+ Accept an invite and create a workspace for the assigned agent.
1917
+
1918
+ EXAMPLES:
1919
+ meshcode join mc_invite_abc123 --display-name "Alice"
1920
+ """,
1921
+ "invites": """meshcode invites <project>
1922
+
1923
+ List outstanding and redeemed invites for a meshwork.
1924
+ """,
1925
+ "members": """meshcode members <project>
1926
+
1927
+ List all members (owner + invited) of a meshwork.
1928
+ """,
1929
+ "whoami": """meshcode whoami
1930
+
1931
+ Show the currently logged-in user (email, user_id, profile).
1932
+ """,
1933
+ "prefs": """meshcode prefs [key] [value]
1934
+
1935
+ View or set preferences (permission-mode, auto-update, etc.).
1936
+
1937
+ EXAMPLES:
1938
+ meshcode prefs # show all
1939
+ meshcode prefs permission-mode bypass # set
1940
+ """,
1941
+ "profiles": """meshcode profiles
1942
+
1943
+ List all stored keychain profiles (default + per-meshwork guest keys).
1859
1944
  """,
1860
1945
  }
1861
1946
 
@@ -1981,6 +2066,9 @@ if __name__ == "__main__":
1981
2066
  from_a, to_a = target.split(":", 1)
1982
2067
  else:
1983
2068
  from_a, to_a = "?", target
2069
+ if not message.strip():
2070
+ print("[meshcode] ERROR: message cannot be empty. Usage: meshcode send <project> <from>:<to> <message>")
2071
+ sys.exit(1)
1984
2072
  send_msg(proj, from_a, to_a, message, compact=compact)
1985
2073
 
1986
2074
  elif cmd == "broadcast":
@@ -1989,12 +2077,15 @@ if __name__ == "__main__":
1989
2077
  from_a = flags["from"]
1990
2078
  message = flags.get("msg", flags.get("message", " ".join(pos)))
1991
2079
  msg_type = flags.get("type", "broadcast")
1992
- broadcast(proj, from_a, message, msg_type)
1993
2080
  else:
1994
2081
  proj = pos[0] if len(pos) > 0 else "default"
1995
2082
  from_a = pos[1] if len(pos) > 1 else "?"
1996
2083
  message = " ".join(pos[2:]) if len(pos) > 2 else ""
1997
- broadcast(proj, from_a, message)
2084
+ msg_type = "broadcast"
2085
+ if not message.strip():
2086
+ print("[meshcode] ERROR: message cannot be empty. Usage: meshcode broadcast <project> <from> <message>")
2087
+ sys.exit(1)
2088
+ broadcast(proj, from_a, message, msg_type)
1998
2089
 
1999
2090
  elif cmd == "read":
2000
2091
  proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
@@ -2007,8 +2098,14 @@ if __name__ == "__main__":
2007
2098
  elif cmd == "watch":
2008
2099
  proj = flags.get("project", pos[0] if len(pos) > 0 else "default")
2009
2100
  name = flags.get("name", pos[1] if len(pos) > 1 else "agent")
2010
- interval = int(flags.get("interval", pos[2] if len(pos) > 2 else "10"))
2011
- timeout = int(flags.get("timeout", pos[3] if len(pos) > 3 else "0"))
2101
+ try:
2102
+ interval = int(flags.get("interval", pos[2] if len(pos) > 2 else "10"))
2103
+ except (ValueError, TypeError):
2104
+ interval = 10
2105
+ try:
2106
+ timeout = int(flags.get("timeout", pos[3] if len(pos) > 3 else "0"))
2107
+ except (ValueError, TypeError):
2108
+ timeout = 0
2012
2109
  watch(proj, name, interval, timeout)
2013
2110
 
2014
2111
  elif cmd == "board":
@@ -2230,12 +2327,15 @@ if __name__ == "__main__":
2230
2327
  print(" meshcode scan # reads from clipboard")
2231
2328
  print(" cat identicon.txt | meshcode scan # reads from stdin")
2232
2329
  sys.exit(1)
2233
- # Look for agent or meshwork/agent
2234
- match = _re.search(r'⟨\s*(\S+)\s*⟩', art_text)
2235
- if not match:
2330
+ # Find ALL identicon tags, prefer the one with project/agent format.
2331
+ # Identicons contain two tags: ⟨ agent ⟩ and ⟨ project/agent ⟩.
2332
+ # re.search returns the first (agent-only), missing the project.
2333
+ all_tags = _re.findall(r'⟨\s*(\S+)\s*⟩', art_text)
2334
+ if not all_tags:
2236
2335
  print("[meshcode] No identicon tag found. Expected: ⟨ agent_name ⟩")
2237
2336
  sys.exit(1)
2238
- tag = match.group(1)
2337
+ # Prefer project/agent tag, fall back to agent-only
2338
+ tag = next((t for t in all_tags if "/" in t), all_tags[0])
2239
2339
  if "/" in tag:
2240
2340
  _proj, _agent = tag.split("/", 1)
2241
2341
  else:
@@ -65,7 +65,7 @@ class _CircuitBreaker:
65
65
  return self.state == self.OPEN
66
66
 
67
67
 
68
- _circuit = _CircuitBreaker(failure_threshold=5, recovery_timeout=30.0)
68
+ _circuit = _CircuitBreaker(failure_threshold=5, recovery_timeout=10.0)
69
69
 
70
70
  # Bake in production defaults — RLS-protected publishable key, safe to ship.
71
71
  _DEFAULT_SUPABASE_URL = "https://gjinagyyjttyxnaoavnz.supabase.co"
@@ -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
@@ -864,6 +864,19 @@ except Exception as _e:
864
864
  log.warning(f"could not fetch agent profile: {_e}")
865
865
 
866
866
 
867
+ # ── Leader/commander detection (single source of truth) ───────
868
+ _LEADER_KEYWORDS = (
869
+ 'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
870
+ 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
871
+ 'chief', 'captain', 'boss', 'head agent',
872
+ )
873
+
874
+ def _is_leader_agent() -> bool:
875
+ """Check if this agent is a leader/commander based on name + role keywords."""
876
+ haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
877
+ return any(k in haystack for k in _LEADER_KEYWORDS)
878
+
879
+
867
880
  def _build_instructions() -> str:
868
881
  """Build the system-instructions block injected via the MCP InitializeResult.
869
882
 
@@ -923,7 +936,9 @@ meshcode_expand_link(). No sensitive msgs cross-mesh.
923
936
 
924
937
  MEMORY: meshcode_remember(key, value) persists across sessions.
925
938
  meshcode_recall(key?) retrieves. meshcode_forget(key) deletes.
926
- Auto-remember after each task: mistakes, feedback, patterns, preferences.
939
+ Only remember reusable learnings: mistakes, feedback, patterns, preferences.
940
+ Do NOT save task summaries — tasks already persist in the task system.
941
+ Do NOT use memory for session state or ephemeral data.
927
942
  Save reusable code patterns as template_* keys for instant recall.
928
943
 
929
944
  SCRATCHPAD: meshcode_scratchpad_set/get for shared meshwork-level context
@@ -937,15 +952,7 @@ what CLI command to run next (e.g. "meshcode run backend in a new terminal").
937
952
  Setup help → README.md or https://meshcode.io/docs
938
953
  """
939
954
  # Inject commander protocol if this agent is a leader
940
- # Match leader-like agent names and roles across languages.
941
- # Substring match: "orchestrator" hits on "orchestrat", "coordinador" hits on "coordinat".
942
- _leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
943
- _LEADER_KEYWORDS = (
944
- 'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
945
- 'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
946
- 'chief', 'captain', 'boss', 'head agent',
947
- )
948
- is_leader = any(k in _leader_haystack for k in _LEADER_KEYWORDS)
955
+ is_leader = _is_leader_agent()
949
956
  if is_leader:
950
957
  base += """
951
958
  COMMANDER PROTOCOL (you are the team lead):
@@ -1398,6 +1405,9 @@ async def meshcode_debug_sleep(seconds: int = 30) -> Dict[str, Any]:
1398
1405
  def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1399
1406
  sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1400
1407
  """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. Pass encrypted=True for secrets/credentials (AES-256-GCM)."""
1408
+ if not to or not to.strip():
1409
+ return {"error": "recipient 'to' cannot be empty"}
1410
+ to = to.strip()
1401
1411
  if isinstance(message, str):
1402
1412
  # Auto-wrap strings into dict. Warn if very long but don't reject.
1403
1413
  if len(message) > 2000:
@@ -1605,6 +1615,21 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1605
1615
  or t.get("assignee") == AGENT_NAME # Directly assigned to me
1606
1616
  )
1607
1617
  ]
1618
+ # For leader agents: also include unclaimed '*' tasks as pending
1619
+ # so commanders auto-triage them instead of letting them pile up.
1620
+ is_leader = _is_leader_agent()
1621
+ if is_leader:
1622
+ wildcard_tasks = [
1623
+ {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
1624
+ for t in tasks
1625
+ if t.get("status") == "open"
1626
+ and t.get("assignee") == "*"
1627
+ and not t.get("claimed_by")
1628
+ ]
1629
+ pending.extend(wildcard_tasks)
1630
+ # Sort by priority so urgent tasks surface first
1631
+ _PRIORITY_ORDER = {"urgent": 0, "high": 1, "normal": 2, "low": 3}
1632
+ pending.sort(key=lambda t: _PRIORITY_ORDER.get(t.get("priority", "normal"), 2))
1608
1633
  return pending if pending else None
1609
1634
  except Exception:
1610
1635
  return None
@@ -1630,14 +1655,17 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1630
1655
  global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS, _LAST_SEEN_TS
1631
1656
 
1632
1657
  # PRODUCT RULE 1: If agent has open tasks, refuse to wait. Work first.
1658
+ # Exception: commander/leader agents can wait while monitoring — they
1659
+ # delegate tasks and need to stay in the wait loop to receive reports.
1633
1660
  pending_tasks = _get_pending_tasks_summary()
1634
1661
  if pending_tasks:
1635
- return {
1636
- "refused": True,
1637
- "reason": "You have open tasks. Work them before entering wait.",
1638
- "pending_tasks": pending_tasks,
1639
- "count": len(pending_tasks),
1640
- }
1662
+ if not _is_leader_agent():
1663
+ return {
1664
+ "refused": True,
1665
+ "reason": "You have open tasks. Work them before entering wait.",
1666
+ "pending_tasks": pending_tasks,
1667
+ "count": len(pending_tasks),
1668
+ }
1641
1669
 
1642
1670
  # PRODUCT RULE 2: If agent has unread messages in DB, refuse to wait.
1643
1671
  try:
@@ -2050,8 +2078,10 @@ def meshcode_init(project: str, agent: str, role: str = "") -> Dict[str, Any]:
2050
2078
  def meshcode_task_create(title: str, description: str = "", assignee: str = "*",
2051
2079
  priority: str = "normal", parent_task_id: Optional[str] = None) -> Dict[str, Any]:
2052
2080
  """Create task. assignee="*" for any, priority: low/normal/high/urgent."""
2081
+ if not title or not title.strip():
2082
+ return {"error": "title cannot be empty"}
2053
2083
  api_key = _get_api_key()
2054
- result = be.task_create(api_key, _PROJECT_ID, AGENT_NAME, title,
2084
+ result = be.task_create(api_key, _PROJECT_ID, AGENT_NAME, title.strip(),
2055
2085
  description=description, assignee=assignee,
2056
2086
  priority=priority, parent_task_id=parent_task_id)
2057
2087
  # Auto-notify assignee so they wake from meshcode_wait
@@ -2122,7 +2152,7 @@ def meshcode_task_claim(task_id: str) -> Dict[str, Any]:
2122
2152
  @mcp.tool()
2123
2153
  @with_working_status
2124
2154
  def meshcode_task_complete(task_id: str, summary: str = "", force: bool = False) -> Dict[str, Any]:
2125
- """Complete a claimed task with summary. Auto-remembers the task summary.
2155
+ """Complete a claimed task with summary.
2126
2156
 
2127
2157
  Refuses if the task has open subtasks, unless force=True is passed with a
2128
2158
  reason in `summary`. This prevents closing a parent while lanes are still
@@ -2148,25 +2178,8 @@ def meshcode_task_complete(task_id: str, summary: str = "", force: bool = False)
2148
2178
  except Exception:
2149
2179
  pass # Best-effort check; don't block on listing failure.
2150
2180
  result = be.task_complete(api_key, _PROJECT_ID, task_id, AGENT_NAME, summary=summary)
2151
- # Auto-remember task completion for future context
2152
- if isinstance(result, dict) and result.get("ok") and summary:
2153
- try:
2154
- import threading
2155
- def _auto_remember():
2156
- try:
2157
- be.sb_rpc("mc_memory_set", {
2158
- "p_api_key": api_key,
2159
- "p_agent_name": AGENT_NAME,
2160
- "p_key": f"task_{task_id[:8]}",
2161
- "p_value": {"title": result.get("title", ""), "summary": summary, "completed": True},
2162
- "p_tier": "episodic",
2163
- "p_project_name": PROJECT_NAME,
2164
- })
2165
- except Exception:
2166
- pass
2167
- threading.Thread(target=_auto_remember, daemon=True).start()
2168
- except Exception:
2169
- pass
2181
+ # Task data persists in the task system do NOT duplicate to memory.
2182
+ # Samuel: "los tasks no deben guardarse en memoria, para eso salen en tasks"
2170
2183
  return result
2171
2184
 
2172
2185
 
@@ -2268,6 +2281,7 @@ def meshcode_auto_wake() -> Dict[str, Any]:
2268
2281
  suggestions: List[Dict[str, str]] = []
2269
2282
 
2270
2283
  # 1. Check for stale agents (heartbeat >10 min, not offline/sleeping)
2284
+ agents = [] # Initialize before try so downstream checks don't NameError
2271
2285
  try:
2272
2286
  agents = be.get_board(_PROJECT_ID)
2273
2287
  import datetime as _dt
@@ -2607,12 +2621,15 @@ source: meshcode
2607
2621
  @mcp.tool()
2608
2622
  @with_working_status
2609
2623
  def meshcode_remember(key: str, value: Any) -> Dict[str, Any]:
2610
- """Store a persistent memory (survives restarts). Auto-remember after each task.
2624
+ """Store a persistent memory (survives restarts). Only for reusable learnings, NOT task data.
2611
2625
 
2612
2626
  Args:
2613
2627
  key: Short key (e.g. "team_conventions").
2614
2628
  value: Any JSON-serializable value.
2615
2629
  """
2630
+ if not key or not key.strip():
2631
+ return {"error": "key cannot be empty"}
2632
+ key = key.strip()
2616
2633
  api_key = _get_api_key()
2617
2634
  json_value = value if isinstance(value, (dict, list)) else json.dumps(value)
2618
2635
  if isinstance(json_value, str):
@@ -2653,12 +2670,29 @@ def meshcode_recall(key: Optional[str] = None) -> Dict[str, Any]:
2653
2670
  "p_key": key,
2654
2671
  })
2655
2672
  else:
2656
- return be.sb_rpc("mc_memory_list", {
2673
+ # Return critical tier (full content) + counts of other tiers.
2674
+ # Episodic memories are large and should be searched on demand
2675
+ # via meshcode_recall_search(), not dumped into context.
2676
+ critical = be.sb_rpc("mc_memory_list", {
2657
2677
  "p_api_key": api_key,
2658
2678
  "p_agent_name": AGENT_NAME,
2659
2679
  "p_tier": "critical",
2660
2680
  "p_project_name": PROJECT_NAME,
2661
2681
  })
2682
+ # Also fetch reference tier keys (lightweight, useful context)
2683
+ reference = be.sb_rpc("mc_memory_list", {
2684
+ "p_api_key": api_key,
2685
+ "p_agent_name": AGENT_NAME,
2686
+ "p_tier": "reference",
2687
+ "p_project_name": PROJECT_NAME,
2688
+ })
2689
+ if isinstance(critical, dict) and critical.get("ok"):
2690
+ ref_keys = []
2691
+ if isinstance(reference, dict) and reference.get("ok"):
2692
+ ref_keys = [m.get("key") for m in reference.get("memories", [])]
2693
+ critical["reference_keys"] = ref_keys
2694
+ critical["tip"] = "Use meshcode_recall_search(query) to search episodic memories."
2695
+ return critical
2662
2696
 
2663
2697
 
2664
2698
  @mcp.tool()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.35
3
+ Version: 2.10.37
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.35"
7
+ version = "2.10.37"
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