meshcode 2.10.48__tar.gz → 2.10.50__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 (32) hide show
  1. {meshcode-2.10.48 → meshcode-2.10.50}/PKG-INFO +1 -1
  2. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/meshcode_mcp/realtime.py +34 -4
  4. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/meshcode_mcp/server.py +150 -79
  5. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.10.48 → meshcode-2.10.50}/pyproject.toml +1 -1
  7. {meshcode-2.10.48 → meshcode-2.10.50}/README.md +0 -0
  8. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/ascii_art.py +0 -0
  9. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/cli.py +0 -0
  10. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/comms_v4.py +0 -0
  11. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/invites.py +0 -0
  12. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/launcher.py +0 -0
  13. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/launcher_install.py +0 -0
  14. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/meshcode_mcp/backend.py +0 -0
  17. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.48 → meshcode-2.10.50}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.48 → meshcode-2.10.50}/setup.cfg +0 -0
  32. {meshcode-2.10.48 → meshcode-2.10.50}/tests/test_status_enum_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.48
3
+ Version: 2.10.50
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.10.48"
2
+ __version__ = "2.10.50"
@@ -173,6 +173,18 @@ class RealtimeListener:
173
173
  "schema": "meshcode",
174
174
  "table": "mc_messages",
175
175
  "filter": "to_agent=eq.*",
176
+ },
177
+ {
178
+ "event": "INSERT",
179
+ "schema": "meshcode",
180
+ "table": "mc_tasks",
181
+ "filter": f"assignee=eq.{url_quote(self.agent_name, safe='')}",
182
+ },
183
+ {
184
+ "event": "UPDATE",
185
+ "schema": "meshcode",
186
+ "table": "mc_tasks",
187
+ "filter": f"assignee=eq.{url_quote(self.agent_name, safe='')}",
176
188
  }
177
189
  ]
178
190
  }
@@ -260,7 +272,22 @@ class RealtimeListener:
260
272
  # {"event": "postgres_changes", "payload": {"data": {"record": {...}, "type": "INSERT", ...}}}
261
273
  if event == "postgres_changes":
262
274
  data = payload.get("data") or {}
263
- if data.get("type") == "INSERT":
275
+ table = data.get("table", (data.get("record") or {}).get("_table", ""))
276
+ change_type = data.get("type")
277
+
278
+ # ── mc_tasks events: wake the agent when a task is assigned/updated ──
279
+ if table == "mc_tasks" and change_type in ("INSERT", "UPDATE"):
280
+ record = data.get("record") or {}
281
+ assignee = record.get("assignee", "")
282
+ if assignee == self.agent_name and record.get("status") in ("open", "in_progress"):
283
+ log.info(f"task event: {change_type} task '{record.get('title', '?')[:60]}' for {self.agent_name}")
284
+ try:
285
+ self.message_event.set()
286
+ except Exception:
287
+ pass
288
+ return
289
+
290
+ if change_type == "INSERT":
264
291
  record = data.get("record") or {}
265
292
  to = record.get("to_agent")
266
293
  from_agent = record.get("from_agent")
@@ -302,13 +329,16 @@ class RealtimeListener:
302
329
 
303
330
  def drain(self) -> list:
304
331
  """Pop and return all queued messages."""
305
- out = list(self.queue)
306
- self.queue.clear()
307
- # Queue is empty reset the wake event so the next wait blocks again.
332
+ # Clear event FIRST, then drain queue. If a message arrives between
333
+ # clear() and the drain, it re-sets the event and lands in queue —
334
+ # the next wait_for_message returns immediately. This avoids the race
335
+ # where clear() after drain() could eat a wake signal.
308
336
  try:
309
337
  self.message_event.clear()
310
338
  except Exception:
311
339
  pass
340
+ out = list(self.queue)
341
+ self.queue.clear()
312
342
  return out
313
343
 
314
344
  async def wait_for_message(self, timeout: Optional[float] = None) -> bool:
@@ -13,6 +13,7 @@ import logging
13
13
  import os
14
14
  import sys
15
15
  import hashlib as _hashlib
16
+ import threading as _threading
16
17
  import traceback as _traceback
17
18
  from collections import deque
18
19
  from contextlib import asynccontextmanager
