meshcode 2.11.108__tar.gz → 2.11.109__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.108 → meshcode-2.11.109}/PKG-INFO +1 -1
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/__init__.py +1 -1
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/hostd.py +85 -2
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/server.py +11 -18
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/run_agent.py +16 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/self_update.py +50 -7
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.108 → meshcode-2.11.109}/pyproject.toml +1 -1
- {meshcode-2.11.108 → meshcode-2.11.109}/README.md +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/__main__.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/_session_handoff_template 2.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/_session_handoff_template 3.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/claude_update 2.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/claude_update 3.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/cli.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/compat.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/daemon.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/doctor.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/hostd 2.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/invites.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/launcher.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/preferences.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/secrets.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/up 2.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/up.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/upload.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/setup.cfg +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject 2.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject 3.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_core.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_doctor.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -561,11 +561,17 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
561
561
|
except Exception:
|
|
562
562
|
pass
|
|
563
563
|
continue
|
|
564
|
+
# Part 2 (Samuel req #2): for a VISIBLE recycle, snapshot the OLD window
|
|
565
|
+
# pid(s) BEFORE spawning the fresh one — so the new pid is never in the
|
|
566
|
+
# close set (commander q1: never touch the fresh terminal).
|
|
567
|
+
_old_vis_pids = _discover_agent_pids(_target) if (_is_recycle and _visible) else []
|
|
564
568
|
_log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
|
|
565
569
|
f"({'VISIBLE ' if _visible else ''}stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
|
|
566
570
|
if _spawn_agent(proj, agent, headless=_hl):
|
|
567
571
|
_record_spawn(_target) # count the terminal we just opened, against the breaker
|
|
568
572
|
if _is_recycle:
|
|
573
|
+
if _visible and _old_vis_pids:
|
|
574
|
+
_close_old_visible_recycle(_target, _old_vis_pids) # close old window (DRY-RUN first)
|
|
569
575
|
_rpc("mc_record_recycle",
|
|
570
576
|
{"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
|
|
571
577
|
n += 1
|
|
@@ -667,6 +673,69 @@ def _recycle_blocked(st, target, now=None):
|
|
|
667
673
|
return None
|
|
668
674
|
|
|
669
675
|
|
|
676
|
+
# Samuel rule 2026-06-04: never recycle a CONNECTED agent (live MCP session) except
|
|
677
|
+
# the >3h uptime lifecycle. BUSY_STATUSES (working/online/busy) MISSES a connected-
|
|
678
|
+
# but-idle agent (status idle/standby, window open, heartbeat fresh) — a fresh
|
|
679
|
+
# heartbeat is the stronger 'live session' signal, so an idle-but-connected agent
|
|
680
|
+
# was being version-recycled out from under the user (the storm he kept seeing).
|
|
681
|
+
_CONNECTED_HEARTBEAT_S = _env_int("MESHCODE_CONNECTED_HEARTBEAT_SEC", 60, 10)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _agent_connected(a) -> bool:
|
|
685
|
+
"""True if the agent has a live MCP session: an explicitly-busy status OR a
|
|
686
|
+
very-recent heartbeat (window open even when idle/standby)."""
|
|
687
|
+
if (a.get("status") or "") in BUSY_STATUSES:
|
|
688
|
+
return True
|
|
689
|
+
hb = a.get("heartbeat_age_s")
|
|
690
|
+
try:
|
|
691
|
+
return hb is not None and float(hb) < _CONNECTED_HEARTBEAT_S
|
|
692
|
+
except (TypeError, ValueError):
|
|
693
|
+
return False
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# Part 2 (Samuel req #2 2026-06-04): on a VISIBLE recycle, close the OLD window so
|
|
697
|
+
# old+new don't both stay open (audit gap 6a203baa). DRY-RUN first (commander q2 +
|
|
698
|
+
# reaper safe-arm pattern): log-only until the logs confirm it's ONLY the old pid.
|
|
699
|
+
_CLOSE_OLD_VISIBLE_DRYRUN = True
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _pid_alive(pid) -> bool:
|
|
703
|
+
if not pid:
|
|
704
|
+
return False
|
|
705
|
+
try:
|
|
706
|
+
os.kill(int(pid), 0)
|
|
707
|
+
return True
|
|
708
|
+
except ProcessLookupError:
|
|
709
|
+
return False
|
|
710
|
+
except PermissionError:
|
|
711
|
+
return True # exists, owned by another uid — treat as alive (don't guess)
|
|
712
|
+
except Exception:
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _close_old_visible_recycle(target: str, old_pids) -> int:
|
|
717
|
+
"""Close the OLD window's still-alive process on a VISIBLE recycle. `old_pids`
|
|
718
|
+
is the PRE-SPAWN snapshot, so the freshly-opened window's pid is excluded by
|
|
719
|
+
construction — we NEVER touch the fresh terminal (commander q1). Graceful
|
|
720
|
+
self-exit (the stop-hook ends the session on must_exit=recycle) is PRIMARY
|
|
721
|
+
(q3): an already-exited old pid is skipped. DRY-RUN first (q2): log the
|
|
722
|
+
would-close pid; flip _CLOSE_OLD_VISIBLE_DRYRUN=False to arm once logs show
|
|
723
|
+
it's only the old pid. Real kill reuses _kill_headless_pid's cmdline guard."""
|
|
724
|
+
n = 0
|
|
725
|
+
for pid in old_pids:
|
|
726
|
+
if not _pid_alive(pid):
|
|
727
|
+
continue # already self-closed gracefully (q3 primary) — nothing to do
|
|
728
|
+
if _CLOSE_OLD_VISIBLE_DRYRUN:
|
|
729
|
+
_log(f"CLOSE-OLD-VISIBLE-DRYRUN {target}: WOULD close old window pid {pid} "
|
|
730
|
+
f"(visible recycle; fresh window already spawned + excluded) — log-only. "
|
|
731
|
+
f"Flip _CLOSE_OLD_VISIBLE_DRYRUN=False after confirming it's ONLY the old pid.")
|
|
732
|
+
continue
|
|
733
|
+
if _kill_headless_pid(target, pid):
|
|
734
|
+
_log(f"CLOSE-OLD-VISIBLE {target}: closed old window pid {pid} (visible recycle; kept fresh window)")
|
|
735
|
+
n += 1
|
|
736
|
+
return n
|
|
737
|
+
|
|
738
|
+
|
|
670
739
|
def _spawn_rate_ok(target: str):
|
|
671
740
|
"""Anti-spam circuit breaker. Returns (ok, tripped_burst, reason).
|
|
672
741
|
|
|
@@ -1054,6 +1123,9 @@ def _do_recycle_enforce(api_key: str, host_id: str) -> int:
|
|
|
1054
1123
|
agent (headless_pids, with _kill_headless_pid's reuse-guard) — NEVER blind cmdline. After the
|
|
1055
1124
|
kill it goes stale and the recycle FAST-PATH in _do_respawns relaunches it within seconds ->
|
|
1056
1125
|
SessionStart restores the handoff. Returns number force-killed."""
|
|
1126
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): RECYCLE disabled in prod — hard no-op
|
|
1127
|
+
# (no recycles are triggered, so there is nothing to enforce). Crash-RESPAWN unaffected.
|
|
1128
|
+
return 0
|
|
1057
1129
|
res = _rpc("mc_recycle_enforce_candidates", {"p_api_key": api_key, "p_host_id": host_id})
|
|
1058
1130
|
if not res or not res.get("ok"):
|
|
1059
1131
|
return 0
|
|
@@ -1099,6 +1171,11 @@ def _do_recycle_enforce(api_key: str, host_id: str) -> int:
|
|
|
1099
1171
|
|
|
1100
1172
|
def _do_recycles(api_key: str, host_id: str) -> int:
|
|
1101
1173
|
"""Uptime-based recycle at task boundary. Returns number recycled."""
|
|
1174
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): the RECYCLE feature is disabled in
|
|
1175
|
+
# prod (unreliable — kept causing version/env-mismatch storms). Hard no-op in source so
|
|
1176
|
+
# it stays dead even if a schedule row reappears. Crash-RESPAWN (_do_respawns) is
|
|
1177
|
+
# UNAFFECTED — only RECYCLE triggers are killed.
|
|
1178
|
+
return 0
|
|
1102
1179
|
cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
|
|
1103
1180
|
if not cfg or not cfg.get("ok"):
|
|
1104
1181
|
return 0
|
|
@@ -1489,6 +1566,10 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
|
|
|
1489
1566
|
mid-task), rate-limited (<=1 version-recycle per agent / 30min), recycle-not-kill (clean handoff via
|
|
1490
1567
|
mc_request_recycle -> agent exits at its boundary -> _do_respawns relaunches on the new version).
|
|
1491
1568
|
Recorded via mc_record_recycle (NEVER counts against the mig406 crash respawn cap). Owner-scoped."""
|
|
1569
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): RECYCLE disabled in prod — hard no-op.
|
|
1570
|
+
# This was the env-mismatch storm source; fixed-at-source via run_agent env-sync, but the
|
|
1571
|
+
# whole recycle feature is being removed per owner. Crash-RESPAWN is unaffected.
|
|
1572
|
+
return 0
|
|
1492
1573
|
cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
|
|
1493
1574
|
if not cfg or not cfg.get("ok"):
|
|
1494
1575
|
return 0
|
|
@@ -1513,8 +1594,10 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
|
|
|
1513
1594
|
continue # agent already on the on-disk version (or newer) — nothing to do
|
|
1514
1595
|
except Exception:
|
|
1515
1596
|
continue
|
|
1516
|
-
if (a
|
|
1517
|
-
continue #
|
|
1597
|
+
if _agent_connected(a):
|
|
1598
|
+
continue # Samuel rule: never version-recycle a CONNECTED agent (live MCP session,
|
|
1599
|
+
# even if idle/standby) — the >3h uptime lifecycle (_do_recycles) is the
|
|
1600
|
+
# only recycle that may touch a connected agent.
|
|
1518
1601
|
proj, agent = a.get("project_name"), a.get("name")
|
|
1519
1602
|
if not proj or not agent:
|
|
1520
1603
|
continue
|
|
@@ -6164,12 +6164,10 @@ def meshcode_recycle_agent(name: str, visible: bool = False) -> Dict[str, Any]:
|
|
|
6164
6164
|
visible: True = respawn as a visible focused window; default False =
|
|
6165
6165
|
preserve the agent's current headless/visible state.
|
|
6166
6166
|
"""
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
"p_visible": bool(visible),
|
|
6172
|
-
})
|
|
6167
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): recycle is disabled in prod
|
|
6168
|
+
# (unreliable). No-op — do not call the RPC.
|
|
6169
|
+
return {"ok": False, "error_code": "recycle_disabled",
|
|
6170
|
+
"error": "recycle is disabled (feature removed — was unreliable). No action taken."}
|
|
6173
6171
|
|
|
6174
6172
|
|
|
6175
6173
|
@mcp.tool()
|
|
@@ -6187,12 +6185,9 @@ def meshcode_recycle_fleet(visible: bool = False) -> Dict[str, Any]:
|
|
|
6187
6185
|
visible: True = respawn each as a visible focused window; default False
|
|
6188
6186
|
= preserve each agent's current headless/visible state.
|
|
6189
6187
|
"""
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
"p_agent": None, # NULL = whole fleet (all running agents)
|
|
6194
|
-
"p_visible": bool(visible),
|
|
6195
|
-
})
|
|
6188
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): recycle is disabled in prod. No-op.
|
|
6189
|
+
return {"ok": False, "error_code": "recycle_disabled",
|
|
6190
|
+
"error": "recycle is disabled (feature removed — was unreliable). No action taken."}
|
|
6196
6191
|
|
|
6197
6192
|
|
|
6198
6193
|
@mcp.tool()
|
|
@@ -6219,12 +6214,10 @@ def meshcode_set_recycle_schedule(interval_hours: int = 6, enabled: bool = True,
|
|
|
6219
6214
|
"error_code": "not_yet_supported",
|
|
6220
6215
|
"error": "per-agent recycle schedule is a fast-follow; pass agent=None for the mesh-global schedule.",
|
|
6221
6216
|
}
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
"p_interval_hours": int(interval_hours),
|
|
6227
|
-
})
|
|
6217
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): auto-recycle scheduling is disabled
|
|
6218
|
+
# in prod. No-op — never (re)enable a schedule.
|
|
6219
|
+
return {"ok": False, "error_code": "recycle_disabled",
|
|
6220
|
+
"error": "auto-recycle scheduling is disabled (feature removed — was unreliable). No action taken."}
|
|
6228
6221
|
|
|
6229
6222
|
|
|
6230
6223
|
@mcp.tool()
|
|
@@ -820,6 +820,22 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
820
820
|
print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` to fix.", file=sys.stderr)
|
|
821
821
|
return 2
|
|
822
822
|
|
|
823
|
+
# Env-mismatch storm fix (2.11.109): the non-blocking auto-pip above updated the
|
|
824
|
+
# LAUNCHER env (sys.executable). But THIS agent's MCP server runs from this
|
|
825
|
+
# workspace's .mcp.json `command` python — that's the env that reports
|
|
826
|
+
# cli_version. Sync IT to the launcher's installed version so hostd
|
|
827
|
+
# version-recycle CONVERGES instead of looping forever. Non-blocking, best-effort.
|
|
828
|
+
if not dry_run:
|
|
829
|
+
try:
|
|
830
|
+
_doc = json.loads(mcp_json_path.read_text(encoding="utf-8"))
|
|
831
|
+
for _srv in (_doc.get("mcpServers") or {}).values():
|
|
832
|
+
_cmd = _srv.get("command")
|
|
833
|
+
if _cmd:
|
|
834
|
+
self_update.sync_agent_env(_cmd)
|
|
835
|
+
break
|
|
836
|
+
except Exception:
|
|
837
|
+
pass
|
|
838
|
+
|
|
823
839
|
# ── Validate stop hook exists (required for wait-loop integrity) ─
|
|
824
840
|
# If the workspace was created on an old CLI that didn't install hooks
|
|
825
841
|
# (or someone wiped .claude/), Claude Code has nothing blocking turn-end
|
|
@@ -275,6 +275,7 @@ state_dir.mkdir(parents=True, exist_ok=True)
|
|
|
275
275
|
mode = sys.argv[1] if len(sys.argv) > 1 else "pip"
|
|
276
276
|
target_version = sys.argv[2] if len(sys.argv) > 2 else None
|
|
277
277
|
site_flag = sys.argv[3] if len(sys.argv) > 3 else "system"
|
|
278
|
+
target_python = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else None # agent MCP-server env
|
|
278
279
|
|
|
279
280
|
try:
|
|
280
281
|
if mode == "pipx":
|
|
@@ -282,11 +283,12 @@ try:
|
|
|
282
283
|
else:
|
|
283
284
|
# --no-cache-dir (task 14782bb4 / urgent): never let pip serve a STALE cached wheel — always
|
|
284
285
|
# fetch the true latest from PyPI (auto-update was grabbing an old cached version otherwise).
|
|
285
|
-
|
|
286
|
+
exe = target_python or sys.executable
|
|
287
|
+
cmd = [exe, "-m", "pip", "install", "-U", "--no-cache-dir",
|
|
286
288
|
"--disable-pip-version-check", "--quiet"]
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
if site_flag == "user":
|
|
289
|
+
# --user only for the LAUNCHER's own user-site install; an explicit
|
|
290
|
+
# target_python (the agent's MCP env / venv) installs into ITS env.
|
|
291
|
+
if site_flag == "user" and not target_python:
|
|
290
292
|
cmd.append("--user")
|
|
291
293
|
cmd.append("meshcode")
|
|
292
294
|
with open(log_path, "ab") as logf:
|
|
@@ -315,12 +317,13 @@ finally:
|
|
|
315
317
|
'''
|
|
316
318
|
|
|
317
319
|
|
|
318
|
-
def _spawn_background_updater(target_version: str) -> bool:
|
|
320
|
+
def _spawn_background_updater(target_version: str, target_python: Optional[str] = None) -> bool:
|
|
319
321
|
"""Spawn a fully detached subprocess that runs the updater.
|
|
320
322
|
|
|
321
323
|
The parent (current `meshcode run`) returns immediately. The child
|
|
322
324
|
runs pip install in the background, writes the result to disk, and
|
|
323
|
-
exits. Next `meshcode run` consumes the result.
|
|
325
|
+
exits. Next `meshcode run` consumes the result. `target_python` (when set)
|
|
326
|
+
is the agent's MCP-server env — pip installs into IT, not the launcher.
|
|
324
327
|
"""
|
|
325
328
|
if not _acquire_lock():
|
|
326
329
|
return False
|
|
@@ -331,7 +334,7 @@ def _spawn_background_updater(target_version: str) -> bool:
|
|
|
331
334
|
# We pass the runner code via stdin so we don't need to ship a
|
|
332
335
|
# second .py file. The child reads it from sys.stdin and execs it.
|
|
333
336
|
runner = f"import sys; exec(sys.stdin.read())"
|
|
334
|
-
args = [sys.executable, "-c", runner, mode, target_version, site_flag]
|
|
337
|
+
args = [sys.executable, "-c", runner, mode, target_version, site_flag, target_python or ""]
|
|
335
338
|
|
|
336
339
|
try:
|
|
337
340
|
if sys.platform == "win32":
|
|
@@ -418,6 +421,46 @@ def check_and_maybe_update(verbose: bool = False) -> None:
|
|
|
418
421
|
print(f"[meshcode] downloading {latest} in background...", file=sys.stderr)
|
|
419
422
|
|
|
420
423
|
|
|
424
|
+
def _env_version(python_exe: str) -> Optional[str]:
|
|
425
|
+
"""meshcode.__version__ as seen by ANOTHER python env (the agent's MCP server)."""
|
|
426
|
+
try:
|
|
427
|
+
out = subprocess.run(
|
|
428
|
+
[python_exe, "-c", "import meshcode,sys; sys.stdout.write(meshcode.__version__)"],
|
|
429
|
+
capture_output=True, text=True, timeout=10).stdout.strip()
|
|
430
|
+
return out or None
|
|
431
|
+
except Exception:
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def sync_agent_env(mcp_python: str, verbose: bool = False) -> None:
|
|
436
|
+
"""Bring the agent's MCP-SERVER env (mcp_python, from the workspace .mcp.json
|
|
437
|
+
`command`) up to the LAUNCHER's installed meshcode version.
|
|
438
|
+
|
|
439
|
+
The MCP server is the env that reports cli_version. If it lags hostd's on-disk
|
|
440
|
+
version, hostd version-recycles the agent FOREVER (the env-mismatch storm),
|
|
441
|
+
because run_agent's normal auto-pip only updates the LAUNCHER env
|
|
442
|
+
(sys.executable). This syncs the env that actually matters. Non-blocking
|
|
443
|
+
(background pip). No-op if same env / already current / opted out / unreadable.
|
|
444
|
+
"""
|
|
445
|
+
try:
|
|
446
|
+
if not mcp_python:
|
|
447
|
+
return
|
|
448
|
+
if os.path.realpath(mcp_python) == os.path.realpath(sys.executable):
|
|
449
|
+
return # same env — the normal launcher update already covers it
|
|
450
|
+
if update_disabled():
|
|
451
|
+
return
|
|
452
|
+
launcher_ver = _current_version()
|
|
453
|
+
env_ver = _env_version(mcp_python)
|
|
454
|
+
if not launcher_ver or not env_ver:
|
|
455
|
+
return
|
|
456
|
+
if _is_newer(launcher_ver, env_ver):
|
|
457
|
+
if _spawn_background_updater(launcher_ver, mcp_python) and verbose:
|
|
458
|
+
print(f"[meshcode] syncing agent env {env_ver} -> {launcher_ver} in background...",
|
|
459
|
+
file=sys.stderr)
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
|
|
421
464
|
# ============================================================
|
|
422
465
|
# Blocking variant — used by `meshcode run` to guarantee the editor
|
|
423
466
|
# subprocess inherits the latest meshcode_mcp/server.py on disk.
|
|
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
|