meshcode 2.0.5__tar.gz → 2.0.7__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.0.5 → meshcode-2.0.7}/PKG-INFO +1 -1
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/__init__.py +1 -1
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/backend.py +14 -2
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/realtime.py +4 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/server.py +121 -15
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.0.5 → meshcode-2.0.7}/pyproject.toml +1 -1
- {meshcode-2.0.5 → meshcode-2.0.7}/README.md +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/cli.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/comms_v4.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/invites.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/launcher.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/launcher_install.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/preferences.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/run_agent.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/secrets.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/self_update.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/setup_clients.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.7}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.0.
|
|
2
|
+
__version__ = "2.0.7"
|
|
@@ -319,10 +319,22 @@ def record_event(api_key, project_id, agent_name, session_id, event_type, payloa
|
|
|
319
319
|
})
|
|
320
320
|
|
|
321
321
|
|
|
322
|
-
def get_history(project_id: str, limit: int = 20) -> List[Dict]:
|
|
322
|
+
def get_history(project_id: str, limit: int = 20, agent_filter: str = "") -> List[Dict]:
|
|
323
|
+
filters = f"project_id=eq.{project_id}&type=neq.ack"
|
|
324
|
+
if agent_filter:
|
|
325
|
+
filters += f"&or=(from_agent.eq.{quote(agent_filter)},to_agent.eq.{quote(agent_filter)})"
|
|
323
326
|
return sb_select(
|
|
324
327
|
"mc_messages",
|
|
325
|
-
|
|
328
|
+
filters,
|
|
326
329
|
order="created_at.desc",
|
|
327
330
|
limit=limit,
|
|
328
331
|
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_message_by_id(project_id: str, msg_id: str) -> Optional[Dict]:
|
|
335
|
+
results = sb_select(
|
|
336
|
+
"mc_messages",
|
|
337
|
+
f"project_id=eq.{project_id}&id=eq.{msg_id}",
|
|
338
|
+
limit=1,
|
|
339
|
+
)
|
|
340
|
+
return results[0] if results else None
|
|
@@ -216,6 +216,10 @@ class RealtimeListener:
|
|
|
216
216
|
except Exception as e:
|
|
217
217
|
log.warning(f"notify_callback failed: {e}")
|
|
218
218
|
|
|
219
|
+
def peek(self) -> list:
|
|
220
|
+
"""Return queued messages without removing them."""
|
|
221
|
+
return list(self.queue)
|
|
222
|
+
|
|
219
223
|
def drain(self) -> list:
|
|
220
224
|
"""Pop and return all queued messages."""
|
|
221
225
|
out = list(self.queue)
|
|
@@ -329,13 +329,21 @@ if not _flip_status("idle", ""):
|
|
|
329
329
|
|
|
330
330
|
|
|
331
331
|
# ============================================================
|
|
332
|
-
#
|
|
333
|
-
#
|
|
332
|
+
# Automatic Agent Status State Machine
|
|
333
|
+
# States: ONLINE → WORKING → ONLINE → WAITING → ONLINE → IDLE → OFFLINE
|
|
334
|
+
# Transitions driven by tool calls and meshcode_wait, not the LLM.
|
|
334
335
|
# ============================================================
|
|
335
336
|
import functools as _functools
|
|
336
337
|
import threading as _threading
|
|
338
|
+
import time as _time
|
|
337
339
|
|
|
338
340
|
_flip_lock = _threading.Lock()
|
|
341
|
+
_current_state = "online"
|
|
342
|
+
_last_tool_at = _time.time()
|
|
343
|
+
_current_tool = ""
|
|
344
|
+
_IDLE_THRESHOLD_S = 120 # seconds without tool call → IDLE
|
|
345
|
+
_PROCESSING_COOLDOWN_S = 15 # seconds after tool returns before flipping to ONLINE
|
|
346
|
+
_processing_timer: Optional[_threading.Timer] = None
|
|
339
347
|
|
|
340
348
|
|
|
341
349
|
async def _async_flip_status(status: str, task: str = "") -> None:
|
|
@@ -362,6 +370,28 @@ def _schedule_flip(status: str, task: str = "") -> None:
|
|
|
362
370
|
).start()
|
|
363
371
|
|
|
364
372
|
|
|
373
|
+
def _set_state(state: str, tool: str = "") -> None:
|
|
374
|
+
"""Update the state machine and broadcast to dashboard."""
|
|
375
|
+
global _current_state, _current_tool, _last_tool_at, _processing_timer
|
|
376
|
+
# Cancel any pending processing→online timer
|
|
377
|
+
if _processing_timer is not None:
|
|
378
|
+
_processing_timer.cancel()
|
|
379
|
+
_processing_timer = None
|
|
380
|
+
_current_state = state
|
|
381
|
+
_current_tool = tool
|
|
382
|
+
if state == "working":
|
|
383
|
+
_last_tool_at = _time.time()
|
|
384
|
+
_schedule_flip(state, tool)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _processing_to_online() -> None:
|
|
388
|
+
"""Called after PROCESSING cooldown expires — flip to ONLINE."""
|
|
389
|
+
global _processing_timer
|
|
390
|
+
_processing_timer = None
|
|
391
|
+
if _current_state == "processing":
|
|
392
|
+
_set_state("online", "")
|
|
393
|
+
|
|
394
|
+
|
|
365
395
|
def with_working_status(func):
|
|
366
396
|
name = func.__name__
|
|
367
397
|
skip = (name == "meshcode_wait")
|
|
@@ -370,26 +400,37 @@ def with_working_status(func):
|
|
|
370
400
|
async def awrapper(*args, **kwargs):
|
|
371
401
|
_check_hot_reload()
|
|
372
402
|
if not skip:
|
|
373
|
-
|
|
403
|
+
_set_state("working", name)
|
|
374
404
|
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
|
|
375
405
|
try:
|
|
376
406
|
return await func(*args, **kwargs)
|
|
377
407
|
finally:
|
|
378
408
|
if not skip:
|
|
379
|
-
|
|
409
|
+
global _last_tool_at, _processing_timer
|
|
410
|
+
_last_tool_at = _time.time()
|
|
411
|
+
# Enter PROCESSING cooldown — stays green for 15s
|
|
412
|
+
_set_state("processing", "processing...")
|
|
413
|
+
_processing_timer = _threading.Timer(_PROCESSING_COOLDOWN_S, _processing_to_online)
|
|
414
|
+
_processing_timer.daemon = True
|
|
415
|
+
_processing_timer.start()
|
|
380
416
|
return awrapper
|
|
381
417
|
else:
|
|
382
418
|
@_functools.wraps(func)
|
|
383
419
|
def swrapper(*args, **kwargs):
|
|
384
420
|
_check_hot_reload()
|
|
385
421
|
if not skip:
|
|
386
|
-
|
|
422
|
+
_set_state("working", name)
|
|
387
423
|
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
|
|
388
424
|
try:
|
|
389
425
|
return func(*args, **kwargs)
|
|
390
426
|
finally:
|
|
391
427
|
if not skip:
|
|
392
|
-
|
|
428
|
+
global _last_tool_at, _processing_timer
|
|
429
|
+
_last_tool_at = _time.time()
|
|
430
|
+
_set_state("processing", "processing...")
|
|
431
|
+
_processing_timer = _threading.Timer(_PROCESSING_COOLDOWN_S, _processing_to_online)
|
|
432
|
+
_processing_timer.daemon = True
|
|
433
|
+
_processing_timer.start()
|
|
393
434
|
return swrapper
|
|
394
435
|
|
|
395
436
|
|
|
@@ -659,9 +700,13 @@ def _heartbeat_thread_fn():
|
|
|
659
700
|
while not _heartbeat_stop.is_set():
|
|
660
701
|
try:
|
|
661
702
|
be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
|
|
662
|
-
#
|
|
703
|
+
# Idle detection: only from online/processing states (NOT waiting)
|
|
704
|
+
# Agents in meshcode_wait should stay WAITING, not flip to IDLE
|
|
705
|
+
if _current_state in ("online", "processing") and (_time.time() - _last_tool_at) > _IDLE_THRESHOLD_S:
|
|
706
|
+
_set_state("idle", f"idle ({int((_time.time() - _last_tool_at) / 60)}m)")
|
|
707
|
+
# Sync current state to DB (in case realtime missed it)
|
|
663
708
|
try:
|
|
664
|
-
be.set_status(_PROJECT_ID, AGENT_NAME,
|
|
709
|
+
be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool)
|
|
665
710
|
except Exception:
|
|
666
711
|
pass
|
|
667
712
|
if _REALTIME and not _REALTIME.is_connected:
|
|
@@ -838,6 +883,58 @@ def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
838
883
|
return split
|
|
839
884
|
|
|
840
885
|
|
|
886
|
+
@mcp.tool()
|
|
887
|
+
@with_working_status
|
|
888
|
+
def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dict[str, Any]:
|
|
889
|
+
"""View recent message history (both read and unread). Use when you need
|
|
890
|
+
context from past conversations or lost messages after context compression.
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
limit: Max messages to return (default 20).
|
|
894
|
+
agent_filter: Optional agent name to filter (shows messages to/from that agent).
|
|
895
|
+
"""
|
|
896
|
+
raw = be.get_history(_PROJECT_ID, limit=limit, agent_filter=agent_filter or "")
|
|
897
|
+
messages = [
|
|
898
|
+
{
|
|
899
|
+
"from": m.get("from_agent", ""),
|
|
900
|
+
"to": m.get("to_agent", ""),
|
|
901
|
+
"type": m.get("type", "msg"),
|
|
902
|
+
"ts": m.get("created_at", ""),
|
|
903
|
+
"payload": m.get("payload", {}),
|
|
904
|
+
"id": m.get("id", ""),
|
|
905
|
+
"read": m.get("read", False),
|
|
906
|
+
}
|
|
907
|
+
for m in raw
|
|
908
|
+
]
|
|
909
|
+
return {"ok": True, "messages": messages, "count": len(messages)}
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
@mcp.tool()
|
|
913
|
+
@with_working_status
|
|
914
|
+
def meshcode_read_message(msg_id: str) -> Dict[str, Any]:
|
|
915
|
+
"""Fetch a specific message by ID. Use when you need to re-read a message
|
|
916
|
+
after context was compressed or session restarted.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
msg_id: The UUID of the message to fetch.
|
|
920
|
+
"""
|
|
921
|
+
msg = be.get_message_by_id(_PROJECT_ID, msg_id)
|
|
922
|
+
if not msg:
|
|
923
|
+
return {"error": "message not found", "msg_id": msg_id}
|
|
924
|
+
return {
|
|
925
|
+
"ok": True,
|
|
926
|
+
"message": {
|
|
927
|
+
"from": msg.get("from_agent", ""),
|
|
928
|
+
"to": msg.get("to_agent", ""),
|
|
929
|
+
"type": msg.get("type", "msg"),
|
|
930
|
+
"ts": msg.get("created_at", ""),
|
|
931
|
+
"payload": msg.get("payload", {}),
|
|
932
|
+
"id": msg.get("id", ""),
|
|
933
|
+
"read": msg.get("read", False),
|
|
934
|
+
},
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
841
938
|
def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
842
939
|
"""Return {reason, from} if the list contains a global_done signal, else None."""
|
|
843
940
|
for m in messages:
|
|
@@ -861,8 +958,12 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
861
958
|
"""
|
|
862
959
|
global _IN_WAIT
|
|
863
960
|
_IN_WAIT = True
|
|
961
|
+
_set_state("waiting", "listening for messages")
|
|
864
962
|
try:
|
|
865
|
-
|
|
963
|
+
result = await _meshcode_wait_inner(actual_timeout=max(1, int(timeout_seconds)), include_acks=include_acks)
|
|
964
|
+
if result.get("got_message"):
|
|
965
|
+
_set_state("online", "")
|
|
966
|
+
return result
|
|
866
967
|
finally:
|
|
867
968
|
_IN_WAIT = False
|
|
868
969
|
|
|
@@ -961,14 +1062,18 @@ def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
961
1062
|
the realtime listener connected).
|
|
962
1063
|
"""
|
|
963
1064
|
pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
964
|
-
|
|
965
|
-
|
|
1065
|
+
# Peek at realtime buffer WITHOUT draining — check is non-destructive
|
|
1066
|
+
realtime_buffered = _REALTIME.peek() if _REALTIME else []
|
|
1067
|
+
# Don't mark as seen — meshcode_check is a peek, not a consume
|
|
1068
|
+
deduped = [m for m in realtime_buffered if _seen_key(m) not in _SEEN_MSG_IDS]
|
|
966
1069
|
|
|
967
1070
|
# Fallback: if realtime buffer is empty but DB has pending messages,
|
|
968
|
-
# fetch them from the DB
|
|
1071
|
+
# fetch them from the DB. Use mark_read=False so meshcode_check is a
|
|
1072
|
+
# non-destructive peek — messages stay pending until meshcode_wait or
|
|
1073
|
+
# meshcode_read consumes them.
|
|
969
1074
|
if not deduped and pending > 0:
|
|
970
|
-
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME)
|
|
971
|
-
deduped =
|
|
1075
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False)
|
|
1076
|
+
deduped = [
|
|
972
1077
|
{
|
|
973
1078
|
"from": m["from_agent"],
|
|
974
1079
|
"type": m.get("type", "msg"),
|
|
@@ -978,7 +1083,8 @@ def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
978
1083
|
"parent_id": m.get("parent_msg_id"),
|
|
979
1084
|
}
|
|
980
1085
|
for m in raw
|
|
981
|
-
|
|
1086
|
+
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
|
|
1087
|
+
]
|
|
982
1088
|
|
|
983
1089
|
split = _split_messages(deduped)
|
|
984
1090
|
if not include_acks:
|
|
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
|