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.
- {meshcode-2.10.52 → meshcode-2.10.54}/PKG-INFO +1 -1
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/__init__.py +1 -1
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/server.py +275 -44
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.52 → meshcode-2.10.54}/pyproject.toml +1 -1
- {meshcode-2.10.52 → meshcode-2.10.54}/README.md +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/cli.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/invites.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/launcher.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/preferences.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/secrets.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/self_update.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/setup.cfg +0 -0
- {meshcode-2.10.52 → meshcode-2.10.54}/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.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
|
-
#
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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
|
-
#
|
|
2017
|
-
#
|
|
2018
|
-
|
|
2019
|
-
|
|
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
|
-
#
|
|
3766
|
-
#
|
|
3767
|
-
#
|
|
3768
|
-
#
|
|
3769
|
-
#
|
|
3770
|
-
#
|
|
3771
|
-
#
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
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
|
-
|
|
3783
|
-
|
|
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
|
-
|
|
3789
|
-
# Reset exception restart count — this is a clean return, not a crash
|
|
3790
|
-
_restart_count = 0
|
|
4021
|
+
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
|