meshcode 2.10.51__tar.gz → 2.10.53__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.51 → meshcode-2.10.53}/PKG-INFO +1 -1
  2. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/__init__.py +1 -1
  3. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/meshcode_mcp/server.py +162 -20
  4. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode.egg-info/PKG-INFO +1 -1
  5. {meshcode-2.10.51 → meshcode-2.10.53}/pyproject.toml +1 -1
  6. {meshcode-2.10.51 → meshcode-2.10.53}/README.md +0 -0
  7. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/ascii_art.py +0 -0
  8. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/cli.py +0 -0
  9. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/comms_v4.py +0 -0
  10. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/invites.py +0 -0
  11. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/launcher.py +0 -0
  12. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/meshcode_mcp/backend.py +0 -0
  16. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/meshcode_mcp/realtime.py +0 -0
  17. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/preferences.py +0 -0
  21. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/secrets.py +0 -0
  24. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/self_update.py +0 -0
  25. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode.egg-info/requires.txt +0 -0
  30. {meshcode-2.10.51 → meshcode-2.10.53}/meshcode.egg-info/top_level.txt +0 -0
  31. {meshcode-2.10.51 → meshcode-2.10.53}/setup.cfg +0 -0
  32. {meshcode-2.10.51 → meshcode-2.10.53}/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.51
