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.
Files changed (29) hide show
  1. {meshcode-2.0.5 → meshcode-2.0.7}/PKG-INFO +1 -1
  2. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/__init__.py +1 -1
  3. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/backend.py +14 -2
  4. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/realtime.py +4 -0
  5. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/server.py +121 -15
  6. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.0.5 → meshcode-2.0.7}/pyproject.toml +1 -1
  8. {meshcode-2.0.5 → meshcode-2.0.7}/README.md +0 -0
  9. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/cli.py +0 -0
  10. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/comms_v4.py +0 -0
  11. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/invites.py +0 -0
  12. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/launcher.py +0 -0
  13. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/preferences.py +0 -0
  19. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/protocol_v2.py +0 -0
  20. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/run_agent.py +0 -0
  21. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/secrets.py +0 -0
  22. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/self_update.py +0 -0
  23. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode/setup_clients.py +0 -0
  24. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/SOURCES.txt +0 -0
  25. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/dependency_links.txt +0 -0
  26. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/entry_points.txt +0 -0
  27. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/requires.txt +0 -0
  28. {meshcode-2.0.5 → meshcode-2.0.7}/meshcode.egg-info/top_level.txt +0 -0
  29. {meshcode-2.0.5 → meshcode-2.0.7}/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.7
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.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
- f"project_id=eq.{project_id}&type=neq.ack",
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
- # 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:
@@ -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
- return await _meshcode_wait_inner(actual_timeout=max(1, int(timeout_seconds)), include_acks=include_acks)
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
- realtime_buffered = _REALTIME.drain() if _REALTIME else []
965
- deduped = _filter_and_mark(realtime_buffered)
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 so they're not invisible to the agent.
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 = _filter_and_mark([
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.0.5
3
+ Version: 2.0.7
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.7"
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