meshcode 2.0.5__tar.gz → 2.0.6__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.6}/PKG-INFO +1 -1
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/__init__.py +1 -1
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/realtime.py +4 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/server.py +69 -15
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.0.5 → meshcode-2.0.6}/pyproject.toml +1 -1
- {meshcode-2.0.5 → meshcode-2.0.6}/README.md +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/cli.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/comms_v4.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/invites.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/launcher.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/launcher_install.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/preferences.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/run_agent.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/secrets.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/self_update.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/setup_clients.py +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.0.5 → meshcode-2.0.6}/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.6"
|
|
@@ -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:
|
|
@@ -861,8 +906,12 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
861
906
|
"""
|
|
862
907
|
global _IN_WAIT
|
|
863
908
|
_IN_WAIT = True
|
|
909
|
+
_set_state("waiting", "listening for messages")
|
|
864
910
|
try:
|
|
865
|
-
|
|
911
|
+
result = await _meshcode_wait_inner(actual_timeout=max(1, int(timeout_seconds)), include_acks=include_acks)
|
|
912
|
+
if result.get("got_message"):
|
|
913
|
+
_set_state("online", "")
|
|
914
|
+
return result
|
|
866
915
|
finally:
|
|
867
916
|
_IN_WAIT = False
|
|
868
917
|
|
|
@@ -961,14 +1010,18 @@ def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
961
1010
|
the realtime listener connected).
|
|
962
1011
|
"""
|
|
963
1012
|
pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
|
|
964
|
-
|
|
965
|
-
|
|
1013
|
+
# Peek at realtime buffer WITHOUT draining — check is non-destructive
|
|
1014
|
+
realtime_buffered = _REALTIME.peek() if _REALTIME else []
|
|
1015
|
+
# Don't mark as seen — meshcode_check is a peek, not a consume
|
|
1016
|
+
deduped = [m for m in realtime_buffered if _seen_key(m) not in _SEEN_MSG_IDS]
|
|
966
1017
|
|
|
967
1018
|
# Fallback: if realtime buffer is empty but DB has pending messages,
|
|
968
|
-
# fetch them from the DB
|
|
1019
|
+
# fetch them from the DB. Use mark_read=False so meshcode_check is a
|
|
1020
|
+
# non-destructive peek — messages stay pending until meshcode_wait or
|
|
1021
|
+
# meshcode_read consumes them.
|
|
969
1022
|
if not deduped and pending > 0:
|
|
970
|
-
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME)
|
|
971
|
-
deduped =
|
|
1023
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False)
|
|
1024
|
+
deduped = [
|
|
972
1025
|
{
|
|
973
1026
|
"from": m["from_agent"],
|
|
974
1027
|
"type": m.get("type", "msg"),
|
|
@@ -978,7 +1031,8 @@ def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
978
1031
|
"parent_id": m.get("parent_msg_id"),
|
|
979
1032
|
}
|
|
980
1033
|
for m in raw
|
|
981
|
-
|
|
1034
|
+
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
|
|
1035
|
+
]
|
|
982
1036
|
|
|
983
1037
|
split = _split_messages(deduped)
|
|
984
1038
|
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
|
|
File without changes
|