3
+ Version: 2.10.53
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.51"
2
+ __version__ = "2.10.53"
@@ -1871,23 +1871,130 @@ _AUTO_SLEEP_THRESHOLD = int(os.environ.get("MESHCODE_AUTO_SLEEP_SECONDS", "600")
1871
1871
  _STAY_AWAKE = False # Set by meshcode_set_status("online") — prevents auto-sleep
1872
1872
  _LAST_SEEN_TS: Optional[str] = None # auto-persisted for message dedup
1873
1873
 
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)
1874
+ # ============================================================
1875
+ # Local disk state — survives MCP respawn instantly.
1876
+ # 2.10.53 fix for the ESC-kills-MCP friction: when Claude Code closes
1877
+ # stdin (ESC, /mcp disconnect), our process exits but Claude Code spawns
1878
+ # a fresh subprocess on next interaction. Without disk persistence, the
1879
+ # new process must round-trip mesh memory RPC to restore last_seen and
1880
+ # starts with an EMPTY dedup cache — 200ms-3s gap during which old
1881
+ # messages can be re-delivered to the LLM.
1882
+ #
1883
+ # With disk persistence, the new process restores state in <5ms and
1884
+ # never re-delivers messages it already showed the agent.
1885
+ # ============================================================
1886
+ def _state_file_path() -> str:
1887
+ """Path to the local state file for this agent."""
1888
+ state_dir = os.path.join(os.path.expanduser("~"), ".meshcode")
1889
+ try:
1890
+ os.makedirs(state_dir, exist_ok=True)
1891
+ except Exception:
1892
+ pass
1893
+ safe = f"state_{PROJECT_NAME}_{AGENT_NAME}.json".replace("/", "_").replace(" ", "_")
1894
+ return os.path.join(state_dir, safe)
1895
+
1896
+
1897
+ def _load_state_from_disk() -> Dict[str, Any]:
1898
+ """Load persisted state. Returns empty dict on any failure."""
1899
+ try:
1900
+ path = _state_file_path()
1901
+ if not os.path.exists(path):
1902
+ return {}
1903
+ with open(path, "r") as f:
1904
+ data = json.load(f)
1905
+ if not isinstance(data, dict):
1906
+ return {}
1907
+ return data
1908
+ except Exception:
1909
+ return {}
1910
+
1911
+
1912
+ _STATE_SAVE_LOCK = _threading.Lock()
1913
+ _STATE_LAST_SAVE_AT = 0.0
1914
+ _STATE_SAVE_DEBOUNCE_S = 1.0
1915
+
1916
+
1917
+ def _save_state_to_disk(force: bool = False) -> None:
1918
+ """Persist current state to disk. Debounced unless force=True.
1919
+
1920
+ Atomic via .tmp + os.replace to prevent half-written files if SIGKILL
1921
+ arrives mid-write.
1922
+ """
1923
+ global _STATE_LAST_SAVE_AT
1924
+ with _STATE_SAVE_LOCK:
1925
+ now = _time.monotonic()
1926
+ if not force and (now - _STATE_LAST_SAVE_AT) < _STATE_SAVE_DEBOUNCE_S:
1927
+ return
1928
+ _STATE_LAST_SAVE_AT = now
1929
+ try:
1930
+ with _SEEN_LOCK:
1931
+ # Persist last 200 dedup keys (most recent) — bounded so disk
1932
+ # write stays fast even on long-running sessions.
1933
+ seen_keys = list(_SEEN_MSG_ORDER)[-200:]
1934
+ data = {
1935
+ "last_seen": _LAST_SEEN_TS,
1936
+ "seen_keys": seen_keys,
1937
+ "agent": AGENT_NAME,
1938
+ "project": PROJECT_NAME,
1939
+ "saved_at": _time.time(),
1940
+ "instance_id": _INSTANCE_ID if "_INSTANCE_ID" in globals() else None,
1941
+ }
1942
+ path = _state_file_path()
1943
+ tmp = path + ".tmp"
1944
+ with open(tmp, "w") as f:
1945
+ json.dump(data, f)
1946
+ os.replace(tmp, path)
1947
+ except Exception:
1948
+ pass # Never let state save failure break tool flow
1949
+
1950
+
1951
+ def _save_state_async() -> None:
1952
+ """Schedule a debounced background save. Safe to call from any thread."""
1953
+ try:
1954
+ _threading.Thread(
1955
+ target=_save_state_to_disk,
1956
+ daemon=True,
1957
+ name="meshcode-state-save",
1958
+ ).start()
1959
+ except Exception:
1960
+ pass
1961
+
1962
+
1963
+ # Hydrate _LAST_SEEN_TS — local disk first (instant), mesh memory fallback.
1964
+ _disk_state = _load_state_from_disk()
1965
+ if isinstance(_disk_state.get("last_seen"), str) and _disk_state["last_seen"]:
1966
+ _LAST_SEEN_TS = str(_disk_state["last_seen"])
1967
+ print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from local disk", file=sys.stderr)
1968
+ # Pre-populate dedup cache so we don't re-deliver messages already shown
1969
+ # to the LLM in the previous session.
1970
+ _persisted_keys = _disk_state.get("seen_keys", [])
1971
+ if isinstance(_persisted_keys, list) and _persisted_keys:
1972
+ with _SEEN_LOCK:
1973
+ _now_mono = _time.monotonic()
1974
+ for _k in _persisted_keys:
1975
+ if isinstance(_k, str) and _k not in _SEEN_MSG_IDS:
1976
+ _SEEN_MSG_IDS[_k] = _now_mono
1977
+ _SEEN_MSG_ORDER.append(_k)
1978
+ print(f"[meshcode] Restored {len(_persisted_keys)} dedup keys from local disk", file=sys.stderr)
1979
+
1980
+ # Mesh-memory hydration — only if disk had nothing.
1981
+ if _LAST_SEEN_TS is None:
1982
+ try:
1983
+ _ls_result = be.sb_rpc("mc_memory_get", {
1984
+ "p_api_key": _get_api_key(),
1985
+ "p_agent_name": AGENT_NAME,
1986
+ "p_key": "last_seen",
1987
+ })
1988
+ if isinstance(_ls_result, dict) and _ls_result.get("ok"):
1989
+ _ls_val = _ls_result.get("value")
1990
+ if isinstance(_ls_val, dict) and _ls_val.get("_raw"):
1991
+ _LAST_SEEN_TS = str(_ls_val["_raw"])
1992
+ elif isinstance(_ls_val, str):
1993
+ _LAST_SEEN_TS = _ls_val
1994
+ if _LAST_SEEN_TS:
1995
+ print(f"[meshcode] Restored last_seen={_LAST_SEEN_TS} from mesh memory.", file=sys.stderr)
1996
+ except Exception as _e:
1997
+ print(f"[meshcode] Could not restore last_seen: {_e}", file=sys.stderr)
1891
1998
 
1892
1999
  # Fallback: if last_seen is still None, default to now minus 5 minutes
1893
2000
  # to avoid flooding agent with ancient messages on cold boot.
