meshcode 2.10.52__tar.gz → 2.10.54__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.52 → meshcode-2.10.54}/PKG-INFO +1 -1
  2. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/server.py +275 -44
  4. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/PKG-INFO +1 -1
  5. {meshcode-2.10.52 → meshcode-2.10.54}/pyproject.toml +1 -1
  6. {meshcode-2.10.52 → meshcode-2.10.54}/README.md +0 -0
  7. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/ascii_art.py +0 -0
  8. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/cli.py +0 -0
  9. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/comms_v4.py +0 -0
  10. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/invites.py +0 -0
  11. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/launcher.py +0 -0
  12. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/backend.py +0 -0
  16. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/realtime.py +0 -0
  17. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.52 → meshcode-2.10.54}/setup.cfg +0 -0
  32. {meshcode-2.10.52 → meshcode-2.10.54}/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.52
3
+ Version: 2.10.54
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.52"
2
+ __version__ = "2.10.54"
@@ -721,6 +721,52 @@ def with_working_status(func):
721
721
  # to prevent cascade through FastMCP into the event loop.
722
722
  # Return an error dict instead of propagating BaseException.
723
723
  log.debug(f"[meshcode] tool {name} cancelled by client (ESC)")
724
+ # 2.10.54 deaf-agent fix: for meshcode_wait specifically, the
725
+ # inner cancel handler already tries to drain pending messages.
726
+ # If we still got CancelledError here, the inner drain failed —
727
+ # do a last-ditch DB read so the LLM sees pending mail instead
728
+ # of a generic error and goes silent.
729
+ if name == "meshcode_wait":
730
+ try:
731
+ _t = asyncio.current_task()
732
+ if _t is not None and hasattr(_t, "uncancel"):
733
+ _t.uncancel()
734
+ except Exception:
735
+ pass
736
+ try:
737
+ _ak = _get_api_key()
738
+ if _ak:
739
+ _raw = be.read_inbox(
740
+ _PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_ak
741
+ )
742
+ if _raw:
743
+ _msgs = [
744
+ {
745
+ "from": m["from_agent"],
746
+ "type": m.get("type", "msg"),
747
+ "ts": m.get("created_at"),
748
+ "payload": m.get("payload", {}),
749
+ "id": m.get("id"),
750
+ "parent_id": m.get("parent_msg_id"),
751
+ }
752
+ for m in _raw
753
+ ]
754
+ _filter_and_mark(_msgs)
755
+ _split = _split_messages(_msgs)
756
+ if _split["messages"] or _split["done_signals"]:
757
+ log.info(
758
+ f"[meshcode] wrapper-level cancel-drain rescued "
759
+ f"{_split['count']} messages"
760
+ )
761
+ return {
762
+ "got_message": True,
763
+ "cancelled_during_wait": True,
764
+ **_split,
765
+ }
766
+ except Exception as _final_err:
767
+ log.debug(
768
+ f"[meshcode] wrapper cancel-drain failed: {_final_err}"
769
+ )
724
770
  return {"error": "cancelled_by_client", "tool": name}
725
771
  except Exception as e:
726
772
  if not skip:
