meshcode 1.3.0__tar.gz → 1.4.0__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-1.3.0 → meshcode-1.4.0}/PKG-INFO +1 -1
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/__init__.py +1 -1
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/meshcode_mcp/realtime.py +26 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/meshcode_mcp/server.py +268 -67
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/setup_clients.py +33 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-1.3.0 → meshcode-1.4.0}/pyproject.toml +1 -1
- {meshcode-1.3.0 → meshcode-1.4.0}/README.md +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/cli.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/comms_v4.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/launcher.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/launcher_install.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/protocol_v2.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode/run_agent.py +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-1.3.0 → meshcode-1.4.0}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.4.0"
|
|
@@ -44,6 +44,9 @@ class RealtimeListener:
|
|
|
44
44
|
self._task: Optional[asyncio.Task] = None
|
|
45
45
|
self._stop = asyncio.Event()
|
|
46
46
|
self._connected = False
|
|
47
|
+
# Event fired whenever a new message is appended to the queue.
|
|
48
|
+
# meshcode_wait awaits this instead of polling → zero-cost idle.
|
|
49
|
+
self.message_event = asyncio.Event()
|
|
47
50
|
|
|
48
51
|
@property
|
|
49
52
|
def ws_url(self) -> str:
|
|
@@ -163,6 +166,11 @@ class RealtimeListener:
|
|
|
163
166
|
"id": record.get("id"),
|
|
164
167
|
}
|
|
165
168
|
self.queue.append(enriched)
|
|
169
|
+
# Wake any meshcode_wait blocked on this event.
|
|
170
|
+
try:
|
|
171
|
+
self.message_event.set()
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
166
174
|
log.info(f"new message from {enriched['from']}")
|
|
167
175
|
if self.notify_callback:
|
|
168
176
|
try:
|
|
@@ -174,8 +182,26 @@ class RealtimeListener:
|
|
|
174
182
|
"""Pop and return all queued messages."""
|
|
175
183
|
out = list(self.queue)
|
|
176
184
|
self.queue.clear()
|
|
185
|
+
# Queue is empty → reset the wake event so the next wait blocks again.
|
|
186
|
+
try:
|
|
187
|
+
self.message_event.clear()
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
177
190
|
return out
|
|
178
191
|
|
|
192
|
+
async def wait_for_message(self, timeout: Optional[float] = None) -> bool:
|
|
193
|
+
"""Block until a new message lands in the queue (or timeout).
|
|
194
|
+
|
|
195
|
+
Returns True if woken by a message, False on timeout. This is the
|
|
196
|
+
core of the zero-cost idle loop: while awaiting, the event loop
|
|
197
|
+
does ZERO work — no polling, no Supabase calls, no token cost.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
await asyncio.wait_for(self.message_event.wait(), timeout=timeout)
|
|
201
|
+
return True
|
|
202
|
+
except asyncio.TimeoutError:
|
|
203
|
+
return False
|
|
204
|
+
|
|
179
205
|
@property
|
|
180
206
|
def is_connected(self) -> bool:
|
|
181
207
|
return self._connected
|
|
@@ -12,8 +12,75 @@ import json
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
|
+
from collections import deque
|
|
15
16
|
from contextlib import asynccontextmanager
|
|
16
|
-
from typing import Any, Dict, List, Optional
|
|
17
|
+
from typing import Any, Dict, List, Optional, Union
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================
|
|
21
|
+
# Dedupe: track IDs of messages we've already returned from
|
|
22
|
+
# meshcode_wait / meshcode_check so the same row doesn't show
|
|
23
|
+
# up twice when realtime + polled paths race.
|
|
24
|
+
# ============================================================
|
|
25
|
+
_SEEN_MSG_IDS: set = set()
|
|
26
|
+
_SEEN_MSG_ORDER: deque = deque()
|
|
27
|
+
_SEEN_MSG_CAP = 1000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _seen_key(msg: Dict[str, Any]) -> str:
|
|
31
|
+
"""Build a dedupe key. Prefer the real row id; fall back to a synthetic."""
|
|
32
|
+
mid = msg.get("id")
|
|
33
|
+
if mid:
|
|
34
|
+
return str(mid)
|
|
35
|
+
payload = msg.get("payload") or {}
|
|
36
|
+
try:
|
|
37
|
+
payload_str = json.dumps(payload, sort_keys=True, default=str)[:50]
|
|
38
|
+
except Exception:
|
|
39
|
+
payload_str = str(payload)[:50]
|
|
40
|
+
return f"{msg.get('from') or msg.get('from_agent')}|{msg.get('ts') or msg.get('created_at')}|{payload_str}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _mark_seen(key: str) -> None:
|
|
44
|
+
if key in _SEEN_MSG_IDS:
|
|
45
|
+
return
|
|
46
|
+
_SEEN_MSG_IDS.add(key)
|
|
47
|
+
_SEEN_MSG_ORDER.append(key)
|
|
48
|
+
while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
|
|
49
|
+
old = _SEEN_MSG_ORDER.popleft()
|
|
50
|
+
_SEEN_MSG_IDS.discard(old)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _filter_and_mark(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
54
|
+
"""Drop already-seen messages; mark the rest as seen."""
|
|
55
|
+
out = []
|
|
56
|
+
for m in messages:
|
|
57
|
+
k = _seen_key(m)
|
|
58
|
+
if k in _SEEN_MSG_IDS:
|
|
59
|
+
continue
|
|
60
|
+
_mark_seen(k)
|
|
61
|
+
out.append(m)
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _split_messages(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
66
|
+
"""Split a list of normalized message dicts into messages / acks / done_signals."""
|
|
67
|
+
real: List[Dict[str, Any]] = []
|
|
68
|
+
acks: List[Dict[str, Any]] = []
|
|
69
|
+
dones: List[Dict[str, Any]] = []
|
|
70
|
+
for m in messages:
|
|
71
|
+
t = m.get("type", "msg")
|
|
72
|
+
if t == "ack":
|
|
73
|
+
acks.append(m)
|
|
74
|
+
elif t == "done":
|
|
75
|
+
dones.append(m)
|
|
76
|
+
else:
|
|
77
|
+
real.append(m)
|
|
78
|
+
return {
|
|
79
|
+
"messages": real,
|
|
80
|
+
"acks": acks,
|
|
81
|
+
"done_signals": dones,
|
|
82
|
+
"count": len(real),
|
|
83
|
+
}
|
|
17
84
|
|
|
18
85
|
from . import backend as be
|
|
19
86
|
from .realtime import RealtimeListener
|
|
@@ -111,6 +178,54 @@ if not _flip_status("online", "MCP session active"):
|
|
|
111
178
|
print(f"[meshcode-mcp] WARNING: could not flip status to online", file=sys.stderr)
|
|
112
179
|
|
|
113
180
|
|
|
181
|
+
# ============================================================
|
|
182
|
+
# Single-instance lease — prevent split-brain when two windows
|
|
183
|
+
# run `meshcode run <same-agent>` simultaneously.
|
|
184
|
+
# ============================================================
|
|
185
|
+
import uuid as _uuid
|
|
186
|
+
_INSTANCE_ID = f"mcp-{_uuid.uuid4().hex[:12]}"
|
|
187
|
+
|
|
188
|
+
def _acquire_lease() -> bool:
|
|
189
|
+
api_key = os.environ.get("MESHCODE_API_KEY", "")
|
|
190
|
+
if not api_key:
|
|
191
|
+
return True # legacy clients without api_key skip lease check
|
|
192
|
+
try:
|
|
193
|
+
r = be.sb_rpc("mc_acquire_agent_lease", {
|
|
194
|
+
"p_api_key": api_key,
|
|
195
|
+
"p_project_id": _PROJECT_ID,
|
|
196
|
+
"p_agent_name": AGENT_NAME,
|
|
197
|
+
"p_instance_id": _INSTANCE_ID,
|
|
198
|
+
})
|
|
199
|
+
if isinstance(r, dict) and r.get("ok"):
|
|
200
|
+
return True
|
|
201
|
+
if isinstance(r, dict) and r.get("error"):
|
|
202
|
+
print(f"[meshcode-mcp] LEASE DENIED: {r['error']}", file=sys.stderr)
|
|
203
|
+
print(f"[meshcode-mcp] Another window is already running this agent.", file=sys.stderr)
|
|
204
|
+
print(f"[meshcode-mcp] Close the other window first, or use a different agent name.", file=sys.stderr)
|
|
205
|
+
return False
|
|
206
|
+
except Exception as e:
|
|
207
|
+
print(f"[meshcode-mcp] WARNING: lease RPC failed: {e}", file=sys.stderr)
|
|
208
|
+
return True # don't hard-fail on transient network issues
|
|
209
|
+
|
|
210
|
+
if not _acquire_lease():
|
|
211
|
+
sys.exit(2)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _release_lease() -> None:
|
|
215
|
+
api_key = os.environ.get("MESHCODE_API_KEY", "")
|
|
216
|
+
if not api_key:
|
|
217
|
+
return
|
|
218
|
+
try:
|
|
219
|
+
be.sb_rpc("mc_release_agent_lease", {
|
|
220
|
+
"p_api_key": api_key,
|
|
221
|
+
"p_project_id": _PROJECT_ID,
|
|
222
|
+
"p_agent_name": AGENT_NAME,
|
|
223
|
+
"p_instance_id": _INSTANCE_ID,
|
|
224
|
+
})
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
114
229
|
# ============================================================
|
|
115
230
|
# Agent identity from Supabase profile (for system instructions)
|
|
116
231
|
# ============================================================
|
|
@@ -180,10 +295,19 @@ YOUR DEFAULT BEHAVIOR LOOP (do this without being asked):
|
|
|
180
295
|
user prompt. Read it, decide if it requires action or a reply, take
|
|
181
296
|
action, send a reply via meshcode_send if appropriate, then call
|
|
182
297
|
meshcode_wait AGAIN to keep listening.
|
|
183
|
-
5. When meshcode_wait returns timed_out
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
298
|
+
5. When meshcode_wait returns timed_out, call it AGAIN — but apply
|
|
299
|
+
TOKEN-EFFICIENT WAIT BACKOFF: double the previous timeout each
|
|
300
|
+
consecutive timeout (60 → 120 → 240 → 480 → 960 → 1800s, cap at
|
|
301
|
+
1800). Reset to 60s as soon as you receive ANY mesh message. Do
|
|
302
|
+
not return to the user. The loop is your default state — you are
|
|
303
|
+
a long-running mesh agent, not a one-shot chatbot.
|
|
304
|
+
|
|
305
|
+
6. GLOBAL DONE: when ANY agent (typically commander) calls
|
|
306
|
+
meshcode_done(reason), every other agent's meshcode_wait returns
|
|
307
|
+
with {got_done: true, reason}. Treat this as "task complete, exit
|
|
308
|
+
the loop". Do NOT call meshcode_wait again. Return cleanly to the
|
|
309
|
+
user with a summary of what the mesh accomplished. Stay quiet
|
|
310
|
+
until the user gives a new task.
|
|
187
311
|
|
|
188
312
|
YOU SHOULD ONLY BREAK OUT OF THE meshcode_wait LOOP IF:
|
|
189
313
|
- The human user explicitly says "stop" or pressed Ctrl+C.
|
|
@@ -213,6 +337,7 @@ YOUR FIRST ACTIONS WHEN THIS SESSION STARTS:
|
|
|
213
337
|
|
|
214
338
|
AVAILABLE MESH TOOLS (all from this MCP server):
|
|
215
339
|
meshcode_wait — block until a mesh message arrives (your idle state)
|
|
340
|
+
meshcode_done — broadcast a GLOBAL DONE to break every agent out of wait
|
|
216
341
|
meshcode_send — send a message to another mesh agent
|
|
217
342
|
meshcode_check — non-blocking peek at the inbox count
|
|
218
343
|
meshcode_read — drain inbox, mark messages read
|
|
@@ -282,13 +407,19 @@ async def lifespan(_app):
|
|
|
282
407
|
try:
|
|
283
408
|
yield {"realtime": _REALTIME}
|
|
284
409
|
finally:
|
|
285
|
-
log.info("lifespan shutdown — stopping heartbeat + realtime")
|
|
410
|
+
log.info("lifespan shutdown — stopping heartbeat + realtime + releasing lease")
|
|
286
411
|
hb_task.cancel()
|
|
287
412
|
try:
|
|
288
413
|
await hb_task
|
|
289
414
|
except asyncio.CancelledError:
|
|
290
415
|
pass
|
|
291
416
|
await _REALTIME.stop()
|
|
417
|
+
# Flip to offline + release lease so the dashboard reflects reality
|
|
418
|
+
# within seconds (not waiting for the 30s cron to notice).
|
|
419
|
+
try:
|
|
420
|
+
_release_lease()
|
|
421
|
+
except Exception as _e:
|
|
422
|
+
log.warning(f"could not release lease: {_e}")
|
|
292
423
|
|
|
293
424
|
|
|
294
425
|
# ============================================================
|
|
@@ -311,13 +442,24 @@ except Exception:
|
|
|
311
442
|
# ----------------- TOOLS -----------------
|
|
312
443
|
|
|
313
444
|
@mcp.tool()
|
|
314
|
-
def meshcode_send(to: str,
|
|
445
|
+
def meshcode_send(to: str, message: Any) -> Dict[str, Any]:
|
|
315
446
|
"""Send a message to another agent in the meshwork.
|
|
316
447
|
|
|
448
|
+
If you only have plain text, just pass it as a string and it will be
|
|
449
|
+
wrapped as {text: ...} automatically. For protocol payloads (need / done
|
|
450
|
+
/ fyi / blocked), pass a dict.
|
|
451
|
+
|
|
317
452
|
Args:
|
|
318
453
|
to: Name of the recipient agent.
|
|
319
|
-
|
|
454
|
+
message: Either a plain string (auto-wrapped as {"text": message}) or
|
|
455
|
+
a structured dict payload (use protocol keys: need/done/fyi/blocked).
|
|
320
456
|
"""
|
|
457
|
+
if isinstance(message, str):
|
|
458
|
+
payload: Dict[str, Any] = {"text": message}
|
|
459
|
+
elif isinstance(message, dict):
|
|
460
|
+
payload = message
|
|
461
|
+
else:
|
|
462
|
+
payload = {"text": str(message)}
|
|
321
463
|
return be.send_message(_PROJECT_ID, AGENT_NAME, to, payload, msg_type="msg")
|
|
322
464
|
|
|
323
465
|
|
|
@@ -339,84 +481,141 @@ def meshcode_broadcast(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
339
481
|
|
|
340
482
|
@mcp.tool()
|
|
341
483
|
def meshcode_read() -> Dict[str, Any]:
|
|
342
|
-
"""Read all pending (unread) messages for this agent. Marks them as read and ACKs senders.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
484
|
+
"""Read all pending (unread) messages for this agent. Marks them as read and ACKs senders.
|
|
485
|
+
|
|
486
|
+
Returns a split shape: `messages` (real msgs), `acks`, and `done_signals`
|
|
487
|
+
— so you don't have to filter by type yourself.
|
|
488
|
+
"""
|
|
489
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME)
|
|
490
|
+
normalized = [
|
|
491
|
+
{
|
|
492
|
+
"from": m["from_agent"],
|
|
493
|
+
"type": m.get("type", "msg"),
|
|
494
|
+
"ts": m.get("created_at"),
|
|
495
|
+
"payload": m.get("payload", {}),
|
|
496
|
+
"id": m.get("id"),
|
|
497
|
+
}
|
|
498
|
+
for m in raw
|
|
499
|
+
]
|
|
500
|
+
deduped = _filter_and_mark(normalized)
|
|
501
|
+
return _split_messages(deduped)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
505
|
+
"""Return {reason, from} if the list contains a global_done signal, else None."""
|
|
506
|
+
for m in messages:
|
|
507
|
+
if m.get("type") == "done":
|
|
508
|
+
p = m.get("payload") or {}
|
|
509
|
+
if p.get("type") == "global_done" or p.get("reason"):
|
|
510
|
+
return {
|
|
511
|
+
"reason": p.get("reason"),
|
|
512
|
+
"from": p.get("from") or m.get("from"),
|
|
513
|
+
}
|
|
514
|
+
return None
|
|
356
515
|
|
|
357
516
|
|
|
358
517
|
@mcp.tool()
|
|
359
|
-
async def meshcode_wait(timeout_seconds: int =
|
|
518
|
+
async def meshcode_wait(timeout_seconds: int = 120) -> Dict[str, Any]:
|
|
360
519
|
"""LONG-POLL: Block until a new message arrives for this agent (or timeout).
|
|
361
520
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
521
|
+
This is a TRUE async long-poll. While idle, the MCP server blocks on a
|
|
522
|
+
single asyncio Event — zero CPU, zero Supabase calls, zero token cost on
|
|
523
|
+
the Claude side (the tool call literally has not returned yet). It wakes
|
|
524
|
+
INSTANTLY the moment a mesh message lands via the Realtime websocket.
|
|
525
|
+
|
|
526
|
+
TOKEN-EFFICIENT WAIT BACKOFF: when this returns timed_out, call it again
|
|
527
|
+
with double the previous timeout (60 → 120 → 240 → 480 → 960 → 1800s,
|
|
528
|
+
cap at 1800). Reset to 60s as soon as you receive ANY mesh message. This
|
|
529
|
+
minimizes the cost of being idle.
|
|
368
530
|
|
|
369
|
-
|
|
370
|
-
|
|
531
|
+
GLOBAL DONE: if any agent calls meshcode_done(reason), this tool returns
|
|
532
|
+
with {got_done: true, reason, from}. Treat that as "task complete, exit
|
|
533
|
+
the loop". Do NOT call meshcode_wait again after a got_done. Return
|
|
534
|
+
cleanly to the user with a summary and stay quiet until a new task.
|
|
371
535
|
|
|
372
536
|
Args:
|
|
373
|
-
timeout_seconds: How long to block before returning empty (default
|
|
537
|
+
timeout_seconds: How long to block before returning empty (default 120s).
|
|
374
538
|
Pick something < your client's tool-call timeout.
|
|
375
539
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if
|
|
540
|
+
actual_timeout = max(1, int(timeout_seconds))
|
|
541
|
+
|
|
542
|
+
def _return_from_buffered(buffered: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
543
|
+
deduped = _filter_and_mark(buffered)
|
|
544
|
+
if not deduped:
|
|
545
|
+
return None
|
|
546
|
+
split = _split_messages(deduped)
|
|
547
|
+
out: Dict[str, Any] = {
|
|
548
|
+
"got_message": True,
|
|
549
|
+
"source": "realtime",
|
|
550
|
+
**split,
|
|
551
|
+
}
|
|
552
|
+
done = _detect_global_done(deduped)
|
|
553
|
+
if done:
|
|
554
|
+
out["got_done"] = True
|
|
555
|
+
out["reason"] = done["reason"]
|
|
556
|
+
out["from"] = done["from"]
|
|
557
|
+
return out
|
|
558
|
+
|
|
559
|
+
# 1) Drain anything already buffered from before this call.
|
|
560
|
+
if _REALTIME:
|
|
561
|
+
pre = _REALTIME.drain()
|
|
562
|
+
if pre:
|
|
563
|
+
shaped = _return_from_buffered(pre)
|
|
564
|
+
if shaped:
|
|
565
|
+
return shaped
|
|
566
|
+
|
|
567
|
+
# 2) Real async wait — zero CPU, zero Supabase calls.
|
|
568
|
+
woke = await _REALTIME.wait_for_message(timeout=float(actual_timeout))
|
|
569
|
+
if woke:
|
|
381
570
|
buffered = _REALTIME.drain()
|
|
382
571
|
if buffered:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
# 2) Safety net: poll Supabase directly in case realtime missed something
|
|
390
|
-
try:
|
|
391
|
-
pending_count = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
392
|
-
except Exception:
|
|
393
|
-
pending_count = 0
|
|
394
|
-
if pending_count > 0:
|
|
395
|
-
messages = be.read_inbox(_PROJECT_ID, AGENT_NAME)
|
|
396
|
-
return {
|
|
397
|
-
"got_message": True,
|
|
398
|
-
"source": "polled",
|
|
399
|
-
"count": len(messages),
|
|
400
|
-
"messages": [
|
|
401
|
-
{
|
|
402
|
-
"from": m["from_agent"],
|
|
403
|
-
"type": m.get("type", "msg"),
|
|
404
|
-
"ts": m.get("created_at"),
|
|
405
|
-
"payload": m.get("payload", {}),
|
|
406
|
-
}
|
|
407
|
-
for m in messages
|
|
408
|
-
],
|
|
409
|
-
}
|
|
410
|
-
await asyncio.sleep(poll_interval)
|
|
572
|
+
shaped = _return_from_buffered(buffered)
|
|
573
|
+
if shaped:
|
|
574
|
+
return shaped
|
|
575
|
+
else:
|
|
576
|
+
# Realtime unavailable — plain sleep fallback so we still honor timeout.
|
|
577
|
+
await asyncio.sleep(actual_timeout)
|
|
411
578
|
|
|
412
579
|
return {
|
|
413
580
|
"got_message": False,
|
|
414
581
|
"timed_out": True,
|
|
415
582
|
"agent": AGENT_NAME,
|
|
416
|
-
"hint":
|
|
583
|
+
"hint": (
|
|
584
|
+
"No messages arrived. Per TOKEN-EFFICIENT WAIT BACKOFF: call "
|
|
585
|
+
"meshcode_wait again with DOUBLE the previous timeout "
|
|
586
|
+
"(60→120→240→480→960→1800, cap 1800). Reset to 60s on any "
|
|
587
|
+
"received mesh message."
|
|
588
|
+
),
|
|
417
589
|
}
|
|
418
590
|
|
|
419
591
|
|
|
592
|
+
@mcp.tool()
|
|
593
|
+
def meshcode_done(reason: str) -> Dict[str, Any]:
|
|
594
|
+
"""Broadcast a GLOBAL DONE signal to every other agent in the meshwork.
|
|
595
|
+
|
|
596
|
+
Typically called by the commander when the overall task is complete.
|
|
597
|
+
Every other agent's meshcode_wait() will return immediately with
|
|
598
|
+
{got_done: true, reason, from}, breaking them cleanly out of their idle
|
|
599
|
+
loop so they can report to the human and stop burning tokens.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
reason: Short human-readable reason, e.g. "tests green, PR merged".
|
|
603
|
+
"""
|
|
604
|
+
agents = be.get_board(_PROJECT_ID)
|
|
605
|
+
payload = {"reason": reason, "from": AGENT_NAME, "type": "global_done"}
|
|
606
|
+
sent = 0
|
|
607
|
+
for a in agents:
|
|
608
|
+
name = a.get("name")
|
|
609
|
+
if name and name != AGENT_NAME:
|
|
610
|
+
try:
|
|
611
|
+
be.send_message(_PROJECT_ID, AGENT_NAME, name, payload, msg_type="done")
|
|
612
|
+
sent += 1
|
|
613
|
+
except Exception as e:
|
|
614
|
+
log.warning(f"meshcode_done: failed to notify {name}: {e}")
|
|
615
|
+
log.info(f"global done broadcast from {AGENT_NAME}: reason={reason!r} notified={sent}")
|
|
616
|
+
return {"ok": True, "global_done": True, "agents_notified": sent, "reason": reason}
|
|
617
|
+
|
|
618
|
+
|
|
420
619
|
@mcp.tool()
|
|
421
620
|
def meshcode_check() -> Dict[str, Any]:
|
|
422
621
|
"""Quick poll: returns pending message count + any messages buffered by the
|
|
@@ -427,12 +626,14 @@ def meshcode_check() -> Dict[str, Any]:
|
|
|
427
626
|
"""
|
|
428
627
|
pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
429
628
|
realtime_buffered = _REALTIME.drain() if _REALTIME else []
|
|
629
|
+
deduped = _filter_and_mark(realtime_buffered)
|
|
630
|
+
split = _split_messages(deduped)
|
|
430
631
|
return {
|
|
431
632
|
"pending": pending,
|
|
432
633
|
"agent": AGENT_NAME,
|
|
433
634
|
"project": PROJECT_NAME,
|
|
434
635
|
"realtime_connected": _REALTIME.is_connected if _REALTIME else False,
|
|
435
|
-
|
|
636
|
+
**split,
|
|
436
637
|
}
|
|
437
638
|
|
|
438
639
|
|
|
@@ -167,6 +167,39 @@ def setup_workspace(project: str, agent: str, role: str = "") -> int:
|
|
|
167
167
|
}
|
|
168
168
|
_atomic_write_json(registry_path, registry)
|
|
169
169
|
|
|
170
|
+
# Drop a README.md so the agent's working directory isn't empty on boot.
|
|
171
|
+
readme_body = f"""# {project} — {agent}
|
|
172
|
+
|
|
173
|
+
This is the MeshCode workspace for the **{agent}** agent in the **{project}** meshwork.
|
|
174
|
+
|
|
175
|
+
You are connected to the rest of your team via the meshcode MCP server, which is loaded
|
|
176
|
+
automatically when you open this directory in Claude Code, Cursor, or Cline.
|
|
177
|
+
|
|
178
|
+
## Your identity
|
|
179
|
+
- Project: {project}
|
|
180
|
+
- Agent name: {agent}
|
|
181
|
+
- Role: {role or "(set in dashboard)"}
|
|
182
|
+
|
|
183
|
+
## How this works
|
|
184
|
+
- Use the meshcode_* tools to communicate with other agents in the mesh.
|
|
185
|
+
- Use meshcode_status to see who else is in the meshwork.
|
|
186
|
+
- Use meshcode_send(to, message) to send messages.
|
|
187
|
+
- Use meshcode_wait to listen for incoming messages (your default idle state).
|
|
188
|
+
- Use meshcode_done(reason) when the team's task is complete.
|
|
189
|
+
|
|
190
|
+
## To launch this agent again
|
|
191
|
+
```bash
|
|
192
|
+
meshcode run {agent}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This is your workspace dir — feel free to scaffold any files your task needs here, or
|
|
196
|
+
work in a different repo by `cd`ing elsewhere after launch.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
(ws / "README.md").write_text(readme_body)
|
|
200
|
+
except Exception as _e:
|
|
201
|
+
print(f"[meshcode] WARNING: could not write README.md: {_e}", file=sys.stderr)
|
|
202
|
+
|
|
170
203
|
print(f"[meshcode] ✓ Workspace created for agent '{agent}' (project: {project})")
|
|
171
204
|
print(f"[meshcode] Path: {ws}")
|
|
172
205
|
print(f"[meshcode]")
|
|
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
|