@@ -2069,6 +2176,7 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
2069
2176
  latest_ts = max((m.get("ts", "") for m in msgs), default="")
2070
2177
  if latest_ts:
2071
2178
  _LAST_SEEN_TS = latest_ts
2179
+ _save_state_async() # 2.10.53: persist to disk for fast respawn
2072
2180
  try:
2073
2181
  be.sb_rpc("mc_memory_set", {
2074
2182
  "p_api_key": _get_api_key(),
@@ -2395,6 +2503,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark
2395
2503
  latest_ts = max((str(m.get("ts", "")) for m in deduped), default=None)
2396
2504
  if latest_ts and (not _LAST_SEEN_TS or latest_ts > _LAST_SEEN_TS):
2397
2505
  _LAST_SEEN_TS = latest_ts
2506
+ _save_state_async() # 2.10.53: persist to disk for fast respawn
2398
2507
 
2399
2508
  split = _split_messages(deduped)
2400
2509
  if not include_acks:
@@ -3741,6 +3850,9 @@ def run_server():
3741
3850
  except SystemExit:
3742
3851
  raise
3743
3852
  except BaseException as _e:
3853
+ # Crash path: an exception escaped mcp.run(). Retry the event loop
3854
+ # up to 20 times. This is the path that has saved us from
3855
+ # CancelledError cascades since 2.10.29.
3744
3856
  _restart_count += 1
3745
3857
  try:
3746
3858
  sys.stderr.write(
@@ -3750,8 +3862,6 @@ def run_server():
3750
3862
  sys.stderr.flush()
3751
3863
  except Exception:
3752
3864
  pass
3753
- # If we're restarting more than a few times a second, something
3754
- # is wrong (config error, unrecoverable import). Bail.
3755
3865
  if _restart_count > 20:
3756
3866
  try:
3757
3867
  sys.stderr.write("[meshcode-mcp] too many restarts; exiting\n")
@@ -3760,5 +3870,37 @@ def run_server():
3760
3870
  break
3761
3871
  _time_mod.sleep(0.3)
3762
3872
  continue
3763
- # Clean return = stdin EOF = Claude Code closed the pipe = real shutdown
3873
+
3874
+ # ===== Clean return: stdin EOF =====
3875
+ # Claude Code closed the stdio pipe (ESC, /mcp disconnect, or full
3876
+ # exit). The 2.10.52 "retry mcp.run() 5x with 2s sleep" bandaid was
3877
+ # REVERTED in 2.10.53 — empirically it does not help:
3878
+ # 1. mcp.run() returns immediately on already-closed stdin, so the
3879
+ # 5 retries burn in microseconds, not 10 seconds.
3880
+ # 2. Claude Code's /mcp reconnect spawns a NEW subprocess, it does
3881
+ # NOT re-attach to the dead-pipe one.
3882
+ # 3. The new subprocess kills us via _kill_stale_mcp_process +
3883
+ # lockfile SIGKILL — so any "wait for reconnect" logic adds
3884
+ # zombie-process time that the new spawn has to clean up.
3885
+ #
3886
+ # The correct fix lives in TWO places:
3887
+ # - Pre-shutdown: persist last_seen + dedup cache to disk so the
3888
+ # next boot resumes seamlessly (handled here, force=True).
3889
+ # - Post-respawn: hydrate from disk before mesh memory (handled
3890
+ # in the _LAST_SEEN_TS section near the top of the module).
3891
+ # Together they collapse the "lost messages on respawn" gap from
3892
+ # 200ms-3s to <5ms.
3893
+ try:
3894
+ _save_state_to_disk(force=True)
3895
+ except Exception:
3896
+ pass
3897
+ try:
3898
+ sys.stderr.write(
3899
+ "[meshcode-mcp] stdin EOF — Claude Code closed pipe (ESC, "
3900
+ "/mcp disconnect, or exit). State persisted to disk; next "
3901
+ "MCP spawn will resume seamlessly. Exiting cleanly.\n"
3902
+ )
3903
+ sys.stderr.flush()
3904
+ except Exception:
3905
+ pass
3764
3906
  break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.51
3
+ Version: 2.10.53
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.51"
7
+ version = "2.10.53"
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