@@ -1871,23 +1917,130 @@ _AUTO_SLEEP_THRESHOLD = int(os.environ.get("MESHCODE_AUTO_SLEEP_SECONDS", "600")
1871
1917
  _STAY_AWAKE = False # Set by meshcode_set_status("online") — prevents auto-sleep
1872
1918
  _LAST_SEEN_TS: Optional[str] = None # auto-persisted for message dedup
1873
1919
 
1874
- # Hydrate _LAST_SEEN_TS from mesh memory on boot so restarts skip old messages
1875
- try:
1876
- _ls_result = be.sb_rpc("mc_memory_get", {
1877
- "p_api_key": _get_api_key(),
1878
- "p_agent_name": AGENT_NAME,
1879
- "p_key": "last_seen",
1880
- })
1881
- if isinstance(_ls_result, dict) and _ls_result.get("ok"):
1882
- _ls_val = _ls_result.get("value")
1883
- if isinstance(_ls_val, dict) and _ls_val.get("_raw"):
1884
- _LAST_SEEN_TS = str(_ls_val["_raw"])
1885
- elif isinstance(_ls_val, str):
1886
- _LAST_SEEN_TS = _ls_val
1887
- if _LAST_SEEN_TS:
1888
- print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from mesh memory.", file=sys.stderr)
1889
- except Exception as _e:
1890
- print(f"[meshcode] Could not restore last_seen: {_e}", file=sys.stderr)
1920
+ # ============================================================
1921
+ # Local disk state — survives MCP respawn instantly.
1922
+ # 2.10.53 fix for the ESC-kills-MCP friction: when Claude Code closes
1923
+ # stdin (ESC, /mcp disconnect), our process exits but Claude Code spawns
1924
+ # a fresh subprocess on next interaction. Without disk persistence, the
1925
+ # new process must round-trip mesh memory RPC to restore last_seen and
1926
+ # starts with an EMPTY dedup cache — 200ms-3s gap during which old
1927
+ # messages can be re-delivered to the LLM.
1928
+ #
1929
+ # With disk persistence, the new process restores state in <5ms and
1930
+ # never re-delivers messages it already showed the agent.
1931
+ # ============================================================
1932
+ def _state_file_path() -> str:
1933
+ """Path to the local state file for this agent."""
1934
+ state_dir = os.path.join(os.path.expanduser("~"), ".meshcode")
1935
+ try:
1936
+ os.makedirs(state_dir, exist_ok=True)
1937
+ except Exception:
1938
+ pass
1939
+ safe = f"state_{PROJECT_NAME}_{AGENT_NAME}.json".replace("/", "_").replace(" ", "_")
1940
+ return os.path.join(state_dir, safe)
1941
+
1942
+
1943
+ def _load_state_from_disk() -> Dict[str, Any]:
1944
+ """Load persisted state. Returns empty dict on any failure."""
1945
+ try:
1946
+ path = _state_file_path()
1947
+ if not os.path.exists(path):
1948
+ return {}
1949
+ with open(path, "r") as f:
1950
+ data = json.load(f)
1951
+ if not isinstance(data, dict):
1952
+ return {}
1953
+ return data
1954
+ except Exception:
1955
+ return {}
1956
+
1957
+
1958
+ _STATE_SAVE_LOCK = _threading.Lock()
1959
+ _STATE_LAST_SAVE_AT = 0.0
1960
+ _STATE_SAVE_DEBOUNCE_S = 1.0
1961
+
1962
+
1963
+ def _save_state_to_disk(force: bool = False) -> None:
1964
+ """Persist current state to disk. Debounced unless force=True.
1965
+
1966
+ Atomic via .tmp + os.replace to prevent half-written files if SIGKILL
1967
+ arrives mid-write.
1968
+ """
1969
+ global _STATE_LAST_SAVE_AT
1970
+ with _STATE_SAVE_LOCK:
1971
+ now = _time.monotonic()
1972
+ if not force and (now - _STATE_LAST_SAVE_AT) < _STATE_SAVE_DEBOUNCE_S:
1973
+ return
1974
+ _STATE_LAST_SAVE_AT = now
1975
+ try:
1976
+ with _SEEN_LOCK:
1977
+ # Persist last 200 dedup keys (most recent) — bounded so disk
1978
+ # write stays fast even on long-running sessions.
1979
+ seen_keys = list(_SEEN_MSG_ORDER)[-200:]
1980
+ data = {
1981
+ "last_seen": _LAST_SEEN_TS,
1982
+ "seen_keys": seen_keys,
1983
+ "agent": AGENT_NAME,
1984
+ "project": PROJECT_NAME,
1985
+ "saved_at": _time.time(),
1986
+ "instance_id": _INSTANCE_ID if "_INSTANCE_ID" in globals() else None,
1987
+ }
1988
+ path = _state_file_path()
1989
+ tmp = path + ".tmp"
1990
+ with open(tmp, "w") as f:
1991
+ json.dump(data, f)
1992
+ os.replace(tmp, path)
1993
+ except Exception:
1994
+ pass # Never let state save failure break tool flow
1995
+
1996
+
1997
+ def _save_state_async() -> None:
1998
+ """Schedule a debounced background save. Safe to call from any thread."""
1999
+ try:
2000
+ _threading.Thread(
2001
+ target=_save_state_to_disk,
2002
+ daemon=True,
2003
+ name="meshcode-state-save",
2004
+ ).start()
2005
+ except Exception:
2006
+ pass
2007
+
2008
+
2009
+ # Hydrate _LAST_SEEN_TS — local disk first (instant), mesh memory fallback.
2010
+ _disk_state = _load_state_from_disk()
2011
+ if isinstance(_disk_state.get("last_seen"), str) and _disk_state["last_seen"]:
2012
+ _LAST_SEEN_TS = str(_disk_state["last_seen"])
2013
+ print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from local disk", file=sys.stderr)
2014
+ # Pre-populate dedup cache so we don't re-deliver messages already shown
2015
+ # to the LLM in the previous session.
2016
+ _persisted_keys = _disk_state.get("seen_keys", [])
2017
+ if isinstance(_persisted_keys, list) and _persisted_keys:
2018
+ with _SEEN_LOCK:
2019
+ _now_mono = _time.monotonic()
2020
+ for _k in _persisted_keys:
2021
+ if isinstance(_k, str) and _k not in _SEEN_MSG_IDS:
2022
+ _SEEN_MSG_IDS[_k] = _now_mono
2023
+ _SEEN_MSG_ORDER.append(_k)
2024
+ print(f"[meshcode] Restored {len(_persisted_keys)} dedup keys from local disk", file=sys.stderr)
2025
+
2026
+ # Mesh-memory hydration — only if disk had nothing.
2027
+ if _LAST_SEEN_TS is None:
2028
+ try:
2029
+ _ls_result = be.sb_rpc("mc_memory_get", {
2030
+ "p_api_key": _get_api_key(),
2031
+ "p_agent_name": AGENT_NAME,
2032
+ "p_key": "last_seen",
2033
+ })
2034
+ if isinstance(_ls_result, dict) and _ls_result.get("ok"):
2035
+ _ls_val = _ls_result.get("value")
2036
+ if isinstance(_ls_val, dict) and _ls_val.get("_raw"):
2037
+ _LAST_SEEN_TS = str(_ls_val["_raw"])
2038
+ elif isinstance(_ls_val, str):
2039
+ _LAST_SEEN_TS = _ls_val
2040
+ if _LAST_SEEN_TS:
2041
+ print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from mesh memory.", file=sys.stderr)
2042
+ except Exception as _e:
2043
+ print(f"[meshcode] Could not restore last_seen: {_e}", file=sys.stderr)
1891
2044
 
1892
2045
  # Fallback: if last_seen is still None, default to now minus 5 minutes
1893
2046
  # to avoid flooding agent with ancient messages on cold boot.
@@ -2013,10 +2166,79 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
2013
2166
  try:
2014
2167
  result = await _meshcode_wait_inner(actual_timeout=capped_timeout, include_acks=include_acks)
2015
2168
  except asyncio.CancelledError:
2016
- # Safety net: if CancelledError escapes _meshcode_wait_inner
2017
- # despite the inner shield, catch it here to prevent cascade.
2018
- log.debug("[meshcode] meshcode_wait outer loop caught CancelledError")
2019
- result = {"timed_out": True, "reason": "cancelled_by_client"}
2169
+ # 2.10.54 deaf-agent fix: when ESC fires during meshcode_wait,
2170
+ # Python keeps the task in cancelled state every subsequent
2171
+ # await re-raises CancelledError, even if we caught the first
2172
+ # one. That trapped us in a loop that eventually returned
2173
+ # `{"error": "cancelled_by_client"}` to the LLM, which never
2174
+ # saw the messages that arrived during the cancel window —
2175
+ # they sat in the queue/DB unread (the "deaf agents" bug).
2176
+ #
2177
+ # Fix: uncancel the current task so we can run a final drain
2178
+ # cycle without re-raising. Then aggressively pull from BOTH
2179
+ # the realtime buffer AND the DB and return whatever we find
2180
+ # to the LLM. If nothing is pending, return cancelled-timeout
2181
+ # cleanly so the LLM's wait loop continues normally.
2182
+ log.info("[meshcode] meshcode_wait caught CancelledError — uncancelling + final drain")
2183
+ try:
2184
+ _t = asyncio.current_task()
2185
+ if _t is not None and hasattr(_t, "uncancel"):
2186
+ _t.uncancel()
2187
+ except Exception:
2188
+ pass
2189
+
2190
+ _final_msgs: List[Dict[str, Any]] = []
2191
+ # 1) Drain realtime buffer (messages received during cancel window)
2192
+ if _REALTIME:
2193
+ try:
2194
+ _rt_buf = _REALTIME.drain()
2195
+ if _rt_buf:
2196
+ _final_msgs.extend(_rt_buf)
2197
+ except Exception as _drain_err:
2198
+ log.debug(f"[meshcode] cancel-drain realtime failed: {_drain_err}")
2199
+ # 2) Pull from DB (catches messages realtime missed)
2200
+ try:
2201
+ _api_key = _get_api_key()
2202
+ if _api_key:
2203
+ _db_unread = be.read_inbox(
2204
+ _PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_api_key
2205
+ )
2206
+ if _db_unread:
2207
+ _final_msgs.extend([
2208
+ {
2209
+ "from": m["from_agent"],
2210
+ "type": m.get("type", "msg"),
2211
+ "ts": m.get("created_at"),
2212
+ "payload": m.get("payload", {}),
2213
+ "id": m.get("id"),
2214
+ "parent_id": m.get("parent_msg_id"),
2215
+ }
2216
+ for m in _db_unread
2217
+ ])
2218
+ except Exception as _db_err:
2219
+ log.debug(f"[meshcode] cancel-drain DB failed: {_db_err}")
2220
+
2221
+ if _final_msgs:
2222
+ # Dedupe + filter, then return as got_message so the LLM
2223
+ # processes them instead of seeing a generic cancel error.
2224
+ _filter_and_mark(_final_msgs)
2225
+ _split = _split_messages(_final_msgs)
2226
+ if not include_acks:
2227
+ _split["acks"] = []
2228
+ if _split["messages"] or _split["done_signals"]:
2229
+ log.info(
2230
+ f"[meshcode] cancel-drain rescued {_split['count']} pending "
2231
+ f"messages from going deaf"
2232
+ )
2233
+ result = {
2234
+ "got_message": True,
2235
+ "cancelled_during_wait": True,
2236
+ **_split,
2237
+ }
2238
+ else:
2239
+ result = {"timed_out": True, "reason": "cancelled_by_client"}
2240
+ else:
2241
+ result = {"timed_out": True, "reason": "cancelled_by_client"}
2020
2242
 
2021
2243
  if result.get("got_message"):
2022
2244
  # Real message arrived — return to agent for processing
@@ -2069,6 +2291,7 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
2069
2291
  latest_ts = max((m.get("ts", "") for m in msgs), default="")
2070
2292
  if latest_ts:
2071
2293
  _LAST_SEEN_TS = latest_ts
2294
+ _save_state_async() # 2.10.53: persist to disk for fast respawn
2072
2295
  try:
2073
2296
  be.sb_rpc("mc_memory_set", {
2074
2297
  "p_api_key": _get_api_key(),
@@ -2395,6 +2618,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark
2395
2618
  latest_ts = max((str(m.get("ts", "")) for m in deduped), default=None)
2396
2619
  if latest_ts and (not _LAST_SEEN_TS or latest_ts > _LAST_SEEN_TS):
2397
2620
  _LAST_SEEN_TS = latest_ts
2621
+ _save_state_async() # 2.10.53: persist to disk for fast respawn
2398
2622
 
2399
2623
  split = _split_messages(deduped)
2400
2624
  if not include_acks:
@@ -3735,13 +3959,15 @@ def run_server():
3735
3959
  # `mcp.run()` normally, which breaks the loop.
3736
3960
  import time as _time_mod
3737
3961
  _restart_count = 0
3738
- _clean_return_count = 0
3739
3962
  while True:
3740
3963
  try:
3741
3964
  mcp.run()
3742
3965
  except SystemExit:
3743
3966
  raise
3744
3967
  except BaseException as _e:
3968
+ # Crash path: an exception escaped mcp.run(). Retry the event loop
3969
+ # up to 20 times. This is the path that has saved us from
3970
+ # CancelledError cascades since 2.10.29.
3745
3971
  _restart_count += 1
3746
3972
  try:
3747
3973
  sys.stderr.write(
@@ -3751,8 +3977,6 @@ def run_server():
3751
3977
  sys.stderr.flush()
3752
3978
  except Exception:
3753
3979
  pass
3754
- # If we're restarting more than a few times a second, something
3755
- # is wrong (config error, unrecoverable import). Bail.
3756
3980
  if _restart_count > 20:
3757
3981
  try:
3758
3982
  sys.stderr.write("[meshcode-mcp] too many restarts; exiting\n")
@@ -3762,29 +3986,36 @@ def run_server():
3762
3986
  _time_mod.sleep(0.3)
3763
3987
  continue
3764
3988
 
3765
- # mcp.run() returned cleanly (stdin EOF). This can mean:
3766
- # a) Claude Code intentionally shut us down (process exit)
3767
- # b) Claude Code closed the pipe temporarily due to ESC/cancel
3768
- # Instead of exiting immediately, wait briefly and retry — Claude
3769
- # Code's /mcp reconnect will re-attach stdin within seconds.
3770
- # Only exit after multiple consecutive clean returns with no
3771
- # recovery, which means the parent process truly wants us dead.
3772
- _clean_return_count += 1
3773
- if _clean_return_count > 5:
3774
- try:
3775
- sys.stderr.write("[meshcode-mcp] stdin EOF persisted after 5 retries; exiting\n")
3776
- sys.stderr.flush()
3777
- except Exception:
3778
- pass
3779
- break
3989
+ # ===== Clean return: stdin EOF =====
3990
+ # Claude Code closed the stdio pipe (ESC, /mcp disconnect, or full
3991
+ # exit). The 2.10.52 "retry mcp.run() 5x with 2s sleep" bandaid was
3992
+ # REVERTED in 2.10.53 empirically it does not help:
3993
+ # 1. mcp.run() returns immediately on already-closed stdin, so the
3994
+ # 5 retries burn in microseconds, not 10 seconds.
3995
+ # 2. Claude Code's /mcp reconnect spawns a NEW subprocess, it does
3996
+ # NOT re-attach to the dead-pipe one.
3997
+ # 3. The new subprocess kills us via _kill_stale_mcp_process +
3998
+ # lockfile SIGKILL — so any "wait for reconnect" logic adds
3999
+ # zombie-process time that the new spawn has to clean up.
4000
+ #
4001
+ # The correct fix lives in TWO places:
4002
+ # - Pre-shutdown: persist last_seen + dedup cache to disk so the
4003
+ # next boot resumes seamlessly (handled here, force=True).
4004
+ # - Post-respawn: hydrate from disk before mesh memory (handled
4005
+ # in the _LAST_SEEN_TS section near the top of the module).
4006
+ # Together they collapse the "lost messages on respawn" gap from
4007
+ # 200ms-3s to <5ms.
4008
+ try:
4009
+ _save_state_to_disk(force=True)
4010
+ except Exception:
4011
+ pass
3780
4012
  try:
3781
4013
  sys.stderr.write(
3782
- f"[meshcode-mcp] mcp.run() returned cleanly (stdin EOF, attempt {_clean_return_count}/5); "
3783
- f"waiting 2s for Claude Code reconnect...\n"
4014
+ "[meshcode-mcp] stdin EOF Claude Code closed pipe (ESC, "
4015
+ "/mcp disconnect, or exit). State persisted to disk; next "
4016
+ "MCP spawn will resume seamlessly. Exiting cleanly.\n"
3784
4017
  )
3785
4018
  sys.stderr.flush()
3786
4019
  except Exception:
3787
4020
  pass
3788
- _time_mod.sleep(2.0)
3789
- # Reset exception restart count — this is a clean return, not a crash
3790
- _restart_count = 0
4021
+ break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.52
3
+ Version: 2.10.54
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.52"
7
+ version = "2.10.54"
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