@@ -104,12 +105,14 @@ _SEEN_MSG_IDS: dict = {} # key -> timestamp (monotonic)
104
105
  _SEEN_MSG_ORDER: deque = deque()
105
106
  _SEEN_MSG_CAP = 2000
106
107
  _SEEN_TTL = 300.0 # 5 minutes
108
+ _SEEN_LOCK = _threading.Lock() # Guards _SEEN_MSG_IDS + _SEEN_MSG_ORDER
107
109
 
108
110
  # ============================================================
109
111
  # Auto-wake: when agent is NOT in meshcode_wait and a message
110
112
  # arrives, inject text into the terminal to wake the agent.
111
113
  # ============================================================
112
114
  _IN_WAIT = False # True while meshcode_wait is blocking
115
+ _STATE_LOCK = _threading.Lock() # Guards _IN_WAIT, _CURRENT_STATE, _last_tool_at
113
116
  # Default OFF — keystroke injection can corrupt stdin on some terminals.
114
117
  # The primary nudge path is now in comms_v4.nudge_agent() which uses
115
118
  # platform-specific window activation (SetForegroundWindow on Windows,
@@ -292,8 +295,8 @@ def _seen_key(msg: Dict[str, Any]) -> str:
292
295
  return f"{msg.get('from') or msg.get('from_agent')}|{msg.get('ts') or msg.get('created_at')}|{payload_str}"
293
296
 
294
297
 
295
- def _evict_expired() -> None:
296
- """Remove entries older than _SEEN_TTL from the dedup cache."""
298
+ def _evict_expired_unlocked() -> None:
299
+ """Remove entries older than _SEEN_TTL. Caller MUST hold _SEEN_LOCK."""
297
300
  now = _time.monotonic()
298
301
  while _SEEN_MSG_ORDER:
299
302
  oldest_key = _SEEN_MSG_ORDER[0]
@@ -305,31 +308,43 @@ def _evict_expired() -> None:
305
308
  break
306
309
 
307
310
 
311
+ def _evict_expired() -> None:
312
+ """Thread-safe wrapper for eviction."""
313
+ with _SEEN_LOCK:
314
+ _evict_expired_unlocked()
315
+
316
+
308
317
  def _mark_seen(key: str) -> None:
309
- now = _time.monotonic()
310
- if key in _SEEN_MSG_IDS:
311
- # Refresh timestamp on re-sight (extends TTL)
318
+ with _SEEN_LOCK:
319
+ now = _time.monotonic()
320
+ if key in _SEEN_MSG_IDS:
321
+ _SEEN_MSG_IDS[key] = now
322
+ return
312
323
  _SEEN_MSG_IDS[key] = now
313
- return
314
- _SEEN_MSG_IDS[key] = now
315
- _SEEN_MSG_ORDER.append(key)
316
- # Evict expired + cap enforcement
317
- _evict_expired()
318
- while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
319
- old = _SEEN_MSG_ORDER.popleft()
320
- _SEEN_MSG_IDS.pop(old, None)
324
+ _SEEN_MSG_ORDER.append(key)
325
+ _evict_expired_unlocked()
326
+ while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
327
+ old = _SEEN_MSG_ORDER.popleft()
328
+ _SEEN_MSG_IDS.pop(old, None)
321
329
 
322
330
 
