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.
Files changed (99) hide show
  1. {meshcode-2.11.108 → meshcode-2.11.109}/PKG-INFO +1 -1
  2. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/hostd.py +85 -2
  4. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/server.py +11 -18
  5. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/run_agent.py +16 -0
  6. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/self_update.py +50 -7
  7. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.11.108 → meshcode-2.11.109}/pyproject.toml +1 -1
  9. {meshcode-2.11.108 → meshcode-2.11.109}/README.md +0 -0
  10. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/__main__.py +0 -0
  11. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/_session_handoff_template 2.py +0 -0
  12. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/_session_handoff_template 3.py +0 -0
  13. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/_session_handoff_template.py +0 -0
  14. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/_stop_hook_template.py +0 -0
  15. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/ascii_art.py +0 -0
  16. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/atomic_push.py +0 -0
  17. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/claude_update 2.py +0 -0
  18. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/claude_update 3.py +0 -0
  19. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/claude_update.py +0 -0
  20. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/cli.py +0 -0
  21. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/comms_v4.py +0 -0
  22. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/compat.py +0 -0
  23. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/daemon.py +0 -0
  24. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/date_parse.py +0 -0
  25. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/doctor.py +0 -0
  26. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/error_hints.py +0 -0
  27. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/exceptions.py +0 -0
  28. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/hostd 2.py +0 -0
  29. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/invites.py +0 -0
  30. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/launcher.py +0 -0
  31. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/launcher_install.py +0 -0
  32. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/__init__.py +0 -0
  33. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/__main__.py +0 -0
  34. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/backend.py +0 -0
  35. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/realtime.py +0 -0
  36. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  37. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_backend.py +0 -0
  38. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  39. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  40. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  41. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  42. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  43. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/preferences.py +0 -0
  44. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/protocol_handler.py +0 -0
  45. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/protocol_v2.py +0 -0
  46. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/quickstart.py +0 -0
  47. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/rpc_allowlist.py +0 -0
  48. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/scripts/check_secrets.py +0 -0
  49. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/scripts/race_rate_harness.py +0 -0
  50. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/secrets.py +0 -0
  51. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/setup_clients.py +0 -0
  52. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/supervisor.py +0 -0
  53. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/up 2.py +0 -0
  54. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/up.py +0 -0
  55. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode/upload.py +0 -0
  56. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/SOURCES.txt +0 -0
  57. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/dependency_links.txt +0 -0
  58. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/entry_points.txt +0 -0
  59. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/requires.txt +0 -0
  60. {meshcode-2.11.108 → meshcode-2.11.109}/meshcode.egg-info/top_level.txt +0 -0
  61. {meshcode-2.11.108 → meshcode-2.11.109}/setup.cfg +0 -0
  62. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_auto_update_hardening.py +0 -0
  63. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_closegap_1.py +0 -0
  64. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_closegap_2.py +0 -0
  65. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_closegap_3.py +0 -0
  66. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject 2.py +0 -0
  67. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject 3.py +0 -0
  68. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject.py +0 -0
  69. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_boot_bug_regression.py +0 -0
  70. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_color_truecolor.py +0 -0
  71. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_core.py +0 -0
  72. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_cross_agent_messaging.py +0 -0
  73. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_date_parse.py +0 -0
  74. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_doctor.py +0 -0
  75. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_epistemic_v1_python_sdk.py +0 -0
  76. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  77. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_esc_deaf_state.py +0 -0
  78. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_exceptions.py +0 -0
  79. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_file_upload.py +0 -0
  80. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_init_device_code.py +0 -0
  81. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_install_guard.py +0 -0
  82. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_lease_sigterm_release.py +0 -0
  83. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_mark_read_batch.py +0 -0
  84. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_marketplace_ratings.py +0 -0
  85. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_migration_integrity.py +0 -0
  86. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_realtime_event_freshness.py +0 -0
  87. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_rls_cross_tenant.py +0 -0
  88. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_rpc_grants.py +0 -0
  89. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_rpc_migrations.py +0 -0
  90. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_run_agent_dry_run.py +0 -0
  91. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_run_agent_no_server_import.py +0 -0
  92. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_security_regressions.py +0 -0
  93. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_self_update_user_site.py +0 -0
  94. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_sentinel.py +0 -0
  95. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_setup_path.py +0 -0
  96. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_sleep_signals.py +0 -0
  97. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_status_enum_coverage.py +0 -0
  98. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_stay_on_loop_hook.py +0 -0
  99. {meshcode-2.11.108 → meshcode-2.11.109}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.108
3
+ Version: 2.11.109
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,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.108"
2
+ __version__ = "2.11.109"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -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.get("status") or "") in BUSY_STATUSES:
1517
- continue # safe-point ONLY never recycle a working agent mid-task
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
- return be.sb_rpc("mc_recycle_as_agent", {
6168
- "p_api_key": _get_api_key(),
6169
- "p_project_id": _PROJECT_ID,
6170
- "p_agent": name,
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
- return be.sb_rpc("mc_recycle_as_agent", {
6191
- "p_api_key": _get_api_key(),
6192
- "p_project_id": _PROJECT_ID,
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
- return be.sb_rpc("mc_set_recycle_schedule_as_agent", {
6223
- "p_api_key": _get_api_key(),
6224
- "p_project_id": _PROJECT_ID,
6225
- "p_enabled": bool(enabled),
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
- cmd = [sys.executable, "-m", "pip", "install", "-U", "--no-cache-dir",
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
- # Keep upgrade in the same site as the current install so the
288
- # user-PATH shim (~/Library/Python/X.Y/bin) doesn't get orphaned.
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.108
3
+ Version: 2.11.109
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.11.108"
7
+ version = "2.11.109"
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
File without changes