meshcode 2.10.36__tar.gz → 2.10.37__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.10.36 → meshcode-2.10.37}/PKG-INFO +1 -1
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/__init__.py +1 -1
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/meshcode_mcp/backend.py +41 -29
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/meshcode_mcp/realtime.py +17 -5
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/meshcode_mcp/server.py +37 -21
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.36 → meshcode-2.10.37}/pyproject.toml +1 -1
- {meshcode-2.10.36 → meshcode-2.10.37}/README.md +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/cli.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/invites.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/launcher.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/preferences.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/secrets.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/self_update.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/setup.cfg +0 -0
- {meshcode-2.10.36 → meshcode-2.10.37}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.10.
|
|
2
|
+
__version__ = "2.10.37"
|
|
@@ -105,69 +105,81 @@ SCHEMA = "meshcode"
|
|
|
105
105
|
# ── Persistent HTTPS Connection Pool ──────────────────────────────
|
|
106
106
|
# Each urlopen() opens a new TCP+TLS connection (~1-2s overhead).
|
|
107
107
|
# This pool reuses connections, cutting typical latency from ~3s to ~100ms.
|
|
108
|
+
# Pool of N connections (default 3) to avoid lock contention between the
|
|
109
|
+
# heartbeat thread, tool call handlers, and background recording thread.
|
|
108
110
|
class _ConnectionPool:
|
|
109
|
-
"""Thread-safe
|
|
111
|
+
"""Thread-safe HTTPS connection pool to Supabase (N connections, round-robin)."""
|
|
110
112
|
|
|
111
|
-
def __init__(self, url: str, max_idle: float = 30.0):
|
|
113
|
+
def __init__(self, url: str, pool_size: int = 3, max_idle: float = 30.0):
|
|
112
114
|
parsed = urlparse(url)
|
|
113
115
|
self._host = parsed.hostname
|
|
114
116
|
self._port = parsed.port or 443
|
|
115
|
-
self.
|
|
116
|
-
self.
|
|
117
|
+
self._pool_size = pool_size
|
|
118
|
+
self._conns: List[Optional[http.client.HTTPSConnection]] = [None] * pool_size
|
|
119
|
+
self._locks = [_threading.Lock() for _ in range(pool_size)]
|
|
120
|
+
self._last_used = [0.0] * pool_size
|
|
117
121
|
self._max_idle = max_idle
|
|
118
|
-
self._last_used = 0.0
|
|
119
122
|
self._ctx = ssl.create_default_context()
|
|
123
|
+
self._counter = 0
|
|
124
|
+
self._counter_lock = _threading.Lock()
|
|
125
|
+
|
|
126
|
+
def _pick_slot(self) -> int:
|
|
127
|
+
"""Round-robin slot selection."""
|
|
128
|
+
with self._counter_lock:
|
|
129
|
+
slot = self._counter % self._pool_size
|
|
130
|
+
self._counter += 1
|
|
131
|
+
return slot
|
|
120
132
|
|
|
121
|
-
def _get_conn(self) -> http.client.HTTPSConnection:
|
|
133
|
+
def _get_conn(self, slot: int) -> http.client.HTTPSConnection:
|
|
122
134
|
now = _time.monotonic()
|
|
123
|
-
if self.
|
|
124
|
-
|
|
125
|
-
if now - self._last_used > self._max_idle:
|
|
135
|
+
if self._conns[slot] is not None:
|
|
136
|
+
if now - self._last_used[slot] > self._max_idle:
|
|
126
137
|
try:
|
|
127
|
-
self.
|
|
138
|
+
self._conns[slot].close()
|
|
128
139
|
except Exception:
|
|
129
140
|
pass
|
|
130
|
-
self.
|
|
131
|
-
if self.
|
|
132
|
-
self.
|
|
141
|
+
self._conns[slot] = None
|
|
142
|
+
if self._conns[slot] is None:
|
|
143
|
+
self._conns[slot] = http.client.HTTPSConnection(
|
|
133
144
|
self._host, self._port, timeout=10, context=self._ctx
|
|
134
145
|
)
|
|
135
|
-
return self.
|
|
146
|
+
return self._conns[slot]
|
|
136
147
|
|
|
137
148
|
def request(self, method: str, path: str, body: Optional[bytes], headers: Dict[str, str]) -> tuple:
|
|
138
|
-
"""Returns (status, response_body_str). Thread-safe with
|
|
139
|
-
|
|
149
|
+
"""Returns (status, response_body_str). Thread-safe with per-slot locking."""
|
|
150
|
+
slot = self._pick_slot()
|
|
151
|
+
with self._locks[slot]:
|
|
140
152
|
for attempt in range(2):
|
|
141
|
-
conn = self._get_conn()
|
|
153
|
+
conn = self._get_conn(slot)
|
|
142
154
|
try:
|
|
143
155
|
conn.request(method, path, body=body, headers=headers)
|
|
144
156
|
resp = conn.getresponse()
|
|
145
157
|
data = resp.read().decode("utf-8")
|
|
146
|
-
self._last_used = _time.monotonic()
|
|
158
|
+
self._last_used[slot] = _time.monotonic()
|
|
147
159
|
return resp.status, data
|
|
148
160
|
except (http.client.RemoteDisconnected, BrokenPipeError,
|
|
149
161
|
ConnectionResetError, OSError) as e:
|
|
150
|
-
# Connection went stale — close and retry once
|
|
151
162
|
try:
|
|
152
|
-
self.
|
|
163
|
+
self._conns[slot].close()
|
|
153
164
|
except Exception:
|
|
154
165
|
pass
|
|
155
|
-
self.
|
|
166
|
+
self._conns[slot] = None
|
|
156
167
|
if attempt == 0:
|
|
157
168
|
continue
|
|
158
169
|
raise
|
|
159
170
|
|
|
160
171
|
def close(self):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
for i in range(self._pool_size):
|
|
173
|
+
with self._locks[i]:
|
|
174
|
+
if self._conns[i]:
|
|
175
|
+
try:
|
|
176
|
+
self._conns[i].close()
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
self._conns[i] = None
|
|
168
180
|
|
|
169
181
|
|
|
170
|
-
_pool = _ConnectionPool(SUPABASE_URL)
|
|
182
|
+
_pool = _ConnectionPool(SUPABASE_URL, pool_size=3)
|
|
171
183
|
|
|
172
184
|
|
|
173
185
|
def _now_iso() -> str:
|
|
@@ -53,9 +53,12 @@ class RealtimeListener:
|
|
|
53
53
|
self.notify_callback = notify_callback
|
|
54
54
|
self.service_role_key = service_role_key
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
|
|
56
|
+
# Message queue — drained by meshcode_check/meshcode_wait.
|
|
57
|
+
# Cap at 2000 (was 500). When full, oldest messages are dropped
|
|
58
|
+
# and _dropped_count is incremented so the agent knows.
|
|
59
|
+
self.queue: Deque[Dict] = deque(maxlen=2000)
|
|
58
60
|
self._overflow_warned = False
|
|
61
|
+
self._dropped_count = 0
|
|
59
62
|
self._task: Optional[asyncio.Task] = None
|
|
60
63
|
# asyncio.Event() in Py3.10+ no longer requires a running loop, but
|
|
61
64
|
# on older Python or certain Windows event-loop policies it can
|
|
@@ -272,9 +275,14 @@ class RealtimeListener:
|
|
|
272
275
|
"id": record.get("id"),
|
|
273
276
|
"parent_id": record.get("parent_msg_id"),
|
|
274
277
|
}
|
|
275
|
-
if len(self.queue) >=
|
|
276
|
-
|
|
277
|
-
self.
|
|
278
|
+
if len(self.queue) >= self.queue.maxlen:
|
|
279
|
+
# Queue is full — this append will drop the oldest message
|
|
280
|
+
self._dropped_count += 1
|
|
281
|
+
if not self._overflow_warned:
|
|
282
|
+
log.warning(f"Message queue FULL ({self.queue.maxlen}) — dropping oldest messages ({self._dropped_count} dropped so far)")
|
|
283
|
+
self._overflow_warned = True
|
|
284
|
+
elif len(self.queue) >= self.queue.maxlen * 0.8 and not self._overflow_warned:
|
|
285
|
+
log.warning(f"Message queue at {len(self.queue)}/{self.queue.maxlen} — approaching limit")
|
|
278
286
|
self.queue.append(enriched)
|
|
279
287
|
# Wake any meshcode_wait blocked on this event.
|
|
280
288
|
try:
|
|
@@ -327,6 +335,10 @@ class RealtimeListener:
|
|
|
327
335
|
await self.stop()
|
|
328
336
|
await self.start()
|
|
329
337
|
|
|
338
|
+
@property
|
|
339
|
+
def dropped_count(self) -> int:
|
|
340
|
+
return self._dropped_count
|
|
341
|
+
|
|
330
342
|
@property
|
|
331
343
|
def is_connected(self) -> bool:
|
|
332
344
|
return self._connected
|
|
@@ -864,6 +864,19 @@ except Exception as _e:
|
|
|
864
864
|
log.warning(f"could not fetch agent profile: {_e}")
|
|
865
865
|
|
|
866
866
|
|
|
867
|
+
# ── Leader/commander detection (single source of truth) ───────
|
|
868
|
+
_LEADER_KEYWORDS = (
|
|
869
|
+
'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
|
|
870
|
+
'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
|
|
871
|
+
'chief', 'captain', 'boss', 'head agent',
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
def _is_leader_agent() -> bool:
|
|
875
|
+
"""Check if this agent is a leader/commander based on name + role keywords."""
|
|
876
|
+
haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
|
|
877
|
+
return any(k in haystack for k in _LEADER_KEYWORDS)
|
|
878
|
+
|
|
879
|
+
|
|
867
880
|
def _build_instructions() -> str:
|
|
868
881
|
"""Build the system-instructions block injected via the MCP InitializeResult.
|
|
869
882
|
|
|
@@ -939,15 +952,7 @@ what CLI command to run next (e.g. "meshcode run backend in a new terminal").
|
|
|
939
952
|
Setup help → README.md or https://meshcode.io/docs
|
|
940
953
|
"""
|
|
941
954
|
# Inject commander protocol if this agent is a leader
|
|
942
|
-
|
|
943
|
-
# Substring match: "orchestrator" hits on "orchestrat", "coordinador" hits on "coordinat".
|
|
944
|
-
_leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
|
|
945
|
-
_LEADER_KEYWORDS = (
|
|
946
|
-
'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
|
|
947
|
-
'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
|
|
948
|
-
'chief', 'captain', 'boss', 'head agent',
|
|
949
|
-
)
|
|
950
|
-
is_leader = any(k in _leader_haystack for k in _LEADER_KEYWORDS)
|
|
955
|
+
is_leader = _is_leader_agent()
|
|
951
956
|
if is_leader:
|
|
952
957
|
base += """
|
|
953
958
|
COMMANDER PROTOCOL (you are the team lead):
|
|
@@ -1612,11 +1617,7 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
|
1612
1617
|
]
|
|
1613
1618
|
# For leader agents: also include unclaimed '*' tasks as pending
|
|
1614
1619
|
# so commanders auto-triage them instead of letting them pile up.
|
|
1615
|
-
|
|
1616
|
-
_LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
|
|
1617
|
-
'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
|
|
1618
|
-
'chief', 'captain', 'boss', 'head agent')
|
|
1619
|
-
is_leader = any(k in _leader_haystack for k in _LEADER_KW)
|
|
1620
|
+
is_leader = _is_leader_agent()
|
|
1620
1621
|
if is_leader:
|
|
1621
1622
|
wildcard_tasks = [
|
|
1622
1623
|
{"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
|
|
@@ -1626,6 +1627,9 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
|
1626
1627
|
and not t.get("claimed_by")
|
|
1627
1628
|
]
|
|
1628
1629
|
pending.extend(wildcard_tasks)
|
|
1630
|
+
# Sort by priority so urgent tasks surface first
|
|
1631
|
+
_PRIORITY_ORDER = {"urgent": 0, "high": 1, "normal": 2, "low": 3}
|
|
1632
|
+
pending.sort(key=lambda t: _PRIORITY_ORDER.get(t.get("priority", "normal"), 2))
|
|
1629
1633
|
return pending if pending else None
|
|
1630
1634
|
except Exception:
|
|
1631
1635
|
return None
|
|
@@ -1655,12 +1659,7 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
1655
1659
|
# delegate tasks and need to stay in the wait loop to receive reports.
|
|
1656
1660
|
pending_tasks = _get_pending_tasks_summary()
|
|
1657
1661
|
if pending_tasks:
|
|
1658
|
-
|
|
1659
|
-
_LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
|
|
1660
|
-
'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
|
|
1661
|
-
'chief', 'captain', 'boss', 'head agent')
|
|
1662
|
-
_is_leader = any(k in _leader_haystack for k in _LEADER_KW)
|
|
1663
|
-
if not _is_leader:
|
|
1662
|
+
if not _is_leader_agent():
|
|
1664
1663
|
return {
|
|
1665
1664
|
"refused": True,
|
|
1666
1665
|
"reason": "You have open tasks. Work them before entering wait.",
|
|
@@ -2671,12 +2670,29 @@ def meshcode_recall(key: Optional[str] = None) -> Dict[str, Any]:
|
|
|
2671
2670
|
"p_key": key,
|
|
2672
2671
|
})
|
|
2673
2672
|
else:
|
|
2674
|
-
|
|
2673
|
+
# Return critical tier (full content) + counts of other tiers.
|
|
2674
|
+
# Episodic memories are large and should be searched on demand
|
|
2675
|
+
# via meshcode_recall_search(), not dumped into context.
|
|
2676
|
+
critical = be.sb_rpc("mc_memory_list", {
|
|
2675
2677
|
"p_api_key": api_key,
|
|
2676
2678
|
"p_agent_name": AGENT_NAME,
|
|
2677
2679
|
"p_tier": "critical",
|
|
2678
2680
|
"p_project_name": PROJECT_NAME,
|
|
2679
2681
|
})
|
|
2682
|
+
# Also fetch reference tier keys (lightweight, useful context)
|
|
2683
|
+
reference = be.sb_rpc("mc_memory_list", {
|
|
2684
|
+
"p_api_key": api_key,
|
|
2685
|
+
"p_agent_name": AGENT_NAME,
|
|
2686
|
+
"p_tier": "reference",
|
|
2687
|
+
"p_project_name": PROJECT_NAME,
|
|
2688
|
+
})
|
|
2689
|
+
if isinstance(critical, dict) and critical.get("ok"):
|
|
2690
|
+
ref_keys = []
|
|
2691
|
+
if isinstance(reference, dict) and reference.get("ok"):
|
|
2692
|
+
ref_keys = [m.get("key") for m in reference.get("memories", [])]
|
|
2693
|
+
critical["reference_keys"] = ref_keys
|
|
2694
|
+
critical["tip"] = "Use meshcode_recall_search(query) to search episodic memories."
|
|
2695
|
+
return critical
|
|
2680
2696
|
|
|
2681
2697
|
|
|
2682
2698
|
@mcp.tool()
|
|
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
|
|
File without changes
|
|
File without changes
|