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.
- {meshcode-2.10.35 → meshcode-2.10.37}/PKG-INFO +1 -1
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/__init__.py +1 -1
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/comms_v4.py +108 -8
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/backend.py +42 -30
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/realtime.py +17 -5
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/server.py +73 -39
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.35 → meshcode-2.10.37}/pyproject.toml +1 -1
- {meshcode-2.10.35 → meshcode-2.10.37}/README.md +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/cli.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/invites.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/launcher.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/preferences.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/secrets.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/self_update.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/setup.cfg +0 -0
- {meshcode-2.10.35 → meshcode-2.10.37}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.10.
|
|
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
|
-
|
|
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
|
-
|
|
2011
|
-
|
|
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
|
-
#
|
|
2234
|
-
|
|
2235
|
-
|
|
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
|
|
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=
|
|
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
|
|
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.
|
|
116
|
-
self.
|
|
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.
|
|
124
|
-
|
|
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.
|
|
138
|
+
self._conns[slot].close()
|
|
128
139
|
except Exception:
|
|
129
140
|
pass
|
|
130
|
-
self.
|
|
131
|
-
if self.
|
|
132
|
-
self.
|
|
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.
|
|
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
|
|
139
|
-
|
|
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.
|
|
163
|
+
self._conns[slot].close()
|
|
153
164
|
except Exception:
|
|
154
165
|
pass
|
|
155
|
-
self.
|
|
166
|
+
self._conns[slot] = None
|
|
156
167
|
if attempt == 0:
|
|
157
168
|
continue
|
|
158
169
|
raise
|
|
159
170
|
|
|
160
171
|
def close(self):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
#
|
|
57
|
-
|
|
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) >=
|
|
276
|
-
|
|
277
|
-
self.
|
|
278
|
+
if len(self.queue) >= self.queue.maxlen:
|
|
279
|
+
# Queue is full — this 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
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.
|
|
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
|
-
#
|
|
2152
|
-
|
|
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).
|
|
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
|
-
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|