meshcode 2.10.52__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.
- {meshcode-2.10.52 → meshcode-2.10.53}/PKG-INFO +1 -1
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/__init__.py +1 -1
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/meshcode_mcp/server.py +156 -40
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.52 → meshcode-2.10.53}/pyproject.toml +1 -1
- {meshcode-2.10.52 → meshcode-2.10.53}/README.md +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/cli.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/invites.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/launcher.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/preferences.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/secrets.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/self_update.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/setup.cfg +0 -0
- {meshcode-2.10.52 → meshcode-2.10.53}/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.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
|
-
#
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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:
|
|
@@ -3735,13 +3844,15 @@ def run_server():
|
|
|
3735
3844
|
# `mcp.run()` normally, which breaks the loop.
|
|
3736
3845
|
import time as _time_mod
|
|
3737
3846
|
_restart_count = 0
|
|
3738
|
-
_clean_return_count = 0
|
|
3739
3847
|
while True:
|
|
3740
3848
|
try:
|
|
3741
3849
|
mcp.run()
|
|
3742
3850
|
except SystemExit:
|
|
3743
3851
|
raise
|
|
3744
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.
|
|
3745
3856
|
_restart_count += 1
|
|
3746
3857
|
try:
|
|
3747
3858
|
sys.stderr.write(
|
|
@@ -3751,8 +3862,6 @@ def run_server():
|
|
|
3751
3862
|
sys.stderr.flush()
|
|
3752
3863
|
except Exception:
|
|
3753
3864
|
pass
|
|
3754
|
-
# If we're restarting more than a few times a second, something
|
|
3755
|
-
# is wrong (config error, unrecoverable import). Bail.
|
|
3756
3865
|
if _restart_count > 20:
|
|
3757
3866
|
try:
|
|
3758
3867
|
sys.stderr.write("[meshcode-mcp] too many restarts; exiting\n")
|
|
@@ -3762,29 +3871,36 @@ def run_server():
|
|
|
3762
3871
|
_time_mod.sleep(0.3)
|
|
3763
3872
|
continue
|
|
3764
3873
|
|
|
3765
|
-
#
|
|
3766
|
-
#
|
|
3767
|
-
#
|
|
3768
|
-
#
|
|
3769
|
-
#
|
|
3770
|
-
#
|
|
3771
|
-
#
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
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
|
|
3780
3897
|
try:
|
|
3781
3898
|
sys.stderr.write(
|
|
3782
|
-
|
|
3783
|
-
|
|
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"
|
|
3784
3902
|
)
|
|
3785
3903
|
sys.stderr.flush()
|
|
3786
3904
|
except Exception:
|
|
3787
3905
|
pass
|
|
3788
|
-
|
|
3789
|
-
# Reset exception restart count — this is a clean return, not a crash
|
|
3790
|
-
_restart_count = 0
|
|
3906
|
+
break
|
|
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
|
|
File without changes
|
|
File without changes
|