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.
Files changed (29) hide show
  1. {meshcode-2.0.5 → meshcode-2.0.6}/PKG-INFO +1 -1
  2. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/__init__.py +1 -1
  3. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/realtime.py +4 -0
  4. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/server.py +69 -15
  5. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.0.5 → meshcode-2.0.6}/pyproject.toml +1 -1
  7. {meshcode-2.0.5 → meshcode-2.0.6}/README.md +0 -0
  8. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/cli.py +0 -0
  9. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/comms_v4.py +0 -0
  10. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/invites.py +0 -0
  11. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/launcher.py +0 -0
  12. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/backend.py +0 -0
  16. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/preferences.py +0 -0
  19. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/protocol_v2.py +0 -0
  20. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/run_agent.py +0 -0
  21. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/secrets.py +0 -0
  22. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/self_update.py +0 -0
  23. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode/setup_clients.py +0 -0
  24. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/SOURCES.txt +0 -0
  25. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/dependency_links.txt +0 -0
  26. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/entry_points.txt +0 -0
  27. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/requires.txt +0 -0
  28. {meshcode-2.0.5 → meshcode-2.0.6}/meshcode.egg-info/top_level.txt +0 -0
  29. {meshcode-2.0.5 → meshcode-2.0.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.5
3
+ Version: 2.0.6
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.0.5"
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
- # Working/idle status decorator for @mcp.tool() functions.
333
- # Fire-and-forget flips so tool execution is never blocked.
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
- _schedule_flip("working", name)
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
- _schedule_flip("idle", "")
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
- _schedule_flip("working", name)
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
- _schedule_flip("idle", "")
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
- # Also ensure status is at least "idle" (not "offline") between tool calls
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, "idle", "")
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
- return await _meshcode_wait_inner(actual_timeout=max(1, int(timeout_seconds)), include_acks=include_acks)
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
- realtime_buffered = _REALTIME.drain() if _REALTIME else []
965
- deduped = _filter_and_mark(realtime_buffered)
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 so they're not invisible to the agent.
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 = _filter_and_mark([
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.5
3
+ Version: 2.0.6
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.0.5"
7
+ version = "2.0.6"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes