meshcode 2.11.168__tar.gz → 2.11.171__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.11.168 → meshcode-2.11.171}/PKG-INFO +1 -1
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/__init__.py +1 -1
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/comms_v4.py +1 -31
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/hostd.py +14 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/server.py +73 -10
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/run_agent.py +9 -22
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode.egg-info/SOURCES.txt +0 -1
- {meshcode-2.11.168 → meshcode-2.11.171}/pyproject.toml +1 -1
- {meshcode-2.11.168 → meshcode-2.11.171}/README.md +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/__main__.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/_launch_smoke.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/_update_guard.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/cli.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/compat.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/daemon.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/doctor.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/hooks/push_guard.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/invites.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/launcher.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/preferences.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/secrets.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/self_update.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/terminal_mirror_runner.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/up.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode/upload.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/setup.cfg +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_core.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_doctor.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_ensure_boot_env_urgent_wake.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_fleet_reaper.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_hostd_launch_pinned_env.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_hostd_serve_discovery_split.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_launch_smoke.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_no_appleevents_on_sweep.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_preflight_hb_gate.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_pretrust_claude.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_prompt_dedup_budget.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_push_guard.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_replica_base_workspace_fallback.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_replica_boot_protocol_unconditional.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_rm_guard.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_task_progress.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_terminal_lifecycle.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_up_launch_cmd.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_update_guard.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_urgent_wake_tmux.py +0 -0
- {meshcode-2.11.168 → meshcode-2.11.171}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -3104,7 +3104,7 @@ if __name__ == "__main__":
|
|
|
3104
3104
|
|
|
3105
3105
|
# Auth guard: commands that talk to Supabase need a valid API key.
|
|
3106
3106
|
# doctor, help, version, login, prefs, launcher don't need auth.
|
|
3107
|
-
_NO_AUTH_CMDS = {"doctor", "compat", "upgrade", "self-upgrade", "help", "--help", "-h", "login",
|
|
3107
|
+
_NO_AUTH_CMDS = {"doctor", "smoke", "compat", "upgrade", "self-upgrade", "help", "--help", "-h", "login",
|
|
3108
3108
|
"init", "prefs", "launcher", "--version", "-V", "version",
|
|
3109
3109
|
"whoami", "profiles", "scan", "setup-path"}
|
|
3110
3110
|
if cmd not in _NO_AUTH_CMDS:
|
|
@@ -3603,36 +3603,6 @@ if __name__ == "__main__":
|
|
|
3603
3603
|
autonomous_env = (os.environ.get("MESHCODE_AUTONOMOUS", "").strip().lower()
|
|
3604
3604
|
in ("1", "true", "yes", "on"))
|
|
3605
3605
|
autonomous = bool(flags.get("autonomous")) or autonomous_env
|
|
3606
|
-
# SELF-BUMP-BEFORE-SPAWN (task 36fe573d ext, Samuel 2026-07-02): a human typing
|
|
3607
|
-
# `meshcode run` must land on the LATEST meshcode WITHOUT ever updating by hand —
|
|
3608
|
-
# "meshcode se debe actualizar solo, its the whole point". The CC-autoupdate +
|
|
3609
|
-
# settings.json model-pin-strip fixes live in newer meshcode; a user stuck on an
|
|
3610
|
-
# old CLI (e.g. 2.11.144) would never receive them (chicken-egg). hostd already
|
|
3611
|
-
# runs `meshcode self-upgrade` as a launch pre-step, but a DIRECT human
|
|
3612
|
-
# `meshcode run/go` went straight to spawn and skipped it — this closes that gap.
|
|
3613
|
-
#
|
|
3614
|
-
# Reuses the deferral-safe blocking updater (self_update.check_and_maybe_update_blocking,
|
|
3615
|
-
# force=False): version-check-first + idempotent ("already latest" -> no-op), NEVER
|
|
3616
|
-
# raises (offline/PyPI-fail -> stays on installed), DEFERS when a live agent serve
|
|
3617
|
-
# shares this env (never clobbers a running MCP — the class#2-clobber guard), and
|
|
3618
|
-
# honors the MESHCODE_NO_UPDATE=1 / --no-update opt-out. On a REAL upgrade the OLD
|
|
3619
|
-
# code is already imported in-process, so we RE-EXEC the same argv under the fresh
|
|
3620
|
-
# interpreter/site so THIS spawn runs the new launcher (versioned-env spawn-latest +
|
|
3621
|
-
# CC-autoupdate + pin-strip). The _MESHCODE_SELF_BUMPED sentinel makes the re-exec
|
|
3622
|
-
# one-shot (no loop; the post-exec run is already latest anyway). Skipped on --dry-run.
|
|
3623
|
-
if not dry_run and not os.environ.get("_MESHCODE_SELF_BUMPED"):
|
|
3624
|
-
try:
|
|
3625
|
-
import importlib as _il
|
|
3626
|
-
_su = _il.import_module("meshcode.self_update")
|
|
3627
|
-
_bumped = _su.check_and_maybe_update_blocking(verbose=True, force=False)
|
|
3628
|
-
if _bumped:
|
|
3629
|
-
print(f"[meshcode] re-exec under meshcode {_bumped} (self-bump) ...",
|
|
3630
|
-
file=sys.stderr)
|
|
3631
|
-
os.environ["_MESHCODE_SELF_BUMPED"] = str(_bumped)
|
|
3632
|
-
os.execv(sys.executable,
|
|
3633
|
-
[sys.executable, "-m", "meshcode"] + sys.argv[1:])
|
|
3634
|
-
except Exception as _sbe:
|
|
3635
|
-
print(f"[meshcode] self-bump skipped: {_sbe}", file=sys.stderr)
|
|
3636
3606
|
import importlib
|
|
3637
3607
|
_run = importlib.import_module("meshcode.run_agent").run
|
|
3638
3608
|
sys.exit(_run(agent, project=proj_override, editor_override=editor_override, permission_override=perm_override, dry_run=dry_run, autonomous=autonomous, repo_path=repo_override))
|
|
@@ -864,6 +864,7 @@ _ORPHAN_CLAIMED_LOGGED: set = set()
|
|
|
864
864
|
# C3 (task f9f4a2d3): names rejected by the is_valid_agent_name gate, logged once per
|
|
865
865
|
# name per daemon session so a poisoned DB row raises a visible alert without flooding.
|
|
866
866
|
_INVALID_NAME_LOGGED: set = set()
|
|
867
|
+
_AUTORESPAWN_OFF_LOGGED: set = set() # persistent fleet kill-switch (Samuel 2026-07-01)
|
|
867
868
|
|
|
868
869
|
|
|
869
870
|
def _do_respawns(api_key: str, host_id: str) -> int:
|
|
@@ -936,6 +937,19 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
936
937
|
# do NOT re-record here (that would inflate the count on a mere rate-limit skip).
|
|
937
938
|
_log(f"SKIP respawn {proj}/{agent}: not allowed (count={c.get('respawn_count')}, rate-limited/at-cap)")
|
|
938
939
|
continue
|
|
940
|
+
# PERSISTENT FLEET KILL-SWITCH (Samuel 2026-07-01, terminal-storm middle-ground):
|
|
941
|
+
# if ~/.meshcode/fleet-autorespawn-off exists, suppress ALL auto-respawn. EXPLICIT
|
|
942
|
+
# launches (restart_requested via mc_agent_power / the Launch button) STILL spawn, so
|
|
943
|
+
# commanders boot agents on demand while NOTHING auto-resurrects. Default (no file) =
|
|
944
|
+
# unchanged behavior. The permanent off-switch the 30-min fleet-native-disabled never was.
|
|
945
|
+
if not c.get("restart_requested") and (STATE_DIR / "fleet-autorespawn-off").exists():
|
|
946
|
+
_ao_t = f"{proj}/{agent}"
|
|
947
|
+
if _ao_t not in _AUTORESPAWN_OFF_LOGGED:
|
|
948
|
+
_AUTORESPAWN_OFF_LOGGED.add(_ao_t)
|
|
949
|
+
_log(f"AUTORESPAWN OFF: skip auto-respawn {_ao_t} "
|
|
950
|
+
f"(~/.meshcode/fleet-autorespawn-off present -- persistent kill-switch; "
|
|
951
|
+
f"explicit Launch still honored). Delete that file to re-enable auto-respawn.")
|
|
952
|
+
continue
|
|
939
953
|
# BOOT-AUTOSTART GATE (task b6da0d54; signal fixed in task 8067e04c): don't auto-launch
|
|
940
954
|
# an agent whose desired_state='running' was set BEFORE this hostd started — that's a
|
|
941
955
|
# boot-stale 'running' leftover from a prior session (the "terminals pop at boot" bug),
|
|
@@ -1138,6 +1138,10 @@ def with_working_status(func):
|
|
|
1138
1138
|
_check_hot_reload()
|
|
1139
1139
|
_capture_session() # stash session on first tool call for silent auto-wake
|
|
1140
1140
|
_touch_active_bg() # foreground last_active_at stamp (covers wait too, before skip)
|
|
1141
|
+
if not skip: # meshcode_wait stays ungated (read-only, must always run)
|
|
1142
|
+
_blocked = await _lease_gate_async(name)
|
|
1143
|
+
if _blocked is not None:
|
|
1144
|
+
return _blocked
|
|
1141
1145
|
if not skip:
|
|
1142
1146
|
global _CONSECUTIVE_IDLE_SECONDS
|
|
1143
1147
|
_CONSECUTIVE_IDLE_SECONDS = 0 # any non-wait tool resets idle timer
|
|
@@ -1186,6 +1190,10 @@ def with_working_status(func):
|
|
|
1186
1190
|
_check_hot_reload()
|
|
1187
1191
|
_capture_session() # stash session on first tool call for silent auto-wake
|
|
1188
1192
|
_touch_active_bg() # foreground last_active_at stamp (covers wait too, before skip)
|
|
1193
|
+
if not skip:
|
|
1194
|
+
_blocked = _lease_gate_sync(name)
|
|
1195
|
+
if _blocked is not None:
|
|
1196
|
+
return _blocked
|
|
1189
1197
|
if not skip:
|
|
1190
1198
|
global _CONSECUTIVE_IDLE_SECONDS
|
|
1191
1199
|
_CONSECUTIVE_IDLE_SECONDS = 0 # any non-wait tool resets idle timer
|
|
@@ -1329,6 +1337,40 @@ _INSTANCE_ID = f"mcp-{_uuid.uuid4().hex[:12]}"
|
|
|
1329
1337
|
# false-terminate a legitimately-detached process.
|
|
1330
1338
|
_BOOT_PPID = os.getppid()
|
|
1331
1339
|
|
|
1340
|
+
# ---- Single-instance lease GATE (fix f43cf72f, sec bba8e9d0) ---------------
|
|
1341
|
+
# The lease acquire is deferred off the MCP handshake path (see lifespan +
|
|
1342
|
+
# run_server changes below) so `initialize` answers in <1s instead of blocking
|
|
1343
|
+
# on contended lease RPCs (3x sb_rpc retries x 10s socket timeout). But tools
|
|
1344
|
+
# must NOT dispatch before the lease is held, or a second live instance /
|
|
1345
|
+
# a tombstoned agent could mutate shared state (split-brain / tombstone-bypass).
|
|
1346
|
+
# _LEASE_ACQUIRED is set() ONLY after a genuine acquire; every tool waits on it.
|
|
1347
|
+
_LEASE_ACQUIRED = _threading.Event() # module-level alias (L1024); no plain `import threading` at module scope
|
|
1348
|
+
_LEASE_GATE_TIMEOUT = 30.0 # max seconds a tool blocks waiting for the lease
|
|
1349
|
+
|
|
1350
|
+
def _lease_gate_sync(tool_name: str):
|
|
1351
|
+
"""Block a sync tool until the lease is held. Returns an error dict to
|
|
1352
|
+
short-circuit if the lease never arrives (process is os._exit-ing on
|
|
1353
|
+
conflict, so this tail is rare)."""
|
|
1354
|
+
if _LEASE_ACQUIRED.is_set():
|
|
1355
|
+
return None
|
|
1356
|
+
if _LEASE_ACQUIRED.wait(timeout=_LEASE_GATE_TIMEOUT):
|
|
1357
|
+
return None
|
|
1358
|
+
return {"error": "single-instance lease not yet acquired — retry shortly",
|
|
1359
|
+
"error_code": "lease_pending", "tool": tool_name}
|
|
1360
|
+
|
|
1361
|
+
async def _lease_gate_async(tool_name: str):
|
|
1362
|
+
"""Async twin of _lease_gate_sync — never blocks the event loop."""
|
|
1363
|
+
if _LEASE_ACQUIRED.is_set():
|
|
1364
|
+
return None
|
|
1365
|
+
_loop = asyncio.get_running_loop()
|
|
1366
|
+
_deadline = _loop.time() + _LEASE_GATE_TIMEOUT
|
|
1367
|
+
while not _LEASE_ACQUIRED.is_set():
|
|
1368
|
+
if _loop.time() > _deadline:
|
|
1369
|
+
return {"error": "single-instance lease not yet acquired — retry shortly",
|
|
1370
|
+
"error_code": "lease_pending", "tool": tool_name}
|
|
1371
|
+
await asyncio.sleep(0.05)
|
|
1372
|
+
return None
|
|
1373
|
+
|
|
1332
1374
|
|
|
1333
1375
|
def _stdin_peer_dead() -> bool:
|
|
1334
1376
|
"""Non-destructively check whether stdin's peer has closed.
|
|
@@ -1389,7 +1431,7 @@ def _acquire_lease() -> bool:
|
|
|
1389
1431
|
"p_api_key": api_key,
|
|
1390
1432
|
"p_project_id": _PROJECT_ID,
|
|
1391
1433
|
"p_agent_name": AGENT_NAME,
|
|
1392
|
-
})
|
|
1434
|
+
}, _max_retries=1)
|
|
1393
1435
|
except Exception as e:
|
|
1394
1436
|
# Non-fatal: RPC might not exist on older servers.
|
|
1395
1437
|
_mc_log(f"stale-lease pre-clean skipped: {e}", "warn")
|
|
@@ -1400,7 +1442,7 @@ def _acquire_lease() -> bool:
|
|
|
1400
1442
|
"p_project_id": _PROJECT_ID,
|
|
1401
1443
|
"p_agent_name": AGENT_NAME,
|
|
1402
1444
|
"p_instance_id": _INSTANCE_ID,
|
|
1403
|
-
})
|
|
1445
|
+
}, _max_retries=1)
|
|
1404
1446
|
if isinstance(r, dict) and r.get("ok"):
|
|
1405
1447
|
global _CONSECUTIVE_IDLE_SECONDS
|
|
1406
1448
|
_CONSECUTIVE_IDLE_SECONDS = 0 # P6: reset idle counter on lease success
|
|
@@ -1453,7 +1495,7 @@ def _acquire_lease() -> bool:
|
|
|
1453
1495
|
"p_project_id": _PROJECT_ID,
|
|
1454
1496
|
"p_agent_name": AGENT_NAME,
|
|
1455
1497
|
"p_instance_id": _INSTANCE_ID,
|
|
1456
|
-
})
|
|
1498
|
+
}, _max_retries=1)
|
|
1457
1499
|
if isinstance(r2, dict) and r2.get("ok"):
|
|
1458
1500
|
_mc_log("Lease acquired after force-release.")
|
|
1459
1501
|
return True
|
|
@@ -2730,6 +2772,31 @@ async def lifespan(_app):
|
|
|
2730
2772
|
|
|
2731
2773
|
asyncio.create_task(_bg_realtime_start())
|
|
2732
2774
|
|
|
2775
|
+
# Single-instance lease — acquired OFF the handshake path (fix f43cf72f).
|
|
2776
|
+
# Mirrors the c0e7de87 deferral of Realtime/heartbeat: run the blocking
|
|
2777
|
+
# _acquire_lease() in a worker thread so the lifespan yields immediately and
|
|
2778
|
+
# `initialize` is answered fast; gate all tool dispatch on _LEASE_ACQUIRED
|
|
2779
|
+
# until it lands. Hard-exit (os._exit) on tombstone/kick or a genuine
|
|
2780
|
+
# single-instance conflict — before any gated tool can unblock.
|
|
2781
|
+
async def _bg_acquire_lease():
|
|
2782
|
+
try:
|
|
2783
|
+
ok = await asyncio.to_thread(_acquire_lease)
|
|
2784
|
+
except SystemExit as _se: # "kicked" tombstone path
|
|
2785
|
+
os._exit(_se.code if isinstance(_se.code, int) else 0)
|
|
2786
|
+
return
|
|
2787
|
+
except Exception as _le: # never fatal — degrade to no-lease, stay gated
|
|
2788
|
+
log.warning(f"[meshcode] bg lease acquire error (non-fatal): {_le}")
|
|
2789
|
+
return
|
|
2790
|
+
if not ok: # another live instance holds it
|
|
2791
|
+
try:
|
|
2792
|
+
sys.stderr.write("[meshcode-mcp] lease held by another live instance — exiting\n")
|
|
2793
|
+
sys.stderr.flush()
|
|
2794
|
+
except Exception:
|
|
2795
|
+
pass
|
|
2796
|
+
os._exit(2)
|
|
2797
|
+
_LEASE_ACQUIRED.set() # release the dispatch gate — ONLY on real acquire
|
|
2798
|
+
asyncio.create_task(_bg_acquire_lease())
|
|
2799
|
+
|
|
2733
2800
|
def _initial_heartbeat_bg():
|
|
2734
2801
|
"""Send first heartbeat + flip status to 'idle' in a daemon thread.
|
|
2735
2802
|
|
|
@@ -8262,13 +8329,9 @@ def run_server():
|
|
|
8262
8329
|
# only the lifespan re-runs. `SystemExit` is let through so `sys.exit`
|
|
8263
8330
|
# from config-validation paths still works. stdin EOF returns from
|
|
8264
8331
|
# `mcp.run()` normally, which breaks the loop.
|
|
8265
|
-
# Lease acquire
|
|
8266
|
-
#
|
|
8267
|
-
#
|
|
8268
|
-
# _kill_stale_mcp_process() is already called in the lifespan; only the
|
|
8269
|
-
# lease acquire needs an explicit call here before mcp.run().
|
|
8270
|
-
if not _acquire_lease():
|
|
8271
|
-
sys.exit(2)
|
|
8332
|
+
# Lease acquire is DEFERRED into the lifespan (_bg_acquire_lease, fix
|
|
8333
|
+
# f43cf72f) so it never blocks `initialize`. Tool dispatch is gated on
|
|
8334
|
+
# _LEASE_ACQUIRED; conflict/kick hard-exits from that background task.
|
|
8272
8335
|
|
|
8273
8336
|
import time as _time_mod
|
|
8274
8337
|
_restart_count = 0
|
|
@@ -1718,28 +1718,7 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1718
1718
|
_settings_model = None
|
|
1719
1719
|
_sp = ws / ".claude" / "settings.json"
|
|
1720
1720
|
if _sp.exists():
|
|
1721
|
-
|
|
1722
|
-
_settings_model = _sj.get("model")
|
|
1723
|
-
# DASHBOARD-MODEL-WINS (task 36fe573d, Samuel 2026-07-02): older meshcode
|
|
1724
|
-
# versions auto-pinned "model": PLATFORM_DEFAULT_MODEL into every workspace
|
|
1725
|
-
# settings.json. That legacy pin short-circuits the mc_agent_model_pref RPC
|
|
1726
|
-
# below, so the dashboard model dropdown (Fable 5, etc.) NEVER takes effect.
|
|
1727
|
-
# Current _meshcode_settings_dict() no longer writes it, but existing
|
|
1728
|
-
# workspaces keep the stale key forever (_heal_settings_hooks only touches
|
|
1729
|
-
# hooks). Strip it here ONLY when it equals the platform default (i.e. the
|
|
1730
|
-
# auto-written value) so a human-chosen NON-default model is preserved.
|
|
1731
|
-
# Net effect: dashboard pref becomes authoritative; if there is no pref,
|
|
1732
|
-
# PLATFORM_DEFAULT_MODEL is re-applied a few lines below -> same model, no
|
|
1733
|
-
# regression. Idempotent: after the first launch the key is gone.
|
|
1734
|
-
if _settings_model == PLATFORM_DEFAULT_MODEL:
|
|
1735
|
-
_sj.pop("model", None)
|
|
1736
|
-
try:
|
|
1737
|
-
_sp.write_text(json.dumps(_sj, indent=2) + "\n", encoding="utf-8")
|
|
1738
|
-
except Exception:
|
|
1739
|
-
pass
|
|
1740
|
-
_settings_model = None
|
|
1741
|
-
print("[meshcode] Stripped legacy settings.json model pin "
|
|
1742
|
-
"-> dashboard model_pref is now authoritative", file=sys.stderr)
|
|
1721
|
+
_settings_model = (json.loads(_sp.read_text(encoding="utf-8")) or {}).get("model")
|
|
1743
1722
|
if not _settings_model:
|
|
1744
1723
|
_mcp_cfg = json.loads(mcp_json_path.read_text(encoding="utf-8"))
|
|
1745
1724
|
_env = (next(iter((_mcp_cfg.get("mcpServers") or {}).values()), {}) or {}).get("env", {}) or {}
|
|
@@ -1909,6 +1888,14 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1909
1888
|
# AND the repo-lock dir (whichever claude opens in).
|
|
1910
1889
|
_pretrust_claude_workspace(str(ws), repo_lock)
|
|
1911
1890
|
|
|
1891
|
+
# MCP handshake budget (mesh-dev piece-1, 2026-07-03): Claude Code's default
|
|
1892
|
+
# MCP_TIMEOUT is 5s, but the meshcode MCP server can legitimately take longer
|
|
1893
|
+
# on first connect (lease acquire vs a loaded Supabase pool = today's
|
|
1894
|
+
# fleet-wide "agent won't connect"). Give every launched agent a 240s window
|
|
1895
|
+
# automatically; setdefault so an explicit user/env override still wins.
|
|
1896
|
+
# Root fix (lease deferral off the handshake path) ships separately.
|
|
1897
|
+
os.environ.setdefault("MCP_TIMEOUT", "240000")
|
|
1898
|
+
|
|
1912
1899
|
# Flush all output before exec replaces this process — execvp does NOT
|
|
1913
1900
|
# flush Python file buffers, so any buffered stdout (e.g. the banner)
|
|
1914
1901
|
# would be silently lost.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|