323
331
  def _filter_and_mark(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
324
- """Drop already-seen messages; mark the rest as seen."""
325
- _evict_expired() # Clean stale entries before checking
326
- out = []
327
- for m in messages:
328
- k = _seen_key(m)
329
- if k in _SEEN_MSG_IDS:
330
- continue
331
- _mark_seen(k)
332
- out.append(m)
332
+ """Drop already-seen messages; mark the rest as seen. Thread-safe."""
333
+ with _SEEN_LOCK:
334
+ _evict_expired_unlocked()
335
+ out = []
336
+ for m in messages:
337
+ k = _seen_key(m)
338
+ if k in _SEEN_MSG_IDS:
339
+ continue
340
+ now = _time.monotonic()
341
+ _SEEN_MSG_IDS[k] = now
342
+ _SEEN_MSG_ORDER.append(k)
343
+ out.append(m)
344
+ # Cap enforcement
345
+ while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
346
+ old = _SEEN_MSG_ORDER.popleft()
347
+ _SEEN_MSG_IDS.pop(old, None)
333
348
  return out
334
349
 
335
350
 
@@ -1330,11 +1345,12 @@ def _heartbeat_loop_inner():
1330
1345
  try:
1331
1346
  be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": _SDK_VERSION})
1332
1347
 
1333
- # CPU-based status detection
1348
+ # CPU-based status detection — read shared state under lock
1334
1349
  parent_cpu = _get_parent_cpu()
1335
- cur_state = _current_state
1336
- in_wait = _IN_WAIT
1337
- idle_secs = _time.time() - _last_tool_at
1350
+ with _STATE_LOCK:
1351
+ cur_state = _current_state
1352
+ in_wait = _IN_WAIT
1353
+ idle_secs = _time.time() - _last_tool_at
1338
1354
 
1339
1355
  if _is_windows and lease_counter % 12 == 0:
1340
1356
  # Periodic Windows debug dump (every ~60s on pro, ~180s on free)
@@ -1501,6 +1517,10 @@ async def lifespan(_app):
1501
1517
  _heartbeat_stop.set()
1502
1518
  except Exception:
1503
1519
  pass
1520
+ try:
1521
+ be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "terminal closed", api_key=_get_api_key())
1522
+ except Exception:
1523
+ pass
1504
1524
  try:
1505
1525
  _remove_pid_lockfile()
1506
1526
  except Exception:
@@ -1949,19 +1969,17 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1949
1969
  db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
1950
1970
  if db_pending and db_pending > 0:
1951
1971
  raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_get_api_key())
