meshcode 2.0.8__tar.gz → 2.1.0__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.8 → meshcode-2.1.0}/PKG-INFO +1 -1
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/__init__.py +1 -1
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/meshcode_mcp/backend.py +67 -3
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/meshcode_mcp/server.py +40 -9
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.0.8 → meshcode-2.1.0}/pyproject.toml +1 -1
- {meshcode-2.0.8 → meshcode-2.1.0}/README.md +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/cli.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/comms_v4.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/invites.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/launcher.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/launcher_install.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/preferences.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/run_agent.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/secrets.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/self_update.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode/setup_clients.py +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.0.8 → meshcode-2.1.0}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.0
|
|
2
|
+
__version__ = "2.1.0"
|
|
@@ -105,6 +105,29 @@ def sb_update(table: str, filters: str, updates: Dict) -> Any:
|
|
|
105
105
|
return _request("PATCH", f"{table}?{filters}", data=updates, prefer="return=representation")
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
# Session recording config — set by MCP server at boot
|
|
109
|
+
_recording_enabled = False
|
|
110
|
+
_recording_api_key = ""
|
|
111
|
+
_recording_project_id = ""
|
|
112
|
+
_recording_agent_name = ""
|
|
113
|
+
_recording_session_id = ""
|
|
114
|
+
|
|
115
|
+
# RPCs that should NOT be recorded (internal/heartbeat/recording itself)
|
|
116
|
+
_SKIP_RECORDING = {"mc_heartbeat", "mc_record_event", "mc_agent_set_status_by_api_key",
|
|
117
|
+
"mc_acquire_agent_lease", "mc_release_agent_lease"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def enable_recording(api_key: str, project_id: str, agent_name: str, session_id: str):
|
|
121
|
+
"""Called by MCP server at boot to enable session recording."""
|
|
122
|
+
global _recording_enabled, _recording_api_key, _recording_project_id
|
|
123
|
+
global _recording_agent_name, _recording_session_id
|
|
124
|
+
_recording_enabled = True
|
|
125
|
+
_recording_api_key = api_key
|
|
126
|
+
_recording_project_id = project_id
|
|
127
|
+
_recording_agent_name = agent_name
|
|
128
|
+
_recording_session_id = session_id
|
|
129
|
+
|
|
130
|
+
|
|
108
131
|
def sb_rpc(fn_name: str, params: Dict) -> Any:
|
|
109
132
|
url = f"{SUPABASE_URL}/rest/v1/rpc/{fn_name}"
|
|
110
133
|
body = json.dumps(params).encode("utf-8")
|
|
@@ -112,16 +135,57 @@ def sb_rpc(fn_name: str, params: Dict) -> Any:
|
|
|
112
135
|
try:
|
|
113
136
|
with urlopen(req, timeout=10) as resp:
|
|
114
137
|
raw = resp.read().decode("utf-8")
|
|
115
|
-
|
|
138
|
+
result = json.loads(raw) if raw.strip() else None
|
|
116
139
|
except HTTPError as e:
|
|
117
140
|
err = e.read().decode("utf-8", errors="replace")
|
|
118
141
|
try:
|
|
119
|
-
|
|
142
|
+
result = {"_error": json.loads(err).get("message", err[:200])}
|
|
120
143
|
except Exception:
|
|
121
|
-
|
|
144
|
+
result = {"_error": err[:200]}
|
|
145
|
+
# Record errors too
|
|
146
|
+
if _recording_enabled and fn_name not in _SKIP_RECORDING:
|
|
147
|
+
_bg_record("error", {"rpc": fn_name, "error": str(err)[:200]})
|
|
148
|
+
return result
|
|
122
149
|
except URLError as e:
|
|
123
150
|
return {"_error": str(e.reason)}
|
|
124
151
|
|
|
152
|
+
# Auto-record tool calls to session events (hot-reloadable)
|
|
153
|
+
if _recording_enabled and fn_name not in _SKIP_RECORDING:
|
|
154
|
+
_bg_record("tool_call", {"rpc": fn_name})
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _bg_record(event_type: str, payload: dict):
|
|
160
|
+
"""Background-thread recording — never blocks the caller."""
|
|
161
|
+
import threading
|
|
162
|
+
def _do():
|
|
163
|
+
try:
|
|
164
|
+
sb_rpc_raw("mc_record_event", {
|
|
165
|
+
"p_api_key": _recording_api_key,
|
|
166
|
+
"p_project_id": _recording_project_id,
|
|
167
|
+
"p_agent_name": _recording_agent_name,
|
|
168
|
+
"p_session_id": _recording_session_id,
|
|
169
|
+
"p_event_type": event_type,
|
|
170
|
+
"p_payload": payload,
|
|
171
|
+
})
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
threading.Thread(target=_do, daemon=True).start()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def sb_rpc_raw(fn_name: str, params: Dict) -> Any:
|
|
178
|
+
"""Raw RPC call without recording (to avoid infinite recursion)."""
|
|
179
|
+
url = f"{SUPABASE_URL}/rest/v1/rpc/{fn_name}"
|
|
180
|
+
body = json.dumps(params).encode("utf-8")
|
|
181
|
+
req = Request(url, data=body, method="POST", headers=_headers(content_profile=False))
|
|
182
|
+
try:
|
|
183
|
+
with urlopen(req, timeout=10) as resp:
|
|
184
|
+
raw = resp.read().decode("utf-8")
|
|
185
|
+
return json.loads(raw) if raw.strip() else None
|
|
186
|
+
except Exception:
|
|
187
|
+
return None
|
|
188
|
+
|
|
125
189
|
|
|
126
190
|
# ============================================================
|
|
127
191
|
# Project + agent helpers
|
|
@@ -406,12 +406,10 @@ def with_working_status(func):
|
|
|
406
406
|
return await func(*args, **kwargs)
|
|
407
407
|
finally:
|
|
408
408
|
if not skip:
|
|
409
|
-
global _last_tool_at
|
|
409
|
+
global _last_tool_at
|
|
410
410
|
_last_tool_at = _time.time()
|
|
411
|
-
#
|
|
412
|
-
|
|
413
|
-
_working_timer.daemon = True
|
|
414
|
-
_working_timer.start()
|
|
411
|
+
# Don't flip to online here — CPU-based detection in heartbeat
|
|
412
|
+
# will handle the transition when LLM stops generating
|
|
415
413
|
return awrapper
|
|
416
414
|
else:
|
|
417
415
|
@_functools.wraps(func)
|
|
@@ -688,6 +686,24 @@ async def _on_new_message(msg: Dict[str, Any]) -> None:
|
|
|
688
686
|
_heartbeat_stop = _threading.Event()
|
|
689
687
|
|
|
690
688
|
|
|
689
|
+
def _get_parent_cpu() -> float:
|
|
690
|
+
"""Check parent process CPU usage to detect if LLM is actively generating."""
|
|
691
|
+
try:
|
|
692
|
+
import subprocess as _sp, platform as _pl
|
|
693
|
+
ppid = os.getppid()
|
|
694
|
+
if _pl.system() == "Windows":
|
|
695
|
+
r = _sp.run(["wmic", "process", "where", f"processid={ppid}", "get", "percentprocessortime"],
|
|
696
|
+
capture_output=True, text=True, timeout=5)
|
|
697
|
+
lines = [l.strip() for l in r.stdout.strip().split("\n") if l.strip()]
|
|
698
|
+
return float(lines[-1]) if len(lines) > 1 else 0.0
|
|
699
|
+
else:
|
|
700
|
+
r = _sp.run(["ps", "-p", str(ppid), "-o", "%cpu"], capture_output=True, text=True, timeout=5)
|
|
701
|
+
lines = [l.strip() for l in r.stdout.strip().split("\n") if l.strip()]
|
|
702
|
+
return float(lines[-1]) if len(lines) > 1 else 0.0
|
|
703
|
+
except Exception:
|
|
704
|
+
return 0.0
|
|
705
|
+
|
|
706
|
+
|
|
691
707
|
def _heartbeat_thread_fn():
|
|
692
708
|
"""Heartbeat in a DAEMON THREAD — independent of asyncio event loop.
|
|
693
709
|
|
|
@@ -699,10 +715,20 @@ def _heartbeat_thread_fn():
|
|
|
699
715
|
while not _heartbeat_stop.is_set():
|
|
700
716
|
try:
|
|
701
717
|
be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
|
|
702
|
-
|
|
703
|
-
#
|
|
704
|
-
if _current_state
|
|
705
|
-
|
|
718
|
+
|
|
719
|
+
# CPU-based status detection: check parent process (editor/LLM) CPU usage
|
|
720
|
+
if _current_state not in ("waiting",): # Don't override explicit WAITING
|
|
721
|
+
parent_cpu = _get_parent_cpu()
|
|
722
|
+
if parent_cpu > 5.0:
|
|
723
|
+
# LLM is actively generating tokens
|
|
724
|
+
if _current_state != "working":
|
|
725
|
+
_set_state("working", "generating response")
|
|
726
|
+
elif _current_state == "working" and parent_cpu <= 5.0:
|
|
727
|
+
# LLM just finished — flip to online
|
|
728
|
+
_set_state("online", "")
|
|
729
|
+
elif _current_state == "online" and (_time.time() - _last_tool_at) > _IDLE_THRESHOLD_S:
|
|
730
|
+
_set_state("idle", f"idle ({int((_time.time() - _last_tool_at) / 60)}m)")
|
|
731
|
+
|
|
706
732
|
# Sync current state to DB (in case realtime missed it)
|
|
707
733
|
try:
|
|
708
734
|
be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool)
|
|
@@ -762,6 +788,11 @@ async def lifespan(_app):
|
|
|
762
788
|
hb_thread = _threading.Thread(target=_heartbeat_thread_fn, daemon=True, name="meshcode-heartbeat")
|
|
763
789
|
hb_thread.start()
|
|
764
790
|
log.info(f"lifespan started — Realtime + heartbeat thread active for {AGENT_NAME}")
|
|
791
|
+
# Enable session recording in backend.py (hot-reloadable)
|
|
792
|
+
try:
|
|
793
|
+
be.enable_recording(_get_api_key(), _PROJECT_ID, AGENT_NAME, _SESSION_ID)
|
|
794
|
+
except Exception:
|
|
795
|
+
pass
|
|
765
796
|
_record_event_bg("boot", {"agent": AGENT_NAME, "project": PROJECT_NAME, "session_id": _SESSION_ID})
|
|
766
797
|
try:
|
|
767
798
|
yield {"realtime": _REALTIME}
|
|
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
|