meshcode 2.10.47__tar.gz → 2.10.48__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.47 → meshcode-2.10.48}/PKG-INFO +1 -1
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/__init__.py +1 -1
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/comms_v4.py +155 -41
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/meshcode_mcp/backend.py +86 -15
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/meshcode_mcp/realtime.py +1 -1
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/meshcode_mcp/server.py +95 -7
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/run_agent.py +43 -11
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.47 → meshcode-2.10.48}/pyproject.toml +1 -1
- {meshcode-2.10.47 → meshcode-2.10.48}/README.md +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/cli.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/invites.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/launcher.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/preferences.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/secrets.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/self_update.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/setup.cfg +0 -0
- {meshcode-2.10.47 → meshcode-2.10.48}/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.48"
|
|
@@ -640,11 +640,19 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
|
|
|
640
640
|
# Flip status to 'working' so the dashboard reflects real-time activity.
|
|
641
641
|
# We'll flip back to 'sleeping' (or 'online' if any reply went out) at the end.
|
|
642
642
|
try:
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
{"status": "working", "task": f"headless: msg from {from_agent}",
|
|
643
|
+
_ak = _load_api_key_for_cli()
|
|
644
|
+
_fields = {"status": "working", "task": f"headless: msg from {from_agent}",
|
|
646
645
|
"last_active_at": now_iso(), "last_heartbeat": now_iso(),
|
|
647
|
-
"last_woken_at": now_iso()}
|
|
646
|
+
"last_woken_at": now_iso()}
|
|
647
|
+
if _ak:
|
|
648
|
+
_r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
|
|
649
|
+
"p_agent_name": name, "p_fields": _fields})
|
|
650
|
+
if not (_r and _r.get("ok")):
|
|
651
|
+
sb_update("mc_agents",
|
|
652
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields)
|
|
653
|
+
else:
|
|
654
|
+
sb_update("mc_agents",
|
|
655
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields)
|
|
648
656
|
except Exception:
|
|
649
657
|
pass
|
|
650
658
|
try:
|
|
@@ -692,10 +700,18 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
|
|
|
692
700
|
try:
|
|
693
701
|
# Flip status back to 'sleeping' after the headless turn ends.
|
|
694
702
|
# The agent is identity-only at rest; next message wakes it again.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
703
|
+
_ak = _load_api_key_for_cli()
|
|
704
|
+
_fields = {"status": "sleeping", "task": "Waiting for messages...",
|
|
705
|
+
"last_active_at": now_iso(), "last_heartbeat": now_iso()}
|
|
706
|
+
if _ak:
|
|
707
|
+
_r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
|
|
708
|
+
"p_agent_name": name, "p_fields": _fields})
|
|
709
|
+
if not (_r and _r.get("ok")):
|
|
710
|
+
sb_update("mc_agents",
|
|
711
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields)
|
|
712
|
+
else:
|
|
713
|
+
sb_update("mc_agents",
|
|
714
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields)
|
|
699
715
|
except Exception:
|
|
700
716
|
pass
|
|
701
717
|
|
|
@@ -995,16 +1011,24 @@ def register(project, name, role=""):
|
|
|
995
1011
|
return
|
|
996
1012
|
|
|
997
1013
|
# Update tty/pid (RPC doesn't take these — patch separately)
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1014
|
+
_fields_reg = {"tty": tty, "pid": ppid, "task": role, "last_heartbeat": now_iso()}
|
|
1015
|
+
_r_upd = sb_rpc("mc_update_agent", {"p_api_key": _api_key, "p_project_id": project_id,
|
|
1016
|
+
"p_agent_name": name, "p_fields": _fields_reg})
|
|
1017
|
+
if not (_r_upd and _r_upd.get("ok")):
|
|
1018
|
+
sb_update("mc_agents",
|
|
1019
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", _fields_reg)
|
|
1001
1020
|
|
|
1002
1021
|
# Save local session file (for nudge TTY detection)
|
|
1003
1022
|
session_data = {"project": project, "agent": name, "pid": ppid, "tty": tty, "registered_at": now()}
|
|
1004
1023
|
(SESSIONS_DIR / f"{project}_{name}").write_text(json.dumps(session_data), encoding="utf-8")
|
|
1005
1024
|
|
|
1006
1025
|
# Get all agents in project
|
|
1007
|
-
|
|
1026
|
+
_r_agents = sb_rpc("mc_get_agents", {"p_api_key": _api_key, "p_project_id": project_id,
|
|
1027
|
+
"p_agent_name": None, "p_select": None, "p_limit": None})
|
|
1028
|
+
if _r_agents and _r_agents.get("ok"):
|
|
1029
|
+
agents = _r_agents.get("agents", [])
|
|
1030
|
+
else:
|
|
1031
|
+
agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
|
|
1008
1032
|
agent_names = [a["name"] for a in agents]
|
|
1009
1033
|
|
|
1010
1034
|
# Count pending messages
|
|
@@ -1044,7 +1068,15 @@ def _resolve_agent_name(project_id, name):
|
|
|
1044
1068
|
"""Resolve partial agent name to full registered name."""
|
|
1045
1069
|
if not name or name == "*":
|
|
1046
1070
|
return name
|
|
1047
|
-
|
|
1071
|
+
_ak = _load_api_key_for_cli()
|
|
1072
|
+
agents = None
|
|
1073
|
+
if _ak:
|
|
1074
|
+
_r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1075
|
+
"p_agent_name": None, "p_select": ["name"], "p_limit": 50})
|
|
1076
|
+
if _r and _r.get("ok"):
|
|
1077
|
+
agents = _r.get("agents", [])
|
|
1078
|
+
if agents is None:
|
|
1079
|
+
agents = sb_select("mc_agents", f"project_id=eq.{project_id}", limit=50)
|
|
1048
1080
|
if not agents:
|
|
1049
1081
|
return name
|
|
1050
1082
|
all_names = [a["name"] for a in agents]
|
|
@@ -1107,8 +1139,17 @@ def send_msg(project, from_agent, to_agent, content, msg_type="msg", compact=Fal
|
|
|
1107
1139
|
# and forth and burn tokens forever.
|
|
1108
1140
|
if msg_type not in ("ack", "warning") and from_agent != "system":
|
|
1109
1141
|
if not _is_pure_ack_payload(payload):
|
|
1110
|
-
|
|
1111
|
-
|
|
1142
|
+
_ak = _load_api_key_for_cli()
|
|
1143
|
+
agents = None
|
|
1144
|
+
if _ak:
|
|
1145
|
+
_r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1146
|
+
"p_agent_name": to_agent, "p_select": ["name", "status"],
|
|
1147
|
+
"p_limit": 1})
|
|
1148
|
+
if _r and _r.get("ok"):
|
|
1149
|
+
agents = _r.get("agents", [])
|
|
1150
|
+
if agents is None:
|
|
1151
|
+
agents = sb_select("mc_agents",
|
|
1152
|
+
f"project_id=eq.{project_id}&name=eq.{quote(to_agent)}")
|
|
1112
1153
|
if agents and agents[0].get("status") not in ("offline", "killed"):
|
|
1113
1154
|
nudge_agent(project, to_agent, from_agent)
|
|
1114
1155
|
else:
|
|
@@ -1119,7 +1160,15 @@ def broadcast(project, from_agent, content, msg_type="broadcast"):
|
|
|
1119
1160
|
project_id = get_project_id(project)
|
|
1120
1161
|
if not project_id:
|
|
1121
1162
|
return
|
|
1122
|
-
|
|
1163
|
+
_ak = _load_api_key_for_cli()
|
|
1164
|
+
agents = None
|
|
1165
|
+
if _ak:
|
|
1166
|
+
_r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1167
|
+
"p_agent_name": None, "p_select": ["name"], "p_limit": None})
|
|
1168
|
+
if _r and _r.get("ok"):
|
|
1169
|
+
agents = _r.get("agents", [])
|
|
1170
|
+
if agents is None:
|
|
1171
|
+
agents = sb_select("mc_agents", f"project_id=eq.{project_id}")
|
|
1123
1172
|
count = 0
|
|
1124
1173
|
for agent in agents:
|
|
1125
1174
|
if agent["name"] != from_agent:
|
|
@@ -1191,8 +1240,17 @@ def inbox(project, name):
|
|
|
1191
1240
|
return
|
|
1192
1241
|
|
|
1193
1242
|
# Agent status
|
|
1194
|
-
|
|
1195
|
-
|
|
1243
|
+
_ak = _load_api_key_for_cli()
|
|
1244
|
+
agent_rows = None
|
|
1245
|
+
if _ak:
|
|
1246
|
+
_r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1247
|
+
"p_agent_name": name, "p_select": ["name", "status", "task"],
|
|
1248
|
+
"p_limit": 1})
|
|
1249
|
+
if _r and _r.get("ok"):
|
|
1250
|
+
agent_rows = _r.get("agents", [])
|
|
1251
|
+
if agent_rows is None:
|
|
1252
|
+
agent_rows = sb_select("mc_agents",
|
|
1253
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", limit=1)
|
|
1196
1254
|
if agent_rows:
|
|
1197
1255
|
a = agent_rows[0]
|
|
1198
1256
|
print(f"[{project}] {name} | status: {a.get('status', '?')} | task: {a.get('task', '-')}")
|
|
@@ -1242,9 +1300,16 @@ def hook_check():
|
|
|
1242
1300
|
return
|
|
1243
1301
|
|
|
1244
1302
|
# Always heartbeat — keeps agent alive on dashboard
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1303
|
+
_ak = _load_api_key_for_cli()
|
|
1304
|
+
_hb_fields = {"status": "working", "last_heartbeat": now_iso()}
|
|
1305
|
+
_hb_ok = False
|
|
1306
|
+
if _ak:
|
|
1307
|
+
_r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1308
|
+
"p_agent_name": agent, "p_fields": _hb_fields})
|
|
1309
|
+
_hb_ok = bool(_r and _r.get("ok"))
|
|
1310
|
+
if not _hb_ok:
|
|
1311
|
+
sb_update("mc_agents",
|
|
1312
|
+
f"project_id=eq.{project_id}&name=eq.{quote(agent)}", _hb_fields)
|
|
1248
1313
|
|
|
1249
1314
|
# Check for unread non-ack messages
|
|
1250
1315
|
pending = sb_select("mc_messages",
|
|
@@ -1268,14 +1333,24 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1268
1333
|
if not project_id:
|
|
1269
1334
|
return
|
|
1270
1335
|
|
|
1336
|
+
_ak = _load_api_key_for_cli()
|
|
1337
|
+
|
|
1338
|
+
def _update_agent_status(fields):
|
|
1339
|
+
"""Try RPC first, fall back to direct PostgREST."""
|
|
1340
|
+
if _ak:
|
|
1341
|
+
_r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1342
|
+
"p_agent_name": name, "p_fields": fields})
|
|
1343
|
+
if _r and _r.get("ok"):
|
|
1344
|
+
return
|
|
1345
|
+
sb_update("mc_agents",
|
|
1346
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", fields)
|
|
1347
|
+
|
|
1271
1348
|
import signal
|
|
1272
1349
|
_interrupted = [False]
|
|
1273
1350
|
def handle_interrupt(signum, frame):
|
|
1274
1351
|
_interrupted[0] = True
|
|
1275
1352
|
print(f"\n[{project}] {name}: Watch interrumpido por el usuario.")
|
|
1276
|
-
|
|
1277
|
-
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1278
|
-
{"status": "online", "task": "Listening to user", "last_heartbeat": now_iso()})
|
|
1353
|
+
_update_agent_status({"status": "online", "task": "Listening to user", "last_heartbeat": now_iso()})
|
|
1279
1354
|
sys.exit(0)
|
|
1280
1355
|
try:
|
|
1281
1356
|
signal.signal(signal.SIGINT, handle_interrupt)
|
|
@@ -1283,9 +1358,7 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1283
1358
|
pass # Non-main thread (Windows restriction)
|
|
1284
1359
|
|
|
1285
1360
|
# Update status to standby
|
|
1286
|
-
|
|
1287
|
-
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1288
|
-
{"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1361
|
+
_update_agent_status({"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1289
1362
|
|
|
1290
1363
|
cycle = timeout if timeout > 0 else 600
|
|
1291
1364
|
print(f"[{project}] {name} en watch — poll cada {interval}s, ciclo {cycle}s (auto-loop)")
|
|
@@ -1303,15 +1376,11 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1303
1376
|
print(f"\n*** [{project.upper()}] MENSAJE RECIBIDO ({elapsed}s en standby) ***")
|
|
1304
1377
|
read_messages(project, name, silent=False, send_acks=False)
|
|
1305
1378
|
|
|
1306
|
-
|
|
1307
|
-
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1308
|
-
{"status": "working", "task": "Procesando mensaje recibido", "last_heartbeat": now_iso()})
|
|
1379
|
+
_update_agent_status({"status": "working", "task": "Procesando mensaje recibido", "last_heartbeat": now_iso()})
|
|
1309
1380
|
|
|
1310
1381
|
# Reset to standby and keep polling (don't exit the loop)
|
|
1311
1382
|
time.sleep(2)
|
|
1312
|
-
|
|
1313
|
-
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1314
|
-
{"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1383
|
+
_update_agent_status({"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1315
1384
|
continue
|
|
1316
1385
|
|
|
1317
1386
|
# Heartbeat
|
|
@@ -1320,9 +1389,7 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1320
1389
|
|
|
1321
1390
|
# Cycle ended without messages — check user context, then AUTO-RESTART watch
|
|
1322
1391
|
print(f"[{project}] No messages this cycle. Watch active, still running. Standby.")
|
|
1323
|
-
|
|
1324
|
-
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1325
|
-
{"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1392
|
+
_update_agent_status({"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1326
1393
|
|
|
1327
1394
|
|
|
1328
1395
|
def update_board(project, name, status, task=""):
|
|
@@ -1347,8 +1414,15 @@ def update_board(project, name, status, task=""):
|
|
|
1347
1414
|
if task:
|
|
1348
1415
|
updates["task"] = task
|
|
1349
1416
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1417
|
+
_ak = _load_api_key_for_cli()
|
|
1418
|
+
_ub_ok = False
|
|
1419
|
+
if _ak:
|
|
1420
|
+
_r = sb_rpc("mc_update_agent", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1421
|
+
"p_agent_name": name, "p_fields": updates})
|
|
1422
|
+
_ub_ok = bool(_r and _r.get("ok"))
|
|
1423
|
+
if not _ub_ok:
|
|
1424
|
+
sb_update("mc_agents",
|
|
1425
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}", updates)
|
|
1352
1426
|
print(f"[{project}] {name}: {status} — {task}")
|
|
1353
1427
|
|
|
1354
1428
|
|
|
@@ -1358,7 +1432,15 @@ def show_board(project):
|
|
|
1358
1432
|
print(f"[{project}] Project not found")
|
|
1359
1433
|
return
|
|
1360
1434
|
|
|
1361
|
-
|
|
1435
|
+
_ak = _load_api_key_for_cli()
|
|
1436
|
+
agents = None
|
|
1437
|
+
if _ak:
|
|
1438
|
+
_r = sb_rpc("mc_get_agents", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1439
|
+
"p_agent_name": None, "p_select": None, "p_limit": None})
|
|
1440
|
+
if _r and _r.get("ok"):
|
|
1441
|
+
agents = _r.get("agents", [])
|
|
1442
|
+
if agents is None:
|
|
1443
|
+
agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
|
|
1362
1444
|
if not agents:
|
|
1363
1445
|
print(f"[{project}] Sin agentes")
|
|
1364
1446
|
return
|
|
@@ -1395,7 +1477,14 @@ def show_status(project=None):
|
|
|
1395
1477
|
print(f"{'='*60}")
|
|
1396
1478
|
|
|
1397
1479
|
for proj in projects_data:
|
|
1398
|
-
agents =
|
|
1480
|
+
agents = None
|
|
1481
|
+
if api_key:
|
|
1482
|
+
_r = sb_rpc("mc_get_agents", {"p_api_key": api_key, "p_project_id": proj['id'],
|
|
1483
|
+
"p_agent_name": None, "p_select": None, "p_limit": None})
|
|
1484
|
+
if _r and _r.get("ok"):
|
|
1485
|
+
agents = _r.get("agents", [])
|
|
1486
|
+
if agents is None:
|
|
1487
|
+
agents = sb_select("mc_agents", f"project_id=eq.{proj['id']}", order="name.asc")
|
|
1399
1488
|
if not agents:
|
|
1400
1489
|
continue
|
|
1401
1490
|
|
|
@@ -1494,7 +1583,14 @@ def unregister(project, name):
|
|
|
1494
1583
|
project_id = get_project_id(project)
|
|
1495
1584
|
if not project_id:
|
|
1496
1585
|
return
|
|
1497
|
-
|
|
1586
|
+
_ak = _load_api_key_for_cli()
|
|
1587
|
+
_del_ok = False
|
|
1588
|
+
if _ak:
|
|
1589
|
+
_r = sb_rpc("mc_delete_agent", {"p_api_key": _ak, "p_project_id": project_id,
|
|
1590
|
+
"p_agent_name": name})
|
|
1591
|
+
_del_ok = bool(_r and _r.get("ok"))
|
|
1592
|
+
if not _del_ok:
|
|
1593
|
+
sb_delete("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(name)}")
|
|
1498
1594
|
|
|
1499
1595
|
# Clean local session
|
|
1500
1596
|
session_file = SESSIONS_DIR / f"{project}_{name}"
|
|
@@ -1570,17 +1666,30 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1570
1666
|
except Exception:
|
|
1571
1667
|
pass
|
|
1572
1668
|
|
|
1669
|
+
_ak = _load_api_key_for_cli()
|
|
1573
1670
|
code = (
|
|
1574
1671
|
"import os,sys,time,json,urllib.request\n"
|
|
1575
1672
|
f"project={project!r}; name={name!r}\n"
|
|
1576
1673
|
f"url={SUPABASE_URL!r}; key={SUPABASE_KEY!r}\n"
|
|
1674
|
+
f"api_key={_ak!r}\n"
|
|
1577
1675
|
"import urllib.parse\n"
|
|
1578
1676
|
"def post(path, body):\n"
|
|
1579
1677
|
" req=urllib.request.Request(url+path, data=json.dumps(body).encode(),\n"
|
|
1580
1678
|
" headers={'apikey':key,'Authorization':'Bearer '+key,'Content-Type':'application/json','Accept-Profile':'meshcode','Content-Profile':'meshcode'}, method='POST')\n"
|
|
1581
1679
|
" try: urllib.request.urlopen(req,timeout=10).read()\n"
|
|
1582
1680
|
" except Exception: pass\n"
|
|
1681
|
+
"def rpc(fn, params):\n"
|
|
1682
|
+
" req=urllib.request.Request(url+'/rest/v1/rpc/'+fn, data=json.dumps(params).encode(),\n"
|
|
1683
|
+
" headers={'apikey':key,'Authorization':'Bearer '+key,'Content-Type':'application/json'}, method='POST')\n"
|
|
1684
|
+
" try:\n"
|
|
1685
|
+
" d=json.loads(urllib.request.urlopen(req,timeout=10).read())\n"
|
|
1686
|
+
" return d\n"
|
|
1687
|
+
" except Exception: return None\n"
|
|
1583
1688
|
"def get_pid_for_project():\n"
|
|
1689
|
+
" if api_key:\n"
|
|
1690
|
+
" r=rpc('mc_resolve_project',{'p_api_key':api_key,'p_project_name':project})\n"
|
|
1691
|
+
" if isinstance(r,dict) and r.get('project_id'): return r['project_id']\n"
|
|
1692
|
+
" if isinstance(r,list) and r and r[0].get('project_id'): return r[0]['project_id']\n"
|
|
1584
1693
|
" req=urllib.request.Request(url+'/rest/v1/mc_projects?select=id&name=eq.'+urllib.parse.quote(project), headers={'apikey':key,'Authorization':'Bearer '+key,'Accept-Profile':'meshcode'})\n"
|
|
1585
1694
|
" try:\n"
|
|
1586
1695
|
" d=json.loads(urllib.request.urlopen(req,timeout=10).read())\n"
|
|
@@ -1588,6 +1697,11 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1588
1697
|
" except Exception: return None\n"
|
|
1589
1698
|
"def check_still_leased(pid):\n"
|
|
1590
1699
|
" \"\"\"Return False if force-disconnected (instance_id cleared).\"\"\"\n"
|
|
1700
|
+
" if api_key:\n"
|
|
1701
|
+
" r=rpc('mc_get_agents',{'p_api_key':api_key,'p_project_id':pid,'p_agent_name':name,'p_select':['instance_id'],'p_limit':1})\n"
|
|
1702
|
+
" if r and isinstance(r,dict) and r.get('ok'):\n"
|
|
1703
|
+
" agents=r.get('agents',[])\n"
|
|
1704
|
+
" return bool(agents and agents[0].get('instance_id'))\n"
|
|
1591
1705
|
" try:\n"
|
|
1592
1706
|
" req=urllib.request.Request(url+'/rest/v1/mc_agents?select=instance_id&project_id=eq.'+pid+'&name=eq.'+urllib.parse.quote(name),\n"
|
|
1593
1707
|
" headers={'apikey':key,'Authorization':'Bearer '+key,'Accept-Profile':'meshcode'})\n"
|
|
@@ -334,7 +334,13 @@ _recording_session_id = ""
|
|
|
334
334
|
|
|
335
335
|
# RPCs that should NOT be recorded (internal/heartbeat/recording itself)
|
|
336
336
|
_SKIP_RECORDING = {"mc_heartbeat", "mc_record_event", "mc_agent_set_status_by_api_key",
|
|
337
|
-
"mc_acquire_agent_lease", "mc_release_agent_lease"
|
|
337
|
+
"mc_acquire_agent_lease", "mc_release_agent_lease",
|
|
338
|
+
"mc_update_agent", "mc_get_agents"}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _get_api_key() -> Optional[str]:
|
|
342
|
+
"""Return the current agent's API key (set at boot via enable_recording)."""
|
|
343
|
+
return _recording_api_key or os.environ.get("MESHCODE_API_KEY") or None
|
|
338
344
|
|
|
339
345
|
|
|
340
346
|
def enable_recording(api_key: str, project_id: str, agent_name: str, session_id: str):
|
|
@@ -462,9 +468,23 @@ def register_agent(project: str, name: str, role: str = "", api_key: Optional[st
|
|
|
462
468
|
if not result or (isinstance(result, dict) and result.get("error")):
|
|
463
469
|
return result or {"error": "Failed to register agent"}
|
|
464
470
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
471
|
+
# Prefer SECURITY DEFINER RPC for mc_agents update
|
|
472
|
+
_ak = api_key or _get_api_key()
|
|
473
|
+
_agent_updated = False
|
|
474
|
+
if _ak:
|
|
475
|
+
_upd = sb_rpc("mc_update_agent", {
|
|
476
|
+
"p_api_key": _ak,
|
|
477
|
+
"p_project_id": project_id,
|
|
478
|
+
"p_agent_name": name,
|
|
479
|
+
"p_fields": {"task": role, "last_heartbeat": _now_iso()},
|
|
480
|
+
})
|
|
481
|
+
if isinstance(_upd, dict) and _upd.get("ok"):
|
|
482
|
+
_agent_updated = True
|
|
483
|
+
if not _agent_updated:
|
|
484
|
+
# Fallback: direct PATCH (gradual rollout)
|
|
485
|
+
sb_update("mc_agents",
|
|
486
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
487
|
+
{"task": role, "last_heartbeat": _now_iso()})
|
|
468
488
|
|
|
469
489
|
return {
|
|
470
490
|
"registered": True,
|
|
@@ -522,15 +542,42 @@ def resolve_agent_name(project_id: str, name: str) -> str:
|
|
|
522
542
|
cached = _agent_name_cache_get(project_id, name)
|
|
523
543
|
if cached is not None:
|
|
524
544
|
return cached
|
|
525
|
-
# Exact match — fast path
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
545
|
+
# Exact match — fast path (prefer SECURITY DEFINER RPC)
|
|
546
|
+
api_key = _get_api_key()
|
|
547
|
+
exact = None
|
|
548
|
+
if api_key:
|
|
549
|
+
_res = sb_rpc("mc_get_agents", {
|
|
550
|
+
"p_api_key": api_key,
|
|
551
|
+
"p_project_id": project_id,
|
|
552
|
+
"p_agent_name": name,
|
|
553
|
+
"p_select": None,
|
|
554
|
+
"p_limit": 1,
|
|
555
|
+
})
|
|
556
|
+
if isinstance(_res, dict) and _res.get("ok"):
|
|
557
|
+
exact = _res.get("agents", [])
|
|
558
|
+
if exact is None:
|
|
559
|
+
# Fallback: direct SELECT (gradual rollout)
|
|
560
|
+
exact = sb_select("mc_agents",
|
|
561
|
+
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
562
|
+
limit=1)
|
|
529
563
|
if exact:
|
|
530
564
|
_agent_name_cache_set(project_id, name, name)
|
|
531
565
|
return name
|
|
532
566
|
# Fuzzy: prefix or substring match among registered agents
|
|
533
|
-
agents =
|
|
567
|
+
agents = None
|
|
568
|
+
if api_key:
|
|
569
|
+
_res2 = sb_rpc("mc_get_agents", {
|
|
570
|
+
"p_api_key": api_key,
|
|
571
|
+
"p_project_id": project_id,
|
|
572
|
+
"p_agent_name": None,
|
|
573
|
+
"p_select": ["name"],
|
|
574
|
+
"p_limit": None,
|
|
575
|
+
})
|
|
576
|
+
if isinstance(_res2, dict) and _res2.get("ok"):
|
|
577
|
+
agents = _res2.get("agents", [])
|
|
578
|
+
if agents is None:
|
|
579
|
+
# Fallback: direct SELECT (gradual rollout)
|
|
580
|
+
agents = sb_select("mc_agents", f"project_id=eq.{project_id}&select=name")
|
|
534
581
|
if not agents:
|
|
535
582
|
return name
|
|
536
583
|
all_names = [a["name"] for a in agents]
|
|
@@ -845,7 +892,20 @@ def decrypt_payload(encrypted_b64: str, hex_key: str, aad: Optional[str] = None)
|
|
|
845
892
|
return None
|
|
846
893
|
|
|
847
894
|
|
|
848
|
-
def get_board(project_id: str) -> List[Dict]:
|
|
895
|
+
def get_board(project_id: str, api_key: Optional[str] = None) -> List[Dict]:
|
|
896
|
+
# Prefer SECURITY DEFINER RPC
|
|
897
|
+
_ak = api_key or _get_api_key()
|
|
898
|
+
if _ak:
|
|
899
|
+
_res = sb_rpc("mc_get_agents", {
|
|
900
|
+
"p_api_key": _ak,
|
|
901
|
+
"p_project_id": project_id,
|
|
902
|
+
"p_agent_name": None,
|
|
903
|
+
"p_select": ["name", "role", "status", "task", "last_heartbeat", "registered_at"],
|
|
904
|
+
"p_limit": None,
|
|
905
|
+
})
|
|
906
|
+
if isinstance(_res, dict) and _res.get("ok"):
|
|
907
|
+
return _res.get("agents", [])
|
|
908
|
+
# Fallback: direct SELECT (gradual rollout)
|
|
849
909
|
return sb_select(
|
|
850
910
|
"mc_agents",
|
|
851
911
|
f"project_id=eq.{project_id}&select=name,role,status,task,last_heartbeat,registered_at",
|
|
@@ -875,13 +935,24 @@ def set_status(project_id: str, agent: str, status: str, task: str = "", api_key
|
|
|
875
935
|
return {"ok": True, "status": status}
|
|
876
936
|
# Fall through to direct PATCH if RPC doesn't exist yet
|
|
877
937
|
|
|
878
|
-
# Fallback:
|
|
879
|
-
|
|
938
|
+
# Fallback: try mc_update_agent RPC, then direct PATCH
|
|
939
|
+
_ak = api_key or _get_api_key()
|
|
940
|
+
fields: Dict[str, Any] = {"status": status, "last_heartbeat": _now_iso()}
|
|
880
941
|
if task:
|
|
881
|
-
|
|
942
|
+
fields["task"] = task
|
|
882
943
|
if editor:
|
|
883
|
-
|
|
884
|
-
|
|
944
|
+
fields["editor"] = editor
|
|
945
|
+
if _ak:
|
|
946
|
+
_upd = sb_rpc("mc_update_agent", {
|
|
947
|
+
"p_api_key": _ak,
|
|
948
|
+
"p_project_id": project_id,
|
|
949
|
+
"p_agent_name": agent,
|
|
950
|
+
"p_fields": fields,
|
|
951
|
+
})
|
|
952
|
+
if isinstance(_upd, dict) and _upd.get("ok"):
|
|
953
|
+
return {"ok": True, "status": status}
|
|
954
|
+
# Final fallback: direct PATCH (may silently fail with anon key if RLS blocks)
|
|
955
|
+
result = sb_update("mc_agents", f"project_id=eq.{project_id}&name=eq.{quote(agent)}", fields)
|
|
885
956
|
if isinstance(result, dict) and result.get("_error"):
|
|
886
957
|
return {"error": result["_error"]}
|
|
887
958
|
return {"ok": True, "status": status}
|
|
@@ -143,7 +143,7 @@ class RealtimeListener:
|
|
|
143
143
|
# can hang indefinitely behind a corporate firewall / DNS stall, which
|
|
144
144
|
# blocks the MCP lifespan startup -> Claude Code times out the spawn
|
|
145
145
|
# -> "Failed to reconnect".
|
|
146
|
-
connect_kwargs = {"ping_interval": 20, "ping_timeout":
|
|
146
|
+
connect_kwargs = {"ping_interval": 20, "ping_timeout": 15}
|
|
147
147
|
if _SSL_CTX is not None and self.ws_url.startswith("wss://"):
|
|
148
148
|
connect_kwargs["ssl"] = _SSL_CTX
|
|
149
149
|
ws = await asyncio.wait_for(
|
|
@@ -118,6 +118,72 @@ _IN_WAIT = False # True while meshcode_wait is blocking
|
|
|
118
118
|
# the MCP server side (useful if comms_v4 nudge doesn't reach the agent).
|
|
119
119
|
_AUTO_WAKE = os.environ.get("MESHCODE_AUTO_WAKE", "0").lower() in ("1", "true", "yes")
|
|
120
120
|
|
|
121
|
+
# ============================================================
|
|
122
|
+
# PID lockfile: prevent zombie MCP processes from accumulating.
|
|
123
|
+
# When Claude Code respawns the MCP server, the old process may
|
|
124
|
+
# still be alive. We write our PID to a lockfile on boot and
|
|
125
|
+
# kill any stale process found there first.
|
|
126
|
+
# ============================================================
|
|
127
|
+
import signal as _signal
|
|
128
|
+
import tempfile as _tempfile
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _pid_lockfile_path() -> str:
|
|
132
|
+
"""Return path to the PID lockfile for this agent."""
|
|
133
|
+
agent = os.environ.get("MESHCODE_AGENT", "unknown")
|
|
134
|
+
project = os.environ.get("MESHCODE_PROJECT", "unknown")
|
|
135
|
+
safe_name = f"meshcode_mcp_{project}_{agent}.pid".replace("/", "_").replace(" ", "_")
|
|
136
|
+
return os.path.join(_tempfile.gettempdir(), safe_name)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _kill_stale_mcp_process() -> None:
|
|
140
|
+
"""Kill any stale MCP process for this agent found in the lockfile."""
|
|
141
|
+
lockfile = _pid_lockfile_path()
|
|
142
|
+
if not os.path.exists(lockfile):
|
|
143
|
+
return
|
|
144
|
+
try:
|
|
145
|
+
with open(lockfile, "r") as f:
|
|
146
|
+
old_pid = int(f.read().strip())
|
|
147
|
+
if old_pid == os.getpid():
|
|
148
|
+
return # It's us
|
|
149
|
+
# Check if process is alive
|
|
150
|
+
os.kill(old_pid, 0) # Signal 0 = existence check, no actual signal
|
|
151
|
+
# Process is alive — kill it gracefully, then forcefully
|
|
152
|
+
_mc_log(f"Killing stale MCP process (PID {old_pid})")
|
|
153
|
+
os.kill(old_pid, _signal.SIGTERM)
|
|
154
|
+
import time
|
|
155
|
+
time.sleep(1)
|
|
156
|
+
try:
|
|
157
|
+
os.kill(old_pid, 0) # Still alive?
|
|
158
|
+
os.kill(old_pid, _signal.SIGKILL)
|
|
159
|
+
_mc_log(f"Force-killed stale MCP process (PID {old_pid})")
|
|
160
|
+
except OSError:
|
|
161
|
+
pass # Already dead after SIGTERM
|
|
162
|
+
except (ValueError, FileNotFoundError):
|
|
163
|
+
pass # Corrupt or missing lockfile
|
|
164
|
+
except OSError:
|
|
165
|
+
pass # Process not found (already dead)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _write_pid_lockfile() -> None:
|
|
169
|
+
"""Write current PID to lockfile."""
|
|
170
|
+
lockfile = _pid_lockfile_path()
|
|
171
|
+
try:
|
|
172
|
+
with open(lockfile, "w") as f:
|
|
173
|
+
f.write(str(os.getpid()))
|
|
174
|
+
except Exception as e:
|
|
175
|
+
_mc_log(f"Warning: couldn't write PID lockfile: {e}", level="warn")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _remove_pid_lockfile() -> None:
|
|
179
|
+
"""Remove PID lockfile on shutdown."""
|
|
180
|
+
lockfile = _pid_lockfile_path()
|
|
181
|
+
try:
|
|
182
|
+
if os.path.exists(lockfile):
|
|
183
|
+
os.remove(lockfile)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
121
187
|
|
|
122
188
|
def _try_auto_wake(from_agent: str, preview: str) -> None:
|
|
123
189
|
"""Inject a nudge into the terminal ONLY when agent is truly sleeping.
|
|
@@ -1362,6 +1428,10 @@ async def lifespan(_app):
|
|
|
1362
1428
|
global _REALTIME, _MAIN_LOOP
|
|
1363
1429
|
_MAIN_LOOP = asyncio.get_running_loop()
|
|
1364
1430
|
|
|
1431
|
+
# Kill any stale MCP process for this agent before acquiring lease
|
|
1432
|
+
_kill_stale_mcp_process()
|
|
1433
|
+
_write_pid_lockfile()
|
|
1434
|
+
|
|
1365
1435
|
import platform as _pl
|
|
1366
1436
|
if _pl.system() == "Windows":
|
|
1367
1437
|
log.info(
|
|
@@ -1431,6 +1501,10 @@ async def lifespan(_app):
|
|
|
1431
1501
|
_heartbeat_stop.set()
|
|
1432
1502
|
except Exception:
|
|
1433
1503
|
pass
|
|
1504
|
+
try:
|
|
1505
|
+
_remove_pid_lockfile()
|
|
1506
|
+
except Exception:
|
|
1507
|
+
pass
|
|
1434
1508
|
try:
|
|
1435
1509
|
_record_event_bg("shutdown", {"agent": AGENT_NAME, "session_id": _SESSION_ID})
|
|
1436
1510
|
except Exception:
|
|
@@ -2068,9 +2142,14 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
2068
2142
|
woke = await asyncio.shield(
|
|
2069
2143
|
_REALTIME.wait_for_message(timeout=float(actual_timeout))
|
|
2070
2144
|
)
|
|
2071
|
-
except
|
|
2072
|
-
log.debug(
|
|
2145
|
+
except asyncio.CancelledError:
|
|
2146
|
+
log.debug("[meshcode] wait_for_message cancelled by ESC")
|
|
2073
2147
|
return {"timed_out": True, "reason": "cancelled_by_client"}
|
|
2148
|
+
except Exception as _wait_exc:
|
|
2149
|
+
# Non-cancellation errors (e.g. connection dropped) — log and
|
|
2150
|
+
# fall through to DB fallback instead of masking the error.
|
|
2151
|
+
log.warning(f"[meshcode] wait_for_message error: {type(_wait_exc).__name__}: {_wait_exc}")
|
|
2152
|
+
woke = False
|
|
2074
2153
|
if woke:
|
|
2075
2154
|
buffered = _REALTIME.drain()
|
|
2076
2155
|
if buffered:
|
|
@@ -2174,13 +2253,16 @@ def meshcode_done(reason: str) -> Dict[str, Any]:
|
|
|
2174
2253
|
|
|
2175
2254
|
@mcp.tool()
|
|
2176
2255
|
@with_working_status
|
|
2177
|
-
def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> Dict[str, Any]:
|
|
2256
|
+
def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark_read: bool = False) -> Dict[str, Any]:
|
|
2178
2257
|
"""Peek at inbox (non-destructive). Returns pending count + new messages.
|
|
2179
2258
|
|
|
2180
2259
|
Args:
|
|
2181
2260
|
include_acks: Include ack messages in response.
|
|
2182
2261
|
since: ISO-8601 timestamp. Only return messages newer than this.
|
|
2183
2262
|
Use meshcode_remember("last_seen", ts) to persist across sessions.
|
|
2263
|
+
mark_read: When True, consume messages (mark as read in DB) instead of
|
|
2264
|
+
just peeking. Useful during boot when meshcode_wait() refuses
|
|
2265
|
+
to run because of open tasks.
|
|
2184
2266
|
"""
|
|
2185
2267
|
global _LAST_SEEN_TS
|
|
2186
2268
|
pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
|
|
@@ -2190,11 +2272,9 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
|
|
|
2190
2272
|
deduped = [m for m in realtime_buffered if _seen_key(m) not in _SEEN_MSG_IDS]
|
|
2191
2273
|
|
|
2192
2274
|
# Fallback: if realtime buffer is empty but DB has pending messages,
|
|
2193
|
-
# fetch them from the DB.
|
|
2194
|
-
# non-destructive peek — messages stay pending until meshcode_wait or
|
|
2195
|
-
# meshcode_read consumes them.
|
|
2275
|
+
# fetch them from the DB. mark_read controls whether we consume or peek.
|
|
2196
2276
|
if not deduped and pending > 0:
|
|
2197
|
-
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=
|
|
2277
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=mark_read, api_key=_get_api_key())
|
|
2198
2278
|
deduped = [
|
|
2199
2279
|
{
|
|
2200
2280
|
"from": m["from_agent"],
|
|
@@ -2213,6 +2293,14 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
|
|
|
2213
2293
|
if effective_since:
|
|
2214
2294
|
deduped = [m for m in deduped if m.get("ts") and str(m["ts"]) > effective_since]
|
|
2215
2295
|
|
|
2296
|
+
# When mark_read=True, update tracking state so messages aren't re-processed
|
|
2297
|
+
if mark_read and deduped:
|
|
2298
|
+
for m in deduped:
|
|
2299
|
+
_SEEN_MSG_IDS.add(_seen_key(m))
|
|
2300
|
+
latest_ts = max((str(m.get("ts", "")) for m in deduped), default=None)
|
|
2301
|
+
if latest_ts and (not _LAST_SEEN_TS or latest_ts > _LAST_SEEN_TS):
|
|
2302
|
+
_LAST_SEEN_TS = latest_ts
|
|
2303
|
+
|
|
2216
2304
|
split = _split_messages(deduped)
|
|
2217
2305
|
if not include_acks:
|
|
2218
2306
|
split["acks"] = []
|
|
@@ -71,17 +71,49 @@ def _fetch_or_generate_art(agent: str, project: str) -> tuple:
|
|
|
71
71
|
if not proj_data or not proj_data.get("project_id"):
|
|
72
72
|
return generate_art(agent), agent, None
|
|
73
73
|
project_id = proj_data["project_id"]
|
|
74
|
-
# Step 2: fetch existing art + role
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
# Step 2: fetch existing art + role via mc_get_agents RPC
|
|
75
|
+
data = None
|
|
76
|
+
try:
|
|
77
|
+
rpc_body = json.dumps({
|
|
78
|
+
"p_api_key": api_key,
|
|
79
|
+
"p_project_id": project_id,
|
|
80
|
+
"p_agent_name": agent,
|
|
81
|
+
"p_select": ["ascii_art", "role", "id"],
|
|
82
|
+
})
|
|
83
|
+
rpc_req = Request(
|
|
84
|
+
f"{sb['SUPABASE_URL']}/rest/v1/rpc/mc_get_agents",
|
|
85
|
+
data=rpc_body.encode(),
|
|
86
|
+
method="POST",
|
|
87
|
+
headers={
|
|
88
|
+
"apikey": sb["SUPABASE_KEY"],
|
|
89
|
+
"Authorization": f"Bearer {sb['SUPABASE_KEY']}",
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
"Content-Profile": "meshcode",
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
with urlopen(rpc_req, timeout=5) as resp:
|
|
95
|
+
rpc_result = json.loads(resp.read().decode())
|
|
96
|
+
if isinstance(rpc_result, dict) and rpc_result.get("ok"):
|
|
97
|
+
data = rpc_result.get("agents") or []
|
|
98
|
+
except Exception:
|
|
99
|
+
pass # fall through to legacy direct REST call
|
|
100
|
+
|
|
101
|
+
# Legacy fallback: direct REST call to mc_agents table
|
|
102
|
+
if data is None:
|
|
103
|
+
try:
|
|
104
|
+
req = Request(
|
|
105
|
+
f"{sb['SUPABASE_URL']}/rest/v1/mc_agents?select=ascii_art,role,id&name=eq.{urllib.parse.quote(agent)}&project_id=eq.{project_id}",
|
|
106
|
+
headers={
|
|
107
|
+
"apikey": sb["SUPABASE_KEY"],
|
|
108
|
+
"Authorization": f"Bearer {sb['SUPABASE_KEY']}",
|
|
109
|
+
"Accept-Profile": "meshcode",
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
with urlopen(req, timeout=5) as resp:
|
|
113
|
+
data = json.loads(resp.read().decode())
|
|
114
|
+
except Exception:
|
|
115
|
+
data = []
|
|
116
|
+
|
|
85
117
|
if data:
|
|
86
118
|
agent_id = data[0].get("id")
|
|
87
119
|
# Step 3: fetch profile color
|
|
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
|