1952
- msgs = [
1953
- {"from": m["from_agent"], "type": m.get("type", "msg"),
1954
- "ts": m.get("created_at"), "payload": m.get("payload", {}),
1955
- "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
1956
- for m in raw
1957
- ]
1958
- # Dedup against already-seen messages (fixes race where
1959
- # background mark_read hasn't completed yet)
1960
- deduped = _filter_and_mark(msgs)
1961
- if not deduped:
1962
- pass # All messages already seen — fall through to wait loop
1963
- else:
1964
- split = _split_messages(deduped)
1972
+ if raw:
1973
+ msgs = [
1974
+ {"from": m["from_agent"], "type": m.get("type", "msg"),
1975
+ "ts": m.get("created_at"), "payload": m.get("payload", {}),
1976
+ "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
1977
+ for m in raw
1978
+ ]
1979
+ # DB is source of truth: if unread in DB, deliver to agent.
1980
+ # Don't let in-memory dedup cache block delivery.
1981
+ _filter_and_mark(msgs) # mark as seen but don't filter
1982
+ split = _split_messages(msgs)
1965
1983
  # Only refuse for real messages — ack-only batches should not block wait
1966
1984
  if split["messages"] or split["done_signals"]:
1967
1985
  return {
@@ -1974,7 +1992,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1974
1992
  except Exception:
1975
1993
  pass
1976
1994
 
1977
- _IN_WAIT = True
1995
+ with _STATE_LOCK:
1996
+ _IN_WAIT = True
1978
1997
  _set_state("waiting", "listening for messages")
1979
1998
  capped_timeout = min(max(1, int(timeout_seconds)), 20)
1980
1999
  try:
@@ -1996,6 +2015,13 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
1996
2015
  _CONSECUTIVE_IDLE_SECONDS = 0
1997
2016
  break
1998
2017
 
2018
+ # Tasks detected mid-wait (via Realtime mc_tasks subscription
2019
+ # or periodic sub-iteration polling) — return immediately.
2020
+ if result.get("pending_tasks"):
2021
+ _set_state("online", "")
2022
+ _CONSECUTIVE_IDLE_SECONDS = 0
2023
+ break
2024
+
1999
2025
  if result.get("timed_out"):
2000
2026
  _CONSECUTIVE_IDLE_SECONDS += capped_timeout
2001
2027
 
@@ -2047,7 +2073,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
2047
2073
  log.debug(f"last_seen memory persist failed: {e}")
2048
2074
  return result
2049
2075
  finally:
2050
- _IN_WAIT = False
2076
+ with _STATE_LOCK:
2077
+ _IN_WAIT = False
2051
2078
 
2052
2079
 
2053
2080
  def _mark_realtime_msgs_read_in_db(messages: List[Dict[str, Any]]) -> None:
@@ -2111,6 +2138,9 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2111
2138
  "got_message": True,
2112
2139
  **split,
2113
2140
  }
2141
+ # Expose queue overflow metric so agents know messages were lost
2142
+ if _REALTIME and _REALTIME.dropped_count > 0:
2143
+ out["dropped_messages"] = _REALTIME.dropped_count
2114
2144
  done = _detect_global_done(deduped)
2115
2145
  if done:
2116
2146
  out["got_done"] = True
@@ -2130,26 +2160,61 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2130
2160
  _rt_live = _REALTIME and _REALTIME.is_subscribed
2131
2161
 
2132
2162
  if _rt_live:
2133
- # 2a) Real async wait zero CPU, zero Supabase calls.
2134
- # Shield from CancelledError: when Claude Code presses ESC, it cancels
2135
- # the in-flight asyncio task. CancelledError is a BaseException that
2136
- # bypasses most try/except, cascades through FastMCP's tool handler,
2137
- # and unwinds the event loop — killing the MCP server. By shielding
2138
- # the await and catching CancelledError, we return a clean timeout
2139
- # result instead. The mcp.run() retry loop restarts the event loop
2140
- # without the lifespan cascade.
2141
- try:
2142
- woke = await asyncio.shield(
2143
- _REALTIME.wait_for_message(timeout=float(actual_timeout))
2144
- )
2145
- except asyncio.CancelledError:
2146
- log.debug("[meshcode] wait_for_message cancelled by ESC")
2147
- return {"timed_out": True, "reason": "cancelled_by_client"}
2148
- except Exception as _wait_exc:
2149
- # Non-cancellation errors (e.g. connection dropped) — log and
2150
- # fall through to DB fallback instead of masking the error.
2151
- log.warning(f"[meshcode] wait_for_message error: {type(_wait_exc).__name__}: {_wait_exc}")
2152
- woke = False
2163
+ # 2a) Realtime wait with DB safety net.
2164
+ # Split into 5s sub-waits. Between each sub-wait, also check DB
2165
+ # as a safety net Realtime WS can die silently and is_subscribed
2166
+ # stays True, so we must not rely on it exclusively.
2167
+ _rt_sub_timeout = 5.0
2168
+ _rt_elapsed = 0.0
2169
+ woke = False
2170
+ while _rt_elapsed < actual_timeout:
2171
+ _this_wait = min(_rt_sub_timeout, actual_timeout - _rt_elapsed)
2172
+ try:
2173
+ woke = await asyncio.shield(
2174
+ _REALTIME.wait_for_message(timeout=_this_wait)
2175
+ )
2176
+ except asyncio.CancelledError:
2177
+ log.debug("[meshcode] wait_for_message cancelled by ESC")
2178
+ return {"timed_out": True, "reason": "cancelled_by_client"}
2179
+ except Exception as _wait_exc:
2180
+ log.warning(f"[meshcode] wait_for_message error: {type(_wait_exc).__name__}: {_wait_exc}")
2181
+ woke = False
2182
+ break # Fall through to DB fallback
2183
+ if woke:
2184
+ break
2185
+ _rt_elapsed += _this_wait
2186
+ # Check for new tasks between sub-iterations
2187
+ _sub_tasks = _get_pending_tasks_summary()
2188
+ if _sub_tasks:
2189
+ return {"timed_out": False, "got_message": False, "pending_tasks": _sub_tasks, "reason": "task_detected_mid_wait"}
2190
+ # DB safety net: check for unread messages even when Realtime
2191
+ # claims to be alive. Realtime WS can die silently.
2192
+ try:
2193
+ _safety_key = _get_api_key()
2194
+ if _safety_key:
2195
+ _safety_cnt = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_safety_key)
2196
+ if _safety_cnt and _safety_cnt > 0:
2197
+ log.info(f"[meshcode] DB safety net: {_safety_cnt} unread msgs despite Realtime — reading from DB")
2198
+ _safety_raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_safety_key)
2199
+ if _safety_raw:
2200
+ _safety_msgs = [
2201
+ {"from": m["from_agent"], "type": m.get("type", "msg"),
2202
+ "ts": m.get("created_at"), "payload": m.get("payload", {}),
2203
+ "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
2204
+ for m in _safety_raw
2205
+ ]
2206
+ _filter_and_mark(_safety_msgs)
2207
+ _safety_split = _split_messages(_safety_msgs)
2208
+ if not include_acks:
2209
+ _safety_split["acks"] = []
2210
+ if _safety_split["messages"] or _safety_split["done_signals"]:
2211
+ return {"got_message": True, **_safety_split}
2212
+ except Exception as _db_err:
2213
+ log.warning(f"[meshcode] DB safety net error: {_db_err}")
2214
+ # Health check: if subscription dropped, switch to DB poll
2215
+ if not (_REALTIME and _REALTIME.is_subscribed):
2216
+ log.info("[meshcode] Realtime subscription lost mid-wait — switching to DB poll")
2217
+ break
2153
2218
  if woke:
2154
2219
  buffered = _REALTIME.drain()
2155
2220
  if buffered:
@@ -2181,15 +2246,17 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2181
2246
  "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
2182
2247
  for m in raw
2183
2248
  ]
