meshcode 2.5.7__tar.gz → 2.6.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-2.5.7 → meshcode-2.6.0}/PKG-INFO +1 -1
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/__init__.py +1 -1
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/comms_v4.py +4 -3
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/backend.py +93 -9
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/server.py +124 -13
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.5.7 → meshcode-2.6.0}/pyproject.toml +1 -1
- {meshcode-2.5.7 → meshcode-2.6.0}/README.md +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/cli.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/invites.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/launcher.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/launcher_install.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/preferences.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/run_agent.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/secrets.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/self_update.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/setup_clients.py +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/setup.cfg +0 -0
- {meshcode-2.5.7 → meshcode-2.6.0}/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.0"
|
|
@@ -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):
|
|
@@ -244,8 +244,8 @@ def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any,
|
|
|
244
244
|
if encrypted and api_key:
|
|
245
245
|
mesh_key = get_mesh_key(api_key, project_id)
|
|
246
246
|
if mesh_key:
|
|
247
|
-
encrypted_data = encrypt_payload(payload, mesh_key)
|
|
248
|
-
payload = {"_encrypted": encrypted_data}
|
|
247
|
+
encrypted_data = encrypt_payload(payload, mesh_key, aad=project_id)
|
|
248
|
+
payload = {"_encrypted": encrypted_data, "_aad": project_id}
|
|
249
249
|
actual_encrypted = True
|
|
250
250
|
|
|
251
251
|
# Use validated SECURITY DEFINER RPC when api_key is provided
|
|
@@ -302,6 +302,56 @@ def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any,
|
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Optional[str] = None) -> List[Dict]:
|
|
305
|
+
# Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
|
|
306
|
+
if api_key:
|
|
307
|
+
rpc_result = sb_rpc("mc_read_inbox", {
|
|
308
|
+
"p_api_key": api_key,
|
|
309
|
+
"p_project_id": project_id,
|
|
310
|
+
"p_agent_name": agent,
|
|
311
|
+
"p_mark_read": mark_read,
|
|
312
|
+
})
|
|
313
|
+
if isinstance(rpc_result, dict) and rpc_result.get("ok"):
|
|
314
|
+
messages = rpc_result.get("messages", [])
|
|
315
|
+
if isinstance(messages, list):
|
|
316
|
+
# Auto-decrypt and ACK (mark_read already handled by RPC)
|
|
317
|
+
mesh_key = None
|
|
318
|
+
for m in messages:
|
|
319
|
+
p = m.get("payload")
|
|
320
|
+
if isinstance(p, dict) and "_encrypted" in p:
|
|
321
|
+
if mesh_key is None:
|
|
322
|
+
mesh_key = get_mesh_key(api_key, project_id)
|
|
323
|
+
if mesh_key:
|
|
324
|
+
msg_aad = p.get("_aad", project_id)
|
|
325
|
+
decrypted = decrypt_payload(p["_encrypted"], mesh_key, aad=msg_aad)
|
|
326
|
+
if decrypted is not None:
|
|
327
|
+
m["payload"] = decrypted
|
|
328
|
+
m["_was_encrypted"] = True
|
|
329
|
+
if mark_read and messages:
|
|
330
|
+
import datetime as _dt
|
|
331
|
+
now = _dt.datetime.now(_dt.timezone.utc)
|
|
332
|
+
def _is_stale_rpc(m):
|
|
333
|
+
ts = m.get("created_at")
|
|
334
|
+
if not ts:
|
|
335
|
+
return True
|
|
336
|
+
try:
|
|
337
|
+
dt = _dt.datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
|
|
338
|
+
return (now - dt).total_seconds() > 60
|
|
339
|
+
except Exception:
|
|
340
|
+
return True
|
|
341
|
+
ack_targets = {
|
|
342
|
+
m.get("from_agent", "")
|
|
343
|
+
for m in messages
|
|
344
|
+
if m.get("type") not in ("ack", "broadcast") and _is_stale_rpc(m)
|
|
345
|
+
}
|
|
346
|
+
for sender in ack_targets:
|
|
347
|
+
if sender:
|
|
348
|
+
send_message(project_id, agent, sender,
|
|
349
|
+
{"text": f"{agent} read your message"},
|
|
350
|
+
msg_type="ack", api_key=api_key)
|
|
351
|
+
return messages
|
|
352
|
+
# If RPC doesn't exist yet, fall through to direct query
|
|
353
|
+
|
|
354
|
+
# Fallback: direct SELECT (tests/legacy — requires anon RLS bypass)
|
|
305
355
|
messages = sb_select(
|
|
306
356
|
"mc_messages",
|
|
307
357
|
f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false",
|
|
@@ -316,7 +366,8 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
|
|
|
316
366
|
if mesh_key is None:
|
|
317
367
|
mesh_key = get_mesh_key(api_key, project_id)
|
|
318
368
|
if mesh_key:
|
|
319
|
-
|
|
369
|
+
msg_aad = p.get("_aad", project_id)
|
|
370
|
+
decrypted = decrypt_payload(p["_encrypted"], mesh_key, aad=msg_aad)
|
|
320
371
|
if decrypted is not None:
|
|
321
372
|
m["payload"] = decrypted
|
|
322
373
|
m["_was_encrypted"] = True
|
|
@@ -365,7 +416,19 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
|
|
|
365
416
|
return messages
|
|
366
417
|
|
|
367
418
|
|
|
368
|
-
def count_pending(project_id: str, agent: str) -> int:
|
|
419
|
+
def count_pending(project_id: str, agent: str, api_key: Optional[str] = None) -> int:
|
|
420
|
+
# Use SECURITY DEFINER RPC when api_key is available
|
|
421
|
+
if api_key:
|
|
422
|
+
result = sb_rpc("mc_count_pending", {
|
|
423
|
+
"p_api_key": api_key,
|
|
424
|
+
"p_project_id": project_id,
|
|
425
|
+
"p_agent_name": agent,
|
|
426
|
+
})
|
|
427
|
+
if isinstance(result, dict) and result.get("ok"):
|
|
428
|
+
return result.get("count", 0)
|
|
429
|
+
# Fall through to direct query if RPC doesn't exist yet
|
|
430
|
+
|
|
431
|
+
# Fallback: direct SELECT (tests/legacy)
|
|
369
432
|
pending = sb_select(
|
|
370
433
|
"mc_messages",
|
|
371
434
|
f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false&type=neq.ack",
|
|
@@ -396,8 +459,15 @@ def get_mesh_key(api_key: str, project_id: str) -> Optional[str]:
|
|
|
396
459
|
return None
|
|
397
460
|
|
|
398
461
|
|
|
399
|
-
def encrypt_payload(payload: Dict, hex_key: str) -> str:
|
|
400
|
-
"""Encrypt a JSON payload using AES-256-GCM
|
|
462
|
+
def encrypt_payload(payload: Dict, hex_key: str, aad: Optional[str] = None) -> str:
|
|
463
|
+
"""Encrypt a JSON payload using AES-256-GCM with optional AAD.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
payload: JSON-serializable dict to encrypt.
|
|
467
|
+
hex_key: Hex-encoded AES-256 key.
|
|
468
|
+
aad: Additional Authenticated Data (e.g. project_id) to bind
|
|
469
|
+
ciphertext to context and prevent replay/swap attacks.
|
|
470
|
+
"""
|
|
401
471
|
import base64
|
|
402
472
|
try:
|
|
403
473
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
@@ -406,14 +476,19 @@ def encrypt_payload(payload: Dict, hex_key: str) -> str:
|
|
|
406
476
|
key = bytes.fromhex(hex_key)
|
|
407
477
|
nonce = os.urandom(12) # 96-bit nonce for GCM
|
|
408
478
|
plaintext = json.dumps(payload).encode("utf-8")
|
|
479
|
+
aad_bytes = aad.encode("utf-8") if aad else None
|
|
409
480
|
aesgcm = AESGCM(key)
|
|
410
|
-
ciphertext = aesgcm.encrypt(nonce, plaintext,
|
|
481
|
+
ciphertext = aesgcm.encrypt(nonce, plaintext, aad_bytes)
|
|
411
482
|
# Format: base64(nonce + ciphertext)
|
|
412
483
|
return base64.b64encode(nonce + ciphertext).decode("ascii")
|
|
413
484
|
|
|
414
485
|
|
|
415
|
-
def decrypt_payload(encrypted_b64: str, hex_key: str) -> Optional[Dict]:
|
|
416
|
-
"""Decrypt an AES-256-GCM encrypted payload. Returns None on failure.
|
|
486
|
+
def decrypt_payload(encrypted_b64: str, hex_key: str, aad: Optional[str] = None) -> Optional[Dict]:
|
|
487
|
+
"""Decrypt an AES-256-GCM encrypted payload. Returns None on failure.
|
|
488
|
+
|
|
489
|
+
Tries with AAD first, falls back to no-AAD for backward compat
|
|
490
|
+
with messages encrypted before AAD was added.
|
|
491
|
+
"""
|
|
417
492
|
import base64
|
|
418
493
|
try:
|
|
419
494
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
@@ -425,6 +500,15 @@ def decrypt_payload(encrypted_b64: str, hex_key: str) -> Optional[Dict]:
|
|
|
425
500
|
nonce = raw[:12]
|
|
426
501
|
ciphertext = raw[12:]
|
|
427
502
|
aesgcm = AESGCM(key)
|
|
503
|
+
aad_bytes = aad.encode("utf-8") if aad else None
|
|
504
|
+
# Try with AAD first
|
|
505
|
+
if aad_bytes:
|
|
506
|
+
try:
|
|
507
|
+
plaintext = aesgcm.decrypt(nonce, ciphertext, aad_bytes)
|
|
508
|
+
return json.loads(plaintext.decode("utf-8"))
|
|
509
|
+
except Exception:
|
|
510
|
+
pass # Fall through to no-AAD for backward compat
|
|
511
|
+
# Try without AAD (legacy messages)
|
|
428
512
|
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
|
|
429
513
|
return json.loads(plaintext.decode("utf-8"))
|
|
430
514
|
except Exception:
|
|
@@ -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)
|
|
@@ -1148,13 +1157,15 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
|
|
|
1148
1157
|
"p_api_key": api_key,
|
|
1149
1158
|
"p_source_project": PROJECT_NAME,
|
|
1150
1159
|
"p_target_project": target_meshwork,
|
|
1160
|
+
"p_agent_name": AGENT_NAME,
|
|
1151
1161
|
})
|
|
1152
1162
|
if not isinstance(key_result, dict) or not key_result.get("ok"):
|
|
1153
1163
|
err = key_result.get("error", "unknown") if isinstance(key_result, dict) else "RPC failed"
|
|
1154
1164
|
return {"error": f"cross-mesh encryption failed: {err}"}
|
|
1155
1165
|
tgt_key = key_result["key"]
|
|
1156
|
-
|
|
1157
|
-
|
|
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}
|
|
1158
1169
|
|
|
1159
1170
|
result = be.sb_rpc("mc_send_cross_mesh", {
|
|
1160
1171
|
"p_api_key": api_key,
|
|
@@ -1278,6 +1289,28 @@ def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, An
|
|
|
1278
1289
|
# Auto-sleep: track consecutive idle timeouts to auto-sleep after threshold
|
|
1279
1290
|
_CONSECUTIVE_IDLE_SECONDS = 0
|
|
1280
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
|
|
1281
1314
|
|
|
1282
1315
|
|
|
1283
1316
|
@mcp.tool()
|
|
@@ -1289,6 +1322,41 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1289
1322
|
timeout_seconds: Max wait time in seconds (default 120, hard cap 120).
|
|
1290
1323
|
"""
|
|
1291
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
|
+
|
|
1292
1360
|
_IN_WAIT = True
|
|
1293
1361
|
_set_state("waiting", "listening for messages")
|
|
1294
1362
|
# Universal hard cap: even if a caller passes a larger value (e.g. 1800),
|
|
@@ -1323,6 +1391,18 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1323
1391
|
"threshold": _AUTO_SLEEP_THRESHOLD,
|
|
1324
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.",
|
|
1325
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
|
|
1326
1406
|
return result
|
|
1327
1407
|
finally:
|
|
1328
1408
|
_IN_WAIT = False
|
|
@@ -1387,7 +1467,15 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
1387
1467
|
# Realtime unavailable — plain sleep fallback so we still honor timeout.
|
|
1388
1468
|
await asyncio.sleep(actual_timeout)
|
|
1389
1469
|
|
|
1390
|
-
|
|
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
|
|
1391
1479
|
|
|
1392
1480
|
|
|
1393
1481
|
@mcp.tool()
|
|
@@ -1426,7 +1514,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
|
|
|
1426
1514
|
since: ISO-8601 timestamp. Only return messages newer than this.
|
|
1427
1515
|
Use meshcode_remember("last_seen", ts) to persist across sessions.
|
|
1428
1516
|
"""
|
|
1429
|
-
pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
1517
|
+
pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
|
|
1430
1518
|
# Peek at realtime buffer WITHOUT draining — check is non-destructive
|
|
1431
1519
|
realtime_buffered = _REALTIME.peek() if _REALTIME else []
|
|
1432
1520
|
# Don't mark as seen — meshcode_check is a peek, not a consume
|
|
@@ -1451,20 +1539,26 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
|
|
|
1451
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
|
|
1452
1540
|
]
|
|
1453
1541
|
|
|
1454
|
-
#
|
|
1455
|
-
|
|
1456
|
-
|
|
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]
|
|
1457
1546
|
|
|
1458
1547
|
split = _split_messages(deduped)
|
|
1459
1548
|
if not include_acks:
|
|
1460
1549
|
split["acks"] = []
|
|
1461
|
-
|
|
1462
|
-
"pending": pending if not
|
|
1550
|
+
result = {
|
|
1551
|
+
"pending": pending if not effective_since else len(split.get("messages", [])),
|
|
1463
1552
|
"agent": AGENT_NAME,
|
|
1464
1553
|
"project": PROJECT_NAME,
|
|
1465
1554
|
"realtime_connected": _REALTIME.is_connected if _REALTIME else False,
|
|
1466
1555
|
**split,
|
|
1467
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
|
|
1468
1562
|
|
|
1469
1563
|
|
|
1470
1564
|
@mcp.tool()
|
|
@@ -1499,9 +1593,18 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
|
|
|
1499
1593
|
"""Update your status in the board.
|
|
1500
1594
|
|
|
1501
1595
|
Args:
|
|
1502
|
-
status: One of: working, idle, standby, blocked, done, online.
|
|
1596
|
+
status: One of: working, idle, standby, blocked, done, online, sleeping.
|
|
1503
1597
|
task: Optional human-readable task description.
|
|
1504
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
|
+
}
|
|
1505
1608
|
return be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
|
|
1506
1609
|
|
|
1507
1610
|
|
|
@@ -2079,7 +2182,15 @@ def history_resource() -> str:
|
|
|
2079
2182
|
# ============================================================
|
|
2080
2183
|
|
|
2081
2184
|
def _auto_update() -> None:
|
|
2082
|
-
"""
|
|
2185
|
+
"""Upgrade meshcode from PyPI in background — opt-in via MESHCODE_AUTO_UPDATE=1.
|
|
2186
|
+
|
|
2187
|
+
Disabled by default to mitigate supply chain risk: if the PyPI account
|
|
2188
|
+
is compromised, every agent would silently install malicious code.
|
|
2189
|
+
"""
|
|
2190
|
+
if os.environ.get("MESHCODE_AUTO_UPDATE", "0") != "1":
|
|
2191
|
+
log.debug("[meshcode] Auto-update disabled (set MESHCODE_AUTO_UPDATE=1 to enable)")
|
|
2192
|
+
return
|
|
2193
|
+
|
|
2083
2194
|
import threading
|
|
2084
2195
|
|
|
2085
2196
|
def _upgrade():
|
|
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
|