meshcode 2.5.8__tar.gz → 2.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshcode-2.5.8 → meshcode-2.6.1}/PKG-INFO +1 -1
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/__init__.py +1 -1
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/comms_v4.py +4 -3
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/backend.py +4 -2
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/server.py +118 -16
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.5.8 → meshcode-2.6.1}/pyproject.toml +1 -1
- {meshcode-2.5.8 → meshcode-2.6.1}/README.md +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/cli.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/invites.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/launcher.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/launcher_install.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/preferences.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/run_agent.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/secrets.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/self_update.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode/setup_clients.py +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/setup.cfg +0 -0
- {meshcode-2.5.8 → meshcode-2.6.1}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.
|
|
2
|
+
__version__ = "2.6.1"
|
|
@@ -470,9 +470,10 @@ def _throttle_spawn_ok(project, name, max_per_5min=5):
|
|
|
470
470
|
|
|
471
471
|
|
|
472
472
|
def _headless_spawn_allowed():
|
|
473
|
-
"""
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
"""Headless spawn is ON by default for mesh agents — sleeping agents auto-wake
|
|
474
|
+
when a message/task arrives. Opt OUT with MESHCODE_ALLOW_HEADLESS_SPAWN=0."""
|
|
475
|
+
val = os.environ.get("MESHCODE_ALLOW_HEADLESS_SPAWN", "1").strip().lower()
|
|
476
|
+
return val not in ("0", "false", "no", "off")
|
|
476
477
|
|
|
477
478
|
|
|
478
479
|
def spawn_headless_agent(project, name, project_id, message_body, from_agent):
|
|
@@ -321,7 +321,8 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
|
|
|
321
321
|
if mesh_key is None:
|
|
322
322
|
mesh_key = get_mesh_key(api_key, project_id)
|
|
323
323
|
if mesh_key:
|
|
324
|
-
|
|
324
|
+
msg_aad = p.get("_aad", project_id)
|
|
325
|
+
decrypted = decrypt_payload(p["_encrypted"], mesh_key, aad=msg_aad)
|
|
325
326
|
if decrypted is not None:
|
|
326
327
|
m["payload"] = decrypted
|
|
327
328
|
m["_was_encrypted"] = True
|
|
@@ -365,7 +366,8 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
|
|
|
365
366
|
if mesh_key is None:
|
|
366
367
|
mesh_key = get_mesh_key(api_key, project_id)
|
|
367
368
|
if mesh_key:
|
|
368
|
-
|
|
369
|
+
msg_aad = p.get("_aad", project_id)
|
|
370
|
+
decrypted = decrypt_payload(p["_encrypted"], mesh_key, aad=msg_aad)
|
|
369
371
|
if decrypted is not None:
|
|
370
372
|
m["payload"] = decrypted
|
|
371
373
|
m["_was_encrypted"] = True
|
|
@@ -1124,15 +1124,24 @@ except Exception:
|
|
|
1124
1124
|
@mcp.tool()
|
|
1125
1125
|
@with_working_status
|
|
1126
1126
|
def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
|
|
1127
|
-
sensitive: bool = False, encrypted: bool =
|
|
1128
|
-
"""Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports.
|
|
1127
|
+
sensitive: bool = False, encrypted: bool = True) -> Dict[str, Any]:
|
|
1128
|
+
"""Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. All messages encrypted by default (AES-256-GCM). Pass encrypted=False to send plaintext."""
|
|
1129
1129
|
if isinstance(message, str):
|
|
1130
|
+
# Auto-wrap strings but warn if too long — prefer structured JSON
|
|
1131
|
+
if len(message) > 400:
|
|
1132
|
+
return {"error": f"plain text message too long ({len(message)} chars). Use a dict payload with structured fields, or use meshcode_task_create for long content. Messages must be structured JSON <2000 chars."}
|
|
1130
1133
|
payload: Dict[str, Any] = {"text": message}
|
|
1131
1134
|
elif isinstance(message, dict):
|
|
1132
1135
|
payload = message
|
|
1133
1136
|
else:
|
|
1134
1137
|
payload = {"text": str(message)}
|
|
1135
1138
|
|
|
1139
|
+
# Enforce message size limit — long content belongs in task descriptions
|
|
1140
|
+
import json as _json
|
|
1141
|
+
_payload_len = len(_json.dumps(payload, default=str))
|
|
1142
|
+
if _payload_len > 2000:
|
|
1143
|
+
return {"error": f"message too large ({_payload_len} chars). Use meshcode_task_create for long content. Messages must be structured JSON <2000 chars."}
|
|
1144
|
+
|
|
1136
1145
|
# Cross-mesh routing: if 'to' contains '@', parse as agent@meshwork
|
|
1137
1146
|
if "@" in to:
|
|
1138
1147
|
target_agent, target_meshwork = to.split("@", 1)
|
|
@@ -1154,8 +1163,9 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
|
|
|
1154
1163
|
err = key_result.get("error", "unknown") if isinstance(key_result, dict) else "RPC failed"
|
|
1155
1164
|
return {"error": f"cross-mesh encryption failed: {err}"}
|
|
1156
1165
|
tgt_key = key_result["key"]
|
|
1157
|
-
|
|
1158
|
-
|
|
1166
|
+
tgt_project_id = key_result.get("target_project_id", "")
|
|
1167
|
+
encrypted_data = be.encrypt_payload(payload, tgt_key, aad=tgt_project_id)
|
|
1168
|
+
payload = {"_encrypted": encrypted_data, "_aad": tgt_project_id}
|
|
1159
1169
|
|
|
1160
1170
|
result = be.sb_rpc("mc_send_cross_mesh", {
|
|
1161
1171
|
"p_api_key": api_key,
|
|
@@ -1279,6 +1289,28 @@ def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, An
|
|
|
1279
1289
|
# Auto-sleep: track consecutive idle timeouts to auto-sleep after threshold
|
|
1280
1290
|
_CONSECUTIVE_IDLE_SECONDS = 0
|
|
1281
1291
|
_AUTO_SLEEP_THRESHOLD = int(os.environ.get("MESHCODE_AUTO_SLEEP_SECONDS", "600")) # default 10min
|
|
1292
|
+
_LAST_SEEN_TS: Optional[str] = None # auto-persisted for message dedup
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
1296
|
+
"""Fetch open/in_progress tasks assigned to this agent. Returns compact list or None."""
|
|
1297
|
+
try:
|
|
1298
|
+
api_key = _get_api_key()
|
|
1299
|
+
if not api_key:
|
|
1300
|
+
return None
|
|
1301
|
+
result = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter=None)
|
|
1302
|
+
if not isinstance(result, dict) or not result.get("ok"):
|
|
1303
|
+
return None
|
|
1304
|
+
tasks = result.get("tasks", [])
|
|
1305
|
+
pending = [
|
|
1306
|
+
{"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
|
|
1307
|
+
for t in tasks
|
|
1308
|
+
if t.get("status") in ("open", "in_progress")
|
|
1309
|
+
and (t.get("assigned_to") in (AGENT_NAME, "*", None) or t.get("claimed_by") == AGENT_NAME)
|
|
1310
|
+
]
|
|
1311
|
+
return pending if pending else None
|
|
1312
|
+
except Exception:
|
|
1313
|
+
return None
|
|
1282
1314
|
|
|
1283
1315
|
|
|
1284
1316
|
@mcp.tool()
|
|
@@ -1290,6 +1322,41 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1290
1322
|
timeout_seconds: Max wait time in seconds (default 120, hard cap 120).
|
|
1291
1323
|
"""
|
|
1292
1324
|
global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS
|
|
1325
|
+
|
|
1326
|
+
# PRODUCT RULE 1: If agent has open tasks, refuse to wait. Work first.
|
|
1327
|
+
pending_tasks = _get_pending_tasks_summary()
|
|
1328
|
+
if pending_tasks:
|
|
1329
|
+
return {
|
|
1330
|
+
"refused": True,
|
|
1331
|
+
"reason": "You have open tasks. Work them before entering wait.",
|
|
1332
|
+
"pending_tasks": pending_tasks,
|
|
1333
|
+
"count": len(pending_tasks),
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
# PRODUCT RULE 2: If agent has unread messages in DB, refuse to wait.
|
|
1337
|
+
# The in-memory dedupe (_SEEN_MSG_IDS) can mark messages as "seen" via
|
|
1338
|
+
# realtime without the agent actually processing them. Always check DB.
|
|
1339
|
+
try:
|
|
1340
|
+
db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
1341
|
+
if db_pending and db_pending > 0:
|
|
1342
|
+
# Fetch and return the messages instead of waiting
|
|
1343
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False, api_key=_get_api_key())
|
|
1344
|
+
msgs = [
|
|
1345
|
+
{"from": m["from_agent"], "type": m.get("type", "msg"),
|
|
1346
|
+
"ts": m.get("created_at"), "payload": m.get("payload", {}),
|
|
1347
|
+
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
1348
|
+
for m in raw
|
|
1349
|
+
]
|
|
1350
|
+
split = _split_messages(msgs)
|
|
1351
|
+
return {
|
|
1352
|
+
"refused": True,
|
|
1353
|
+
"reason": f"You have {db_pending} unread messages. Process them before waiting.",
|
|
1354
|
+
"got_message": True,
|
|
1355
|
+
**split,
|
|
1356
|
+
}
|
|
1357
|
+
except Exception:
|
|
1358
|
+
pass
|
|
1359
|
+
|
|
1293
1360
|
_IN_WAIT = True
|
|
1294
1361
|
_set_state("waiting", "listening for messages")
|
|
1295
1362
|
# Universal hard cap: even if a caller passes a larger value (e.g. 1800),
|
|
@@ -1324,6 +1391,18 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1324
1391
|
"threshold": _AUTO_SLEEP_THRESHOLD,
|
|
1325
1392
|
"instruction": "You have been idle too long. Status set to sleeping. Exit the wait loop to save resources. You will be woken by auto_wake if a new message arrives.",
|
|
1326
1393
|
}
|
|
1394
|
+
# Auto-inject pending tasks so agents don't forget to check
|
|
1395
|
+
pending_tasks = _get_pending_tasks_summary()
|
|
1396
|
+
if pending_tasks:
|
|
1397
|
+
result["pending_tasks"] = pending_tasks
|
|
1398
|
+
# Track last seen timestamp for message dedup
|
|
1399
|
+
global _LAST_SEEN_TS
|
|
1400
|
+
if result.get("got_message"):
|
|
1401
|
+
msgs = result.get("messages", [])
|
|
1402
|
+
if msgs:
|
|
1403
|
+
latest_ts = max((m.get("ts", "") for m in msgs), default="")
|
|
1404
|
+
if latest_ts:
|
|
1405
|
+
_LAST_SEEN_TS = latest_ts
|
|
1327
1406
|
return result
|
|
1328
1407
|
finally:
|
|
1329
1408
|
_IN_WAIT = False
|
|
@@ -1388,7 +1467,15 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
1388
1467
|
# Realtime unavailable — plain sleep fallback so we still honor timeout.
|
|
1389
1468
|
await asyncio.sleep(actual_timeout)
|
|
1390
1469
|
|
|
1391
|
-
|
|
1470
|
+
# Check if there's any pending work before returning timeout
|
|
1471
|
+
pending_tasks = _get_pending_tasks_summary()
|
|
1472
|
+
out: Dict[str, Any] = {"timed_out": True}
|
|
1473
|
+
if pending_tasks:
|
|
1474
|
+
out["pending_tasks"] = pending_tasks
|
|
1475
|
+
else:
|
|
1476
|
+
out["no_work"] = True
|
|
1477
|
+
out["hint"] = "No messages or tasks. Safe to sleep — launcher daemon will wake you on new messages."
|
|
1478
|
+
return out
|
|
1392
1479
|
|
|
1393
1480
|
|
|
1394
1481
|
@mcp.tool()
|
|
@@ -1452,20 +1539,26 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
|
|
|
1452
1539
|
if _seen_key({"id": m.get("id"), "from": m.get("from_agent"), "payload": m.get("payload", {}), "ts": m.get("created_at")}) not in _SEEN_MSG_IDS
|
|
1453
1540
|
]
|
|
1454
1541
|
|
|
1455
|
-
#
|
|
1456
|
-
|
|
1457
|
-
|
|
1542
|
+
# Auto-apply last_seen_ts if no explicit since provided
|
|
1543
|
+
effective_since = since or _LAST_SEEN_TS
|
|
1544
|
+
if effective_since:
|
|
1545
|
+
deduped = [m for m in deduped if m.get("ts") and str(m["ts"]) > effective_since]
|
|
1458
1546
|
|
|
1459
1547
|
split = _split_messages(deduped)
|
|
1460
1548
|
if not include_acks:
|
|
1461
1549
|
split["acks"] = []
|
|
1462
|
-
|
|
1463
|
-
"pending": pending if not
|
|
1550
|
+
result = {
|
|
1551
|
+
"pending": pending if not effective_since else len(split.get("messages", [])),
|
|
1464
1552
|
"agent": AGENT_NAME,
|
|
1465
1553
|
"project": PROJECT_NAME,
|
|
1466
1554
|
"realtime_connected": _REALTIME.is_connected if _REALTIME else False,
|
|
1467
1555
|
**split,
|
|
1468
1556
|
}
|
|
1557
|
+
# Auto-inject pending tasks
|
|
1558
|
+
pending_tasks = _get_pending_tasks_summary()
|
|
1559
|
+
if pending_tasks:
|
|
1560
|
+
result["pending_tasks"] = pending_tasks
|
|
1561
|
+
return result
|
|
1469
1562
|
|
|
1470
1563
|
|
|
1471
1564
|
@mcp.tool()
|
|
@@ -1500,9 +1593,18 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
|
|
|
1500
1593
|
"""Update your status in the board.
|
|
1501
1594
|
|
|
1502
1595
|
Args:
|
|
1503
|
-
status: One of: working, idle, standby, blocked, done, online.
|
|
1596
|
+
status: One of: working, idle, standby, blocked, done, online, sleeping.
|
|
1504
1597
|
task: Optional human-readable task description.
|
|
1505
1598
|
"""
|
|
1599
|
+
# PRODUCT RULE: Cannot sleep/idle/standby with open tasks. Work first.
|
|
1600
|
+
if status in ("sleeping", "idle", "standby"):
|
|
1601
|
+
pending_tasks = _get_pending_tasks_summary()
|
|
1602
|
+
if pending_tasks:
|
|
1603
|
+
return {
|
|
1604
|
+
"refused": True,
|
|
1605
|
+
"reason": f"Cannot set status '{status}' — you have {len(pending_tasks)} open tasks. Work them first.",
|
|
1606
|
+
"pending_tasks": pending_tasks,
|
|
1607
|
+
}
|
|
1506
1608
|
return be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
|
|
1507
1609
|
|
|
1508
1610
|
|
|
@@ -2080,13 +2182,13 @@ def history_resource() -> str:
|
|
|
2080
2182
|
# ============================================================
|
|
2081
2183
|
|
|
2082
2184
|
def _auto_update() -> None:
|
|
2083
|
-
"""
|
|
2185
|
+
"""Silently upgrade meshcode from PyPI in background on every launch.
|
|
2084
2186
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2187
|
+
Ensures all agents always run the latest version. Disable with
|
|
2188
|
+
MESHCODE_AUTO_UPDATE=0 if needed.
|
|
2087
2189
|
"""
|
|
2088
|
-
if os.environ.get("MESHCODE_AUTO_UPDATE", "
|
|
2089
|
-
log.debug("[meshcode] Auto-update disabled (
|
|
2190
|
+
if os.environ.get("MESHCODE_AUTO_UPDATE", "1").lower() in ("0", "false", "no"):
|
|
2191
|
+
log.debug("[meshcode] Auto-update disabled (MESHCODE_AUTO_UPDATE=0)")
|
|
2090
2192
|
return
|
|
2091
2193
|
|
|
2092
2194
|
import threading
|
|
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
|