2184
- deduped = _filter_and_mark(msgs)
2185
- if deduped:
2186
- split = _split_messages(deduped)
2187
- if not include_acks:
2188
- split["acks"] = []
2189
- if split["messages"] or split["done_signals"]:
2190
- return {"got_message": True, **split}
2249
+ # DB is source of truth: if read=false in DB, deliver
2250
+ # regardless of in-memory dedup cache (fixes stuck loop
2251
+ # where dedup blocks delivery but mark_read already fired).
2252
+ _filter_and_mark(msgs) # mark as seen but don't filter
2253
+ split = _split_messages(msgs)
2254
+ if not include_acks:
2255
+ split["acks"] = []
2256
+ if split["messages"] or split["done_signals"]:
2257
+ return {"got_message": True, **split}
2191
2258
  except Exception as e:
2192
- log.debug(f"DB poll fallback error: {e}")
2259
+ log.warning(f"DB poll fallback error: {e}")
2193
2260
 
2194
2261
  # Final fallback: one last DB check (covers realtime path missing msgs)
2195
2262
  try:
@@ -2205,15 +2272,15 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
2205
2272
  "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
2206
2273
  for m in raw
2207
2274
  ]
2208
- deduped = _filter_and_mark(msgs)
2209
- if deduped:
2210
- split = _split_messages(deduped)
2211
- if not include_acks:
2212
- split["acks"] = []
2213
- if split["messages"] or split["done_signals"]:
2214
- return {"got_message": True, **split}
2275
+ # DB is source of truth: deliver all unread messages.
2276
+ _filter_and_mark(msgs) # mark as seen but don't filter
2277
+ split = _split_messages(msgs)
2278
+ if not include_acks:
2279
+ split["acks"] = []
2280
+ if split["messages"] or split["done_signals"]:
2281
+ return {"got_message": True, **split}
2215
2282
  except Exception as e:
2216
- log.debug(f"final DB fallback error: {e}")
2283
+ log.warning(f"final DB fallback error: {e}")
2217
2284
 
2218
2285
  # Check if there's any pending work before returning timeout
2219
2286
  pending_tasks = _get_pending_tasks_summary()
@@ -2295,8 +2362,12 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark
2295
2362
 
2296
2363
  # When mark_read=True, update tracking state so messages aren't re-processed
2297
2364
  if mark_read and deduped:
2298
- for m in deduped:
2299
- _SEEN_MSG_IDS.add(_seen_key(m))
2365
+ with _SEEN_LOCK:
2366
+ _now = _time.monotonic()
2367
+ for m in deduped:
2368
+ k = _seen_key(m)
2369
+ _SEEN_MSG_IDS[k] = _now
2370
+ _SEEN_MSG_ORDER.append(k)
2300
2371
  latest_ts = max((str(m.get("ts", "")) for m in deduped), default=None)
2301
2372
  if latest_ts and (not _LAST_SEEN_TS or latest_ts > _LAST_SEEN_TS):
2302
2373
  _LAST_SEEN_TS = latest_ts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.48
3
+ Version: 2.10.50
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.10.48"
7
+ version = "2.10.50"
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